From a67f429d44c505c963f89ba06f1d32648d84d74d Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 17:26:16 +0300 Subject: [PATCH 01/11] fix(CMakeLists): update project name --- examples/http_hook/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/http_hook/CMakeLists.txt b/examples/http_hook/CMakeLists.txt index 39289112..377f86fe 100644 --- a/examples/http_hook/CMakeLists.txt +++ b/examples/http_hook/CMakeLists.txt @@ -3,4 +3,4 @@ cmake_minimum_required(VERSION 3.5) set(EXTRA_COMPONENT_DIRS ../../) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(wifi_manager) +project(http_hook) From 3cc0279aa3b31a58feadae45625cc9528c33361a Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 17:27:45 +0300 Subject: [PATCH 02/11] fix(idf-component-registry): add mdns component --- examples/http_hook/main/idf_component.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/http_hook/main/idf_component.yml diff --git a/examples/http_hook/main/idf_component.yml b/examples/http_hook/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/examples/http_hook/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true From 072c90db4b1fe531e486d20d0f91f3fffecc434d Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 17:33:13 +0300 Subject: [PATCH 03/11] fix(CMakeLists): add esp wifi components --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 74ea43d5..83e0077c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,12 @@ if(IDF_VERSION_MAJOR GREATER_EQUAL 4) idf_component_register(SRC_DIRS src - REQUIRES log nvs_flash mdns wpa_supplicant lwip esp_http_server + REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server INCLUDE_DIRS src EMBED_FILES src/style.css src/code.js src/index.html) else() set(COMPONENT_SRCDIRS src) set(COMPONENT_ADD_INCLUDEDIRS src) - set(COMPONENT_REQUIRES log nvs_flash mdns wpa_supplicant lwip esp_http_server) + set(COMPONENT_REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server) set(COMPONENT_EMBED_FILES src/style.css src/code.js src/index.html) register_component() endif() From 7d6f139f67f1fffbddf0d28e48e1d35dbbff6a68 Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 17:34:09 +0300 Subject: [PATCH 04/11] fix(gitignore): untrack managed components --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c027e556..ffb6254f 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ wifi_manager/.project sdkconfig sdkconfig.old **/build/ +**/managed_components/ #doxygen Doxyfile wifi_manager/doc/ From e33dd6b111e068e4a991a820f2fe8981928317ac Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 17:35:29 +0300 Subject: [PATCH 05/11] fix(gitignore): untrack dependicies lock --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ffb6254f..5448b48b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ sdkconfig sdkconfig.old **/build/ **/managed_components/ +dependencies.lock #doxygen Doxyfile wifi_manager/doc/ From 5eb81228b6deb0c0d5852b1d2aa97c878c5233ad Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 17:49:44 +0300 Subject: [PATCH 06/11] fix(CMakeLists): update example name --- examples/default_demo/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/default_demo/CMakeLists.txt b/examples/default_demo/CMakeLists.txt index 39289112..6458baae 100644 --- a/examples/default_demo/CMakeLists.txt +++ b/examples/default_demo/CMakeLists.txt @@ -3,4 +3,4 @@ cmake_minimum_required(VERSION 3.5) set(EXTRA_COMPONENT_DIRS ../../) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(wifi_manager) +project(default_demo) From e245367162f13ff42399c1932c7ee3cc36dee636 Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 17:52:43 +0300 Subject: [PATCH 07/11] fix(use-main): fix format specifier --- examples/default_demo/main/user_main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/default_demo/main/user_main.c b/examples/default_demo/main/user_main.c index 4c11767f..6de91819 100644 --- a/examples/default_demo/main/user_main.c +++ b/examples/default_demo/main/user_main.c @@ -47,7 +47,7 @@ static const char TAG[] = "main"; void monitoring_task(void *pvParameter) { for(;;){ - ESP_LOGI(TAG, "free heap: %d",esp_get_free_heap_size()); + ESP_LOGI(TAG, "free heap: %lu",esp_get_free_heap_size()); vTaskDelay( pdMS_TO_TICKS(10000) ); } } From 605a31dbee1a1fa1d213309fd50183992d494973 Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 18:07:19 +0300 Subject: [PATCH 08/11] fix(default-demo-user-main): fix panic abort esp32c3 and some mcus have one core so we cant use monitoring task on them. --- examples/default_demo/main/user_main.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/default_demo/main/user_main.c b/examples/default_demo/main/user_main.c index 6de91819..19af535e 100644 --- a/examples/default_demo/main/user_main.c +++ b/examples/default_demo/main/user_main.c @@ -73,7 +73,8 @@ void app_main() /* register a callback as an example to how you can integrate your code with the wifi manager */ wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok); - +#if CONFIG_SOC_CPU_CORES_NUM > 1 /* your code should go here. Here we simply create a task on core 2 that monitors free heap memory */ xTaskCreatePinnedToCore(&monitoring_task, "monitoring_task", 2048, NULL, 1, NULL, 1); +#endif } From 34e1668b39d9b70bd440b676928baf27cd5fbd39 Mon Sep 17 00:00:00 2001 From: ankayca Date: Sat, 22 Jun 2024 18:08:22 +0300 Subject: [PATCH 09/11] build(idf-component-registry): add mdns component --- examples/default_demo/main/idf_component.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/default_demo/main/idf_component.yml diff --git a/examples/default_demo/main/idf_component.yml b/examples/default_demo/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/examples/default_demo/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true From 1e217ff6b852f3fd8958e83d65490e01f90a82e5 Mon Sep 17 00:00:00 2001 From: ankayca Date: Sun, 30 Jun 2024 01:01:48 +0300 Subject: [PATCH 10/11] chore: Add new files and update configurations for version 0.0.4 --- dist/esp32-wifi-manager_0.0.1.tgz | Bin 0 -> 44263 bytes dist/esp32-wifi-manager_0.0.1/.gitignore | 75 + dist/esp32-wifi-manager_0.0.1/.travis.yml | 122 ++ dist/esp32-wifi-manager_0.0.1/CMakeLists.txt | 12 + dist/esp32-wifi-manager_0.0.1/Kconfig | 81 + dist/esp32-wifi-manager_0.0.1/LICENSE.md | 19 + dist/esp32-wifi-manager_0.0.1/README.md | 225 +++ dist/esp32-wifi-manager_0.0.1/component.mk | 4 + .../examples/default_demo/CMakeLists.txt | 6 + .../examples/default_demo/Makefile | 11 + .../examples/default_demo/main/CMakeLists.txt | 3 + .../examples/default_demo/main/component.mk | 4 + .../default_demo/main/idf_component.yml | 17 + .../examples/default_demo/main/user_main.c | 80 + .../examples/default_demo/sdkconfig.defaults | 1 + .../examples/http_hook/CMakeLists.txt | 6 + .../examples/http_hook/Makefile | 11 + .../examples/http_hook/main/CMakeLists.txt | 3 + .../examples/http_hook/main/component.mk | 4 + .../examples/http_hook/main/idf_component.yml | 17 + .../examples/http_hook/main/user_main.c | 76 + .../examples/http_hook/sdkconfig.defaults | 1 + .../idf_component.yml | 5 + dist/esp32-wifi-manager_0.0.1/src/ap.json | 12 + dist/esp32-wifi-manager_0.0.1/src/code.js | 352 +++++ .../esp32-wifi-manager_0.0.1/src/component.mk | 5 + .../esp32-wifi-manager_0.0.1/src/compress.bat | 2 + dist/esp32-wifi-manager_0.0.1/src/connect | 2 + .../esp32-wifi-manager_0.0.1/src/dns_server.c | 184 +++ .../esp32-wifi-manager_0.0.1/src/dns_server.h | 137 ++ dist/esp32-wifi-manager_0.0.1/src/http_app.c | 479 ++++++ dist/esp32-wifi-manager_0.0.1/src/http_app.h | 73 + dist/esp32-wifi-manager_0.0.1/src/index.html | 136 ++ dist/esp32-wifi-manager_0.0.1/src/json.c | 143 ++ dist/esp32-wifi-manager_0.0.1/src/json.h | 47 + dist/esp32-wifi-manager_0.0.1/src/lock.svg | 5 + dist/esp32-wifi-manager_0.0.1/src/nvs_sync.c | 79 + dist/esp32-wifi-manager_0.0.1/src/nvs_sync.h | 76 + .../esp32-wifi-manager_0.0.1/src/settings.svg | 5 + dist/esp32-wifi-manager_0.0.1/src/status | 1 + dist/esp32-wifi-manager_0.0.1/src/style.css | 248 +++ dist/esp32-wifi-manager_0.0.1/src/wifi0.svg | 7 + dist/esp32-wifi-manager_0.0.1/src/wifi1.svg | 7 + dist/esp32-wifi-manager_0.0.1/src/wifi2.svg | 7 + dist/esp32-wifi-manager_0.0.1/src/wifi3.svg | 7 + .../src/wifi_manager.c | 1336 +++++++++++++++++ .../src/wifi_manager.h | 417 +++++ dist/esp32-wifi-manager_0.0.3.tgz | Bin 0 -> 44304 bytes dist/esp32-wifi-manager_0.0.3/.gitignore | 75 + dist/esp32-wifi-manager_0.0.3/.travis.yml | 122 ++ dist/esp32-wifi-manager_0.0.3/CMakeLists.txt | 12 + dist/esp32-wifi-manager_0.0.3/Kconfig | 81 + dist/esp32-wifi-manager_0.0.3/LICENSE.md | 19 + dist/esp32-wifi-manager_0.0.3/README.md | 225 +++ dist/esp32-wifi-manager_0.0.3/component.mk | 4 + .../examples/default_demo/CMakeLists.txt | 6 + .../examples/default_demo/Makefile | 11 + .../examples/default_demo/main/CMakeLists.txt | 3 + .../examples/default_demo/main/component.mk | 4 + .../default_demo/main/idf_component.yml | 17 + .../examples/default_demo/main/user_main.c | 80 + .../examples/default_demo/sdkconfig.defaults | 1 + .../examples/http_hook/CMakeLists.txt | 6 + .../examples/http_hook/Makefile | 11 + .../examples/http_hook/main/CMakeLists.txt | 3 + .../examples/http_hook/main/component.mk | 4 + .../examples/http_hook/main/idf_component.yml | 17 + .../examples/http_hook/main/user_main.c | 76 + .../examples/http_hook/sdkconfig.defaults | 1 + .../idf_component.yml | 9 + dist/esp32-wifi-manager_0.0.3/src/ap.json | 12 + dist/esp32-wifi-manager_0.0.3/src/code.js | 352 +++++ .../esp32-wifi-manager_0.0.3/src/component.mk | 5 + .../esp32-wifi-manager_0.0.3/src/compress.bat | 2 + dist/esp32-wifi-manager_0.0.3/src/connect | 2 + .../esp32-wifi-manager_0.0.3/src/dns_server.c | 184 +++ .../esp32-wifi-manager_0.0.3/src/dns_server.h | 137 ++ dist/esp32-wifi-manager_0.0.3/src/http_app.c | 479 ++++++ dist/esp32-wifi-manager_0.0.3/src/http_app.h | 73 + dist/esp32-wifi-manager_0.0.3/src/index.html | 136 ++ dist/esp32-wifi-manager_0.0.3/src/json.c | 143 ++ dist/esp32-wifi-manager_0.0.3/src/json.h | 47 + dist/esp32-wifi-manager_0.0.3/src/lock.svg | 5 + dist/esp32-wifi-manager_0.0.3/src/nvs_sync.c | 79 + dist/esp32-wifi-manager_0.0.3/src/nvs_sync.h | 76 + .../esp32-wifi-manager_0.0.3/src/settings.svg | 5 + dist/esp32-wifi-manager_0.0.3/src/status | 1 + dist/esp32-wifi-manager_0.0.3/src/style.css | 248 +++ dist/esp32-wifi-manager_0.0.3/src/wifi0.svg | 7 + dist/esp32-wifi-manager_0.0.3/src/wifi1.svg | 7 + dist/esp32-wifi-manager_0.0.3/src/wifi2.svg | 7 + dist/esp32-wifi-manager_0.0.3/src/wifi3.svg | 7 + .../src/wifi_manager.c | 1336 +++++++++++++++++ .../src/wifi_manager.h | 417 +++++ dist/esp32-wifi-manager_0.0.4.tgz | Bin 0 -> 44333 bytes dist/esp32-wifi-manager_0.0.4/.gitignore | 75 + dist/esp32-wifi-manager_0.0.4/.travis.yml | 122 ++ dist/esp32-wifi-manager_0.0.4/CMakeLists.txt | 12 + dist/esp32-wifi-manager_0.0.4/Kconfig | 81 + dist/esp32-wifi-manager_0.0.4/LICENSE.md | 19 + dist/esp32-wifi-manager_0.0.4/README.md | 225 +++ dist/esp32-wifi-manager_0.0.4/component.mk | 4 + .../examples/default_demo/CMakeLists.txt | 6 + .../examples/default_demo/Makefile | 11 + .../examples/default_demo/main/CMakeLists.txt | 3 + .../examples/default_demo/main/component.mk | 4 + .../default_demo/main/idf_component.yml | 17 + .../examples/default_demo/main/user_main.c | 80 + .../examples/default_demo/sdkconfig.defaults | 1 + .../examples/http_hook/CMakeLists.txt | 6 + .../examples/http_hook/Makefile | 11 + .../examples/http_hook/main/CMakeLists.txt | 3 + .../examples/http_hook/main/component.mk | 4 + .../examples/http_hook/main/idf_component.yml | 17 + .../examples/http_hook/main/user_main.c | 76 + .../examples/http_hook/sdkconfig.defaults | 1 + .../idf_component.yml | 9 + dist/esp32-wifi-manager_0.0.4/src/ap.json | 12 + dist/esp32-wifi-manager_0.0.4/src/code.js | 352 +++++ .../esp32-wifi-manager_0.0.4/src/component.mk | 5 + .../esp32-wifi-manager_0.0.4/src/compress.bat | 2 + dist/esp32-wifi-manager_0.0.4/src/connect | 2 + .../esp32-wifi-manager_0.0.4/src/dns_server.c | 184 +++ .../esp32-wifi-manager_0.0.4/src/dns_server.h | 137 ++ dist/esp32-wifi-manager_0.0.4/src/http_app.c | 479 ++++++ dist/esp32-wifi-manager_0.0.4/src/http_app.h | 73 + dist/esp32-wifi-manager_0.0.4/src/index.html | 136 ++ dist/esp32-wifi-manager_0.0.4/src/json.c | 143 ++ dist/esp32-wifi-manager_0.0.4/src/json.h | 47 + dist/esp32-wifi-manager_0.0.4/src/lock.svg | 5 + dist/esp32-wifi-manager_0.0.4/src/nvs_sync.c | 79 + dist/esp32-wifi-manager_0.0.4/src/nvs_sync.h | 76 + .../esp32-wifi-manager_0.0.4/src/settings.svg | 5 + dist/esp32-wifi-manager_0.0.4/src/status | 1 + dist/esp32-wifi-manager_0.0.4/src/style.css | 248 +++ dist/esp32-wifi-manager_0.0.4/src/wifi0.svg | 7 + dist/esp32-wifi-manager_0.0.4/src/wifi1.svg | 7 + dist/esp32-wifi-manager_0.0.4/src/wifi2.svg | 7 + dist/esp32-wifi-manager_0.0.4/src/wifi3.svg | 7 + .../src/wifi_manager.c | 1336 +++++++++++++++++ .../src/wifi_manager.h | 417 +++++ idf_component.yml | 8 + 142 files changed, 13666 insertions(+) create mode 100644 dist/esp32-wifi-manager_0.0.1.tgz create mode 100644 dist/esp32-wifi-manager_0.0.1/.gitignore create mode 100644 dist/esp32-wifi-manager_0.0.1/.travis.yml create mode 100644 dist/esp32-wifi-manager_0.0.1/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.1/Kconfig create mode 100644 dist/esp32-wifi-manager_0.0.1/LICENSE.md create mode 100644 dist/esp32-wifi-manager_0.0.1/README.md create mode 100644 dist/esp32-wifi-manager_0.0.1/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/default_demo/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/default_demo/Makefile create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/user_main.c create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/default_demo/sdkconfig.defaults create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/http_hook/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/http_hook/Makefile create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/user_main.c create mode 100644 dist/esp32-wifi-manager_0.0.1/examples/http_hook/sdkconfig.defaults create mode 100644 dist/esp32-wifi-manager_0.0.1/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.1/src/ap.json create mode 100644 dist/esp32-wifi-manager_0.0.1/src/code.js create mode 100644 dist/esp32-wifi-manager_0.0.1/src/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.1/src/compress.bat create mode 100644 dist/esp32-wifi-manager_0.0.1/src/connect create mode 100644 dist/esp32-wifi-manager_0.0.1/src/dns_server.c create mode 100644 dist/esp32-wifi-manager_0.0.1/src/dns_server.h create mode 100644 dist/esp32-wifi-manager_0.0.1/src/http_app.c create mode 100644 dist/esp32-wifi-manager_0.0.1/src/http_app.h create mode 100644 dist/esp32-wifi-manager_0.0.1/src/index.html create mode 100644 dist/esp32-wifi-manager_0.0.1/src/json.c create mode 100644 dist/esp32-wifi-manager_0.0.1/src/json.h create mode 100644 dist/esp32-wifi-manager_0.0.1/src/lock.svg create mode 100644 dist/esp32-wifi-manager_0.0.1/src/nvs_sync.c create mode 100644 dist/esp32-wifi-manager_0.0.1/src/nvs_sync.h create mode 100644 dist/esp32-wifi-manager_0.0.1/src/settings.svg create mode 100644 dist/esp32-wifi-manager_0.0.1/src/status create mode 100644 dist/esp32-wifi-manager_0.0.1/src/style.css create mode 100644 dist/esp32-wifi-manager_0.0.1/src/wifi0.svg create mode 100644 dist/esp32-wifi-manager_0.0.1/src/wifi1.svg create mode 100644 dist/esp32-wifi-manager_0.0.1/src/wifi2.svg create mode 100644 dist/esp32-wifi-manager_0.0.1/src/wifi3.svg create mode 100644 dist/esp32-wifi-manager_0.0.1/src/wifi_manager.c create mode 100644 dist/esp32-wifi-manager_0.0.1/src/wifi_manager.h create mode 100644 dist/esp32-wifi-manager_0.0.3.tgz create mode 100644 dist/esp32-wifi-manager_0.0.3/.gitignore create mode 100644 dist/esp32-wifi-manager_0.0.3/.travis.yml create mode 100644 dist/esp32-wifi-manager_0.0.3/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.3/Kconfig create mode 100644 dist/esp32-wifi-manager_0.0.3/LICENSE.md create mode 100644 dist/esp32-wifi-manager_0.0.3/README.md create mode 100644 dist/esp32-wifi-manager_0.0.3/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/default_demo/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/default_demo/Makefile create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/user_main.c create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/default_demo/sdkconfig.defaults create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/http_hook/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/http_hook/Makefile create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/user_main.c create mode 100644 dist/esp32-wifi-manager_0.0.3/examples/http_hook/sdkconfig.defaults create mode 100644 dist/esp32-wifi-manager_0.0.3/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.3/src/ap.json create mode 100644 dist/esp32-wifi-manager_0.0.3/src/code.js create mode 100644 dist/esp32-wifi-manager_0.0.3/src/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.3/src/compress.bat create mode 100644 dist/esp32-wifi-manager_0.0.3/src/connect create mode 100644 dist/esp32-wifi-manager_0.0.3/src/dns_server.c create mode 100644 dist/esp32-wifi-manager_0.0.3/src/dns_server.h create mode 100644 dist/esp32-wifi-manager_0.0.3/src/http_app.c create mode 100644 dist/esp32-wifi-manager_0.0.3/src/http_app.h create mode 100644 dist/esp32-wifi-manager_0.0.3/src/index.html create mode 100644 dist/esp32-wifi-manager_0.0.3/src/json.c create mode 100644 dist/esp32-wifi-manager_0.0.3/src/json.h create mode 100644 dist/esp32-wifi-manager_0.0.3/src/lock.svg create mode 100644 dist/esp32-wifi-manager_0.0.3/src/nvs_sync.c create mode 100644 dist/esp32-wifi-manager_0.0.3/src/nvs_sync.h create mode 100644 dist/esp32-wifi-manager_0.0.3/src/settings.svg create mode 100644 dist/esp32-wifi-manager_0.0.3/src/status create mode 100644 dist/esp32-wifi-manager_0.0.3/src/style.css create mode 100644 dist/esp32-wifi-manager_0.0.3/src/wifi0.svg create mode 100644 dist/esp32-wifi-manager_0.0.3/src/wifi1.svg create mode 100644 dist/esp32-wifi-manager_0.0.3/src/wifi2.svg create mode 100644 dist/esp32-wifi-manager_0.0.3/src/wifi3.svg create mode 100644 dist/esp32-wifi-manager_0.0.3/src/wifi_manager.c create mode 100644 dist/esp32-wifi-manager_0.0.3/src/wifi_manager.h create mode 100644 dist/esp32-wifi-manager_0.0.4.tgz create mode 100644 dist/esp32-wifi-manager_0.0.4/.gitignore create mode 100644 dist/esp32-wifi-manager_0.0.4/.travis.yml create mode 100644 dist/esp32-wifi-manager_0.0.4/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.4/Kconfig create mode 100644 dist/esp32-wifi-manager_0.0.4/LICENSE.md create mode 100644 dist/esp32-wifi-manager_0.0.4/README.md create mode 100644 dist/esp32-wifi-manager_0.0.4/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/default_demo/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/default_demo/Makefile create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/user_main.c create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/default_demo/sdkconfig.defaults create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/http_hook/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/http_hook/Makefile create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/CMakeLists.txt create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/user_main.c create mode 100644 dist/esp32-wifi-manager_0.0.4/examples/http_hook/sdkconfig.defaults create mode 100644 dist/esp32-wifi-manager_0.0.4/idf_component.yml create mode 100644 dist/esp32-wifi-manager_0.0.4/src/ap.json create mode 100644 dist/esp32-wifi-manager_0.0.4/src/code.js create mode 100644 dist/esp32-wifi-manager_0.0.4/src/component.mk create mode 100644 dist/esp32-wifi-manager_0.0.4/src/compress.bat create mode 100644 dist/esp32-wifi-manager_0.0.4/src/connect create mode 100644 dist/esp32-wifi-manager_0.0.4/src/dns_server.c create mode 100644 dist/esp32-wifi-manager_0.0.4/src/dns_server.h create mode 100644 dist/esp32-wifi-manager_0.0.4/src/http_app.c create mode 100644 dist/esp32-wifi-manager_0.0.4/src/http_app.h create mode 100644 dist/esp32-wifi-manager_0.0.4/src/index.html create mode 100644 dist/esp32-wifi-manager_0.0.4/src/json.c create mode 100644 dist/esp32-wifi-manager_0.0.4/src/json.h create mode 100644 dist/esp32-wifi-manager_0.0.4/src/lock.svg create mode 100644 dist/esp32-wifi-manager_0.0.4/src/nvs_sync.c create mode 100644 dist/esp32-wifi-manager_0.0.4/src/nvs_sync.h create mode 100644 dist/esp32-wifi-manager_0.0.4/src/settings.svg create mode 100644 dist/esp32-wifi-manager_0.0.4/src/status create mode 100644 dist/esp32-wifi-manager_0.0.4/src/style.css create mode 100644 dist/esp32-wifi-manager_0.0.4/src/wifi0.svg create mode 100644 dist/esp32-wifi-manager_0.0.4/src/wifi1.svg create mode 100644 dist/esp32-wifi-manager_0.0.4/src/wifi2.svg create mode 100644 dist/esp32-wifi-manager_0.0.4/src/wifi3.svg create mode 100644 dist/esp32-wifi-manager_0.0.4/src/wifi_manager.c create mode 100644 dist/esp32-wifi-manager_0.0.4/src/wifi_manager.h create mode 100644 idf_component.yml diff --git a/dist/esp32-wifi-manager_0.0.1.tgz b/dist/esp32-wifi-manager_0.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..abafc5dd6f992c2e1f275a9119207d956629e754 GIT binary patch literal 44263 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 H0O$h%e@_4& literal 0 HcmV?d00001 diff --git a/dist/esp32-wifi-manager_0.0.1/.gitignore b/dist/esp32-wifi-manager_0.0.1/.gitignore new file mode 100644 index 00000000..5448b48b --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/.gitignore @@ -0,0 +1,75 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Eclipse +.metadata/ +RemoteSystemsTempFiles/.project +.settings/ +*.a +*.o +*.d +wifi_manager/.cproject +wifi_manager/.project +sdkconfig +sdkconfig.old +**/build/ +**/managed_components/ +dependencies.lock +#doxygen +Doxyfile +wifi_manager/doc/ +.project +.cproject + +# Visual Studio Code +.vscode/ diff --git a/dist/esp32-wifi-manager_0.0.1/.travis.yml b/dist/esp32-wifi-manager_0.0.1/.travis.yml new file mode 100644 index 00000000..37c55458 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/.travis.yml @@ -0,0 +1,122 @@ +language: bash + +# when you suspects issues in cache, use the following line to disable cache. +# cache: false +cache: + directories: + - ${HOME}/distfiles + - ${HOME}/.ccache + - ${HOME}/.cache/pip +os: + - linux + +matrix: + include: + - env: + - PROJECT_TARGET="esp32" + - PROJECT_SDK_BRANCH="master" +# - env: +# - PROJECT_TARGET="esp8266" +# - PROJECT_SDK_BRANCH="master" + +addons: + apt: + packages: + - gcc + - wget + - make + - libncurses-dev + - flex + - bison + - python + - python-pip + - gperf + - ccache + +before_install: + # Save path to the git respository + - PROJECT_PATH=$(pwd) + +install: + - export TOOLCHAIN_DIR="${HOME}/${PROJECT_TARGET}" + - | + if [ ${PROJECT_TARGET} == "esp8266" ]; then + export PROJECT_GCC_PREFIX="xtensa-lx106-elf" + export PROJECT_TOOLCHAIN_FILE=xtensa-lx106-elf-linux64-1.22.0-92-g8facf4c-5.2.0.tar.gz + export PROJECT_SDK_NAME="ESP8266_RTOS_SDK" + else + export PROJECT_GCC_PREFIX="xtensa-esp32-elf" + export PROJECT_TOOLCHAIN_FILE=xtensa-esp32-elf-gcc8_2_0-esp32-2019r1-linux-amd64.tar.gz + export PROJECT_SDK_NAME="esp-idf" + fi + - export PROJECT_GCC_FILE="${PROJECT_GCC_PREFIX}-gcc" + - export PROJECT_DISTFILE_DIR="${HOME}/distfiles" + - export IDF_PATH=${TOOLCHAIN_DIR}/${PROJECT_SDK_NAME} + - export PROJECT_LOG="${HOME}/build.log" + - export PROJECT_EXAMPLE_DIR="${PROJECT_PATH}/examples" + # Install ESP32 toochain following steps as desribed + # in http://esp-idf.readthedocs.io/en/latest/linux-setup.html + + # Prepare directory for the toolchain + - mkdir -p ${TOOLCHAIN_DIR} ${PROJECT_DISTFILE_DIR} + # Get SDK from github + - git clone --branch ${PROJECT_SDK_BRANCH} --recursive https://github.com/espressif/${PROJECT_SDK_NAME}.git ${IDF_PATH} + + # Setup ccache to build faster + # XXX when the entire build process exceeds 50 min, th job will be killed + # https://docs.travis-ci.com/user/customizing-the-build/#build-timeouts + - ccache --version + - mkdir ${HOME}/ccache_bin + - (cd ${HOME}/ccache_bin && ln -s /usr/bin/ccache ${PROJECT_GCC_FILE}) + - export CCACHE_BASEDIR=$PROJECT_PATH + - export CCACHE_CPP2=true + + # Get Python requirements + - python -m pip install --user --upgrade pyOpenSSL + - python -m pip install --user -r ${IDF_PATH}/requirements.txt + + # Download binary toolchain if it does not exist + - | + if [ ! -f ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} ]; then + wget -O ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} https://dl.espressif.com/dl/${PROJECT_TOOLCHAIN_FILE} + fi + - tar -xz -C ${TOOLCHAIN_DIR} -f ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} + + # Make toolchains available for all terminal sessions + - export PATH=$HOME/ccache_bin:$PATH:$HOME/${PROJECT_TARGET}/${PROJECT_GCC_PREFIX}/bin + +script: + - rm -f ${PROJECT_LOG} + # XXX surpress log output where possible. when the size exceeds 4 MB, the + # job will be killed. + - | + IGNORE_FILE="travis-ignore" + + case ${PROJECT_TARGET} in + esp32) + ;; + esp8266) + IGNORE_FILE="travis-ignore-esp8266" + # these drivers do not compile for ESP8266 yet + export EXCLUDE_COMPONENTS="encoder max7219 mcp23x17" + ;; + esac + + cd ${PROJECT_EXAMPLE_DIR} + for i in $(ls -d */); do + if [ ! -e ${PROJECT_EXAMPLE_DIR}/${i}/${IGNORE_FILE} ]; then + echo "Building ${i}..." + cd ${PROJECT_EXAMPLE_DIR}/${i} + make defconfig + make -j2 >> ${PROJECT_LOG} + if [ $? -ne 0 ]; then + # when failed, show last 100 lines for debugging, and exit with + # non-zero exit code + tail -n 100 ${PROJECT_LOG} + exit 1 + fi + make clean >/dev/null + # make sure the directory is clean + rm -rf ${i}/sdkconfig ${i}/build + fi + done diff --git a/dist/esp32-wifi-manager_0.0.1/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.1/CMakeLists.txt new file mode 100644 index 00000000..83e0077c --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/CMakeLists.txt @@ -0,0 +1,12 @@ +if(IDF_VERSION_MAJOR GREATER_EQUAL 4) + idf_component_register(SRC_DIRS src + REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server + INCLUDE_DIRS src + EMBED_FILES src/style.css src/code.js src/index.html) +else() + set(COMPONENT_SRCDIRS src) + set(COMPONENT_ADD_INCLUDEDIRS src) + set(COMPONENT_REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server) + set(COMPONENT_EMBED_FILES src/style.css src/code.js src/index.html) + register_component() +endif() diff --git a/dist/esp32-wifi-manager_0.0.1/Kconfig b/dist/esp32-wifi-manager_0.0.1/Kconfig new file mode 100644 index 00000000..59c5a9a8 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/Kconfig @@ -0,0 +1,81 @@ +menu "Wifi Manager Configuration" + +config WIFI_MANAGER_TASK_PRIORITY + int "RTOS Task Priority for the wifi_manager" + default 5 + help + Tasks spawn by the manager will have a priority of WIFI_MANAGER_TASK_PRIORITY-1. For this particular reason, minimum recommended task priority is 2. + +config WIFI_MANAGER_RETRY_TIMER + int "Time (in ms) between each retry attempt" + default 5000 + help + Defines the time to wait before an attempt to re-connect to a saved wifi is made after connection is lost or another unsuccesful attempt is made. + +config WIFI_MANAGER_MAX_RETRY_START_AP + int "Max Retry before starting the AP" + default 3 + help + Defines the maximum number of failed retries allowed before the WiFi manager starts its own access point. + +config WIFI_MANAGER_SHUTDOWN_AP_TIMER + int "Time (in ms) to wait before shutting down the AP" + default 60000 + help + Defines the time (in ms) to wait after a succesful connection before shutting down the access point. + +config WEBAPP_LOCATION + string "Defines the URL where the wifi manager is located" + default "/" + help + This parameter helps you relocate the wifimanager to another URL, for instance /wifimanager/ The trailing slash is important and should be included + +config DEFAULT_AP_SSID + string "Access Point SSID" + default "esp32" + help + SSID (network name) the the esp32 will broadcast. + +config DEFAULT_AP_PASSWORD + string "Access Point Password" + default "esp32pwd" + help + Password used for the Access Point. Leave empty and set AUTH MODE to WIFI_AUTH_OPEN for no password. + +config DEFAULT_AP_CHANNEL + int "Access Point WiFi Channel" + default 1 + help + Be careful you might not see the access point if you use a channel not allowed in your country. + +config DEFAULT_AP_IP + string "Access Point IP Address" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_GATEWAY + string "Access Point IP Gateway" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_NETMASK + string "Access Point Netmask" + default "255.255.255.0" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_MAX_CONNECTIONS + int "Access Point Max Connections" + default 4 + help + Max is 4. + +config DEFAULT_AP_BEACON_INTERVAL + int "Access Point Beacon Interval (ms)" + default 100 + help + 100ms is the recommended default. + +endmenu diff --git a/dist/esp32-wifi-manager_0.0.1/LICENSE.md b/dist/esp32-wifi-manager_0.0.1/LICENSE.md new file mode 100644 index 00000000..5f2ac0bc --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2017-2019 Tony Pottier + +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. diff --git a/dist/esp32-wifi-manager_0.0.1/README.md b/dist/esp32-wifi-manager_0.0.1/README.md new file mode 100644 index 00000000..43248f3d --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/README.md @@ -0,0 +1,225 @@ +# What is esp32-wifi-manager? + +### Build status [![Build Status](https://travis-ci.com/tonyp7/esp32-wifi-manager.svg?branch=master)](https://travis-ci.com/tonyp7/esp32-wifi-manager) + +*esp32-wifi-manager* is a pure C esp-idf component for ESP32 that enables easy management of wifi networks through a web portal. + +*esp32-wifi-manager* is is an all in one wifi scanner, http server & dns daemon living in the least amount of RAM possible. + +*esp32-wifi-manager* will automatically attempt to re-connect to a previously saved network on boot, and if it cannot find a saved wifi it will start its own access point through which you can manage and connect to wifi networks. Upon a succesful connection, the software will shutdown the access point automatically after some time (1 minute by default). + +*esp32-wifi-manager* compiles with esp-idf 4.2 and above. See [Getting Started](#getting-started) to guide you through your first setup. + +# Content + - [Demo](#demo) + - [Look And Feel](#look-and-feel) + - [Getting Started](#getting-started) + - [Requirements](#requirements) + - [Hello World](#hello-world) + - [Configuring the Wifi Manager](#configuring-the-wifi-manager) + - [Adding esp32-wifi-manager to your code](#adding-esp32-wifi-manager-to-your-code) + - [Interacting with the manager](#interacting-with-the-manager) + - [Interacting with the http server](#interacting-with-the-http-server) + - [Thread safety and access to NVS](#thread-safety-and-access-to-nvs) + - [License](#license) + + +# Demo +[![esp32-wifi-manager demo](http://img.youtube.com/vi/hxlZi15bym4/0.jpg)](http://www.youtube.com/watch?v=hxlZi15bym4) + +# Look and Feel +![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-password.png "esp32-wifi-manager") ![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-connected-to.png "esp32-wifi-manager") + +# Getting Started + +## Requirements + +To get you started, esp32-wifi-manager needs: + +- esp-idf **4.2 and up** +- esp32 or esp32-s2 + +There are breaking changes and new features in esp-idf 4.1 and 4.2 which makes esp32-wifi-manager incompatible with anything lower than 4.2. This includes esp_netif (introduced in 4.1) and esp_event_handler_instance_t (introduced in 4.2). It is recommended to compile esp32-wifi-manager with the master tree to avoid any compatibility issue. + +## Hello World + +Clone the repository where you want it to be. If you are unfamiliar with Git, you can use Github Desktop on Windows: + +```bash +git clone https://github.com/tonyp7/esp32-wifi-manager.git +``` + +Navigate under the included example: + +```bash +cd esp32-wifi-manager/examples/default_demo +``` + +Compile the code and load it on your esp32: + +```bash +idf.py build flash monitor +``` + +_Note: while it is encouraged to use the newer build system with idf.py and cmake, esp32-wifi-manager still supports the legacy build system. If you are using make on Linux or make using MSYS2 on Windows, you can still use "make build flash monitor" if you prefer_ + +Now, using any wifi capable device, you will see a new wifi access point named *esp32*. Connect to it using the default password *esp32pwd*. If the captive portal does not pop up on your device, you can access the wifi manager at its default IP address: http://10.10.0.1. + +## Configuring the Wifi Manager + +esp32-wifi-manager can be configured without touching its code. At the project level use: + +```bash +idf.py menuconfig +``` + +Navigate in "Component config" then pick "Wifi Manager Configuration". You will be greeted by the following screen: + +![esp32-wifi-manager-menuconfig](https://idyl.io/wp-content/uploads/2020/08/wifi-manager-menuconfig-800px.png "menuconfig screen") + +You can change the ssid and password of the access point at your convenience, but it is highly recommended to keep default values. Your password should be between 8 and 63 characters long, to comply with the WPA2 standard. If the password is set to an empty value or is less than 8 characters long, esp32-wifi-manager will create its access point as an open wifi network. + +You can also change the values for various timers, for instance how long it takes for the access point to shutdown once a connection is established (default: 60000). While it could be tempting to set this timer to 0, just be warned that in that case the user will never get the feedback that a connection is succesful. Shutting down the AP will instantly kill the current navigating session on the captive portal. + +Finally, you can choose to relocate esp32-wifi-manager to a different URL by changing the default value of "/" to something else, for instance "/wifimanager/". Please note that the trailing slash does matter. This feature is particularly useful in case you want your own webapp to co-exist with esp32-wifi-manager's own web pages. + +# Adding esp32-wifi-manager to your code + +In order to use esp32-wifi-manager effectively in your esp-idf projects, copy the whole esp32-wifi-manager repository (or git clone) into a components subfolder. + +Your project should look like this: + + - project_folder + - build + - components + - esp32-wifi-manager + - main + - main.c + +Under eclipse, this is what a typical project looks like: + +![eclipse project with esp32-wifi-manager](https://idyl.io/wp-content/uploads/2020/07/eclipse-idf-project.png "eclipse project with esp32-wifi-manager") + +Once this is done, you need to edit the CMakeLists.txt file at the root of your project to register the components folder. This is done by adding the following line: + +```cmake +set(EXTRA_COMPONENTS_DIRS components/) +``` + +A typical CmakeLists.txt file should look like this: + +```cmake +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS components/) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(name_of_your_project) +``` + +If you are using the old build system with make instead, you should edit the Makefile instead such as: + +```make +PROJECT_NAME := name_of_your_project +EXTRA_COMPONENT_DIRS := components/ +include $(IDF_PATH)/make/project.mk +``` + +Once this is done, you can now in your user code add the header: + +```c +#include "wifi_manager.h" +``` + +All you need to do now is to call wifi_manager_start(); in your code. See [examples/default_demo](examples/default_demo) if you are uncertain. + + +## Interacting with the manager + +Ther are effectively three different ways you can embed esp32-wifi-manager with your code: +* Just forget about it and poll in your code for wifi connectivity status +* Use event callbacks +* Modify esp32-wifi-manager code directly to fit your needs + +**Event callbacks** are the cleanest way to use the wifi manager and that's the recommended way to do it. A typical use-case would be to get notified when wifi manager finally gets a connection to an access point. In order to do this you can simply define a callback function: + +```c +void cb_connection_ok(void *pvParameter){ + ESP_LOGI(TAG, "I have a connection!"); +} +``` + +Then just register it by calling: + +```c +wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok); +``` + +That's it! Now everytime the event is triggered it will call this function. The [examples/default_demo](examples/default_demo) contains sample code using callbacks. + +### List of events + +The list of possible events you can add a callback to are defined by message_code_t in wifi_manager.h. They are as following: + +* WM_ORDER_START_HTTP_SERVER +* WM_ORDER_STOP_HTTP_SERVER +* WM_ORDER_START_DNS_SERVICE +* WM_ORDER_STOP_DNS_SERVICE +* WM_ORDER_START_WIFI_SCAN +* WM_ORDER_LOAD_AND_RESTORE_STA +* WM_ORDER_CONNECT_STA +* WM_ORDER_DISCONNECT_STA +* WM_ORDER_START_AP +* WM_EVENT_STA_DISCONNECTED +* WM_EVENT_SCAN_DONE +* WM_EVENT_STA_GOT_IP +* WM_ORDER_STOP_AP + +In practice, keeping track of WM_EVENT_STA_GOT_IP and WM_EVENT_STA_DISCONNECTED is key to know whether or not your esp32 has a connection. The other messages can mostly be ignored in a typical application using esp32-wifi-manager. + +### Events parameters + +Callback signature includes a void* pointer. For most events, this additional parameter is empty and sent as a NULL value. A few select events have additional data which can be leveraged by user code. They are listed below: + +* WM_EVENT_SCAN_DONE is sent with a wifi_event_sta_scan_done_t* object. +* WM_EVENT_STA_DISCONNECTED is sent with a wifi_event_sta_disconnected_t* object. +* WM_EVENT_STA_GOT_IP is sent with a ip_event_got_ip_t* object. + +These objects are standard esp-idf structures, and are documented as such in the [official pages](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html). + +The [examples/default_demo](examples/default_demo) demonstrates how you can read a ip_event_got_ip_t object to access the IP address assigned to the esp32. + +## Interacting with the http server + +Because esp32-wifi-manager spawns its own http server, you might want to extend this server to serve your own pages in your application. It is possible to do so by registering your own URL handler using the standard esp_http_server signature: + +```c +esp_err_t my_custom_handler(httpd_req_t *req){ +``` + +And then registering the handler by doing + +```c +http_app_set_handler_hook(HTTP_GET, &my_custom_handler); +``` + +The [examples/http_hook](examples/http_hook) contains an example where a web page is registered at /helloworld + +## Thread safety and access to NVS + +esp32-wifi-manager accesses the non-volatile storage to store and loads its configuration into a dedicated namespace "espwifimgr". If you want to make sure there will never be a conflict with concurrent access to the NVS, you can include nvs_sync.h and use calls to nvs_sync_lock and nvs_sync_unlock. + +```c +nvs_handle handle; + +if(nvs_sync_lock( portMAX_DELAY )){ + if(nvs_open(wifi_manager_nvs_namespace, NVS_READWRITE, &handle) == ESP_OK){ + /* do something with NVS */ + nvs_close(handle); + } + nvs_sync_unlock(); +} +``` +nvs_sync_lock waits for the number of ticks sent to it as a parameter to acquire a mutex. It is recommended to use portMAX_DELAY. In practice, nvs_sync_lock will almost never wait. + + +# License +*esp32-wifi-manager* is MIT licensed. As such, it can be included in any project, commercial or not, as long as you retain original copyright. Please make sure to read the license file. diff --git a/dist/esp32-wifi-manager_0.0.1/component.mk b/dist/esp32-wifi-manager_0.0.1/component.mk new file mode 100644 index 00000000..75e34089 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/component.mk @@ -0,0 +1,4 @@ +COMPONENT_ADD_INCLUDEDIRS = src +COMPONENT_SRCDIRS = src +COMPONENT_DEPENDS = log esp_http_server +COMPONENT_EMBED_FILES := src/style.css src/code.js src/index.html diff --git a/dist/esp32-wifi-manager_0.0.1/examples/default_demo/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/CMakeLists.txt new file mode 100644 index 00000000..6458baae --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS ../../) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(default_demo) diff --git a/dist/esp32-wifi-manager_0.0.1/examples/default_demo/Makefile b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/Makefile new file mode 100644 index 00000000..fb186183 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := wifi_manager + +EXTRA_COMPONENT_DIRS := ../../ + +include $(IDF_PATH)/make/project.mk + diff --git a/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/CMakeLists.txt new file mode 100644 index 00000000..31addfc4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/CMakeLists.txt @@ -0,0 +1,3 @@ +set(COMPONENT_SRCS "user_main.c") + +register_component() diff --git a/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/component.mk b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/component.mk new file mode 100644 index 00000000..a98f634e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/idf_component.yml b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true diff --git a/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/user_main.c b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/user_main.c new file mode 100644 index 00000000..19af535e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/main/user_main.c @@ -0,0 +1,80 @@ +/* +Copyright (c) 2017-2019 Tony Pottier + +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. + +@file main.c +@author Tony Pottier +@brief Entry point for the ESP32 application. +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" + +#include "wifi_manager.h" + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "main"; + +/** + * @brief RTOS task that periodically prints the heap memory available. + * @note Pure debug information, should not be ever started on production code! This is an example on how you can integrate your code with wifi-manager + */ +void monitoring_task(void *pvParameter) +{ + for(;;){ + ESP_LOGI(TAG, "free heap: %lu",esp_get_free_heap_size()); + vTaskDelay( pdMS_TO_TICKS(10000) ); + } +} + + +/** + * @brief this is an exemple of a callback that you can setup in your own app to get notified of wifi manager event. + */ +void cb_connection_ok(void *pvParameter){ + ip_event_got_ip_t* param = (ip_event_got_ip_t*)pvParameter; + + /* transform IP to human readable string */ + char str_ip[16]; + esp_ip4addr_ntoa(¶m->ip_info.ip, str_ip, IP4ADDR_STRLEN_MAX); + + ESP_LOGI(TAG, "I have a connection and my IP is %s!", str_ip); +} + +void app_main() +{ + /* start the wifi manager */ + wifi_manager_start(); + + /* register a callback as an example to how you can integrate your code with the wifi manager */ + wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok); +#if CONFIG_SOC_CPU_CORES_NUM > 1 + /* your code should go here. Here we simply create a task on core 2 that monitors free heap memory */ + xTaskCreatePinnedToCore(&monitoring_task, "monitoring_task", 2048, NULL, 1, NULL, 1); +#endif +} diff --git a/dist/esp32-wifi-manager_0.0.1/examples/default_demo/sdkconfig.defaults b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/sdkconfig.defaults new file mode 100644 index 00000000..f44c06f4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/default_demo/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LWIP_IPV6=y diff --git a/dist/esp32-wifi-manager_0.0.1/examples/http_hook/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/CMakeLists.txt new file mode 100644 index 00000000..377f86fe --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS ../../) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(http_hook) diff --git a/dist/esp32-wifi-manager_0.0.1/examples/http_hook/Makefile b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/Makefile new file mode 100644 index 00000000..fb186183 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := wifi_manager + +EXTRA_COMPONENT_DIRS := ../../ + +include $(IDF_PATH)/make/project.mk + diff --git a/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/CMakeLists.txt new file mode 100644 index 00000000..31addfc4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/CMakeLists.txt @@ -0,0 +1,3 @@ +set(COMPONENT_SRCS "user_main.c") + +register_component() diff --git a/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/component.mk b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/component.mk new file mode 100644 index 00000000..a98f634e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/idf_component.yml b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true diff --git a/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/user_main.c b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/user_main.c new file mode 100644 index 00000000..2446f269 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/main/user_main.c @@ -0,0 +1,76 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file user_main.c +@author Tony Pottier +@brief Entry point for the ESP32 application. +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" + +#include "wifi_manager.h" +#include "http_app.h" + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "main"; + + +static esp_err_t my_get_handler(httpd_req_t *req){ + + /* our custom page sits at /helloworld in this example */ + if(strcmp(req->uri, "/helloworld") == 0){ + + ESP_LOGI(TAG, "Serving page /helloworld"); + + const char* response = "

Hello World!

"; + + httpd_resp_set_status(req, "200 OK"); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, response, strlen(response)); + } + else{ + /* send a 404 otherwise */ + httpd_resp_send_404(req); + } + + return ESP_OK; +} + + +void app_main() +{ + /* start the wifi manager */ + wifi_manager_start(); + + /* set custom handler for the http server + * Now navigate to /helloworld to see the custom page + * */ + http_app_set_handler_hook(HTTP_GET, &my_get_handler); + +} diff --git a/dist/esp32-wifi-manager_0.0.1/examples/http_hook/sdkconfig.defaults b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/sdkconfig.defaults new file mode 100644 index 00000000..f44c06f4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/examples/http_hook/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LWIP_IPV6=y diff --git a/dist/esp32-wifi-manager_0.0.1/idf_component.yml b/dist/esp32-wifi-manager_0.0.1/idf_component.yml new file mode 100644 index 00000000..739f2805 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/idf_component.yml @@ -0,0 +1,5 @@ +branch: update_build_system +description: esp32-wifi-manager +license: MIT +url: https://github.com/ankayca/esp32-wifi-manager/tree/update_build_system +version: 0.0.1 diff --git a/dist/esp32-wifi-manager_0.0.1/src/ap.json b/dist/esp32-wifi-manager_0.0.1/src/ap.json new file mode 100644 index 00000000..de61f86a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/ap.json @@ -0,0 +1,12 @@ +[ +{"ssid":"Pantum-AP-A6D49F","chan":11,"rssi":-55,"auth":4}, +{"ssid":"a0308","chan":1,"rssi":-56,"auth":3}, +{"ssid":"dlink-D9D8","chan":11,"rssi":-82,"auth":4}, +{"ssid":"Linksys06730","chan":7,"rssi":-85,"auth":3}, +{"ssid":"SINGTEL-5171","chan":9,"rssi":-88,"auth":4}, +{"ssid":"1126-1","chan":11,"rssi":-89,"auth":4}, +{"ssid":"The Shah 5GHz-2","chan":1,"rssi":-90,"auth":3}, +{"ssid":"SINGTEL-1D28 (2G)","chan":11,"rssi":-91,"auth":3}, +{"ssid":"dlink-F864","chan":1,"rssi":-92,"auth":4}, +{"ssid":"dlink-74F0","chan":1,"rssi":-93,"auth":4} +] \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/code.js b/dist/esp32-wifi-manager_0.0.1/src/code.js new file mode 100644 index 00000000..934595a9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/code.js @@ -0,0 +1,352 @@ +// save some bytes +const gel = (e) => document.getElementById(e); + +const wifi_div = gel("wifi"); +const connect_div = gel("connect"); +const connect_manual_div = gel("connect_manual"); +const connect_wait_div = gel("connect-wait"); +const connect_details_div = gel("connect-details"); + +function docReady(fn) { + // see if DOM is already available + if ( + document.readyState === "complete" || + document.readyState === "interactive" + ) { + // call on next available tick + setTimeout(fn, 1); + } else { + document.addEventListener("DOMContentLoaded", fn); + } +} + +var selectedSSID = ""; +var refreshAPInterval = null; +var checkStatusInterval = null; + +function stopCheckStatusInterval() { + if (checkStatusInterval != null) { + clearInterval(checkStatusInterval); + checkStatusInterval = null; + } +} + +function stopRefreshAPInterval() { + if (refreshAPInterval != null) { + clearInterval(refreshAPInterval); + refreshAPInterval = null; + } +} + +function startCheckStatusInterval() { + checkStatusInterval = setInterval(checkStatus, 950); +} + +function startRefreshAPInterval() { + refreshAPInterval = setInterval(refreshAP, 3800); +} + +docReady(async function () { + gel("wifi-status").addEventListener( + "click", + () => { + wifi_div.style.display = "none"; + document.getElementById("connect-details").style.display = "block"; + }, + false + ); + + gel("manual_add").addEventListener( + "click", + (e) => { + selectedSSID = e.target.innerText; + + gel("ssid-pwd").textContent = selectedSSID; + wifi_div.style.display = "none"; + connect_manual_div.style.display = "block"; + connect_div.style.display = "none"; + + gel("connect-success").display = "none"; + gel("connect-fail").display = "none"; + }, + false + ); + + gel("wifi-list").addEventListener( + "click", + (e) => { + selectedSSID = e.target.innerText; + gel("ssid-pwd").textContent = selectedSSID; + connect_div.style.display = "block"; + wifi_div.style.display = "none"; + // init_cancel(); + }, + false + ); + + function cancel() { + selectedSSID = ""; + connect_div.style.display = "none"; + connect_manual_div.style.display = "none"; + wifi_div.style.display = "block"; + } + + gel("cancel").addEventListener("click", cancel, false); + + gel("manual_cancel").addEventListener("click", cancel, false); + + gel("join").addEventListener("click", performConnect, false); + + gel("manual_join").addEventListener( + "click", + (e) => { + performConnect("manual"); + }, + false + ); + + gel("ok-details").addEventListener( + "click", + () => { + connect_details_div.style.display = "none"; + wifi_div.style.display = "block"; + }, + false + ); + + gel("ok-credits").addEventListener( + "click", + () => { + gel("credits").style.display = "none"; + gel("app").style.display = "block"; + }, + false + ); + + gel("acredits").addEventListener( + "click", + () => { + event.preventDefault(); + gel("app").style.display = "none"; + gel("credits").style.display = "block"; + }, + false + ); + + gel("ok-connect").addEventListener( + "click", + () => { + connect_wait_div.style.display = "none"; + wifi_div.style.display = "block"; + }, + false + ); + + gel("disconnect").addEventListener( + "click", + () => { + gel("diag-disconnect").style.display = "block"; + gel("connect-details-wrap").classList.add("blur"); + }, + false + ); + + gel("no-disconnect").addEventListener( + "click", + () => { + gel("diag-disconnect").style.display = "none"; + gel("connect-details-wrap").classList.remove("blur"); + }, + false + ); + + gel("yes-disconnect").addEventListener("click", async () => { + stopCheckStatusInterval(); + selectedSSID = ""; + + document.getElementById("diag-disconnect").style.display = "none"; + gel("connect-details-wrap").classList.remove("blur"); + + await fetch("connect.json", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: { timestamp: Date.now() }, + }); + + startCheckStatusInterval(); + + connect_details_div.style.display = "none"; + wifi_div.style.display = "block"; + }); + + //first time the page loads: attempt get the connection status and start the wifi scan + await refreshAP(); + startCheckStatusInterval(); + startRefreshAPInterval(); +}); + +async function performConnect(conntype) { + //stop the status refresh. This prevents a race condition where a status + //request would be refreshed with wrong ip info from a previous connection + //and the request would automatically shows as succesful. + stopCheckStatusInterval(); + + //stop refreshing wifi list + stopRefreshAPInterval(); + + var pwd; + if (conntype == "manual") { + //Grab the manual SSID and PWD + selectedSSID = gel("manual_ssid").value; + pwd = gel("manual_pwd").value; + } else { + pwd = gel("pwd").value; + } + //reset connection + gel("loading").style.display = "block"; + gel("connect-success").style.display = "none"; + gel("connect-fail").style.display = "none"; + + gel("ok-connect").disabled = true; + gel("ssid-wait").textContent = selectedSSID; + connect_div.style.display = "none"; + connect_manual_div.style.display = "none"; + connect_wait_div.style.display = "block"; + + await fetch("connect.json", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Custom-ssid": selectedSSID, + "X-Custom-pwd": pwd, + }, + body: { timestamp: Date.now() }, + }); + + //now we can re-set the intervals regardless of result + startCheckStatusInterval(); + startRefreshAPInterval(); +} + +function rssiToIcon(rssi) { + if (rssi >= -60) { + return "w0"; + } else if (rssi >= -67) { + return "w1"; + } else if (rssi >= -75) { + return "w2"; + } else { + return "w3"; + } +} + +async function refreshAP(url = "ap.json") { + try { + var res = await fetch(url); + var access_points = await res.json(); + if (access_points.length > 0) { + //sort by signal strength + access_points.sort((a, b) => { + var x = a["rssi"]; + var y = b["rssi"]; + return x < y ? 1 : x > y ? -1 : 0; + }); + refreshAPHTML(access_points); + } + } catch (e) { + console.info("Access points returned empty from /ap.json!"); + } +} + +function refreshAPHTML(data) { + var h = ""; + data.forEach(function (e, idx, array) { + let ap_class = idx === array.length - 1 ? "" : " brdb"; + let rssicon = rssiToIcon(e.rssi); + let auth = e.auth == 0 ? "" : "pw"; + h += `
${e.ssid}
\n`; + }); + + gel("wifi-list").innerHTML = h; +} + +async function checkStatus(url = "status.json") { + try { + var response = await fetch(url); + var data = await response.json(); + if (data && data.hasOwnProperty("ssid") && data["ssid"] != "") { + if (data["ssid"] === selectedSSID) { + // Attempting connection + switch (data["urc"]) { + case 0: + console.info("Got connection!"); + document.querySelector( + "#connected-to div div div span" + ).textContent = data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = data["ip"]; + gel("netmask").textContent = data["netmask"]; + gel("gw").textContent = data["gw"]; + gel("wifi-status").style.display = "block"; + + //unlock the wait screen if needed + gel("ok-connect").disabled = false; + + //update wait screen + gel("loading").style.display = "none"; + gel("connect-success").style.display = "block"; + gel("connect-fail").style.display = "none"; + break; + case 1: + console.info("Connection attempt failed!"); + document.querySelector( + "#connected-to div div div span" + ).textContent = data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = "0.0.0.0"; + gel("netmask").textContent = "0.0.0.0"; + gel("gw").textContent = "0.0.0.0"; + + //don't show any connection + gel("wifi-status").display = "none"; + + //unlock the wait screen + gel("ok-connect").disabled = false; + + //update wait screen + gel("loading").display = "none"; + gel("connect-fail").style.display = "block"; + gel("connect-success").style.display = "none"; + break; + } + } else if (data.hasOwnProperty("urc") && data["urc"] === 0) { + console.info("Connection established"); + //ESP32 is already connected to a wifi without having the user do anything + if ( + gel("wifi-status").style.display == "" || + gel("wifi-status").style.display == "none" + ) { + document.querySelector("#connected-to div div div span").textContent = + data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = data["ip"]; + gel("netmask").textContent = data["netmask"]; + gel("gw").textContent = data["gw"]; + gel("wifi-status").style.display = "block"; + } + } + } else if (data.hasOwnProperty("urc") && data["urc"] === 2) { + console.log("Manual disconnect requested..."); + if (gel("wifi-status").style.display == "block") { + gel("wifi-status").style.display = "none"; + } + } + } catch (e) { + console.info("Was not able to fetch /status.json"); + } +} diff --git a/dist/esp32-wifi-manager_0.0.1/src/component.mk b/dist/esp32-wifi-manager_0.0.1/src/component.mk new file mode 100644 index 00000000..0b9d7585 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/dist/esp32-wifi-manager_0.0.1/src/compress.bat b/dist/esp32-wifi-manager_0.0.1/src/compress.bat new file mode 100644 index 00000000..b9caad22 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/compress.bat @@ -0,0 +1,2 @@ +gzip index.html style.css --best --keep --force +pause \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/connect b/dist/esp32-wifi-manager_0.0.1/src/connect new file mode 100644 index 00000000..8c7fe211 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/connect @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/dns_server.c b/dist/esp32-wifi-manager_0.0.1/src/dns_server.c new file mode 100644 index 00000000..6693913a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/dns_server.c @@ -0,0 +1,184 @@ +/* +Copyright (c) 2019 Tony Pottier + +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. + +@file dns_server.c +@author Tony Pottier +@brief Defines an extremely basic DNS server for captive portal functionality. +It's basically a DNS hijack that replies to the esp's address no matter which +request is sent to it. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wifi_manager.h" +#include "dns_server.h" + +static const char TAG[] = "dns_server"; +static TaskHandle_t task_dns_server = NULL; +int socket_fd; + +void dns_server_start() { + if(task_dns_server == NULL){ + xTaskCreate(&dns_server, "dns_server", 3072, NULL, WIFI_MANAGER_TASK_PRIORITY-1, &task_dns_server); + } +} + +void dns_server_stop(){ + if(task_dns_server){ + vTaskDelete(task_dns_server); + close(socket_fd); + task_dns_server = NULL; + } + +} + + + +void dns_server(void *pvParameters) { + + + + struct sockaddr_in ra; + + /* Set redirection DNS hijack to the access point IP */ + ip4_addr_t ip_resolved; + inet_pton(AF_INET, DEFAULT_AP_IP, &ip_resolved); + + + /* Create UDP socket */ + socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (socket_fd < 0){ + ESP_LOGE(TAG, "Failed to create socket"); + exit(0); + } + + /* Bind to port 53 (typical DNS Server port) */ + esp_netif_ip_info_t ip; + esp_netif_t* netif_sta = wifi_manager_get_esp_netif_sta(); + ESP_ERROR_CHECK(esp_netif_get_ip_info(netif_sta, &ip)); + ra.sin_family = AF_INET; + ra.sin_addr.s_addr = ip.ip.addr; + ra.sin_port = htons(53); + if (bind(socket_fd, (struct sockaddr *)&ra, sizeof(struct sockaddr_in)) == -1) { + ESP_LOGE(TAG, "Failed to bind to 53/udp"); + close(socket_fd); + exit(1); + } + + struct sockaddr_in client; + socklen_t client_len; + client_len = sizeof(client); + int length; + uint8_t data[DNS_QUERY_MAX_SIZE]; /* dns query buffer */ + uint8_t response[DNS_ANSWER_MAX_SIZE]; /* dns response buffer */ + char ip_address[INET_ADDRSTRLEN]; /* buffer to store IPs as text. This is only used for debug and serves no other purpose */ + char *domain; /* This is only used for debug and serves no other purpose */ + int err; + + ESP_LOGI(TAG, "DNS Server listening on 53/udp"); + + /* Start loop to process DNS requests */ + for(;;) { + + memset(data, 0x00, sizeof(data)); /* reset buffer */ + length = recvfrom(socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client, &client_len); /* read udp request */ + + /*if the query is bigger than the buffer size we simply ignore it. This case should only happen in case of multiple + * queries within the same DNS packet and is not supported by this simple DNS hijack. */ + if ( length > 0 && ((length + sizeof(dns_answer_t)-1) < DNS_ANSWER_MAX_SIZE) ) { + + data[length] = '\0'; /*in case there's a bogus domain name that isn't null terminated */ + + /* Generate header message */ + memcpy(response, data, sizeof(dns_header_t)); + dns_header_t *dns_header = (dns_header_t*)response; + dns_header->QR = 1; /*response bit */ + dns_header->OPCode = DNS_OPCODE_QUERY; /* no support for other type of response */ + dns_header->AA = 1; /*authoritative answer */ + dns_header->RCode = DNS_REPLY_CODE_NO_ERROR; /* no error */ + dns_header->TC = 0; /*no truncation */ + dns_header->RD = 0; /*no recursion */ + dns_header->ANCount = dns_header->QDCount; /* set answer count = question count -- duhh! */ + dns_header->NSCount = 0x0000; /* name server resource records = 0 */ + dns_header->ARCount = 0x0000; /* resource records = 0 */ + + + /* copy the rest of the query in the response */ + memcpy(response + sizeof(dns_header_t), data + sizeof(dns_header_t), length - sizeof(dns_header_t)); + + + /* extract domain name and request IP for debug */ + inet_ntop(AF_INET, &(client.sin_addr), ip_address, INET_ADDRSTRLEN); + domain = (char*) &data[sizeof(dns_header_t) + 1]; + for(char* c=domain; *c != '\0'; c++){ + if(*c < ' ' || *c > 'z') *c = '.'; /* technically we should test if the first two bits are 00 (e.g. if( (*c & 0xC0) == 0x00) *c = '.') but this makes the code a lot more readable */ + } + ESP_LOGI(TAG, "Replying to DNS request for %s from %s", domain, ip_address); + + + /* create DNS answer at the end of the query*/ + dns_answer_t *dns_answer = (dns_answer_t*)&response[length]; + dns_answer->NAME = __bswap_16(0xC00C); /* This is a pointer to the beginning of the question. As per DNS standard, first two bits must be set to 11 for some odd reason hence 0xC0 */ + dns_answer->TYPE = __bswap_16(DNS_ANSWER_TYPE_A); + dns_answer->CLASS = __bswap_16(DNS_ANSWER_CLASS_IN); + dns_answer->TTL = (uint32_t)0x00000000; /* no caching. Avoids DNS poisoning since this is a DNS hijack */ + dns_answer->RDLENGTH = __bswap_16(0x0004); /* 4 byte => size of an ipv4 address */ + dns_answer->RDATA = ip_resolved.addr; + + err = sendto(socket_fd, response, length+sizeof(dns_answer_t), 0, (struct sockaddr *)&client, client_len); + if (err < 0) { + ESP_LOGE(TAG, "UDP sendto failed: %d", err); + } + } + + taskYIELD(); /* allows the freeRTOS scheduler to take over if needed. DNS daemon should not be taxing on the system */ + + } + close(socket_fd); + + vTaskDelete ( NULL ); +} + + + + diff --git a/dist/esp32-wifi-manager_0.0.1/src/dns_server.h b/dist/esp32-wifi-manager_0.0.1/src/dns_server.h new file mode 100644 index 00000000..d1da1f8c --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/dns_server.h @@ -0,0 +1,137 @@ +/* +Copyright (c) 2019 Tony Pottier + +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. + +@file dns_server.h +@author Tony Pottier +@brief Defines an extremly basic DNS server for captive portal functionality. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +@see http://www.zytrax.com/books/dns/ch15 +*/ + +#ifndef MAIN_DNS_SERVER_H_ +#define MAIN_DNS_SERVER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + + +/** 12 byte header, 64 byte domain name, 4 byte qtype/qclass. This NOT compliant with the RFC, but it's good enough for a captive portal + * if a DNS query is too big it just wont be processed. */ +#define DNS_QUERY_MAX_SIZE 80 + +/** Query + 2 byte ptr, 2 byte type, 2 byte class, 4 byte TTL, 2 byte len, 4 byte data */ +#define DNS_ANSWER_MAX_SIZE (DNS_QUERY_MAX_SIZE+16) + + +/** + * @brief RCODE values used in a DNS header message + */ +typedef enum dns_reply_code_t { + DNS_REPLY_CODE_NO_ERROR = 0, + DNS_REPLY_CODE_FORM_ERROR = 1, + DNS_REPLY_CODE_SERVER_FAILURE = 2, + DNS_REPLY_CODE_NON_EXISTANT_DOMAIN = 3, + DNS_REPLY_CODE_NOT_IMPLEMENTED = 4, + DNS_REPLY_CODE_REFUSED = 5, + DNS_REPLY_CODE_YXDOMAIN = 6, + DNS_REPLY_CODE_YXRRSET = 7, + DNS_REPLY_CODE_NXRRSET = 8 +}dns_reply_code_t; + + + +/** + * @brief OPCODE values used in a DNS header message + */ +typedef enum dns_opcode_code_t { + DNS_OPCODE_QUERY = 0, + DNS_OPCODE_IQUERY = 1, + DNS_OPCODE_STATUS = 2 +}dns_opcode_code_t; + + + +/** + * @brief Represents a 12 byte DNS header. + * __packed__ is needed to prevent potential unwanted memory alignments + */ +typedef struct __attribute__((__packed__)) dns_header_t{ + uint16_t ID; // identification number + uint8_t RD : 1; // recursion desired + uint8_t TC : 1; // truncated message + uint8_t AA : 1; // authoritive answer + uint8_t OPCode : 4; // message_type + uint8_t QR : 1; // query/response flag + uint8_t RCode : 4; // response code + uint8_t Z : 3; // its z! reserved + uint8_t RA : 1; // recursion available + uint16_t QDCount; // number of question entries + uint16_t ANCount; // number of answer entries + uint16_t NSCount; // number of authority entries + uint16_t ARCount; // number of resource entries +}dns_header_t; + + + +typedef enum dns_answer_type_t { + DNS_ANSWER_TYPE_A = 1, + DNS_ANSWER_TYPE_NS = 2, + DNS_ANSWER_TYPE_CNAME = 5, + DNS_ANSWER_TYPE_SOA = 6, + DNS_ANSWER_TYPE_WKS = 11, + DNS_ANSWER_TYPE_PTR = 12, + DNS_ANSWER_TYPE_MX = 15, + DNS_ANSWER_TYPE_SRV = 33, + DNS_ANSWER_TYPE_AAAA = 28 +}dns_answer_type_t; + +typedef enum dns_answer_class_t { + DNS_ANSWER_CLASS_IN = 1 +}dns_answer_class_t; + + + +typedef struct __attribute__((__packed__)) dns_answer_t{ + uint16_t NAME; /* for the sake of simplicity only 16 bit pointers are supported */ + uint16_t TYPE; /* Unsigned 16 bit value. The resource record types - determines the content of the RDATA field. */ + uint16_t CLASS; /* Class of response. */ + uint32_t TTL; /* The time in seconds that the record may be cached. A value of 0 indicates the record should not be cached. */ + uint16_t RDLENGTH; /* Unsigned 16-bit value that defines the length in bytes of the RDATA record. */ + uint32_t RDATA; /* For the sake of simplicity only ipv4 is supported, and as such it's a unsigned 32 bit */ +}dns_answer_t; + +void dns_server(void *pvParameters); +void dns_server_start(); +void dns_server_stop(); + + + +#ifdef __cplusplus +} +#endif + + +#endif /* MAIN_DNS_SERVER_H_ */ diff --git a/dist/esp32-wifi-manager_0.0.1/src/http_app.c b/dist/esp32-wifi-manager_0.0.1/src/http_app.c new file mode 100644 index 00000000..8dbc40ef --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/http_app.c @@ -0,0 +1,479 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file http_app.c +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include "esp_netif.h" +#include + +#include "wifi_manager.h" +#include "http_app.h" + + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "http_server"; + +/* @brief the HTTP server handle */ +static httpd_handle_t httpd_handle = NULL; + +/* function pointers to URI handlers that can be user made */ +esp_err_t (*custom_get_httpd_uri_handler)(httpd_req_t *r) = NULL; +esp_err_t (*custom_post_httpd_uri_handler)(httpd_req_t *r) = NULL; + +/* strings holding the URLs of the wifi manager */ +static char* http_root_url = NULL; +static char* http_redirect_url = NULL; +static char* http_js_url = NULL; +static char* http_css_url = NULL; +static char* http_connect_url = NULL; +static char* http_ap_url = NULL; +static char* http_status_url = NULL; + +/** + * @brief embedded binary data. + * @see file "component.mk" + * @see https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html#embedding-binary-data + */ +extern const uint8_t style_css_start[] asm("_binary_style_css_start"); +extern const uint8_t style_css_end[] asm("_binary_style_css_end"); +extern const uint8_t code_js_start[] asm("_binary_code_js_start"); +extern const uint8_t code_js_end[] asm("_binary_code_js_end"); +extern const uint8_t index_html_start[] asm("_binary_index_html_start"); +extern const uint8_t index_html_end[] asm("_binary_index_html_end"); + + +/* const httpd related values stored in ROM */ +const static char http_200_hdr[] = "200 OK"; +const static char http_302_hdr[] = "302 Found"; +const static char http_400_hdr[] = "400 Bad Request"; +const static char http_404_hdr[] = "404 Not Found"; +const static char http_503_hdr[] = "503 Service Unavailable"; +const static char http_location_hdr[] = "Location"; +const static char http_content_type_html[] = "text/html"; +const static char http_content_type_js[] = "text/javascript"; +const static char http_content_type_css[] = "text/css"; +const static char http_content_type_json[] = "application/json"; +const static char http_cache_control_hdr[] = "Cache-Control"; +const static char http_cache_control_no_cache[] = "no-store, no-cache, must-revalidate, max-age=0"; +const static char http_cache_control_cache[] = "public, max-age=31536000"; +const static char http_pragma_hdr[] = "Pragma"; +const static char http_pragma_no_cache[] = "no-cache"; + + + +esp_err_t http_app_set_handler_hook( httpd_method_t method, esp_err_t (*handler)(httpd_req_t *r) ){ + + if(method == HTTP_GET){ + custom_get_httpd_uri_handler = handler; + return ESP_OK; + } + else if(method == HTTP_POST){ + custom_post_httpd_uri_handler = handler; + return ESP_OK; + } + else{ + return ESP_ERR_INVALID_ARG; + } + +} + + +static esp_err_t http_server_delete_handler(httpd_req_t *req){ + + ESP_LOGI(TAG, "DELETE %s", req->uri); + + /* DELETE /connect.json */ + if(strcmp(req->uri, http_connect_url) == 0){ + wifi_manager_disconnect_async(); + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, NULL, 0); + } + else{ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + + return ESP_OK; +} + + +static esp_err_t http_server_post_handler(httpd_req_t *req){ + + + esp_err_t ret = ESP_OK; + + ESP_LOGI(TAG, "POST %s", req->uri); + + /* POST /connect.json */ + if(strcmp(req->uri, http_connect_url) == 0){ + + + /* buffers for the headers */ + size_t ssid_len = 0, password_len = 0; + char *ssid = NULL, *password = NULL; + + /* len of values provided */ + ssid_len = httpd_req_get_hdr_value_len(req, "X-Custom-ssid"); + password_len = httpd_req_get_hdr_value_len(req, "X-Custom-pwd"); + + + if(ssid_len && ssid_len <= MAX_SSID_SIZE && password_len && password_len <= MAX_PASSWORD_SIZE){ + + /* get the actual value of the headers */ + ssid = malloc(sizeof(char) * (ssid_len + 1)); + password = malloc(sizeof(char) * (password_len + 1)); + httpd_req_get_hdr_value_str(req, "X-Custom-ssid", ssid, ssid_len+1); + httpd_req_get_hdr_value_str(req, "X-Custom-pwd", password, password_len+1); + + wifi_config_t* config = wifi_manager_get_wifi_sta_config(); + memset(config, 0x00, sizeof(wifi_config_t)); + memcpy(config->sta.ssid, ssid, ssid_len); + memcpy(config->sta.password, password, password_len); + ESP_LOGI(TAG, "ssid: %s, password: %s", ssid, password); + ESP_LOGD(TAG, "http_server_post_handler: wifi_manager_connect_async() call"); + wifi_manager_connect_async(); + + /* free memory */ + free(ssid); + free(password); + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, NULL, 0); + + } + else{ + /* bad request the authentification header is not complete/not the correct format */ + httpd_resp_set_status(req, http_400_hdr); + httpd_resp_send(req, NULL, 0); + } + + } + else{ + + if(custom_post_httpd_uri_handler == NULL){ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + else{ + + /* if there's a hook, run it */ + ret = (*custom_post_httpd_uri_handler)(req); + } + } + + return ret; +} + + +static esp_err_t http_server_get_handler(httpd_req_t *req){ + + char* host = NULL; + size_t buf_len; + esp_err_t ret = ESP_OK; + + ESP_LOGD(TAG, "GET %s", req->uri); + + /* Get header value string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1; + if (buf_len > 1) { + host = malloc(buf_len); + if(httpd_req_get_hdr_value_str(req, "Host", host, buf_len) != ESP_OK){ + /* if something is wrong we just 0 the whole memory */ + memset(host, 0x00, buf_len); + } + } + + /* determine if Host is from the STA IP address */ + wifi_manager_lock_sta_ip_string(portMAX_DELAY); + bool access_from_sta_ip = host != NULL?strstr(host, wifi_manager_get_sta_ip_string()):false; + wifi_manager_unlock_sta_ip_string(); + + + if (host != NULL && !strstr(host, DEFAULT_AP_IP) && !access_from_sta_ip) { + + /* Captive Portal functionality */ + /* 302 Redirect to IP of the access point */ + httpd_resp_set_status(req, http_302_hdr); + httpd_resp_set_hdr(req, http_location_hdr, http_redirect_url); + httpd_resp_send(req, NULL, 0); + + } + else{ + + /* GET / */ + if(strcmp(req->uri, http_root_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_html); + httpd_resp_send(req, (char*)index_html_start, index_html_end - index_html_start); + } + /* GET /code.js */ + else if(strcmp(req->uri, http_js_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_js); + httpd_resp_send(req, (char*)code_js_start, code_js_end - code_js_start); + } + /* GET /style.css */ + else if(strcmp(req->uri, http_css_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_css); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_cache); + httpd_resp_send(req, (char*)style_css_start, style_css_end - style_css_start); + } + /* GET /ap.json */ + else if(strcmp(req->uri, http_ap_url) == 0){ + + /* if we can get the mutex, write the last version of the AP list */ + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + char* ap_buf = wifi_manager_get_ap_list_json(); + httpd_resp_send(req, ap_buf, strlen(ap_buf)); + wifi_manager_unlock_json_buffer(); + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /ap.json failed to obtain mutex"); + } + + /* request a wifi scan */ + wifi_manager_scan_async(); + } + /* GET /status.json */ + else if(strcmp(req->uri, http_status_url) == 0){ + + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + char *buff = wifi_manager_get_ip_info_json(); + if(buff){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, buff, strlen(buff)); + wifi_manager_unlock_json_buffer(); + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + } + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /status.json failed to obtain mutex"); + } + } + else{ + + if(custom_get_httpd_uri_handler == NULL){ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + else{ + + /* if there's a hook, run it */ + ret = (*custom_get_httpd_uri_handler)(req); + } + } + + } + + /* memory clean up */ + if(host != NULL){ + free(host); + } + + return ret; + +} + +/* URI wild card for any GET request */ +static const httpd_uri_t http_server_get_request = { + .uri = "*", + .method = HTTP_GET, + .handler = http_server_get_handler +}; + +static const httpd_uri_t http_server_post_request = { + .uri = "*", + .method = HTTP_POST, + .handler = http_server_post_handler +}; + +static const httpd_uri_t http_server_delete_request = { + .uri = "*", + .method = HTTP_DELETE, + .handler = http_server_delete_handler +}; + + +void http_app_stop(){ + + if(httpd_handle != NULL){ + + + /* dealloc URLs */ + if(http_root_url) { + free(http_root_url); + http_root_url = NULL; + } + if(http_redirect_url){ + free(http_redirect_url); + http_redirect_url = NULL; + } + if(http_js_url){ + free(http_js_url); + http_js_url = NULL; + } + if(http_css_url){ + free(http_css_url); + http_css_url = NULL; + } + if(http_connect_url){ + free(http_connect_url); + http_connect_url = NULL; + } + if(http_ap_url){ + free(http_ap_url); + http_ap_url = NULL; + } + if(http_status_url){ + free(http_status_url); + http_status_url = NULL; + } + + /* stop server */ + httpd_stop(httpd_handle); + httpd_handle = NULL; + } +} + + +/** + * @brief helper to generate URLs of the wifi manager + */ +static char* http_app_generate_url(const char* page){ + + char* ret; + + int root_len = strlen(WEBAPP_LOCATION); + const size_t url_sz = sizeof(char) * ( (root_len+1) + ( strlen(page) + 1) ); + + ret = malloc(url_sz); + memset(ret, 0x00, url_sz); + strcpy(ret, WEBAPP_LOCATION); + ret = strcat(ret, page); + + return ret; +} + +void http_app_start(bool lru_purge_enable){ + + esp_err_t err; + + if(httpd_handle == NULL){ + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + /* this is an important option that isn't set up by default. + * We could register all URLs one by one, but this would not work while the fake DNS is active */ + config.uri_match_fn = httpd_uri_match_wildcard; + config.lru_purge_enable = lru_purge_enable; + + /* generate the URLs */ + if(http_root_url == NULL){ + int root_len = strlen(WEBAPP_LOCATION); + + /* all the pages */ + const char page_js[] = "code.js"; + const char page_css[] = "style.css"; + const char page_connect[] = "connect.json"; + const char page_ap[] = "ap.json"; + const char page_status[] = "status.json"; + + /* root url, eg "/" */ + const size_t http_root_url_sz = sizeof(char) * (root_len+1); + http_root_url = malloc(http_root_url_sz); + memset(http_root_url, 0x00, http_root_url_sz); + strcpy(http_root_url, WEBAPP_LOCATION); + + /* redirect url */ + size_t redirect_sz = 22 + root_len + 1; /* strlen(http://255.255.255.255) + strlen("/") + 1 for \0 */ + http_redirect_url = malloc(sizeof(char) * redirect_sz); + *http_redirect_url = '\0'; + + if(root_len == 1){ + snprintf(http_redirect_url, redirect_sz, "http://%s", DEFAULT_AP_IP); + } + else{ + snprintf(http_redirect_url, redirect_sz, "http://%s%s", DEFAULT_AP_IP, WEBAPP_LOCATION); + } + + /* generate the other pages URLs*/ + http_js_url = http_app_generate_url(page_js); + http_css_url = http_app_generate_url(page_css); + http_connect_url = http_app_generate_url(page_connect); + http_ap_url = http_app_generate_url(page_ap); + http_status_url = http_app_generate_url(page_status); + + } + + err = httpd_start(&httpd_handle, &config); + + if (err == ESP_OK) { + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(httpd_handle, &http_server_get_request); + httpd_register_uri_handler(httpd_handle, &http_server_post_request); + httpd_register_uri_handler(httpd_handle, &http_server_delete_request); + } + } + +} diff --git a/dist/esp32-wifi-manager_0.0.1/src/http_app.h b/dist/esp32-wifi-manager_0.0.1/src/http_app.h new file mode 100644 index 00000000..53f9472a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/http_app.h @@ -0,0 +1,73 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file http_app.h +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#ifndef HTTP_APP_H_INCLUDED +#define HTTP_APP_H_INCLUDED + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/** @brief Defines the URL where the wifi manager is located + * By default it is at the server root (ie "/"). If you wish to add your own webpages + * you may want to relocate the wifi manager to another URL, for instance /wifimanager + */ +#define WEBAPP_LOCATION CONFIG_WEBAPP_LOCATION + + +/** + * @brief spawns the http server + */ +void http_app_start(bool lru_purge_enable); + +/** + * @brief stops the http server + */ +void http_app_stop(); + +/** + * @brief sets a hook into the wifi manager URI handlers. Setting the handler to NULL disables the hook. + * @return ESP_OK in case of success, ESP_ERR_INVALID_ARG if the method is unsupported. + */ +esp_err_t http_app_set_handler_hook( httpd_method_t method, esp_err_t (*handler)(httpd_req_t *r) ); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/dist/esp32-wifi-manager_0.0.1/src/index.html b/dist/esp32-wifi-manager_0.0.1/src/index.html new file mode 100644 index 00000000..ce4bf146 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/index.html @@ -0,0 +1,136 @@ + + + + + + + + + esp32-wifi-manager + + +
+
+
+

About this app...

+
+

+
+

esp32-wifi-manager, © 2017-2020, Tony Pottier
Licended under the MIT License.

+

+ This app would not be possible without the following libraries: +

+
    +
  • SpinKit, © 2015, Tobias Ahlin. Licensed under the MIT License.
  • +
  • jQuery, The jQuery Foundation. Licensed under the MIT License.
  • +
  • cJSON, © 2009-2017, Dave Gamble and cJSON contributors. Licensed under the MIT License.
  • +
+
+
+ +
+
+ + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/json.c b/dist/esp32-wifi-manager_0.0.1/src/json.c new file mode 100644 index 00000000..d448711a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/json.c @@ -0,0 +1,143 @@ +/* +@file json.c +@brief handles very basic JSON with a minimal footprint on the system + +This code is a lightly modified version of cJSON 1.4.7. cJSON is licensed under the MIT license: +Copyright (c) 2009 Dave Gamble + +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. + +@see https://github.com/DaveGamble/cJSON +*/ + +#include +#include +#include +#include +#include "json.h" + + +bool json_print_string(const unsigned char *input, unsigned char *output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + //output = ensure(output_buffer, sizeof("\"\""), hooks); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + if (strchr("\"\\\b\f\n\r\t", *input_pointer)) + { + /* one character escape sequence */ + escape_characters++; + } + else if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + /* in the original cJSON it is possible to realloc here in case output buffer is too small. + * This is overkill for an embedded system. */ + output = output_buffer; + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + diff --git a/dist/esp32-wifi-manager_0.0.1/src/json.h b/dist/esp32-wifi-manager_0.0.1/src/json.h new file mode 100644 index 00000000..4c1c8c75 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/json.h @@ -0,0 +1,47 @@ +/* +@file json.h +@brief handles very basic JSON with a minimal footprint on the system + +This code is a lightly modified version of cJSON 1.4.7. cJSON is licensed under the MIT license: +Copyright (c) 2009 Dave Gamble + +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. + +@see https://github.com/DaveGamble/cJSON +*/ + +#ifndef JSON_H_INCLUDED +#define JSON_H_INCLUDED + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Render the cstring provided to a JSON escaped version that can be printed. + * @param input the input buffer to be escaped. + * @param output_buffer the output buffer to write to. You must ensure it is big enough to contain the final string. + * @see cJSON equivlaent static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) + */ +bool json_print_string(const unsigned char *input, unsigned char *output_buffer); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_H_INCLUDED */ diff --git a/dist/esp32-wifi-manager_0.0.1/src/lock.svg b/dist/esp32-wifi-manager_0.0.1/src/lock.svg new file mode 100644 index 00000000..f7ebc7e9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/lock.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/nvs_sync.c b/dist/esp32-wifi-manager_0.0.1/src/nvs_sync.c new file mode 100644 index 00000000..4ceed8bd --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/nvs_sync.c @@ -0,0 +1,79 @@ +/** +Copyright (c) 2020 Tony Pottier + +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. + +@file nvs_sync.c +@author Tony Pottier +@brief Exposes a simple API to synchronize NVS memory read and writes + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include "nvs_sync.h" + + +static SemaphoreHandle_t nvs_sync_mutex = NULL; + +esp_err_t nvs_sync_create(){ + if(nvs_sync_mutex == NULL){ + + nvs_sync_mutex = xSemaphoreCreateMutex(); + + if(nvs_sync_mutex){ + return ESP_OK; + } + else{ + return ESP_FAIL; + } + } + else{ + return ESP_OK; + } +} + +void nvs_sync_free(){ + if(nvs_sync_mutex != NULL){ + vSemaphoreDelete( nvs_sync_mutex ); + nvs_sync_mutex = NULL; + } +} + +bool nvs_sync_lock(TickType_t xTicksToWait){ + if(nvs_sync_mutex){ + if( xSemaphoreTake( nvs_sync_mutex, xTicksToWait ) == pdTRUE ) { + return true; + } + else{ + return false; + } + } + else{ + return false; + } +} + +void nvs_sync_unlock(){ + xSemaphoreGive( nvs_sync_mutex ); +} \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/nvs_sync.h b/dist/esp32-wifi-manager_0.0.1/src/nvs_sync.h new file mode 100644 index 00000000..e03396e5 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/nvs_sync.h @@ -0,0 +1,76 @@ +/** +Copyright (c) 2020 Tony Pottier + +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. + +@file nvs_sync.h +@author Tony Pottier +@brief Exposes a simple API to synchronize NVS memory read and writes + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + + + +#ifndef WIFI_MANAGER_NVS_SYNC_H_INCLUDED +#define WIFI_MANAGER_NVS_SYNC_H_INCLUDED + +#include /* for type bool */ +#include /* for TickType_t */ +#include /* for esp_err_t */ + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Attempts to get hold of the NVS semaphore for a set amount of ticks. + * @note If you are uncertain about the number of ticks to wait use portMAX_DELAY. + * @return true on a succesful lock, false otherwise + */ +bool nvs_sync_lock(TickType_t xTicksToWait); + + +/** + * @brief Releases the NVS semaphore + */ +void nvs_sync_unlock(); + + +/** + * @brief Create the NVS semaphore + * @return ESP_OK: success or if the semaphore already exists + * ESP_FAIL: failure + */ +esp_err_t nvs_sync_create(); + +/** + * @brief Frees memory associated with the NVS semaphore + * @warning Do not delete a semaphore that has tasks blocked on it (tasks that are in the Blocked state waiting for the semaphore to become available). + */ +void nvs_sync_free(); + + +#ifdef __cplusplus +} +#endif + +#endif /* WIFI_MANAGER_NVS_SYNC_H_INCLUDED */ \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/settings.svg b/dist/esp32-wifi-manager_0.0.1/src/settings.svg new file mode 100644 index 00000000..19cacede --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/settings.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/status b/dist/esp32-wifi-manager_0.0.1/src/status new file mode 100644 index 00000000..3824a535 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/status @@ -0,0 +1 @@ +{"ssid":"zodmgbbq","ip":"192.168.1.119","netmask":"255.255.255.0","gw":"192.168.1.1","urc":0} \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/style.css b/dist/esp32-wifi-manager_0.0.1/src/style.css new file mode 100644 index 00000000..5c83cc2f --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/style.css @@ -0,0 +1,248 @@ +body { + background-color: #eee; + border: 0; + margin: 0; + font: 1.1em tahoma, arial, sans-serif; +} +a { + color: darkblue; + transition: color .2s ease-out; + text-decoration: none +} +a:hover { + color: red; +} +input { + display: none; + font: 1.1em tahoma, arial, sans-serif; +} +input:focus, +select:focus, +textarea:focus, +button:focus { + outline: none; +} +input[type="button"] { + width: 100px; + padding: 5px; + text-align: center; + display: block; +} +p { + padding: 10px; +} +#credits { + display: none; +} +#app {} #app-wrap {} #disconnect { + width: 150px; +} +.diag-box { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + height: 100%; + width: 100%; + display: none; +} +.diag-box-win { + position: absolute; + left: 10%; + width: 80%; + text-align: center; + border: 2px outset #888; + background-color: #fff; + border-radius: 10px; + top: 20%; +} +.blur { + -webkit-filter: blur(2px); + -moz-filter: blur(2px); + -ms-filter: blur(2px); + -o-filter: blur(2px); + filter: blur(2px); +} +.ape { + margin-left: 20px; + padding: 10px 0px 10px 10px; +} +.ape:hover { + cursor: pointer; +} +.brdb { + border-bottom: 1px solid #888; +} +header { + background-color: #fff; + border-bottom: 1px solid #888; +} +section { + background-color: #fff; + border-bottom: 1px solid #888; + border-top: 1px solid #888; +} +h1 { + display: block; + text-align: center; + margin: 0; + padding: 15px; + font-size: 1.4em +} +h2 { + margin: 0; + margin-top: 20px; + padding: 10px; + text-transform: uppercase; + color: #888; + font-size: 1.0em +} +h3 { + margin: 0; + text-align: center; + padding: 20px 0px 20px 0px; +} +.gr { + color: green; +} +.rd { + color: red; +} +#wifi-status { + display: none; +} +#connect { + display: none; +} +#connect_manual { + display: none; +} +#manual_ssid { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +#manual_pwd { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +#pwd { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +.buttons { + padding: 15px; +} +#join { + float: right; +} +#manual_join { + float: right; +} +#yes-disconnect { + display: inline-block; + margin-left: 20px; +} +#no-disconnect { + display: inline-block; +} +.ctr { + margin: 0 auto; +} +.tctr { + text-align: center; +} +#connect-wait { + display: none; +} +#connect-success { + display: none; +} +#connect-fail { + display: none; +} +#connect-details { + display: none; +} +.fr { + float: right; + margin-right: 20px; +} +.w0 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPHBhdGggZD0iTTUsMTNMNywxNUM5Ljc2LDEyLjI0IDE0LjI0LDEyLjI0IDE3LDE1TDE5LDEzQzE1LjE0LDkuMTQgOC44Nyw5LjE0IDUsMTNaIiBzdHlsZT0iZmlsbDogYmxhY2s7Ii8+CjxwYXRoIGQ9Ik05LDE3TDEyLDIwTDE1LDE3QzEzLjM1LDE1LjM0IDEwLjY2LDE1LjM0IDksMTdaIiBzdHlsZT0iZmlsbDogYmxhY2s7Ii8+Cjwvc3ZnPgo=') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w1 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPHBhdGggZD0iTTksMTdMMTIsMjBMMTUsMTdDMTMuMzUsMTUuMzQgMTAuNjYsMTUuMzQgOSwxN1oiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPC9zdmc+Cg==') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w2 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNOSwxN0wxMiwyMEwxNSwxN0MxMy4zNSwxNS4zNCAxMC42NiwxNS4zNCA5LDE3WiIgc3R5bGU9ImZpbGw6IGJsYWNrOyIvPgo8L3N2Zz4K') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w3 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNOSwxN0wxMiwyMEwxNSwxN0MxMy4zNSwxNS4zNCAxMC42NiwxNS4zNCA5LDE3WiIgc3R5bGU9ImZpbGw6IGdyYXk7Ii8+Cjwvc3ZnPgo=') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.pw { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIHN0eWxlPSJmaWxsOiBibGFjazsiIGQ9Ik0xOCA4aC0xVjZjMC0yLjc2LTIuMjQtNS01LTVTNyAzLjI0IDcgNnYySDZjLTEuMSAwLTIgLjktMiAydjEwYzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJWMTBjMC0xLjEtLjktMi0yLTJ6bS02IDljLTEuMSAwLTItLjktMi0ycy45LTIgMi0yIDIgLjkgMiAyLS45IDItMiAyem0zLjEtOUg4LjlWNmMwLTEuNzEgMS4zOS0zLjEgMy4xLTMuMSAxLjcxIDAgMy4xIDEuMzkgMy4xIDMuMXYyeiI+PC9wYXRoPgo8L3N2Zz4=') no-repeat right top; + height: 24px; + margin-right: 30px; +} +/* SpinKit is licensed under the MIT License. Copyright (c) 2015 Tobias Ahlin */ + +.spinner { + width: 40px; + height: 40px; + position: relative; + margin: 100px auto; +} +.double-bounce1, +.double-bounce2 { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #333; + opacity: 0.6; + position: absolute; + top: 0; + left: 0; + -webkit-animation: sk-bounce 2.0s infinite ease-in-out; + animation: sk-bounce 2.0s infinite ease-in-out; +} +.double-bounce2 { + -webkit-animation-delay: -1.0s; + animation-delay: -1.0s; +} +@-webkit-keyframes sk-bounce { + 0%, 100% { + -webkit-transform: scale(0.0) + } + 50% { + -webkit-transform: scale(1.0) + } +} +@keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + -webkit-transform: scale(0.0); + } + 50% { + transform: scale(1.0); + -webkit-transform: scale(1.0); + } +} +/* end of SpinKit */ \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.1/src/wifi0.svg b/dist/esp32-wifi-manager_0.0.1/src/wifi0.svg new file mode 100644 index 00000000..394a6495 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/wifi0.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.1/src/wifi1.svg b/dist/esp32-wifi-manager_0.0.1/src/wifi1.svg new file mode 100644 index 00000000..c64211bf --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/wifi1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.1/src/wifi2.svg b/dist/esp32-wifi-manager_0.0.1/src/wifi2.svg new file mode 100644 index 00000000..033b9de9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/wifi2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.1/src/wifi3.svg b/dist/esp32-wifi-manager_0.0.1/src/wifi3.svg new file mode 100644 index 00000000..7cd5667e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/wifi3.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.1/src/wifi_manager.c b/dist/esp32-wifi-manager_0.0.1/src/wifi_manager.c new file mode 100644 index 00000000..3d11a940 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/wifi_manager.c @@ -0,0 +1,1336 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file wifi_manager.c +@author Tony Pottier +@brief Defines all functions necessary for esp32 to connect to a wifi/scan wifis + +Contains the freeRTOS task and all necessary support + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include +#include "esp_system.h" +#include +#include +#include +#include +#include +#include +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_wifi_types.h" +#include "esp_log.h" +#include "nvs.h" +#include "nvs_flash.h" +#include "mdns.h" +#include "lwip/api.h" +#include "lwip/err.h" +#include "lwip/netdb.h" +#include "lwip/ip4_addr.h" + + +#include "json.h" +#include "dns_server.h" +#include "nvs_sync.h" +#include "wifi_manager.h" + + + +/* objects used to manipulate the main queue of events */ +QueueHandle_t wifi_manager_queue; + +/* @brief software timer to wait between each connection retry. + * There is no point hogging a hardware timer for a functionality like this which only needs to be 'accurate enough' */ +TimerHandle_t wifi_manager_retry_timer = NULL; + +/* @brief software timer that will trigger shutdown of the AP after a succesful STA connection + * There is no point hogging a hardware timer for a functionality like this which only needs to be 'accurate enough' */ +TimerHandle_t wifi_manager_shutdown_ap_timer = NULL; + +SemaphoreHandle_t wifi_manager_json_mutex = NULL; +SemaphoreHandle_t wifi_manager_sta_ip_mutex = NULL; +char *wifi_manager_sta_ip = NULL; +uint16_t ap_num = MAX_AP_NUM; +wifi_ap_record_t *accessp_records; +char *accessp_json = NULL; +char *ip_info_json = NULL; +wifi_config_t* wifi_manager_config_sta = NULL; + +/* @brief Array of callback function pointers */ +void (**cb_ptr_arr)(void*) = NULL; + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "wifi_manager"; + +/* @brief task handle for the main wifi_manager task */ +static TaskHandle_t task_wifi_manager = NULL; + +/* @brief netif object for the STATION */ +static esp_netif_t* esp_netif_sta = NULL; + +/* @brief netif object for the ACCESS POINT */ +static esp_netif_t* esp_netif_ap = NULL; + +/** + * The actual WiFi settings in use + */ +struct wifi_settings_t wifi_settings = { + .ap_ssid = DEFAULT_AP_SSID, + .ap_pwd = DEFAULT_AP_PASSWORD, + .ap_channel = DEFAULT_AP_CHANNEL, + .ap_ssid_hidden = DEFAULT_AP_SSID_HIDDEN, + .ap_bandwidth = DEFAULT_AP_BANDWIDTH, + .sta_only = DEFAULT_STA_ONLY, + .sta_power_save = DEFAULT_STA_POWER_SAVE, + .sta_static_ip = 0, +}; + +const char wifi_manager_nvs_namespace[] = "espwifimgr"; + +static EventGroupHandle_t wifi_manager_event_group; + +/* @brief indicate that the ESP32 is currently connected. */ +const int WIFI_MANAGER_WIFI_CONNECTED_BIT = BIT0; + +const int WIFI_MANAGER_AP_STA_CONNECTED_BIT = BIT1; + +/* @brief Set automatically once the SoftAP is started */ +const int WIFI_MANAGER_AP_STARTED_BIT = BIT2; + +/* @brief When set, means a client requested to connect to an access point.*/ +const int WIFI_MANAGER_REQUEST_STA_CONNECT_BIT = BIT3; + +/* @brief This bit is set automatically as soon as a connection was lost */ +const int WIFI_MANAGER_STA_DISCONNECT_BIT = BIT4; + +/* @brief When set, means the wifi manager attempts to restore a previously saved connection at startup. */ +const int WIFI_MANAGER_REQUEST_RESTORE_STA_BIT = BIT5; + +/* @brief When set, means a client requested to disconnect from currently connected AP. */ +const int WIFI_MANAGER_REQUEST_WIFI_DISCONNECT_BIT = BIT6; + +/* @brief When set, means a scan is in progress */ +const int WIFI_MANAGER_SCAN_BIT = BIT7; + +/* @brief When set, means user requested for a disconnect */ +const int WIFI_MANAGER_REQUEST_DISCONNECT_BIT = BIT8; + + + +void wifi_manager_timer_retry_cb( TimerHandle_t xTimer ){ + + ESP_LOGI(TAG, "Retry Timer Tick! Sending ORDER_CONNECT_STA with reason CONNECTION_REQUEST_AUTO_RECONNECT"); + + /* stop the timer */ + xTimerStop( xTimer, (TickType_t) 0 ); + + /* Attempt to reconnect */ + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_AUTO_RECONNECT); + +} + +void wifi_manager_timer_shutdown_ap_cb( TimerHandle_t xTimer){ + + /* stop the timer */ + xTimerStop( xTimer, (TickType_t) 0 ); + + /* Attempt to shutdown AP */ + wifi_manager_send_message(WM_ORDER_STOP_AP, NULL); +} + +void wifi_manager_scan_async(){ + wifi_manager_send_message(WM_ORDER_START_WIFI_SCAN, NULL); +} + +void wifi_manager_disconnect_async(){ + wifi_manager_send_message(WM_ORDER_DISCONNECT_STA, NULL); +} + + +void wifi_manager_start(){ + + /* disable the default wifi logging */ + esp_log_level_set("wifi", ESP_LOG_NONE); + + /* initialize flash memory */ + nvs_flash_init(); + ESP_ERROR_CHECK(nvs_sync_create()); /* semaphore for thread synchronization on NVS memory */ + + /* memory allocation */ + wifi_manager_queue = xQueueCreate( 3, sizeof( queue_message) ); + wifi_manager_json_mutex = xSemaphoreCreateMutex(); + accessp_records = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * MAX_AP_NUM); + accessp_json = (char*)malloc(MAX_AP_NUM * JSON_ONE_APP_SIZE + 4); /* 4 bytes for json encapsulation of "[\n" and "]\0" */ + wifi_manager_clear_access_points_json(); + ip_info_json = (char*)malloc(sizeof(char) * JSON_IP_INFO_SIZE); + wifi_manager_clear_ip_info_json(); + wifi_manager_config_sta = (wifi_config_t*)malloc(sizeof(wifi_config_t)); + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + memset(&wifi_settings.sta_static_ip_config, 0x00, sizeof(esp_netif_ip_info_t)); + cb_ptr_arr = malloc(sizeof(void (*)(void*)) * WM_MESSAGE_CODE_COUNT); + for(int i=0; ista.ssid) != 0){ + /* different ssid or ssid does not exist in flash: save new ssid */ + esp_err = nvs_set_blob(handle, "ssid", wifi_manager_config_sta->sta.ssid, 32); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + ESP_LOGI(TAG, "wifi_manager_wrote wifi_sta_config: ssid:%s",wifi_manager_config_sta->sta.ssid); + + } + + sz = sizeof(tmp_conf.sta.password); + esp_err = nvs_get_blob(handle, "password", tmp_conf.sta.password, &sz); + if( (esp_err == ESP_OK || esp_err == ESP_ERR_NVS_NOT_FOUND) && strcmp( (char*)tmp_conf.sta.password, (char*)wifi_manager_config_sta->sta.password) != 0){ + /* different password or password does not exist in flash: save new password */ + esp_err = nvs_set_blob(handle, "password", wifi_manager_config_sta->sta.password, 64); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + ESP_LOGI(TAG, "wifi_manager_wrote wifi_sta_config: password:%s",wifi_manager_config_sta->sta.password); + } + + sz = sizeof(tmp_settings); + esp_err = nvs_get_blob(handle, "settings", &tmp_settings, &sz); + if( (esp_err == ESP_OK || esp_err == ESP_ERR_NVS_NOT_FOUND) && + ( + strcmp( (char*)tmp_settings.ap_ssid, (char*)wifi_settings.ap_ssid) != 0 || + strcmp( (char*)tmp_settings.ap_pwd, (char*)wifi_settings.ap_pwd) != 0 || + tmp_settings.ap_ssid_hidden != wifi_settings.ap_ssid_hidden || + tmp_settings.ap_bandwidth != wifi_settings.ap_bandwidth || + tmp_settings.sta_only != wifi_settings.sta_only || + tmp_settings.sta_power_save != wifi_settings.sta_power_save || + tmp_settings.ap_channel != wifi_settings.ap_channel + ) + ){ + esp_err = nvs_set_blob(handle, "settings", &wifi_settings, sizeof(wifi_settings)); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_ssid: %s",wifi_settings.ap_ssid); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_pwd: %s",wifi_settings.ap_pwd); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_channel: %i",wifi_settings.ap_channel); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_hidden (1 = yes): %i",wifi_settings.ap_ssid_hidden); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_bandwidth (1 = 20MHz, 2 = 40MHz): %i",wifi_settings.ap_bandwidth); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: sta_only (0 = APSTA, 1 = STA when connected): %i",wifi_settings.sta_only); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: sta_power_save (1 = yes): %i",wifi_settings.sta_power_save); + } + + if(change){ + esp_err = nvs_commit(handle); + } + else{ + ESP_LOGI(TAG, "Wifi config was not saved to flash because no change has been detected."); + } + + if (esp_err != ESP_OK) return esp_err; + + nvs_close(handle); + nvs_sync_unlock(); + + } + else{ + ESP_LOGE(TAG, "wifi_manager_save_sta_config failed to acquire nvs_sync mutex"); + } + + return ESP_OK; +} + +bool wifi_manager_fetch_wifi_sta_config(){ + + nvs_handle handle; + esp_err_t esp_err; + if(nvs_sync_lock( portMAX_DELAY )){ + + esp_err = nvs_open(wifi_manager_nvs_namespace, NVS_READONLY, &handle); + + if(esp_err != ESP_OK){ + nvs_sync_unlock(); + return false; + } + + if(wifi_manager_config_sta == NULL){ + wifi_manager_config_sta = (wifi_config_t*)malloc(sizeof(wifi_config_t)); + } + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + + /* allocate buffer */ + size_t sz = sizeof(wifi_settings); + uint8_t *buff = (uint8_t*)malloc(sizeof(uint8_t) * sz); + memset(buff, 0x00, sizeof(sz)); + + /* ssid */ + sz = sizeof(wifi_manager_config_sta->sta.ssid); + esp_err = nvs_get_blob(handle, "ssid", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(wifi_manager_config_sta->sta.ssid, buff, sz); + + /* password */ + sz = sizeof(wifi_manager_config_sta->sta.password); + esp_err = nvs_get_blob(handle, "password", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(wifi_manager_config_sta->sta.password, buff, sz); + + /* settings */ + sz = sizeof(wifi_settings); + esp_err = nvs_get_blob(handle, "settings", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(&wifi_settings, buff, sz); + + free(buff); + nvs_close(handle); + nvs_sync_unlock(); + + + ESP_LOGI(TAG, "wifi_manager_fetch_wifi_sta_config: ssid:%s password:%s",wifi_manager_config_sta->sta.ssid,wifi_manager_config_sta->sta.password); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_ssid:%s",wifi_settings.ap_ssid); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_pwd:%s",wifi_settings.ap_pwd); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_channel:%i",wifi_settings.ap_channel); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_hidden (1 = yes):%i",wifi_settings.ap_ssid_hidden); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_bandwidth (1 = 20MHz, 2 = 40MHz)%i",wifi_settings.ap_bandwidth); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_only (0 = APSTA, 1 = STA when connected):%i",wifi_settings.sta_only); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_power_save (1 = yes):%i",wifi_settings.sta_power_save); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_static_ip (0 = dhcp client, 1 = static ip):%i",wifi_settings.sta_static_ip); + + return wifi_manager_config_sta->sta.ssid[0] != '\0'; + + + } + else{ + return false; + } + +} + + +void wifi_manager_clear_ip_info_json(){ + strcpy(ip_info_json, "{}\n"); +} + + +void wifi_manager_generate_ip_info_json(update_reason_code_t update_reason_code){ + + wifi_config_t *config = wifi_manager_get_wifi_sta_config(); + if(config){ + + const char *ip_info_json_format = ",\"ip\":\"%s\",\"netmask\":\"%s\",\"gw\":\"%s\",\"urc\":%d}\n"; + + memset(ip_info_json, 0x00, JSON_IP_INFO_SIZE); + + /* to avoid declaring a new buffer we copy the data directly into the buffer at its correct address */ + strcpy(ip_info_json, "{\"ssid\":"); + json_print_string(config->sta.ssid, (unsigned char*)(ip_info_json+strlen(ip_info_json)) ); + + size_t ip_info_json_len = strlen(ip_info_json); + size_t remaining = JSON_IP_INFO_SIZE - ip_info_json_len; + if(update_reason_code == UPDATE_CONNECTION_OK){ + /* rest of the information is copied after the ssid */ + esp_netif_ip_info_t ip_info; + ESP_ERROR_CHECK(esp_netif_get_ip_info(esp_netif_sta, &ip_info)); + + char ip[IP4ADDR_STRLEN_MAX]; /* note: IP4ADDR_STRLEN_MAX is defined in lwip */ + char gw[IP4ADDR_STRLEN_MAX]; + char netmask[IP4ADDR_STRLEN_MAX]; + + esp_ip4addr_ntoa(&ip_info.ip, ip, IP4ADDR_STRLEN_MAX); + esp_ip4addr_ntoa(&ip_info.gw, gw, IP4ADDR_STRLEN_MAX); + esp_ip4addr_ntoa(&ip_info.netmask, netmask, IP4ADDR_STRLEN_MAX); + + + snprintf( (ip_info_json + ip_info_json_len), remaining, ip_info_json_format, + ip, + netmask, + gw, + (int)update_reason_code); + } + else{ + /* notify in the json output the reason code why this was updated without a connection */ + snprintf( (ip_info_json + ip_info_json_len), remaining, ip_info_json_format, + "0", + "0", + "0", + (int)update_reason_code); + } + } + else{ + wifi_manager_clear_ip_info_json(); + } + + +} + + +void wifi_manager_clear_access_points_json(){ + strcpy(accessp_json, "[]\n"); +} +void wifi_manager_generate_acess_points_json(){ + + strcpy(accessp_json, "["); + + + const char oneap_str[] = ",\"chan\":%d,\"rssi\":%d,\"auth\":%d}%c\n"; + + /* stack buffer to hold on to one AP until it's copied over to accessp_json */ + char one_ap[JSON_ONE_APP_SIZE]; + for(int i=0; i and + * . + * + * Another thing deserves our attention is that the default behavior of LwIP is to abort all TCP socket connections on + * receiving the disconnect. Most of time it is not a problem. However, for some special application, this may not be + * what they want, consider following scenarios: + * + * The application creates a TCP connection to maintain the application-level keep-alive data that is sent out + * every 60 seconds. + * + * Due to certain reasons, the Wi-Fi connection is cut off, and the is raised. + * According to the current implementation, all TCP connections will be removed and the keep-alive socket will be + * in a wrong status. However, since the application designer believes that the network layer should NOT care about + * this error at the Wi-Fi layer, the application does not close the socket. + * + * Five seconds later, the Wi-Fi connection is restored because esp_wifi_connect() is called in the application + * event callback function. Moreover, the station connects to the same AP and gets the same IPV4 address as before. + * + * Sixty seconds later, when the application sends out data with the keep-alive socket, the socket returns an error + * and the application closes the socket and re-creates it when necessary. + * + * In above scenario, ideally, the application sockets and the network layer should not be affected, since the Wi-Fi + * connection only fails temporarily and recovers very quickly. The application can enable “Keep TCP connections when + * IP changed” via LwIP menuconfig.*/ + case WIFI_EVENT_STA_DISCONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); + + wifi_event_sta_disconnected_t* wifi_event_sta_disconnected = (wifi_event_sta_disconnected_t*)malloc(sizeof(wifi_event_sta_disconnected_t)); + *wifi_event_sta_disconnected = *( (wifi_event_sta_disconnected_t*)event_data ); + + /* if a DISCONNECT message is posted while a scan is in progress this scan will NEVER end, causing scan to never work again. For this reason SCAN_BIT is cleared too */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_WIFI_CONNECTED_BIT | WIFI_MANAGER_SCAN_BIT); + + /* post disconnect event with reason code */ + wifi_manager_send_message(WM_EVENT_STA_DISCONNECTED, (void*)wifi_event_sta_disconnected ); + break; + + /* This event arises when the AP to which the station is connected changes its authentication mode, e.g., from no auth + * to WPA. Upon receiving this event, the event task will do nothing. Generally, the application event callback does + * not need to handle this either. */ + case WIFI_EVENT_STA_AUTHMODE_CHANGE: + ESP_LOGI(TAG, "WIFI_EVENT_STA_AUTHMODE_CHANGE"); + break; + + case WIFI_EVENT_AP_START: + ESP_LOGI(TAG, "WIFI_EVENT_AP_START"); + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_AP_STARTED_BIT); + break; + + case WIFI_EVENT_AP_STOP: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STOP"); + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_AP_STARTED_BIT); + break; + + /* Every time a station is connected to ESP32 AP, the will arise. Upon receiving this + * event, the event task will do nothing, and the application callback can also ignore it. However, you may want + * to do something, for example, to get the info of the connected STA, etc. */ + case WIFI_EVENT_AP_STACONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STACONNECTED"); + break; + + /* This event can happen in the following scenarios: + * The application calls esp_wifi_disconnect(), or esp_wifi_deauth_sta(), to manually disconnect the station. + * The Wi-Fi driver kicks off the station, e.g. because the AP has not received any packets in the past five minutes, etc. + * The station kicks off the AP. + * When this event happens, the event task will do nothing, but the application event callback needs to do + * something, e.g., close the socket which is related to this station, etc. */ + case WIFI_EVENT_AP_STADISCONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STADISCONNECTED"); + break; + + /* This event is disabled by default. The application can enable it via API esp_wifi_set_event_mask(). + * When this event is enabled, it will be raised each time the AP receives a probe request. */ + case WIFI_EVENT_AP_PROBEREQRECVED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_PROBEREQRECVED"); + break; + + } /* end switch */ + } + else if(event_base == IP_EVENT){ + + switch(event_id){ + + /* This event arises when the DHCP client successfully gets the IPV4 address from the DHCP server, + * or when the IPV4 address is changed. The event means that everything is ready and the application can begin + * its tasks (e.g., creating sockets). + * The IPV4 may be changed because of the following reasons: + * The DHCP client fails to renew/rebind the IPV4 address, and the station’s IPV4 is reset to 0. + * The DHCP client rebinds to a different address. + * The static-configured IPV4 address is changed. + * Whether the IPV4 address is changed or NOT is indicated by field ip_change of ip_event_got_ip_t. + * The socket is based on the IPV4 address, which means that, if the IPV4 changes, all sockets relating to this + * IPV4 will become abnormal. Upon receiving this event, the application needs to close all sockets and recreate + * the application when the IPV4 changes to a valid one. */ + case IP_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP"); + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_WIFI_CONNECTED_BIT); + ip_event_got_ip_t* ip_event_got_ip = (ip_event_got_ip_t*)malloc(sizeof(ip_event_got_ip_t)); + *ip_event_got_ip = *( (ip_event_got_ip_t*)event_data ); + wifi_manager_send_message(WM_EVENT_STA_GOT_IP, (void*)(ip_event_got_ip) ); + break; + + /* This event arises when the IPV6 SLAAC support auto-configures an address for the ESP32, or when this address changes. + * The event means that everything is ready and the application can begin its tasks (e.g., creating sockets). */ + case IP_EVENT_GOT_IP6: + ESP_LOGI(TAG, "IP_EVENT_GOT_IP6"); + break; + + /* This event arises when the IPV4 address become invalid. + * IP_STA_LOST_IP doesn’t arise immediately after the WiFi disconnects, instead it starts an IPV4 address lost timer, + * if the IPV4 address is got before ip lost timer expires, IP_EVENT_STA_LOST_IP doesn’t happen. Otherwise, the event + * arises when IPV4 address lost timer expires. + * Generally the application don’t need to care about this event, it is just a debug event to let the application + * know that the IPV4 address is lost. */ + case IP_EVENT_STA_LOST_IP: + ESP_LOGI(TAG, "IP_EVENT_STA_LOST_IP"); + break; + + } + } + +} + + +wifi_config_t* wifi_manager_get_wifi_sta_config(){ + return wifi_manager_config_sta; +} + + +void wifi_manager_connect_async(){ + /* in order to avoid a false positive on the front end app we need to quickly flush the ip json + * There'se a risk the front end sees an IP or a password error when in fact + * it's a remnant from a previous connection + */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_clear_ip_info_json(); + wifi_manager_unlock_json_buffer(); + } + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_USER); +} + + +char* wifi_manager_get_ip_info_json(){ + return ip_info_json; +} + + +void wifi_manager_destroy(){ + + vTaskDelete(task_wifi_manager); + task_wifi_manager = NULL; + + /* heap buffers */ + free(accessp_records); + accessp_records = NULL; + free(accessp_json); + accessp_json = NULL; + free(ip_info_json); + ip_info_json = NULL; + free(wifi_manager_sta_ip); + wifi_manager_sta_ip = NULL; + if(wifi_manager_config_sta){ + free(wifi_manager_config_sta); + wifi_manager_config_sta = NULL; + } + + /* RTOS objects */ + vSemaphoreDelete(wifi_manager_json_mutex); + wifi_manager_json_mutex = NULL; + vSemaphoreDelete(wifi_manager_sta_ip_mutex); + wifi_manager_sta_ip_mutex = NULL; + vEventGroupDelete(wifi_manager_event_group); + wifi_manager_event_group = NULL; + vQueueDelete(wifi_manager_queue); + wifi_manager_queue = NULL; + + +} + + +void wifi_manager_filter_unique( wifi_ap_record_t * aplist, uint16_t * aps) { + int total_unique; + wifi_ap_record_t * first_free; + total_unique=*aps; + + first_free=NULL; + + for(int i=0; i<*aps-1;i++) { + wifi_ap_record_t * ap = &aplist[i]; + + /* skip the previously removed APs */ + if (ap->ssid[0] == 0) continue; + + /* remove the identical SSID+authmodes */ + for(int j=i+1; j<*aps;j++) { + wifi_ap_record_t * ap1 = &aplist[j]; + if ( (strcmp((const char *)ap->ssid, (const char *)ap1->ssid)==0) && + (ap->authmode == ap1->authmode) ) { /* same SSID, different auth mode is skipped */ + /* save the rssi for the display */ + if ((ap1->rssi) > (ap->rssi)) ap->rssi=ap1->rssi; + /* clearing the record */ + memset(ap1,0, sizeof(wifi_ap_record_t)); + } + } + } + /* reorder the list so APs follow each other in the list */ + for(int i=0; i<*aps;i++) { + wifi_ap_record_t * ap = &aplist[i]; + /* skipping all that has no name */ + if (ap->ssid[0] == 0) { + /* mark the first free slot */ + if (first_free==NULL) first_free=ap; + total_unique--; + continue; + } + if (first_free!=NULL) { + memcpy(first_free, ap, sizeof(wifi_ap_record_t)); + memset(ap,0, sizeof(wifi_ap_record_t)); + /* find the next free slot */ + for(int j=0; j<*aps;j++) { + if (aplist[j].ssid[0]==0) { + first_free=&aplist[j]; + break; + } + } + } + } + /* update the length of the list */ + *aps = total_unique; +} + + +BaseType_t wifi_manager_send_message_to_front(message_code_t code, void *param){ + queue_message msg; + msg.code = code; + msg.param = param; + return xQueueSendToFront( wifi_manager_queue, &msg, portMAX_DELAY); +} + +BaseType_t wifi_manager_send_message(message_code_t code, void *param){ + queue_message msg; + msg.code = code; + msg.param = param; + return xQueueSend( wifi_manager_queue, &msg, portMAX_DELAY); +} + + +void wifi_manager_set_callback(message_code_t message_code, void (*func_ptr)(void*) ){ + + if(cb_ptr_arr && message_code < WM_MESSAGE_CODE_COUNT){ + cb_ptr_arr[message_code] = func_ptr; + } +} + +esp_netif_t* wifi_manager_get_esp_netif_ap(){ + return esp_netif_ap; +} + +esp_netif_t* wifi_manager_get_esp_netif_sta(){ + return esp_netif_sta; +} + +void wifi_manager( void * pvParameters ){ + + + queue_message msg; + BaseType_t xStatus; + EventBits_t uxBits; + uint8_t retries = 0; + + + /* initialize the tcp stack */ + ESP_ERROR_CHECK(esp_netif_init()); + + /* event loop for the wifi driver */ + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + esp_netif_sta = esp_netif_create_default_wifi_sta(); + esp_netif_ap = esp_netif_create_default_wifi_ap(); + + + /* default wifi config */ + wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&wifi_init_config)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + + /* event handler for the connection */ + esp_event_handler_instance_t instance_wifi_event; + esp_event_handler_instance_t instance_ip_event; + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_manager_event_handler, NULL,&instance_wifi_event)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_manager_event_handler, NULL,&instance_ip_event)); + + + /* SoftAP - Wifi Access Point configuration setup */ + wifi_config_t ap_config = { + .ap = { + .ssid_len = 0, + .channel = wifi_settings.ap_channel, + .ssid_hidden = wifi_settings.ap_ssid_hidden, + .max_connection = DEFAULT_AP_MAX_CONNECTIONS, + .beacon_interval = DEFAULT_AP_BEACON_INTERVAL, + }, + }; + memcpy(ap_config.ap.ssid, wifi_settings.ap_ssid , sizeof(wifi_settings.ap_ssid)); + + /* if the password lenght is under 8 char which is the minium for WPA2, the access point starts as open */ + if(strlen( (char*)wifi_settings.ap_pwd) < WPA2_MINIMUM_PASSWORD_LENGTH){ + ap_config.ap.authmode = WIFI_AUTH_OPEN; + memset( ap_config.ap.password, 0x00, sizeof(ap_config.ap.password) ); + } + else{ + ap_config.ap.authmode = WIFI_AUTH_WPA2_PSK; + memcpy(ap_config.ap.password, wifi_settings.ap_pwd, sizeof(wifi_settings.ap_pwd)); + } + + + /* DHCP AP configuration */ + esp_netif_dhcps_stop(esp_netif_ap); /* DHCP client/server must be stopped before setting new IP information. */ + esp_netif_ip_info_t ap_ip_info; + memset(&ap_ip_info, 0x00, sizeof(ap_ip_info)); + inet_pton(AF_INET, DEFAULT_AP_IP, &ap_ip_info.ip); + inet_pton(AF_INET, DEFAULT_AP_GATEWAY, &ap_ip_info.gw); + inet_pton(AF_INET, DEFAULT_AP_NETMASK, &ap_ip_info.netmask); + ESP_ERROR_CHECK(esp_netif_set_ip_info(esp_netif_ap, &ap_ip_info)); + ESP_ERROR_CHECK(esp_netif_dhcps_start(esp_netif_ap)); + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &ap_config)); + ESP_ERROR_CHECK(esp_wifi_set_bandwidth(WIFI_IF_AP, wifi_settings.ap_bandwidth)); + ESP_ERROR_CHECK(esp_wifi_set_ps(wifi_settings.sta_power_save)); + + + /* by default the mode is STA because wifi_manager will not start the access point unless it has to! */ + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); + + /* start http server */ + http_app_start(false); + + /* wifi scanner config */ + wifi_scan_config_t scan_config = { + .ssid = 0, + .bssid = 0, + .channel = 0, + .show_hidden = true + }; + + /* enqueue first event: load previous config */ + wifi_manager_send_message(WM_ORDER_LOAD_AND_RESTORE_STA, NULL); + + + /* main processing loop */ + for(;;){ + xStatus = xQueueReceive( wifi_manager_queue, &msg, portMAX_DELAY ); + + if( xStatus == pdPASS ){ + switch(msg.code){ + + case WM_EVENT_SCAN_DONE:{ + wifi_event_sta_scan_done_t *evt_scan_done = (wifi_event_sta_scan_done_t*)msg.param; + /* only check for AP if the scan is succesful */ + if(evt_scan_done->status == 0){ + /* As input param, it stores max AP number ap_records can hold. As output param, it receives the actual AP number this API returns. + * As a consequence, ap_num MUST be reset to MAX_AP_NUM at every scan */ + ap_num = MAX_AP_NUM; + ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_num, accessp_records)); + /* make sure the http server isn't trying to access the list while it gets refreshed */ + if(wifi_manager_lock_json_buffer( pdMS_TO_TICKS(1000) )){ + /* Will remove the duplicate SSIDs from the list and update ap_num */ + wifi_manager_filter_unique(accessp_records, &ap_num); + wifi_manager_generate_acess_points_json(); + wifi_manager_unlock_json_buffer(); + } + else{ + ESP_LOGE(TAG, "could not get access to json mutex in wifi_scan"); + } + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(evt_scan_done); + } + break; + + case WM_ORDER_START_WIFI_SCAN: + ESP_LOGD(TAG, "MESSAGE: ORDER_START_WIFI_SCAN"); + + /* if a scan is already in progress this message is simply ignored thanks to the WIFI_MANAGER_SCAN_BIT uxBit */ + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if(! (uxBits & WIFI_MANAGER_SCAN_BIT) ){ + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_SCAN_BIT); + ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, false)); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_LOAD_AND_RESTORE_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_LOAD_AND_RESTORE_STA"); + if(wifi_manager_fetch_wifi_sta_config()){ + ESP_LOGI(TAG, "Saved wifi found on startup. Will attempt to connect."); + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_RESTORE_CONNECTION); + } + else{ + /* no wifi saved: start soft AP! This is what should happen during a first run */ + ESP_LOGI(TAG, "No saved wifi found on startup. Starting access point."); + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_CONNECT_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_CONNECT_STA"); + + /* very important: precise that this connection attempt is specifically requested. + * Param in that case is a boolean indicating if the request was made automatically + * by the wifi_manager. + * */ + if((BaseType_t)msg.param == CONNECTION_REQUEST_USER) { + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + } + else if((BaseType_t)msg.param == CONNECTION_REQUEST_RESTORE_CONNECTION) { + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + } + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if( ! (uxBits & WIFI_MANAGER_WIFI_CONNECTED_BIT) ){ + /* update config to latest and attempt connection */ + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, wifi_manager_get_wifi_sta_config())); + + /* if there is a wifi scan in progress abort it first + Calling esp_wifi_scan_stop will trigger a SCAN_DONE event which will reset this bit */ + if(uxBits & WIFI_MANAGER_SCAN_BIT){ + esp_wifi_scan_stop(); + } + ESP_ERROR_CHECK(esp_wifi_connect()); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_EVENT_STA_DISCONNECTED: + ;wifi_event_sta_disconnected_t* wifi_event_sta_disconnected = (wifi_event_sta_disconnected_t*)msg.param; + ESP_LOGI(TAG, "MESSAGE: EVENT_STA_DISCONNECTED with Reason code: %d", wifi_event_sta_disconnected->reason); + + /* this even can be posted in numerous different conditions + * + * 1. SSID password is wrong + * 2. Manual disconnection ordered + * 3. Connection lost + * + * Having clear understand as to WHY the event was posted is key to having an efficient wifi manager + * + * With wifi_manager, we determine: + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT is set, We consider it's a client that requested the connection. + * When SYSTEM_EVENT_STA_DISCONNECTED is posted, it's probably a password/something went wrong with the handshake. + * + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT is set, it's a disconnection that was ASKED by the client (clicking disconnect in the app) + * When SYSTEM_EVENT_STA_DISCONNECTED is posted, saved wifi is erased from the NVS memory. + * + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT and WIFI_MANAGER_REQUEST_STA_CONNECT_BIT are NOT set, it's a lost connection + * + * In this version of the software, reason codes are not used. They are indicated here for potential future usage. + * + * REASON CODE: + * 1 UNSPECIFIED + * 2 AUTH_EXPIRE auth no longer valid, this smells like someone changed a password on the AP + * 3 AUTH_LEAVE + * 4 ASSOC_EXPIRE + * 5 ASSOC_TOOMANY too many devices already connected to the AP => AP fails to respond + * 6 NOT_AUTHED + * 7 NOT_ASSOCED + * 8 ASSOC_LEAVE tested as manual disconnect by user OR in the wireless MAC blacklist + * 9 ASSOC_NOT_AUTHED + * 10 DISASSOC_PWRCAP_BAD + * 11 DISASSOC_SUPCHAN_BAD + * 12 + * 13 IE_INVALID + * 14 MIC_FAILURE + * 15 4WAY_HANDSHAKE_TIMEOUT wrong password! This was personnaly tested on my home wifi with a wrong password. + * 16 GROUP_KEY_UPDATE_TIMEOUT + * 17 IE_IN_4WAY_DIFFERS + * 18 GROUP_CIPHER_INVALID + * 19 PAIRWISE_CIPHER_INVALID + * 20 AKMP_INVALID + * 21 UNSUPP_RSN_IE_VERSION + * 22 INVALID_RSN_IE_CAP + * 23 802_1X_AUTH_FAILED wrong password? + * 24 CIPHER_SUITE_REJECTED + * 200 BEACON_TIMEOUT + * 201 NO_AP_FOUND + * 202 AUTH_FAIL + * 203 ASSOC_FAIL + * 204 HANDSHAKE_TIMEOUT + * + * */ + + /* reset saved sta IP */ + wifi_manager_safe_update_sta_ip_string((uint32_t)0); + + /* if there was a timer on to stop the AP, well now it's time to cancel that since connection was lost! */ + if(xTimerIsTimerActive(wifi_manager_shutdown_ap_timer) == pdTRUE ){ + xTimerStop( wifi_manager_shutdown_ap_timer, (TickType_t)0 ); + } + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if( uxBits & WIFI_MANAGER_REQUEST_STA_CONNECT_BIT ){ + /* there are no retries when it's a user requested connection by design. This avoids a user hanging too much + * in case they typed a wrong password for instance. Here we simply clear the request bit and move on */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_FAILED_ATTEMPT ); + wifi_manager_unlock_json_buffer(); + } + + } + else if (uxBits & WIFI_MANAGER_REQUEST_DISCONNECT_BIT){ + /* user manually requested a disconnect so the lost connection is a normal event. Clear the flag and restart the AP */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_DISCONNECT_BIT); + + /* erase configuration */ + if(wifi_manager_config_sta){ + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + } + + /* regenerate json status */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_USER_DISCONNECT ); + wifi_manager_unlock_json_buffer(); + } + + /* save NVS memory */ + wifi_manager_save_sta_config(); + + /* start SoftAP */ + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + else{ + /* lost connection ? */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_LOST_CONNECTION ); + wifi_manager_unlock_json_buffer(); + } + + /* Start the timer that will try to restore the saved config */ + xTimerStart( wifi_manager_retry_timer, (TickType_t)0 ); + + /* if it was a restore attempt connection, we clear the bit */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + + /* if the AP is not started, we check if we have reached the threshold of failed attempt to start it */ + if(! (uxBits & WIFI_MANAGER_AP_STARTED_BIT) ){ + + /* if the nunber of retries is below the threshold to start the AP, a reconnection attempt is made + * This way we avoid restarting the AP directly in case the connection is mementarily lost */ + if(retries < WIFI_MANAGER_MAX_RETRY_START_AP){ + retries++; + } + else{ + /* In this scenario the connection was lost beyond repair: kick start the AP! */ + retries = 0; + + /* start SoftAP */ + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + } + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(wifi_event_sta_disconnected); + + break; + + case WM_ORDER_START_AP: + ESP_LOGI(TAG, "MESSAGE: ORDER_START_AP"); + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + + /* restart HTTP daemon */ + http_app_stop(); + http_app_start(true); + + /* start DNS */ + dns_server_start(); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_STOP_AP: + ESP_LOGI(TAG, "MESSAGE: ORDER_STOP_AP"); + + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + + /* before stopping the AP, we check that we are still connected. There's a chance that once the timer + * kicks in, for whatever reason the esp32 is already disconnected. + */ + if(uxBits & WIFI_MANAGER_WIFI_CONNECTED_BIT){ + + /* set to STA only */ + esp_wifi_set_mode(WIFI_MODE_STA); + + /* stop DNS */ + dns_server_stop(); + + /* restart HTTP daemon */ + http_app_stop(); + http_app_start(false); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + } + + break; + + case WM_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "WM_EVENT_STA_GOT_IP"); + ip_event_got_ip_t* ip_event_got_ip = (ip_event_got_ip_t*)msg.param; + uxBits = xEventGroupGetBits(wifi_manager_event_group); + + /* reset connection requests bits -- doesn't matter if it was set or not */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + + /* save IP as a string for the HTTP server host */ + wifi_manager_safe_update_sta_ip_string(ip_event_got_ip->ip_info.ip.addr); + + /* save wifi config in NVS if it wasn't a restored of a connection */ + if(uxBits & WIFI_MANAGER_REQUEST_RESTORE_STA_BIT){ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + } + else{ + wifi_manager_save_sta_config(); + } + + /* reset number of retries */ + retries = 0; + + /* refresh JSON with the new IP */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + /* generate the connection info with success */ + wifi_manager_generate_ip_info_json( UPDATE_CONNECTION_OK ); + wifi_manager_unlock_json_buffer(); + } + else { abort(); } + + /* bring down DNS hijack */ + dns_server_stop(); + + /* start the timer that will eventually shutdown the access point + * We check first that it's actually running because in case of a boot and restore connection + * the AP is not even started to begin with. + */ + if(uxBits & WIFI_MANAGER_AP_STARTED_BIT){ + TickType_t t = pdMS_TO_TICKS( WIFI_MANAGER_SHUTDOWN_AP_TIMER ); + + /* if for whatever reason user configured the shutdown timer to be less than 1 tick, the AP is stopped straight away */ + if(t > 0){ + xTimerStart( wifi_manager_shutdown_ap_timer, (TickType_t)0 ); + } + else{ + wifi_manager_send_message(WM_ORDER_STOP_AP, (void*)NULL); + } + + } + + /* callback and free memory allocated for the void* param */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(ip_event_got_ip); + + break; + + case WM_ORDER_DISCONNECT_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_DISCONNECT_STA"); + + /* precise this is coming from a user request */ + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_DISCONNECT_BIT); + + /* order wifi discconect */ + ESP_ERROR_CHECK(esp_wifi_disconnect()); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + default: + break; + + } /* end of switch/case */ + } /* end of if status=pdPASS */ + } /* end of for loop */ + + vTaskDelete( NULL ); + +} + + diff --git a/dist/esp32-wifi-manager_0.0.1/src/wifi_manager.h b/dist/esp32-wifi-manager_0.0.1/src/wifi_manager.h new file mode 100644 index 00000000..cba3b989 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.1/src/wifi_manager.h @@ -0,0 +1,417 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file wifi_manager.h +@author Tony Pottier +@brief Defines all functions necessary for esp32 to connect to a wifi/scan wifis + +Contains the freeRTOS task and all necessary support + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#ifndef WIFI_MANAGER_H_INCLUDED +#define WIFI_MANAGER_H_INCLUDED + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Defines the maximum size of a SSID name. 32 is IEEE standard. + * @warning limit is also hard coded in wifi_config_t. Never extend this value. + */ +#define MAX_SSID_SIZE 32 + +/** + * @brief Defines the maximum size of a WPA2 passkey. 64 is IEEE standard. + * @warning limit is also hard coded in wifi_config_t. Never extend this value. + */ +#define MAX_PASSWORD_SIZE 64 + + +/** + * @brief Defines the maximum number of access points that can be scanned. + * + * To save memory and avoid nasty out of memory errors, + * we can limit the number of APs detected in a wifi scan. + */ +#define MAX_AP_NUM 15 + + +/** + * @brief Defines the maximum number of failed retries allowed before the WiFi manager starts its own access point. + * Setting it to 2 for instance means there will be 3 attempts in total (original request + 2 retries) + */ +#define WIFI_MANAGER_MAX_RETRY_START_AP CONFIG_WIFI_MANAGER_MAX_RETRY_START_AP + +/** + * @brief Time (in ms) between each retry attempt + * Defines the time to wait before an attempt to re-connect to a saved wifi is made after connection is lost or another unsuccesful attempt is made. + */ +#define WIFI_MANAGER_RETRY_TIMER CONFIG_WIFI_MANAGER_RETRY_TIMER + + +/** + * @brief Time (in ms) to wait before shutting down the AP + * Defines the time (in ms) to wait after a succesful connection before shutting down the access point. + */ +#define WIFI_MANAGER_SHUTDOWN_AP_TIMER CONFIG_WIFI_MANAGER_SHUTDOWN_AP_TIMER + + +/** @brief Defines the task priority of the wifi_manager. + * + * Tasks spawn by the manager will have a priority of WIFI_MANAGER_TASK_PRIORITY-1. + * For this particular reason, minimum task priority is 1. It it highly not recommended to set + * it to 1 though as the sub-tasks will now have a priority of 0 which is the priority + * of freeRTOS' idle task. + */ +#define WIFI_MANAGER_TASK_PRIORITY CONFIG_WIFI_MANAGER_TASK_PRIORITY + +/** @brief Defines the auth mode as an access point + * Value must be of type wifi_auth_mode_t + * @see esp_wifi_types.h + * @warning if set to WIFI_AUTH_OPEN, passowrd me be empty. See DEFAULT_AP_PASSWORD. + */ +#define AP_AUTHMODE WIFI_AUTH_WPA2_PSK + +/** @brief Defines visibility of the access point. 0: visible AP. 1: hidden */ +#define DEFAULT_AP_SSID_HIDDEN 0 + +/** @brief Defines access point's name. Default value: esp32. Run 'make menuconfig' to setup your own value or replace here by a string */ +#define DEFAULT_AP_SSID CONFIG_DEFAULT_AP_SSID + +/** @brief Defines access point's password. + * @warning In the case of an open access point, the password must be a null string "" or "\0" if you want to be verbose but waste one byte. + * In addition, the AP_AUTHMODE must be WIFI_AUTH_OPEN + */ +#define DEFAULT_AP_PASSWORD CONFIG_DEFAULT_AP_PASSWORD + +/** @brief Defines the hostname broadcasted by mDNS */ +#define DEFAULT_HOSTNAME "esp32" + +/** @brief Defines access point's bandwidth. + * Value: WIFI_BW_HT20 for 20 MHz or WIFI_BW_HT40 for 40 MHz + * 20 MHz minimize channel interference but is not suitable for + * applications with high data speeds + */ +#define DEFAULT_AP_BANDWIDTH WIFI_BW_HT20 + +/** @brief Defines access point's channel. + * Channel selection is only effective when not connected to another AP. + * Good practice for minimal channel interference to use + * For 20 MHz: 1, 6 or 11 in USA and 1, 5, 9 or 13 in most parts of the world + * For 40 MHz: 3 in USA and 3 or 11 in most parts of the world + */ +#define DEFAULT_AP_CHANNEL CONFIG_DEFAULT_AP_CHANNEL + + + +/** @brief Defines the access point's default IP address. Default: "10.10.0.1 */ +#define DEFAULT_AP_IP CONFIG_DEFAULT_AP_IP + +/** @brief Defines the access point's gateway. This should be the same as your IP. Default: "10.10.0.1" */ +#define DEFAULT_AP_GATEWAY CONFIG_DEFAULT_AP_GATEWAY + +/** @brief Defines the access point's netmask. Default: "255.255.255.0" */ +#define DEFAULT_AP_NETMASK CONFIG_DEFAULT_AP_NETMASK + +/** @brief Defines access point's maximum number of clients. Default: 4 */ +#define DEFAULT_AP_MAX_CONNECTIONS CONFIG_DEFAULT_AP_MAX_CONNECTIONS + +/** @brief Defines access point's beacon interval. 100ms is the recommended default. */ +#define DEFAULT_AP_BEACON_INTERVAL CONFIG_DEFAULT_AP_BEACON_INTERVAL + +/** @brief Defines if esp32 shall run both AP + STA when connected to another AP. + * Value: 0 will have the own AP always on (APSTA mode) + * Value: 1 will turn off own AP when connected to another AP (STA only mode when connected) + * Turning off own AP when connected to another AP minimize channel interference and increase throughput + */ +#define DEFAULT_STA_ONLY 1 + +/** @brief Defines if wifi power save shall be enabled. + * Value: WIFI_PS_NONE for full power (wifi modem always on) + * Value: WIFI_PS_MODEM for power save (wifi modem sleep periodically) + * Note: Power save is only effective when in STA only mode + */ +#define DEFAULT_STA_POWER_SAVE WIFI_PS_NONE + +/** + * @brief Defines the maximum length in bytes of a JSON representation of an access point. + * + * maximum ap string length with full 32 char ssid: 75 + \\n + \0 = 77\n + * example: {"ssid":"abcdefghijklmnopqrstuvwxyz012345","chan":12,"rssi":-100,"auth":4},\n + * BUT: we need to escape JSON. Imagine a ssid full of \" ? so it's 32 more bytes hence 77 + 32 = 99.\n + * this is an edge case but I don't think we should crash in a catastrophic manner just because + * someone decided to have a funny wifi name. + */ +#define JSON_ONE_APP_SIZE 99 + +/** + * @brief Defines the maximum length in bytes of a JSON representation of the IP information + * assuming all ips are 4*3 digits, and all characters in the ssid require to be escaped. + * example: {"ssid":"abcdefghijklmnopqrstuvwxyz012345","ip":"192.168.1.119","netmask":"255.255.255.0","gw":"192.168.1.1","urc":99} + * Run this JS (browser console is easiest) to come to the conclusion that 159 is the worst case. + * ``` + * var a = {"ssid":"abcdefghijklmnopqrstuvwxyz012345","ip":"255.255.255.255","netmask":"255.255.255.255","gw":"255.255.255.255","urc":99}; + * // Replace all ssid characters with a double quote which will have to be escaped + * a.ssid = a.ssid.split('').map(() => '"').join(''); + * console.log(JSON.stringify(a).length); // => 158 +1 for null + * console.log(JSON.stringify(a)); // print it + * ``` + */ +#define JSON_IP_INFO_SIZE 159 + + +/** + * @brief defines the minimum length of an access point password running on WPA2 + */ +#define WPA2_MINIMUM_PASSWORD_LENGTH 8 + + +/** + * @brief Defines the complete list of all messages that the wifi_manager can process. + * + * Some of these message are events ("EVENT"), and some of them are action ("ORDER") + * Each of these messages can trigger a callback function and each callback function is stored + * in a function pointer array for convenience. Because of this behavior, it is extremely important + * to maintain a strict sequence and the top level special element 'MESSAGE_CODE_COUNT' + * + * @see wifi_manager_set_callback + */ +typedef enum message_code_t { + NONE = 0, + WM_ORDER_START_HTTP_SERVER = 1, + WM_ORDER_STOP_HTTP_SERVER = 2, + WM_ORDER_START_DNS_SERVICE = 3, + WM_ORDER_STOP_DNS_SERVICE = 4, + WM_ORDER_START_WIFI_SCAN = 5, + WM_ORDER_LOAD_AND_RESTORE_STA = 6, + WM_ORDER_CONNECT_STA = 7, + WM_ORDER_DISCONNECT_STA = 8, + WM_ORDER_START_AP = 9, + WM_EVENT_STA_DISCONNECTED = 10, + WM_EVENT_SCAN_DONE = 11, + WM_EVENT_STA_GOT_IP = 12, + WM_ORDER_STOP_AP = 13, + WM_MESSAGE_CODE_COUNT = 14 /* important for the callback array */ + +}message_code_t; + +/** + * @brief simplified reason codes for a lost connection. + * + * esp-idf maintains a big list of reason codes which in practice are useless for most typical application. + */ +typedef enum update_reason_code_t { + UPDATE_CONNECTION_OK = 0, + UPDATE_FAILED_ATTEMPT = 1, + UPDATE_USER_DISCONNECT = 2, + UPDATE_LOST_CONNECTION = 3 +}update_reason_code_t; + +typedef enum connection_request_made_by_code_t{ + CONNECTION_REQUEST_NONE = 0, + CONNECTION_REQUEST_USER = 1, + CONNECTION_REQUEST_AUTO_RECONNECT = 2, + CONNECTION_REQUEST_RESTORE_CONNECTION = 3, + CONNECTION_REQUEST_MAX = 0x7fffffff /*force the creation of this enum as a 32 bit int */ +}connection_request_made_by_code_t; + +/** + * The actual WiFi settings in use + */ +struct wifi_settings_t{ + uint8_t ap_ssid[MAX_SSID_SIZE]; + uint8_t ap_pwd[MAX_PASSWORD_SIZE]; + uint8_t ap_channel; + uint8_t ap_ssid_hidden; + wifi_bandwidth_t ap_bandwidth; + bool sta_only; + wifi_ps_type_t sta_power_save; + bool sta_static_ip; + esp_netif_ip_info_t sta_static_ip_config; +}; +extern struct wifi_settings_t wifi_settings; + + +/** + * @brief Structure used to store one message in the queue. + */ +typedef struct{ + message_code_t code; + void *param; +} queue_message; + + +/** + * @brief returns the current esp_netif object for the STAtion + */ +esp_netif_t* wifi_manager_get_esp_netif_sta(); + +/** + * @brief returns the current esp_netif object for the Access Point + */ +esp_netif_t* wifi_manager_get_esp_netif_ap(); + + +/** + * Allocate heap memory for the wifi manager and start the wifi_manager RTOS task + */ +void wifi_manager_start(); + +/** + * Frees up all memory allocated by the wifi_manager and kill the task. + */ +void wifi_manager_destroy(); + +/** + * Filters the AP scan list to unique SSIDs + */ +void filter_unique( wifi_ap_record_t * aplist, uint16_t * ap_num); + +/** + * Main task for the wifi_manager + */ +void wifi_manager( void * pvParameters ); + + +char* wifi_manager_get_ap_list_json(); +char* wifi_manager_get_ip_info_json(); + + +void wifi_manager_scan_async(); + + +/** + * @brief saves the current STA wifi config to flash ram storage. + */ +esp_err_t wifi_manager_save_sta_config(); + +/** + * @brief fetch a previously STA wifi config in the flash ram storage. + * @return true if a previously saved config was found, false otherwise. + */ +bool wifi_manager_fetch_wifi_sta_config(); + +wifi_config_t* wifi_manager_get_wifi_sta_config(); + + +/** + * @brief requests a connection to an access point that will be process in the main task thread. + */ +void wifi_manager_connect_async(); + +/** + * @brief requests a wifi scan + */ +void wifi_manager_scan_awifi_manager_send_messagesync(); + +/** + * @brief requests to disconnect and forget about the access point. + */ +void wifi_manager_disconnect_async(); + +/** + * @brief Tries to get access to json buffer mutex. + * + * The HTTP server can try to access the json to serve clients while the wifi manager thread can try + * to update it. These two tasks are synchronized through a mutex. + * + * The mutex is used by both the access point list json and the connection status json.\n + * These two resources should technically have their own mutex but we lose some flexibility to save + * on memory. + * + * This is a simple wrapper around freeRTOS function xSemaphoreTake. + * + * @param xTicksToWait The time in ticks to wait for the semaphore to become available. + * @return true in success, false otherwise. + */ +bool wifi_manager_lock_json_buffer(TickType_t xTicksToWait); + +/** + * @brief Releases the json buffer mutex. + */ +void wifi_manager_unlock_json_buffer(); + +/** + * @brief Generates the connection status json: ssid and IP addresses. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_generate_ip_info_json(update_reason_code_t update_reason_code); +/** + * @brief Clears the connection status json. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_clear_ip_info_json(); + +/** + * @brief Generates the list of access points after a wifi scan. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_generate_acess_points_json(); + +/** + * @brief Clear the list of access points. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_clear_access_points_json(); + + +/** + * @brief Start the mDNS service + */ +void wifi_manager_initialise_mdns(); + + +bool wifi_manager_lock_sta_ip_string(TickType_t xTicksToWait); +void wifi_manager_unlock_sta_ip_string(); + +/** + * @brief gets the string representation of the STA IP address, e.g.: "192.168.1.69" + */ +char* wifi_manager_get_sta_ip_string(); + +/** + * @brief thread safe char representation of the STA IP update + */ +void wifi_manager_safe_update_sta_ip_string(uint32_t ip); + + +/** + * @brief Register a callback to a custom function when specific event message_code happens. + */ +void wifi_manager_set_callback(message_code_t message_code, void (*func_ptr)(void*) ); + + +BaseType_t wifi_manager_send_message(message_code_t code, void *param); +BaseType_t wifi_manager_send_message_to_front(message_code_t code, void *param); + +#ifdef __cplusplus +} +#endif + +#endif /* WIFI_MANAGER_H_INCLUDED */ diff --git a/dist/esp32-wifi-manager_0.0.3.tgz b/dist/esp32-wifi-manager_0.0.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..0c61738158304c06f3c26f3cc9498070b0f7c755 GIT binary patch literal 44304 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

W3zVE|+-4Gy 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 literal 0 HcmV?d00001 diff --git a/dist/esp32-wifi-manager_0.0.3/.gitignore b/dist/esp32-wifi-manager_0.0.3/.gitignore new file mode 100644 index 00000000..5448b48b --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/.gitignore @@ -0,0 +1,75 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Eclipse +.metadata/ +RemoteSystemsTempFiles/.project +.settings/ +*.a +*.o +*.d +wifi_manager/.cproject +wifi_manager/.project +sdkconfig +sdkconfig.old +**/build/ +**/managed_components/ +dependencies.lock +#doxygen +Doxyfile +wifi_manager/doc/ +.project +.cproject + +# Visual Studio Code +.vscode/ diff --git a/dist/esp32-wifi-manager_0.0.3/.travis.yml b/dist/esp32-wifi-manager_0.0.3/.travis.yml new file mode 100644 index 00000000..37c55458 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/.travis.yml @@ -0,0 +1,122 @@ +language: bash + +# when you suspects issues in cache, use the following line to disable cache. +# cache: false +cache: + directories: + - ${HOME}/distfiles + - ${HOME}/.ccache + - ${HOME}/.cache/pip +os: + - linux + +matrix: + include: + - env: + - PROJECT_TARGET="esp32" + - PROJECT_SDK_BRANCH="master" +# - env: +# - PROJECT_TARGET="esp8266" +# - PROJECT_SDK_BRANCH="master" + +addons: + apt: + packages: + - gcc + - wget + - make + - libncurses-dev + - flex + - bison + - python + - python-pip + - gperf + - ccache + +before_install: + # Save path to the git respository + - PROJECT_PATH=$(pwd) + +install: + - export TOOLCHAIN_DIR="${HOME}/${PROJECT_TARGET}" + - | + if [ ${PROJECT_TARGET} == "esp8266" ]; then + export PROJECT_GCC_PREFIX="xtensa-lx106-elf" + export PROJECT_TOOLCHAIN_FILE=xtensa-lx106-elf-linux64-1.22.0-92-g8facf4c-5.2.0.tar.gz + export PROJECT_SDK_NAME="ESP8266_RTOS_SDK" + else + export PROJECT_GCC_PREFIX="xtensa-esp32-elf" + export PROJECT_TOOLCHAIN_FILE=xtensa-esp32-elf-gcc8_2_0-esp32-2019r1-linux-amd64.tar.gz + export PROJECT_SDK_NAME="esp-idf" + fi + - export PROJECT_GCC_FILE="${PROJECT_GCC_PREFIX}-gcc" + - export PROJECT_DISTFILE_DIR="${HOME}/distfiles" + - export IDF_PATH=${TOOLCHAIN_DIR}/${PROJECT_SDK_NAME} + - export PROJECT_LOG="${HOME}/build.log" + - export PROJECT_EXAMPLE_DIR="${PROJECT_PATH}/examples" + # Install ESP32 toochain following steps as desribed + # in http://esp-idf.readthedocs.io/en/latest/linux-setup.html + + # Prepare directory for the toolchain + - mkdir -p ${TOOLCHAIN_DIR} ${PROJECT_DISTFILE_DIR} + # Get SDK from github + - git clone --branch ${PROJECT_SDK_BRANCH} --recursive https://github.com/espressif/${PROJECT_SDK_NAME}.git ${IDF_PATH} + + # Setup ccache to build faster + # XXX when the entire build process exceeds 50 min, th job will be killed + # https://docs.travis-ci.com/user/customizing-the-build/#build-timeouts + - ccache --version + - mkdir ${HOME}/ccache_bin + - (cd ${HOME}/ccache_bin && ln -s /usr/bin/ccache ${PROJECT_GCC_FILE}) + - export CCACHE_BASEDIR=$PROJECT_PATH + - export CCACHE_CPP2=true + + # Get Python requirements + - python -m pip install --user --upgrade pyOpenSSL + - python -m pip install --user -r ${IDF_PATH}/requirements.txt + + # Download binary toolchain if it does not exist + - | + if [ ! -f ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} ]; then + wget -O ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} https://dl.espressif.com/dl/${PROJECT_TOOLCHAIN_FILE} + fi + - tar -xz -C ${TOOLCHAIN_DIR} -f ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} + + # Make toolchains available for all terminal sessions + - export PATH=$HOME/ccache_bin:$PATH:$HOME/${PROJECT_TARGET}/${PROJECT_GCC_PREFIX}/bin + +script: + - rm -f ${PROJECT_LOG} + # XXX surpress log output where possible. when the size exceeds 4 MB, the + # job will be killed. + - | + IGNORE_FILE="travis-ignore" + + case ${PROJECT_TARGET} in + esp32) + ;; + esp8266) + IGNORE_FILE="travis-ignore-esp8266" + # these drivers do not compile for ESP8266 yet + export EXCLUDE_COMPONENTS="encoder max7219 mcp23x17" + ;; + esac + + cd ${PROJECT_EXAMPLE_DIR} + for i in $(ls -d */); do + if [ ! -e ${PROJECT_EXAMPLE_DIR}/${i}/${IGNORE_FILE} ]; then + echo "Building ${i}..." + cd ${PROJECT_EXAMPLE_DIR}/${i} + make defconfig + make -j2 >> ${PROJECT_LOG} + if [ $? -ne 0 ]; then + # when failed, show last 100 lines for debugging, and exit with + # non-zero exit code + tail -n 100 ${PROJECT_LOG} + exit 1 + fi + make clean >/dev/null + # make sure the directory is clean + rm -rf ${i}/sdkconfig ${i}/build + fi + done diff --git a/dist/esp32-wifi-manager_0.0.3/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.3/CMakeLists.txt new file mode 100644 index 00000000..83e0077c --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/CMakeLists.txt @@ -0,0 +1,12 @@ +if(IDF_VERSION_MAJOR GREATER_EQUAL 4) + idf_component_register(SRC_DIRS src + REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server + INCLUDE_DIRS src + EMBED_FILES src/style.css src/code.js src/index.html) +else() + set(COMPONENT_SRCDIRS src) + set(COMPONENT_ADD_INCLUDEDIRS src) + set(COMPONENT_REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server) + set(COMPONENT_EMBED_FILES src/style.css src/code.js src/index.html) + register_component() +endif() diff --git a/dist/esp32-wifi-manager_0.0.3/Kconfig b/dist/esp32-wifi-manager_0.0.3/Kconfig new file mode 100644 index 00000000..59c5a9a8 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/Kconfig @@ -0,0 +1,81 @@ +menu "Wifi Manager Configuration" + +config WIFI_MANAGER_TASK_PRIORITY + int "RTOS Task Priority for the wifi_manager" + default 5 + help + Tasks spawn by the manager will have a priority of WIFI_MANAGER_TASK_PRIORITY-1. For this particular reason, minimum recommended task priority is 2. + +config WIFI_MANAGER_RETRY_TIMER + int "Time (in ms) between each retry attempt" + default 5000 + help + Defines the time to wait before an attempt to re-connect to a saved wifi is made after connection is lost or another unsuccesful attempt is made. + +config WIFI_MANAGER_MAX_RETRY_START_AP + int "Max Retry before starting the AP" + default 3 + help + Defines the maximum number of failed retries allowed before the WiFi manager starts its own access point. + +config WIFI_MANAGER_SHUTDOWN_AP_TIMER + int "Time (in ms) to wait before shutting down the AP" + default 60000 + help + Defines the time (in ms) to wait after a succesful connection before shutting down the access point. + +config WEBAPP_LOCATION + string "Defines the URL where the wifi manager is located" + default "/" + help + This parameter helps you relocate the wifimanager to another URL, for instance /wifimanager/ The trailing slash is important and should be included + +config DEFAULT_AP_SSID + string "Access Point SSID" + default "esp32" + help + SSID (network name) the the esp32 will broadcast. + +config DEFAULT_AP_PASSWORD + string "Access Point Password" + default "esp32pwd" + help + Password used for the Access Point. Leave empty and set AUTH MODE to WIFI_AUTH_OPEN for no password. + +config DEFAULT_AP_CHANNEL + int "Access Point WiFi Channel" + default 1 + help + Be careful you might not see the access point if you use a channel not allowed in your country. + +config DEFAULT_AP_IP + string "Access Point IP Address" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_GATEWAY + string "Access Point IP Gateway" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_NETMASK + string "Access Point Netmask" + default "255.255.255.0" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_MAX_CONNECTIONS + int "Access Point Max Connections" + default 4 + help + Max is 4. + +config DEFAULT_AP_BEACON_INTERVAL + int "Access Point Beacon Interval (ms)" + default 100 + help + 100ms is the recommended default. + +endmenu diff --git a/dist/esp32-wifi-manager_0.0.3/LICENSE.md b/dist/esp32-wifi-manager_0.0.3/LICENSE.md new file mode 100644 index 00000000..5f2ac0bc --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2017-2019 Tony Pottier + +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. diff --git a/dist/esp32-wifi-manager_0.0.3/README.md b/dist/esp32-wifi-manager_0.0.3/README.md new file mode 100644 index 00000000..43248f3d --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/README.md @@ -0,0 +1,225 @@ +# What is esp32-wifi-manager? + +### Build status [![Build Status](https://travis-ci.com/tonyp7/esp32-wifi-manager.svg?branch=master)](https://travis-ci.com/tonyp7/esp32-wifi-manager) + +*esp32-wifi-manager* is a pure C esp-idf component for ESP32 that enables easy management of wifi networks through a web portal. + +*esp32-wifi-manager* is is an all in one wifi scanner, http server & dns daemon living in the least amount of RAM possible. + +*esp32-wifi-manager* will automatically attempt to re-connect to a previously saved network on boot, and if it cannot find a saved wifi it will start its own access point through which you can manage and connect to wifi networks. Upon a succesful connection, the software will shutdown the access point automatically after some time (1 minute by default). + +*esp32-wifi-manager* compiles with esp-idf 4.2 and above. See [Getting Started](#getting-started) to guide you through your first setup. + +# Content + - [Demo](#demo) + - [Look And Feel](#look-and-feel) + - [Getting Started](#getting-started) + - [Requirements](#requirements) + - [Hello World](#hello-world) + - [Configuring the Wifi Manager](#configuring-the-wifi-manager) + - [Adding esp32-wifi-manager to your code](#adding-esp32-wifi-manager-to-your-code) + - [Interacting with the manager](#interacting-with-the-manager) + - [Interacting with the http server](#interacting-with-the-http-server) + - [Thread safety and access to NVS](#thread-safety-and-access-to-nvs) + - [License](#license) + + +# Demo +[![esp32-wifi-manager demo](http://img.youtube.com/vi/hxlZi15bym4/0.jpg)](http://www.youtube.com/watch?v=hxlZi15bym4) + +# Look and Feel +![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-password.png "esp32-wifi-manager") ![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-connected-to.png "esp32-wifi-manager") + +# Getting Started + +## Requirements + +To get you started, esp32-wifi-manager needs: + +- esp-idf **4.2 and up** +- esp32 or esp32-s2 + +There are breaking changes and new features in esp-idf 4.1 and 4.2 which makes esp32-wifi-manager incompatible with anything lower than 4.2. This includes esp_netif (introduced in 4.1) and esp_event_handler_instance_t (introduced in 4.2). It is recommended to compile esp32-wifi-manager with the master tree to avoid any compatibility issue. + +## Hello World + +Clone the repository where you want it to be. If you are unfamiliar with Git, you can use Github Desktop on Windows: + +```bash +git clone https://github.com/tonyp7/esp32-wifi-manager.git +``` + +Navigate under the included example: + +```bash +cd esp32-wifi-manager/examples/default_demo +``` + +Compile the code and load it on your esp32: + +```bash +idf.py build flash monitor +``` + +_Note: while it is encouraged to use the newer build system with idf.py and cmake, esp32-wifi-manager still supports the legacy build system. If you are using make on Linux or make using MSYS2 on Windows, you can still use "make build flash monitor" if you prefer_ + +Now, using any wifi capable device, you will see a new wifi access point named *esp32*. Connect to it using the default password *esp32pwd*. If the captive portal does not pop up on your device, you can access the wifi manager at its default IP address: http://10.10.0.1. + +## Configuring the Wifi Manager + +esp32-wifi-manager can be configured without touching its code. At the project level use: + +```bash +idf.py menuconfig +``` + +Navigate in "Component config" then pick "Wifi Manager Configuration". You will be greeted by the following screen: + +![esp32-wifi-manager-menuconfig](https://idyl.io/wp-content/uploads/2020/08/wifi-manager-menuconfig-800px.png "menuconfig screen") + +You can change the ssid and password of the access point at your convenience, but it is highly recommended to keep default values. Your password should be between 8 and 63 characters long, to comply with the WPA2 standard. If the password is set to an empty value or is less than 8 characters long, esp32-wifi-manager will create its access point as an open wifi network. + +You can also change the values for various timers, for instance how long it takes for the access point to shutdown once a connection is established (default: 60000). While it could be tempting to set this timer to 0, just be warned that in that case the user will never get the feedback that a connection is succesful. Shutting down the AP will instantly kill the current navigating session on the captive portal. + +Finally, you can choose to relocate esp32-wifi-manager to a different URL by changing the default value of "/" to something else, for instance "/wifimanager/". Please note that the trailing slash does matter. This feature is particularly useful in case you want your own webapp to co-exist with esp32-wifi-manager's own web pages. + +# Adding esp32-wifi-manager to your code + +In order to use esp32-wifi-manager effectively in your esp-idf projects, copy the whole esp32-wifi-manager repository (or git clone) into a components subfolder. + +Your project should look like this: + + - project_folder + - build + - components + - esp32-wifi-manager + - main + - main.c + +Under eclipse, this is what a typical project looks like: + +![eclipse project with esp32-wifi-manager](https://idyl.io/wp-content/uploads/2020/07/eclipse-idf-project.png "eclipse project with esp32-wifi-manager") + +Once this is done, you need to edit the CMakeLists.txt file at the root of your project to register the components folder. This is done by adding the following line: + +```cmake +set(EXTRA_COMPONENTS_DIRS components/) +``` + +A typical CmakeLists.txt file should look like this: + +```cmake +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS components/) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(name_of_your_project) +``` + +If you are using the old build system with make instead, you should edit the Makefile instead such as: + +```make +PROJECT_NAME := name_of_your_project +EXTRA_COMPONENT_DIRS := components/ +include $(IDF_PATH)/make/project.mk +``` + +Once this is done, you can now in your user code add the header: + +```c +#include "wifi_manager.h" +``` + +All you need to do now is to call wifi_manager_start(); in your code. See [examples/default_demo](examples/default_demo) if you are uncertain. + + +## Interacting with the manager + +Ther are effectively three different ways you can embed esp32-wifi-manager with your code: +* Just forget about it and poll in your code for wifi connectivity status +* Use event callbacks +* Modify esp32-wifi-manager code directly to fit your needs + +**Event callbacks** are the cleanest way to use the wifi manager and that's the recommended way to do it. A typical use-case would be to get notified when wifi manager finally gets a connection to an access point. In order to do this you can simply define a callback function: + +```c +void cb_connection_ok(void *pvParameter){ + ESP_LOGI(TAG, "I have a connection!"); +} +``` + +Then just register it by calling: + +```c +wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok); +``` + +That's it! Now everytime the event is triggered it will call this function. The [examples/default_demo](examples/default_demo) contains sample code using callbacks. + +### List of events + +The list of possible events you can add a callback to are defined by message_code_t in wifi_manager.h. They are as following: + +* WM_ORDER_START_HTTP_SERVER +* WM_ORDER_STOP_HTTP_SERVER +* WM_ORDER_START_DNS_SERVICE +* WM_ORDER_STOP_DNS_SERVICE +* WM_ORDER_START_WIFI_SCAN +* WM_ORDER_LOAD_AND_RESTORE_STA +* WM_ORDER_CONNECT_STA +* WM_ORDER_DISCONNECT_STA +* WM_ORDER_START_AP +* WM_EVENT_STA_DISCONNECTED +* WM_EVENT_SCAN_DONE +* WM_EVENT_STA_GOT_IP +* WM_ORDER_STOP_AP + +In practice, keeping track of WM_EVENT_STA_GOT_IP and WM_EVENT_STA_DISCONNECTED is key to know whether or not your esp32 has a connection. The other messages can mostly be ignored in a typical application using esp32-wifi-manager. + +### Events parameters + +Callback signature includes a void* pointer. For most events, this additional parameter is empty and sent as a NULL value. A few select events have additional data which can be leveraged by user code. They are listed below: + +* WM_EVENT_SCAN_DONE is sent with a wifi_event_sta_scan_done_t* object. +* WM_EVENT_STA_DISCONNECTED is sent with a wifi_event_sta_disconnected_t* object. +* WM_EVENT_STA_GOT_IP is sent with a ip_event_got_ip_t* object. + +These objects are standard esp-idf structures, and are documented as such in the [official pages](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html). + +The [examples/default_demo](examples/default_demo) demonstrates how you can read a ip_event_got_ip_t object to access the IP address assigned to the esp32. + +## Interacting with the http server + +Because esp32-wifi-manager spawns its own http server, you might want to extend this server to serve your own pages in your application. It is possible to do so by registering your own URL handler using the standard esp_http_server signature: + +```c +esp_err_t my_custom_handler(httpd_req_t *req){ +``` + +And then registering the handler by doing + +```c +http_app_set_handler_hook(HTTP_GET, &my_custom_handler); +``` + +The [examples/http_hook](examples/http_hook) contains an example where a web page is registered at /helloworld + +## Thread safety and access to NVS + +esp32-wifi-manager accesses the non-volatile storage to store and loads its configuration into a dedicated namespace "espwifimgr". If you want to make sure there will never be a conflict with concurrent access to the NVS, you can include nvs_sync.h and use calls to nvs_sync_lock and nvs_sync_unlock. + +```c +nvs_handle handle; + +if(nvs_sync_lock( portMAX_DELAY )){ + if(nvs_open(wifi_manager_nvs_namespace, NVS_READWRITE, &handle) == ESP_OK){ + /* do something with NVS */ + nvs_close(handle); + } + nvs_sync_unlock(); +} +``` +nvs_sync_lock waits for the number of ticks sent to it as a parameter to acquire a mutex. It is recommended to use portMAX_DELAY. In practice, nvs_sync_lock will almost never wait. + + +# License +*esp32-wifi-manager* is MIT licensed. As such, it can be included in any project, commercial or not, as long as you retain original copyright. Please make sure to read the license file. diff --git a/dist/esp32-wifi-manager_0.0.3/component.mk b/dist/esp32-wifi-manager_0.0.3/component.mk new file mode 100644 index 00000000..75e34089 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/component.mk @@ -0,0 +1,4 @@ +COMPONENT_ADD_INCLUDEDIRS = src +COMPONENT_SRCDIRS = src +COMPONENT_DEPENDS = log esp_http_server +COMPONENT_EMBED_FILES := src/style.css src/code.js src/index.html diff --git a/dist/esp32-wifi-manager_0.0.3/examples/default_demo/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/CMakeLists.txt new file mode 100644 index 00000000..6458baae --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS ../../) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(default_demo) diff --git a/dist/esp32-wifi-manager_0.0.3/examples/default_demo/Makefile b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/Makefile new file mode 100644 index 00000000..fb186183 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := wifi_manager + +EXTRA_COMPONENT_DIRS := ../../ + +include $(IDF_PATH)/make/project.mk + diff --git a/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/CMakeLists.txt new file mode 100644 index 00000000..31addfc4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/CMakeLists.txt @@ -0,0 +1,3 @@ +set(COMPONENT_SRCS "user_main.c") + +register_component() diff --git a/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/component.mk b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/component.mk new file mode 100644 index 00000000..a98f634e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/idf_component.yml b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true diff --git a/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/user_main.c b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/user_main.c new file mode 100644 index 00000000..19af535e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/main/user_main.c @@ -0,0 +1,80 @@ +/* +Copyright (c) 2017-2019 Tony Pottier + +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. + +@file main.c +@author Tony Pottier +@brief Entry point for the ESP32 application. +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" + +#include "wifi_manager.h" + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "main"; + +/** + * @brief RTOS task that periodically prints the heap memory available. + * @note Pure debug information, should not be ever started on production code! This is an example on how you can integrate your code with wifi-manager + */ +void monitoring_task(void *pvParameter) +{ + for(;;){ + ESP_LOGI(TAG, "free heap: %lu",esp_get_free_heap_size()); + vTaskDelay( pdMS_TO_TICKS(10000) ); + } +} + + +/** + * @brief this is an exemple of a callback that you can setup in your own app to get notified of wifi manager event. + */ +void cb_connection_ok(void *pvParameter){ + ip_event_got_ip_t* param = (ip_event_got_ip_t*)pvParameter; + + /* transform IP to human readable string */ + char str_ip[16]; + esp_ip4addr_ntoa(¶m->ip_info.ip, str_ip, IP4ADDR_STRLEN_MAX); + + ESP_LOGI(TAG, "I have a connection and my IP is %s!", str_ip); +} + +void app_main() +{ + /* start the wifi manager */ + wifi_manager_start(); + + /* register a callback as an example to how you can integrate your code with the wifi manager */ + wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok); +#if CONFIG_SOC_CPU_CORES_NUM > 1 + /* your code should go here. Here we simply create a task on core 2 that monitors free heap memory */ + xTaskCreatePinnedToCore(&monitoring_task, "monitoring_task", 2048, NULL, 1, NULL, 1); +#endif +} diff --git a/dist/esp32-wifi-manager_0.0.3/examples/default_demo/sdkconfig.defaults b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/sdkconfig.defaults new file mode 100644 index 00000000..f44c06f4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/default_demo/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LWIP_IPV6=y diff --git a/dist/esp32-wifi-manager_0.0.3/examples/http_hook/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/CMakeLists.txt new file mode 100644 index 00000000..377f86fe --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS ../../) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(http_hook) diff --git a/dist/esp32-wifi-manager_0.0.3/examples/http_hook/Makefile b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/Makefile new file mode 100644 index 00000000..fb186183 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := wifi_manager + +EXTRA_COMPONENT_DIRS := ../../ + +include $(IDF_PATH)/make/project.mk + diff --git a/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/CMakeLists.txt new file mode 100644 index 00000000..31addfc4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/CMakeLists.txt @@ -0,0 +1,3 @@ +set(COMPONENT_SRCS "user_main.c") + +register_component() diff --git a/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/component.mk b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/component.mk new file mode 100644 index 00000000..a98f634e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/idf_component.yml b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true diff --git a/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/user_main.c b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/user_main.c new file mode 100644 index 00000000..2446f269 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/main/user_main.c @@ -0,0 +1,76 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file user_main.c +@author Tony Pottier +@brief Entry point for the ESP32 application. +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" + +#include "wifi_manager.h" +#include "http_app.h" + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "main"; + + +static esp_err_t my_get_handler(httpd_req_t *req){ + + /* our custom page sits at /helloworld in this example */ + if(strcmp(req->uri, "/helloworld") == 0){ + + ESP_LOGI(TAG, "Serving page /helloworld"); + + const char* response = "

Hello World!

"; + + httpd_resp_set_status(req, "200 OK"); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, response, strlen(response)); + } + else{ + /* send a 404 otherwise */ + httpd_resp_send_404(req); + } + + return ESP_OK; +} + + +void app_main() +{ + /* start the wifi manager */ + wifi_manager_start(); + + /* set custom handler for the http server + * Now navigate to /helloworld to see the custom page + * */ + http_app_set_handler_hook(HTTP_GET, &my_get_handler); + +} diff --git a/dist/esp32-wifi-manager_0.0.3/examples/http_hook/sdkconfig.defaults b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/sdkconfig.defaults new file mode 100644 index 00000000..f44c06f4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/examples/http_hook/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LWIP_IPV6=y diff --git a/dist/esp32-wifi-manager_0.0.3/idf_component.yml b/dist/esp32-wifi-manager_0.0.3/idf_component.yml new file mode 100644 index 00000000..60a458ed --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/idf_component.yml @@ -0,0 +1,9 @@ +dependencies: + idf: + version: '>=4.2' +description: esp32-wifi-manager is a pure C esp-idf component for ESP32 that enables + easy management of wifi networks through a web portal. +license: MIT +repository: https://github.com/ankayca/esp32-wifi-manager/tree/update_build_system +url: https://github.com/ankayca/esp32-wifi-manager/tree/update_build_system +version: 0.0.3 diff --git a/dist/esp32-wifi-manager_0.0.3/src/ap.json b/dist/esp32-wifi-manager_0.0.3/src/ap.json new file mode 100644 index 00000000..de61f86a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/ap.json @@ -0,0 +1,12 @@ +[ +{"ssid":"Pantum-AP-A6D49F","chan":11,"rssi":-55,"auth":4}, +{"ssid":"a0308","chan":1,"rssi":-56,"auth":3}, +{"ssid":"dlink-D9D8","chan":11,"rssi":-82,"auth":4}, +{"ssid":"Linksys06730","chan":7,"rssi":-85,"auth":3}, +{"ssid":"SINGTEL-5171","chan":9,"rssi":-88,"auth":4}, +{"ssid":"1126-1","chan":11,"rssi":-89,"auth":4}, +{"ssid":"The Shah 5GHz-2","chan":1,"rssi":-90,"auth":3}, +{"ssid":"SINGTEL-1D28 (2G)","chan":11,"rssi":-91,"auth":3}, +{"ssid":"dlink-F864","chan":1,"rssi":-92,"auth":4}, +{"ssid":"dlink-74F0","chan":1,"rssi":-93,"auth":4} +] \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/code.js b/dist/esp32-wifi-manager_0.0.3/src/code.js new file mode 100644 index 00000000..934595a9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/code.js @@ -0,0 +1,352 @@ +// save some bytes +const gel = (e) => document.getElementById(e); + +const wifi_div = gel("wifi"); +const connect_div = gel("connect"); +const connect_manual_div = gel("connect_manual"); +const connect_wait_div = gel("connect-wait"); +const connect_details_div = gel("connect-details"); + +function docReady(fn) { + // see if DOM is already available + if ( + document.readyState === "complete" || + document.readyState === "interactive" + ) { + // call on next available tick + setTimeout(fn, 1); + } else { + document.addEventListener("DOMContentLoaded", fn); + } +} + +var selectedSSID = ""; +var refreshAPInterval = null; +var checkStatusInterval = null; + +function stopCheckStatusInterval() { + if (checkStatusInterval != null) { + clearInterval(checkStatusInterval); + checkStatusInterval = null; + } +} + +function stopRefreshAPInterval() { + if (refreshAPInterval != null) { + clearInterval(refreshAPInterval); + refreshAPInterval = null; + } +} + +function startCheckStatusInterval() { + checkStatusInterval = setInterval(checkStatus, 950); +} + +function startRefreshAPInterval() { + refreshAPInterval = setInterval(refreshAP, 3800); +} + +docReady(async function () { + gel("wifi-status").addEventListener( + "click", + () => { + wifi_div.style.display = "none"; + document.getElementById("connect-details").style.display = "block"; + }, + false + ); + + gel("manual_add").addEventListener( + "click", + (e) => { + selectedSSID = e.target.innerText; + + gel("ssid-pwd").textContent = selectedSSID; + wifi_div.style.display = "none"; + connect_manual_div.style.display = "block"; + connect_div.style.display = "none"; + + gel("connect-success").display = "none"; + gel("connect-fail").display = "none"; + }, + false + ); + + gel("wifi-list").addEventListener( + "click", + (e) => { + selectedSSID = e.target.innerText; + gel("ssid-pwd").textContent = selectedSSID; + connect_div.style.display = "block"; + wifi_div.style.display = "none"; + // init_cancel(); + }, + false + ); + + function cancel() { + selectedSSID = ""; + connect_div.style.display = "none"; + connect_manual_div.style.display = "none"; + wifi_div.style.display = "block"; + } + + gel("cancel").addEventListener("click", cancel, false); + + gel("manual_cancel").addEventListener("click", cancel, false); + + gel("join").addEventListener("click", performConnect, false); + + gel("manual_join").addEventListener( + "click", + (e) => { + performConnect("manual"); + }, + false + ); + + gel("ok-details").addEventListener( + "click", + () => { + connect_details_div.style.display = "none"; + wifi_div.style.display = "block"; + }, + false + ); + + gel("ok-credits").addEventListener( + "click", + () => { + gel("credits").style.display = "none"; + gel("app").style.display = "block"; + }, + false + ); + + gel("acredits").addEventListener( + "click", + () => { + event.preventDefault(); + gel("app").style.display = "none"; + gel("credits").style.display = "block"; + }, + false + ); + + gel("ok-connect").addEventListener( + "click", + () => { + connect_wait_div.style.display = "none"; + wifi_div.style.display = "block"; + }, + false + ); + + gel("disconnect").addEventListener( + "click", + () => { + gel("diag-disconnect").style.display = "block"; + gel("connect-details-wrap").classList.add("blur"); + }, + false + ); + + gel("no-disconnect").addEventListener( + "click", + () => { + gel("diag-disconnect").style.display = "none"; + gel("connect-details-wrap").classList.remove("blur"); + }, + false + ); + + gel("yes-disconnect").addEventListener("click", async () => { + stopCheckStatusInterval(); + selectedSSID = ""; + + document.getElementById("diag-disconnect").style.display = "none"; + gel("connect-details-wrap").classList.remove("blur"); + + await fetch("connect.json", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: { timestamp: Date.now() }, + }); + + startCheckStatusInterval(); + + connect_details_div.style.display = "none"; + wifi_div.style.display = "block"; + }); + + //first time the page loads: attempt get the connection status and start the wifi scan + await refreshAP(); + startCheckStatusInterval(); + startRefreshAPInterval(); +}); + +async function performConnect(conntype) { + //stop the status refresh. This prevents a race condition where a status + //request would be refreshed with wrong ip info from a previous connection + //and the request would automatically shows as succesful. + stopCheckStatusInterval(); + + //stop refreshing wifi list + stopRefreshAPInterval(); + + var pwd; + if (conntype == "manual") { + //Grab the manual SSID and PWD + selectedSSID = gel("manual_ssid").value; + pwd = gel("manual_pwd").value; + } else { + pwd = gel("pwd").value; + } + //reset connection + gel("loading").style.display = "block"; + gel("connect-success").style.display = "none"; + gel("connect-fail").style.display = "none"; + + gel("ok-connect").disabled = true; + gel("ssid-wait").textContent = selectedSSID; + connect_div.style.display = "none"; + connect_manual_div.style.display = "none"; + connect_wait_div.style.display = "block"; + + await fetch("connect.json", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Custom-ssid": selectedSSID, + "X-Custom-pwd": pwd, + }, + body: { timestamp: Date.now() }, + }); + + //now we can re-set the intervals regardless of result + startCheckStatusInterval(); + startRefreshAPInterval(); +} + +function rssiToIcon(rssi) { + if (rssi >= -60) { + return "w0"; + } else if (rssi >= -67) { + return "w1"; + } else if (rssi >= -75) { + return "w2"; + } else { + return "w3"; + } +} + +async function refreshAP(url = "ap.json") { + try { + var res = await fetch(url); + var access_points = await res.json(); + if (access_points.length > 0) { + //sort by signal strength + access_points.sort((a, b) => { + var x = a["rssi"]; + var y = b["rssi"]; + return x < y ? 1 : x > y ? -1 : 0; + }); + refreshAPHTML(access_points); + } + } catch (e) { + console.info("Access points returned empty from /ap.json!"); + } +} + +function refreshAPHTML(data) { + var h = ""; + data.forEach(function (e, idx, array) { + let ap_class = idx === array.length - 1 ? "" : " brdb"; + let rssicon = rssiToIcon(e.rssi); + let auth = e.auth == 0 ? "" : "pw"; + h += `
${e.ssid}
\n`; + }); + + gel("wifi-list").innerHTML = h; +} + +async function checkStatus(url = "status.json") { + try { + var response = await fetch(url); + var data = await response.json(); + if (data && data.hasOwnProperty("ssid") && data["ssid"] != "") { + if (data["ssid"] === selectedSSID) { + // Attempting connection + switch (data["urc"]) { + case 0: + console.info("Got connection!"); + document.querySelector( + "#connected-to div div div span" + ).textContent = data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = data["ip"]; + gel("netmask").textContent = data["netmask"]; + gel("gw").textContent = data["gw"]; + gel("wifi-status").style.display = "block"; + + //unlock the wait screen if needed + gel("ok-connect").disabled = false; + + //update wait screen + gel("loading").style.display = "none"; + gel("connect-success").style.display = "block"; + gel("connect-fail").style.display = "none"; + break; + case 1: + console.info("Connection attempt failed!"); + document.querySelector( + "#connected-to div div div span" + ).textContent = data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = "0.0.0.0"; + gel("netmask").textContent = "0.0.0.0"; + gel("gw").textContent = "0.0.0.0"; + + //don't show any connection + gel("wifi-status").display = "none"; + + //unlock the wait screen + gel("ok-connect").disabled = false; + + //update wait screen + gel("loading").display = "none"; + gel("connect-fail").style.display = "block"; + gel("connect-success").style.display = "none"; + break; + } + } else if (data.hasOwnProperty("urc") && data["urc"] === 0) { + console.info("Connection established"); + //ESP32 is already connected to a wifi without having the user do anything + if ( + gel("wifi-status").style.display == "" || + gel("wifi-status").style.display == "none" + ) { + document.querySelector("#connected-to div div div span").textContent = + data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = data["ip"]; + gel("netmask").textContent = data["netmask"]; + gel("gw").textContent = data["gw"]; + gel("wifi-status").style.display = "block"; + } + } + } else if (data.hasOwnProperty("urc") && data["urc"] === 2) { + console.log("Manual disconnect requested..."); + if (gel("wifi-status").style.display == "block") { + gel("wifi-status").style.display = "none"; + } + } + } catch (e) { + console.info("Was not able to fetch /status.json"); + } +} diff --git a/dist/esp32-wifi-manager_0.0.3/src/component.mk b/dist/esp32-wifi-manager_0.0.3/src/component.mk new file mode 100644 index 00000000..0b9d7585 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/dist/esp32-wifi-manager_0.0.3/src/compress.bat b/dist/esp32-wifi-manager_0.0.3/src/compress.bat new file mode 100644 index 00000000..b9caad22 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/compress.bat @@ -0,0 +1,2 @@ +gzip index.html style.css --best --keep --force +pause \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/connect b/dist/esp32-wifi-manager_0.0.3/src/connect new file mode 100644 index 00000000..8c7fe211 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/connect @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/dns_server.c b/dist/esp32-wifi-manager_0.0.3/src/dns_server.c new file mode 100644 index 00000000..6693913a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/dns_server.c @@ -0,0 +1,184 @@ +/* +Copyright (c) 2019 Tony Pottier + +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. + +@file dns_server.c +@author Tony Pottier +@brief Defines an extremely basic DNS server for captive portal functionality. +It's basically a DNS hijack that replies to the esp's address no matter which +request is sent to it. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wifi_manager.h" +#include "dns_server.h" + +static const char TAG[] = "dns_server"; +static TaskHandle_t task_dns_server = NULL; +int socket_fd; + +void dns_server_start() { + if(task_dns_server == NULL){ + xTaskCreate(&dns_server, "dns_server", 3072, NULL, WIFI_MANAGER_TASK_PRIORITY-1, &task_dns_server); + } +} + +void dns_server_stop(){ + if(task_dns_server){ + vTaskDelete(task_dns_server); + close(socket_fd); + task_dns_server = NULL; + } + +} + + + +void dns_server(void *pvParameters) { + + + + struct sockaddr_in ra; + + /* Set redirection DNS hijack to the access point IP */ + ip4_addr_t ip_resolved; + inet_pton(AF_INET, DEFAULT_AP_IP, &ip_resolved); + + + /* Create UDP socket */ + socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (socket_fd < 0){ + ESP_LOGE(TAG, "Failed to create socket"); + exit(0); + } + + /* Bind to port 53 (typical DNS Server port) */ + esp_netif_ip_info_t ip; + esp_netif_t* netif_sta = wifi_manager_get_esp_netif_sta(); + ESP_ERROR_CHECK(esp_netif_get_ip_info(netif_sta, &ip)); + ra.sin_family = AF_INET; + ra.sin_addr.s_addr = ip.ip.addr; + ra.sin_port = htons(53); + if (bind(socket_fd, (struct sockaddr *)&ra, sizeof(struct sockaddr_in)) == -1) { + ESP_LOGE(TAG, "Failed to bind to 53/udp"); + close(socket_fd); + exit(1); + } + + struct sockaddr_in client; + socklen_t client_len; + client_len = sizeof(client); + int length; + uint8_t data[DNS_QUERY_MAX_SIZE]; /* dns query buffer */ + uint8_t response[DNS_ANSWER_MAX_SIZE]; /* dns response buffer */ + char ip_address[INET_ADDRSTRLEN]; /* buffer to store IPs as text. This is only used for debug and serves no other purpose */ + char *domain; /* This is only used for debug and serves no other purpose */ + int err; + + ESP_LOGI(TAG, "DNS Server listening on 53/udp"); + + /* Start loop to process DNS requests */ + for(;;) { + + memset(data, 0x00, sizeof(data)); /* reset buffer */ + length = recvfrom(socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client, &client_len); /* read udp request */ + + /*if the query is bigger than the buffer size we simply ignore it. This case should only happen in case of multiple + * queries within the same DNS packet and is not supported by this simple DNS hijack. */ + if ( length > 0 && ((length + sizeof(dns_answer_t)-1) < DNS_ANSWER_MAX_SIZE) ) { + + data[length] = '\0'; /*in case there's a bogus domain name that isn't null terminated */ + + /* Generate header message */ + memcpy(response, data, sizeof(dns_header_t)); + dns_header_t *dns_header = (dns_header_t*)response; + dns_header->QR = 1; /*response bit */ + dns_header->OPCode = DNS_OPCODE_QUERY; /* no support for other type of response */ + dns_header->AA = 1; /*authoritative answer */ + dns_header->RCode = DNS_REPLY_CODE_NO_ERROR; /* no error */ + dns_header->TC = 0; /*no truncation */ + dns_header->RD = 0; /*no recursion */ + dns_header->ANCount = dns_header->QDCount; /* set answer count = question count -- duhh! */ + dns_header->NSCount = 0x0000; /* name server resource records = 0 */ + dns_header->ARCount = 0x0000; /* resource records = 0 */ + + + /* copy the rest of the query in the response */ + memcpy(response + sizeof(dns_header_t), data + sizeof(dns_header_t), length - sizeof(dns_header_t)); + + + /* extract domain name and request IP for debug */ + inet_ntop(AF_INET, &(client.sin_addr), ip_address, INET_ADDRSTRLEN); + domain = (char*) &data[sizeof(dns_header_t) + 1]; + for(char* c=domain; *c != '\0'; c++){ + if(*c < ' ' || *c > 'z') *c = '.'; /* technically we should test if the first two bits are 00 (e.g. if( (*c & 0xC0) == 0x00) *c = '.') but this makes the code a lot more readable */ + } + ESP_LOGI(TAG, "Replying to DNS request for %s from %s", domain, ip_address); + + + /* create DNS answer at the end of the query*/ + dns_answer_t *dns_answer = (dns_answer_t*)&response[length]; + dns_answer->NAME = __bswap_16(0xC00C); /* This is a pointer to the beginning of the question. As per DNS standard, first two bits must be set to 11 for some odd reason hence 0xC0 */ + dns_answer->TYPE = __bswap_16(DNS_ANSWER_TYPE_A); + dns_answer->CLASS = __bswap_16(DNS_ANSWER_CLASS_IN); + dns_answer->TTL = (uint32_t)0x00000000; /* no caching. Avoids DNS poisoning since this is a DNS hijack */ + dns_answer->RDLENGTH = __bswap_16(0x0004); /* 4 byte => size of an ipv4 address */ + dns_answer->RDATA = ip_resolved.addr; + + err = sendto(socket_fd, response, length+sizeof(dns_answer_t), 0, (struct sockaddr *)&client, client_len); + if (err < 0) { + ESP_LOGE(TAG, "UDP sendto failed: %d", err); + } + } + + taskYIELD(); /* allows the freeRTOS scheduler to take over if needed. DNS daemon should not be taxing on the system */ + + } + close(socket_fd); + + vTaskDelete ( NULL ); +} + + + + diff --git a/dist/esp32-wifi-manager_0.0.3/src/dns_server.h b/dist/esp32-wifi-manager_0.0.3/src/dns_server.h new file mode 100644 index 00000000..d1da1f8c --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/dns_server.h @@ -0,0 +1,137 @@ +/* +Copyright (c) 2019 Tony Pottier + +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. + +@file dns_server.h +@author Tony Pottier +@brief Defines an extremly basic DNS server for captive portal functionality. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +@see http://www.zytrax.com/books/dns/ch15 +*/ + +#ifndef MAIN_DNS_SERVER_H_ +#define MAIN_DNS_SERVER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + + +/** 12 byte header, 64 byte domain name, 4 byte qtype/qclass. This NOT compliant with the RFC, but it's good enough for a captive portal + * if a DNS query is too big it just wont be processed. */ +#define DNS_QUERY_MAX_SIZE 80 + +/** Query + 2 byte ptr, 2 byte type, 2 byte class, 4 byte TTL, 2 byte len, 4 byte data */ +#define DNS_ANSWER_MAX_SIZE (DNS_QUERY_MAX_SIZE+16) + + +/** + * @brief RCODE values used in a DNS header message + */ +typedef enum dns_reply_code_t { + DNS_REPLY_CODE_NO_ERROR = 0, + DNS_REPLY_CODE_FORM_ERROR = 1, + DNS_REPLY_CODE_SERVER_FAILURE = 2, + DNS_REPLY_CODE_NON_EXISTANT_DOMAIN = 3, + DNS_REPLY_CODE_NOT_IMPLEMENTED = 4, + DNS_REPLY_CODE_REFUSED = 5, + DNS_REPLY_CODE_YXDOMAIN = 6, + DNS_REPLY_CODE_YXRRSET = 7, + DNS_REPLY_CODE_NXRRSET = 8 +}dns_reply_code_t; + + + +/** + * @brief OPCODE values used in a DNS header message + */ +typedef enum dns_opcode_code_t { + DNS_OPCODE_QUERY = 0, + DNS_OPCODE_IQUERY = 1, + DNS_OPCODE_STATUS = 2 +}dns_opcode_code_t; + + + +/** + * @brief Represents a 12 byte DNS header. + * __packed__ is needed to prevent potential unwanted memory alignments + */ +typedef struct __attribute__((__packed__)) dns_header_t{ + uint16_t ID; // identification number + uint8_t RD : 1; // recursion desired + uint8_t TC : 1; // truncated message + uint8_t AA : 1; // authoritive answer + uint8_t OPCode : 4; // message_type + uint8_t QR : 1; // query/response flag + uint8_t RCode : 4; // response code + uint8_t Z : 3; // its z! reserved + uint8_t RA : 1; // recursion available + uint16_t QDCount; // number of question entries + uint16_t ANCount; // number of answer entries + uint16_t NSCount; // number of authority entries + uint16_t ARCount; // number of resource entries +}dns_header_t; + + + +typedef enum dns_answer_type_t { + DNS_ANSWER_TYPE_A = 1, + DNS_ANSWER_TYPE_NS = 2, + DNS_ANSWER_TYPE_CNAME = 5, + DNS_ANSWER_TYPE_SOA = 6, + DNS_ANSWER_TYPE_WKS = 11, + DNS_ANSWER_TYPE_PTR = 12, + DNS_ANSWER_TYPE_MX = 15, + DNS_ANSWER_TYPE_SRV = 33, + DNS_ANSWER_TYPE_AAAA = 28 +}dns_answer_type_t; + +typedef enum dns_answer_class_t { + DNS_ANSWER_CLASS_IN = 1 +}dns_answer_class_t; + + + +typedef struct __attribute__((__packed__)) dns_answer_t{ + uint16_t NAME; /* for the sake of simplicity only 16 bit pointers are supported */ + uint16_t TYPE; /* Unsigned 16 bit value. The resource record types - determines the content of the RDATA field. */ + uint16_t CLASS; /* Class of response. */ + uint32_t TTL; /* The time in seconds that the record may be cached. A value of 0 indicates the record should not be cached. */ + uint16_t RDLENGTH; /* Unsigned 16-bit value that defines the length in bytes of the RDATA record. */ + uint32_t RDATA; /* For the sake of simplicity only ipv4 is supported, and as such it's a unsigned 32 bit */ +}dns_answer_t; + +void dns_server(void *pvParameters); +void dns_server_start(); +void dns_server_stop(); + + + +#ifdef __cplusplus +} +#endif + + +#endif /* MAIN_DNS_SERVER_H_ */ diff --git a/dist/esp32-wifi-manager_0.0.3/src/http_app.c b/dist/esp32-wifi-manager_0.0.3/src/http_app.c new file mode 100644 index 00000000..8dbc40ef --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/http_app.c @@ -0,0 +1,479 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file http_app.c +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include "esp_netif.h" +#include + +#include "wifi_manager.h" +#include "http_app.h" + + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "http_server"; + +/* @brief the HTTP server handle */ +static httpd_handle_t httpd_handle = NULL; + +/* function pointers to URI handlers that can be user made */ +esp_err_t (*custom_get_httpd_uri_handler)(httpd_req_t *r) = NULL; +esp_err_t (*custom_post_httpd_uri_handler)(httpd_req_t *r) = NULL; + +/* strings holding the URLs of the wifi manager */ +static char* http_root_url = NULL; +static char* http_redirect_url = NULL; +static char* http_js_url = NULL; +static char* http_css_url = NULL; +static char* http_connect_url = NULL; +static char* http_ap_url = NULL; +static char* http_status_url = NULL; + +/** + * @brief embedded binary data. + * @see file "component.mk" + * @see https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html#embedding-binary-data + */ +extern const uint8_t style_css_start[] asm("_binary_style_css_start"); +extern const uint8_t style_css_end[] asm("_binary_style_css_end"); +extern const uint8_t code_js_start[] asm("_binary_code_js_start"); +extern const uint8_t code_js_end[] asm("_binary_code_js_end"); +extern const uint8_t index_html_start[] asm("_binary_index_html_start"); +extern const uint8_t index_html_end[] asm("_binary_index_html_end"); + + +/* const httpd related values stored in ROM */ +const static char http_200_hdr[] = "200 OK"; +const static char http_302_hdr[] = "302 Found"; +const static char http_400_hdr[] = "400 Bad Request"; +const static char http_404_hdr[] = "404 Not Found"; +const static char http_503_hdr[] = "503 Service Unavailable"; +const static char http_location_hdr[] = "Location"; +const static char http_content_type_html[] = "text/html"; +const static char http_content_type_js[] = "text/javascript"; +const static char http_content_type_css[] = "text/css"; +const static char http_content_type_json[] = "application/json"; +const static char http_cache_control_hdr[] = "Cache-Control"; +const static char http_cache_control_no_cache[] = "no-store, no-cache, must-revalidate, max-age=0"; +const static char http_cache_control_cache[] = "public, max-age=31536000"; +const static char http_pragma_hdr[] = "Pragma"; +const static char http_pragma_no_cache[] = "no-cache"; + + + +esp_err_t http_app_set_handler_hook( httpd_method_t method, esp_err_t (*handler)(httpd_req_t *r) ){ + + if(method == HTTP_GET){ + custom_get_httpd_uri_handler = handler; + return ESP_OK; + } + else if(method == HTTP_POST){ + custom_post_httpd_uri_handler = handler; + return ESP_OK; + } + else{ + return ESP_ERR_INVALID_ARG; + } + +} + + +static esp_err_t http_server_delete_handler(httpd_req_t *req){ + + ESP_LOGI(TAG, "DELETE %s", req->uri); + + /* DELETE /connect.json */ + if(strcmp(req->uri, http_connect_url) == 0){ + wifi_manager_disconnect_async(); + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, NULL, 0); + } + else{ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + + return ESP_OK; +} + + +static esp_err_t http_server_post_handler(httpd_req_t *req){ + + + esp_err_t ret = ESP_OK; + + ESP_LOGI(TAG, "POST %s", req->uri); + + /* POST /connect.json */ + if(strcmp(req->uri, http_connect_url) == 0){ + + + /* buffers for the headers */ + size_t ssid_len = 0, password_len = 0; + char *ssid = NULL, *password = NULL; + + /* len of values provided */ + ssid_len = httpd_req_get_hdr_value_len(req, "X-Custom-ssid"); + password_len = httpd_req_get_hdr_value_len(req, "X-Custom-pwd"); + + + if(ssid_len && ssid_len <= MAX_SSID_SIZE && password_len && password_len <= MAX_PASSWORD_SIZE){ + + /* get the actual value of the headers */ + ssid = malloc(sizeof(char) * (ssid_len + 1)); + password = malloc(sizeof(char) * (password_len + 1)); + httpd_req_get_hdr_value_str(req, "X-Custom-ssid", ssid, ssid_len+1); + httpd_req_get_hdr_value_str(req, "X-Custom-pwd", password, password_len+1); + + wifi_config_t* config = wifi_manager_get_wifi_sta_config(); + memset(config, 0x00, sizeof(wifi_config_t)); + memcpy(config->sta.ssid, ssid, ssid_len); + memcpy(config->sta.password, password, password_len); + ESP_LOGI(TAG, "ssid: %s, password: %s", ssid, password); + ESP_LOGD(TAG, "http_server_post_handler: wifi_manager_connect_async() call"); + wifi_manager_connect_async(); + + /* free memory */ + free(ssid); + free(password); + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, NULL, 0); + + } + else{ + /* bad request the authentification header is not complete/not the correct format */ + httpd_resp_set_status(req, http_400_hdr); + httpd_resp_send(req, NULL, 0); + } + + } + else{ + + if(custom_post_httpd_uri_handler == NULL){ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + else{ + + /* if there's a hook, run it */ + ret = (*custom_post_httpd_uri_handler)(req); + } + } + + return ret; +} + + +static esp_err_t http_server_get_handler(httpd_req_t *req){ + + char* host = NULL; + size_t buf_len; + esp_err_t ret = ESP_OK; + + ESP_LOGD(TAG, "GET %s", req->uri); + + /* Get header value string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1; + if (buf_len > 1) { + host = malloc(buf_len); + if(httpd_req_get_hdr_value_str(req, "Host", host, buf_len) != ESP_OK){ + /* if something is wrong we just 0 the whole memory */ + memset(host, 0x00, buf_len); + } + } + + /* determine if Host is from the STA IP address */ + wifi_manager_lock_sta_ip_string(portMAX_DELAY); + bool access_from_sta_ip = host != NULL?strstr(host, wifi_manager_get_sta_ip_string()):false; + wifi_manager_unlock_sta_ip_string(); + + + if (host != NULL && !strstr(host, DEFAULT_AP_IP) && !access_from_sta_ip) { + + /* Captive Portal functionality */ + /* 302 Redirect to IP of the access point */ + httpd_resp_set_status(req, http_302_hdr); + httpd_resp_set_hdr(req, http_location_hdr, http_redirect_url); + httpd_resp_send(req, NULL, 0); + + } + else{ + + /* GET / */ + if(strcmp(req->uri, http_root_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_html); + httpd_resp_send(req, (char*)index_html_start, index_html_end - index_html_start); + } + /* GET /code.js */ + else if(strcmp(req->uri, http_js_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_js); + httpd_resp_send(req, (char*)code_js_start, code_js_end - code_js_start); + } + /* GET /style.css */ + else if(strcmp(req->uri, http_css_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_css); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_cache); + httpd_resp_send(req, (char*)style_css_start, style_css_end - style_css_start); + } + /* GET /ap.json */ + else if(strcmp(req->uri, http_ap_url) == 0){ + + /* if we can get the mutex, write the last version of the AP list */ + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + char* ap_buf = wifi_manager_get_ap_list_json(); + httpd_resp_send(req, ap_buf, strlen(ap_buf)); + wifi_manager_unlock_json_buffer(); + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /ap.json failed to obtain mutex"); + } + + /* request a wifi scan */ + wifi_manager_scan_async(); + } + /* GET /status.json */ + else if(strcmp(req->uri, http_status_url) == 0){ + + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + char *buff = wifi_manager_get_ip_info_json(); + if(buff){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, buff, strlen(buff)); + wifi_manager_unlock_json_buffer(); + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + } + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /status.json failed to obtain mutex"); + } + } + else{ + + if(custom_get_httpd_uri_handler == NULL){ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + else{ + + /* if there's a hook, run it */ + ret = (*custom_get_httpd_uri_handler)(req); + } + } + + } + + /* memory clean up */ + if(host != NULL){ + free(host); + } + + return ret; + +} + +/* URI wild card for any GET request */ +static const httpd_uri_t http_server_get_request = { + .uri = "*", + .method = HTTP_GET, + .handler = http_server_get_handler +}; + +static const httpd_uri_t http_server_post_request = { + .uri = "*", + .method = HTTP_POST, + .handler = http_server_post_handler +}; + +static const httpd_uri_t http_server_delete_request = { + .uri = "*", + .method = HTTP_DELETE, + .handler = http_server_delete_handler +}; + + +void http_app_stop(){ + + if(httpd_handle != NULL){ + + + /* dealloc URLs */ + if(http_root_url) { + free(http_root_url); + http_root_url = NULL; + } + if(http_redirect_url){ + free(http_redirect_url); + http_redirect_url = NULL; + } + if(http_js_url){ + free(http_js_url); + http_js_url = NULL; + } + if(http_css_url){ + free(http_css_url); + http_css_url = NULL; + } + if(http_connect_url){ + free(http_connect_url); + http_connect_url = NULL; + } + if(http_ap_url){ + free(http_ap_url); + http_ap_url = NULL; + } + if(http_status_url){ + free(http_status_url); + http_status_url = NULL; + } + + /* stop server */ + httpd_stop(httpd_handle); + httpd_handle = NULL; + } +} + + +/** + * @brief helper to generate URLs of the wifi manager + */ +static char* http_app_generate_url(const char* page){ + + char* ret; + + int root_len = strlen(WEBAPP_LOCATION); + const size_t url_sz = sizeof(char) * ( (root_len+1) + ( strlen(page) + 1) ); + + ret = malloc(url_sz); + memset(ret, 0x00, url_sz); + strcpy(ret, WEBAPP_LOCATION); + ret = strcat(ret, page); + + return ret; +} + +void http_app_start(bool lru_purge_enable){ + + esp_err_t err; + + if(httpd_handle == NULL){ + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + /* this is an important option that isn't set up by default. + * We could register all URLs one by one, but this would not work while the fake DNS is active */ + config.uri_match_fn = httpd_uri_match_wildcard; + config.lru_purge_enable = lru_purge_enable; + + /* generate the URLs */ + if(http_root_url == NULL){ + int root_len = strlen(WEBAPP_LOCATION); + + /* all the pages */ + const char page_js[] = "code.js"; + const char page_css[] = "style.css"; + const char page_connect[] = "connect.json"; + const char page_ap[] = "ap.json"; + const char page_status[] = "status.json"; + + /* root url, eg "/" */ + const size_t http_root_url_sz = sizeof(char) * (root_len+1); + http_root_url = malloc(http_root_url_sz); + memset(http_root_url, 0x00, http_root_url_sz); + strcpy(http_root_url, WEBAPP_LOCATION); + + /* redirect url */ + size_t redirect_sz = 22 + root_len + 1; /* strlen(http://255.255.255.255) + strlen("/") + 1 for \0 */ + http_redirect_url = malloc(sizeof(char) * redirect_sz); + *http_redirect_url = '\0'; + + if(root_len == 1){ + snprintf(http_redirect_url, redirect_sz, "http://%s", DEFAULT_AP_IP); + } + else{ + snprintf(http_redirect_url, redirect_sz, "http://%s%s", DEFAULT_AP_IP, WEBAPP_LOCATION); + } + + /* generate the other pages URLs*/ + http_js_url = http_app_generate_url(page_js); + http_css_url = http_app_generate_url(page_css); + http_connect_url = http_app_generate_url(page_connect); + http_ap_url = http_app_generate_url(page_ap); + http_status_url = http_app_generate_url(page_status); + + } + + err = httpd_start(&httpd_handle, &config); + + if (err == ESP_OK) { + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(httpd_handle, &http_server_get_request); + httpd_register_uri_handler(httpd_handle, &http_server_post_request); + httpd_register_uri_handler(httpd_handle, &http_server_delete_request); + } + } + +} diff --git a/dist/esp32-wifi-manager_0.0.3/src/http_app.h b/dist/esp32-wifi-manager_0.0.3/src/http_app.h new file mode 100644 index 00000000..53f9472a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/http_app.h @@ -0,0 +1,73 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file http_app.h +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#ifndef HTTP_APP_H_INCLUDED +#define HTTP_APP_H_INCLUDED + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/** @brief Defines the URL where the wifi manager is located + * By default it is at the server root (ie "/"). If you wish to add your own webpages + * you may want to relocate the wifi manager to another URL, for instance /wifimanager + */ +#define WEBAPP_LOCATION CONFIG_WEBAPP_LOCATION + + +/** + * @brief spawns the http server + */ +void http_app_start(bool lru_purge_enable); + +/** + * @brief stops the http server + */ +void http_app_stop(); + +/** + * @brief sets a hook into the wifi manager URI handlers. Setting the handler to NULL disables the hook. + * @return ESP_OK in case of success, ESP_ERR_INVALID_ARG if the method is unsupported. + */ +esp_err_t http_app_set_handler_hook( httpd_method_t method, esp_err_t (*handler)(httpd_req_t *r) ); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/dist/esp32-wifi-manager_0.0.3/src/index.html b/dist/esp32-wifi-manager_0.0.3/src/index.html new file mode 100644 index 00000000..ce4bf146 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/index.html @@ -0,0 +1,136 @@ + + + + + + + + + esp32-wifi-manager + + +
+
+ +
+
+

Enter Details

+
+

Manual Connection

+
+ + +
+
+ + +
+
+
+
+

Enter Password

+
+

Password for

+
+ +
+
+ + +
+
+
+
+

Please wait...

+
+

Connecting to

+
+
+
+

You may lose wifi access while the esp32 recalibrates its radio. Please wait until your device automatically reconnects. This can take up to 30s.

+
+
+

Success!

+
+
+

Connection failed

+

Please double-check wifi password if any and make sure the access point has good signal.

+
+
+
+ +
+
+
+
+
+

+
+

+
+
+ +
+
+

IP Address

+
+
IP Address:
+
Subnet Mask:
+
Default Gateway:
+
+
+ +
+
+
+
+

Are you sure you would like to disconnect from this wifi?

+
+ + +
+
+
+
+
+
+
+
+

About this app...

+
+

+
+

esp32-wifi-manager, © 2017-2020, Tony Pottier
Licended under the MIT License.

+

+ This app would not be possible without the following libraries: +

+
    +
  • SpinKit, © 2015, Tobias Ahlin. Licensed under the MIT License.
  • +
  • jQuery, The jQuery Foundation. Licensed under the MIT License.
  • +
  • cJSON, © 2009-2017, Dave Gamble and cJSON contributors. Licensed under the MIT License.
  • +
+
+
+ +
+
+ + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/json.c b/dist/esp32-wifi-manager_0.0.3/src/json.c new file mode 100644 index 00000000..d448711a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/json.c @@ -0,0 +1,143 @@ +/* +@file json.c +@brief handles very basic JSON with a minimal footprint on the system + +This code is a lightly modified version of cJSON 1.4.7. cJSON is licensed under the MIT license: +Copyright (c) 2009 Dave Gamble + +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. + +@see https://github.com/DaveGamble/cJSON +*/ + +#include +#include +#include +#include +#include "json.h" + + +bool json_print_string(const unsigned char *input, unsigned char *output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + //output = ensure(output_buffer, sizeof("\"\""), hooks); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + if (strchr("\"\\\b\f\n\r\t", *input_pointer)) + { + /* one character escape sequence */ + escape_characters++; + } + else if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + /* in the original cJSON it is possible to realloc here in case output buffer is too small. + * This is overkill for an embedded system. */ + output = output_buffer; + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + diff --git a/dist/esp32-wifi-manager_0.0.3/src/json.h b/dist/esp32-wifi-manager_0.0.3/src/json.h new file mode 100644 index 00000000..4c1c8c75 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/json.h @@ -0,0 +1,47 @@ +/* +@file json.h +@brief handles very basic JSON with a minimal footprint on the system + +This code is a lightly modified version of cJSON 1.4.7. cJSON is licensed under the MIT license: +Copyright (c) 2009 Dave Gamble + +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. + +@see https://github.com/DaveGamble/cJSON +*/ + +#ifndef JSON_H_INCLUDED +#define JSON_H_INCLUDED + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Render the cstring provided to a JSON escaped version that can be printed. + * @param input the input buffer to be escaped. + * @param output_buffer the output buffer to write to. You must ensure it is big enough to contain the final string. + * @see cJSON equivlaent static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) + */ +bool json_print_string(const unsigned char *input, unsigned char *output_buffer); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_H_INCLUDED */ diff --git a/dist/esp32-wifi-manager_0.0.3/src/lock.svg b/dist/esp32-wifi-manager_0.0.3/src/lock.svg new file mode 100644 index 00000000..f7ebc7e9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/lock.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/nvs_sync.c b/dist/esp32-wifi-manager_0.0.3/src/nvs_sync.c new file mode 100644 index 00000000..4ceed8bd --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/nvs_sync.c @@ -0,0 +1,79 @@ +/** +Copyright (c) 2020 Tony Pottier + +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. + +@file nvs_sync.c +@author Tony Pottier +@brief Exposes a simple API to synchronize NVS memory read and writes + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include "nvs_sync.h" + + +static SemaphoreHandle_t nvs_sync_mutex = NULL; + +esp_err_t nvs_sync_create(){ + if(nvs_sync_mutex == NULL){ + + nvs_sync_mutex = xSemaphoreCreateMutex(); + + if(nvs_sync_mutex){ + return ESP_OK; + } + else{ + return ESP_FAIL; + } + } + else{ + return ESP_OK; + } +} + +void nvs_sync_free(){ + if(nvs_sync_mutex != NULL){ + vSemaphoreDelete( nvs_sync_mutex ); + nvs_sync_mutex = NULL; + } +} + +bool nvs_sync_lock(TickType_t xTicksToWait){ + if(nvs_sync_mutex){ + if( xSemaphoreTake( nvs_sync_mutex, xTicksToWait ) == pdTRUE ) { + return true; + } + else{ + return false; + } + } + else{ + return false; + } +} + +void nvs_sync_unlock(){ + xSemaphoreGive( nvs_sync_mutex ); +} \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/nvs_sync.h b/dist/esp32-wifi-manager_0.0.3/src/nvs_sync.h new file mode 100644 index 00000000..e03396e5 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/nvs_sync.h @@ -0,0 +1,76 @@ +/** +Copyright (c) 2020 Tony Pottier + +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. + +@file nvs_sync.h +@author Tony Pottier +@brief Exposes a simple API to synchronize NVS memory read and writes + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + + + +#ifndef WIFI_MANAGER_NVS_SYNC_H_INCLUDED +#define WIFI_MANAGER_NVS_SYNC_H_INCLUDED + +#include /* for type bool */ +#include /* for TickType_t */ +#include /* for esp_err_t */ + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Attempts to get hold of the NVS semaphore for a set amount of ticks. + * @note If you are uncertain about the number of ticks to wait use portMAX_DELAY. + * @return true on a succesful lock, false otherwise + */ +bool nvs_sync_lock(TickType_t xTicksToWait); + + +/** + * @brief Releases the NVS semaphore + */ +void nvs_sync_unlock(); + + +/** + * @brief Create the NVS semaphore + * @return ESP_OK: success or if the semaphore already exists + * ESP_FAIL: failure + */ +esp_err_t nvs_sync_create(); + +/** + * @brief Frees memory associated with the NVS semaphore + * @warning Do not delete a semaphore that has tasks blocked on it (tasks that are in the Blocked state waiting for the semaphore to become available). + */ +void nvs_sync_free(); + + +#ifdef __cplusplus +} +#endif + +#endif /* WIFI_MANAGER_NVS_SYNC_H_INCLUDED */ \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/settings.svg b/dist/esp32-wifi-manager_0.0.3/src/settings.svg new file mode 100644 index 00000000..19cacede --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/settings.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/status b/dist/esp32-wifi-manager_0.0.3/src/status new file mode 100644 index 00000000..3824a535 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/status @@ -0,0 +1 @@ +{"ssid":"zodmgbbq","ip":"192.168.1.119","netmask":"255.255.255.0","gw":"192.168.1.1","urc":0} \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/style.css b/dist/esp32-wifi-manager_0.0.3/src/style.css new file mode 100644 index 00000000..5c83cc2f --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/style.css @@ -0,0 +1,248 @@ +body { + background-color: #eee; + border: 0; + margin: 0; + font: 1.1em tahoma, arial, sans-serif; +} +a { + color: darkblue; + transition: color .2s ease-out; + text-decoration: none +} +a:hover { + color: red; +} +input { + display: none; + font: 1.1em tahoma, arial, sans-serif; +} +input:focus, +select:focus, +textarea:focus, +button:focus { + outline: none; +} +input[type="button"] { + width: 100px; + padding: 5px; + text-align: center; + display: block; +} +p { + padding: 10px; +} +#credits { + display: none; +} +#app {} #app-wrap {} #disconnect { + width: 150px; +} +.diag-box { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + height: 100%; + width: 100%; + display: none; +} +.diag-box-win { + position: absolute; + left: 10%; + width: 80%; + text-align: center; + border: 2px outset #888; + background-color: #fff; + border-radius: 10px; + top: 20%; +} +.blur { + -webkit-filter: blur(2px); + -moz-filter: blur(2px); + -ms-filter: blur(2px); + -o-filter: blur(2px); + filter: blur(2px); +} +.ape { + margin-left: 20px; + padding: 10px 0px 10px 10px; +} +.ape:hover { + cursor: pointer; +} +.brdb { + border-bottom: 1px solid #888; +} +header { + background-color: #fff; + border-bottom: 1px solid #888; +} +section { + background-color: #fff; + border-bottom: 1px solid #888; + border-top: 1px solid #888; +} +h1 { + display: block; + text-align: center; + margin: 0; + padding: 15px; + font-size: 1.4em +} +h2 { + margin: 0; + margin-top: 20px; + padding: 10px; + text-transform: uppercase; + color: #888; + font-size: 1.0em +} +h3 { + margin: 0; + text-align: center; + padding: 20px 0px 20px 0px; +} +.gr { + color: green; +} +.rd { + color: red; +} +#wifi-status { + display: none; +} +#connect { + display: none; +} +#connect_manual { + display: none; +} +#manual_ssid { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +#manual_pwd { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +#pwd { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +.buttons { + padding: 15px; +} +#join { + float: right; +} +#manual_join { + float: right; +} +#yes-disconnect { + display: inline-block; + margin-left: 20px; +} +#no-disconnect { + display: inline-block; +} +.ctr { + margin: 0 auto; +} +.tctr { + text-align: center; +} +#connect-wait { + display: none; +} +#connect-success { + display: none; +} +#connect-fail { + display: none; +} +#connect-details { + display: none; +} +.fr { + float: right; + margin-right: 20px; +} +.w0 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPHBhdGggZD0iTTUsMTNMNywxNUM5Ljc2LDEyLjI0IDE0LjI0LDEyLjI0IDE3LDE1TDE5LDEzQzE1LjE0LDkuMTQgOC44Nyw5LjE0IDUsMTNaIiBzdHlsZT0iZmlsbDogYmxhY2s7Ii8+CjxwYXRoIGQ9Ik05LDE3TDEyLDIwTDE1LDE3QzEzLjM1LDE1LjM0IDEwLjY2LDE1LjM0IDksMTdaIiBzdHlsZT0iZmlsbDogYmxhY2s7Ii8+Cjwvc3ZnPgo=') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w1 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPHBhdGggZD0iTTksMTdMMTIsMjBMMTUsMTdDMTMuMzUsMTUuMzQgMTAuNjYsMTUuMzQgOSwxN1oiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPC9zdmc+Cg==') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w2 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNOSwxN0wxMiwyMEwxNSwxN0MxMy4zNSwxNS4zNCAxMC42NiwxNS4zNCA5LDE3WiIgc3R5bGU9ImZpbGw6IGJsYWNrOyIvPgo8L3N2Zz4K') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w3 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNOSwxN0wxMiwyMEwxNSwxN0MxMy4zNSwxNS4zNCAxMC42NiwxNS4zNCA5LDE3WiIgc3R5bGU9ImZpbGw6IGdyYXk7Ii8+Cjwvc3ZnPgo=') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.pw { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIHN0eWxlPSJmaWxsOiBibGFjazsiIGQ9Ik0xOCA4aC0xVjZjMC0yLjc2LTIuMjQtNS01LTVTNyAzLjI0IDcgNnYySDZjLTEuMSAwLTIgLjktMiAydjEwYzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJWMTBjMC0xLjEtLjktMi0yLTJ6bS02IDljLTEuMSAwLTItLjktMi0ycy45LTIgMi0yIDIgLjkgMiAyLS45IDItMiAyem0zLjEtOUg4LjlWNmMwLTEuNzEgMS4zOS0zLjEgMy4xLTMuMSAxLjcxIDAgMy4xIDEuMzkgMy4xIDMuMXYyeiI+PC9wYXRoPgo8L3N2Zz4=') no-repeat right top; + height: 24px; + margin-right: 30px; +} +/* SpinKit is licensed under the MIT License. Copyright (c) 2015 Tobias Ahlin */ + +.spinner { + width: 40px; + height: 40px; + position: relative; + margin: 100px auto; +} +.double-bounce1, +.double-bounce2 { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #333; + opacity: 0.6; + position: absolute; + top: 0; + left: 0; + -webkit-animation: sk-bounce 2.0s infinite ease-in-out; + animation: sk-bounce 2.0s infinite ease-in-out; +} +.double-bounce2 { + -webkit-animation-delay: -1.0s; + animation-delay: -1.0s; +} +@-webkit-keyframes sk-bounce { + 0%, 100% { + -webkit-transform: scale(0.0) + } + 50% { + -webkit-transform: scale(1.0) + } +} +@keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + -webkit-transform: scale(0.0); + } + 50% { + transform: scale(1.0); + -webkit-transform: scale(1.0); + } +} +/* end of SpinKit */ \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.3/src/wifi0.svg b/dist/esp32-wifi-manager_0.0.3/src/wifi0.svg new file mode 100644 index 00000000..394a6495 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/wifi0.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.3/src/wifi1.svg b/dist/esp32-wifi-manager_0.0.3/src/wifi1.svg new file mode 100644 index 00000000..c64211bf --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/wifi1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.3/src/wifi2.svg b/dist/esp32-wifi-manager_0.0.3/src/wifi2.svg new file mode 100644 index 00000000..033b9de9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/wifi2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.3/src/wifi3.svg b/dist/esp32-wifi-manager_0.0.3/src/wifi3.svg new file mode 100644 index 00000000..7cd5667e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/wifi3.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.3/src/wifi_manager.c b/dist/esp32-wifi-manager_0.0.3/src/wifi_manager.c new file mode 100644 index 00000000..3d11a940 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/wifi_manager.c @@ -0,0 +1,1336 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file wifi_manager.c +@author Tony Pottier +@brief Defines all functions necessary for esp32 to connect to a wifi/scan wifis + +Contains the freeRTOS task and all necessary support + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include +#include "esp_system.h" +#include +#include +#include +#include +#include +#include +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_wifi_types.h" +#include "esp_log.h" +#include "nvs.h" +#include "nvs_flash.h" +#include "mdns.h" +#include "lwip/api.h" +#include "lwip/err.h" +#include "lwip/netdb.h" +#include "lwip/ip4_addr.h" + + +#include "json.h" +#include "dns_server.h" +#include "nvs_sync.h" +#include "wifi_manager.h" + + + +/* objects used to manipulate the main queue of events */ +QueueHandle_t wifi_manager_queue; + +/* @brief software timer to wait between each connection retry. + * There is no point hogging a hardware timer for a functionality like this which only needs to be 'accurate enough' */ +TimerHandle_t wifi_manager_retry_timer = NULL; + +/* @brief software timer that will trigger shutdown of the AP after a succesful STA connection + * There is no point hogging a hardware timer for a functionality like this which only needs to be 'accurate enough' */ +TimerHandle_t wifi_manager_shutdown_ap_timer = NULL; + +SemaphoreHandle_t wifi_manager_json_mutex = NULL; +SemaphoreHandle_t wifi_manager_sta_ip_mutex = NULL; +char *wifi_manager_sta_ip = NULL; +uint16_t ap_num = MAX_AP_NUM; +wifi_ap_record_t *accessp_records; +char *accessp_json = NULL; +char *ip_info_json = NULL; +wifi_config_t* wifi_manager_config_sta = NULL; + +/* @brief Array of callback function pointers */ +void (**cb_ptr_arr)(void*) = NULL; + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "wifi_manager"; + +/* @brief task handle for the main wifi_manager task */ +static TaskHandle_t task_wifi_manager = NULL; + +/* @brief netif object for the STATION */ +static esp_netif_t* esp_netif_sta = NULL; + +/* @brief netif object for the ACCESS POINT */ +static esp_netif_t* esp_netif_ap = NULL; + +/** + * The actual WiFi settings in use + */ +struct wifi_settings_t wifi_settings = { + .ap_ssid = DEFAULT_AP_SSID, + .ap_pwd = DEFAULT_AP_PASSWORD, + .ap_channel = DEFAULT_AP_CHANNEL, + .ap_ssid_hidden = DEFAULT_AP_SSID_HIDDEN, + .ap_bandwidth = DEFAULT_AP_BANDWIDTH, + .sta_only = DEFAULT_STA_ONLY, + .sta_power_save = DEFAULT_STA_POWER_SAVE, + .sta_static_ip = 0, +}; + +const char wifi_manager_nvs_namespace[] = "espwifimgr"; + +static EventGroupHandle_t wifi_manager_event_group; + +/* @brief indicate that the ESP32 is currently connected. */ +const int WIFI_MANAGER_WIFI_CONNECTED_BIT = BIT0; + +const int WIFI_MANAGER_AP_STA_CONNECTED_BIT = BIT1; + +/* @brief Set automatically once the SoftAP is started */ +const int WIFI_MANAGER_AP_STARTED_BIT = BIT2; + +/* @brief When set, means a client requested to connect to an access point.*/ +const int WIFI_MANAGER_REQUEST_STA_CONNECT_BIT = BIT3; + +/* @brief This bit is set automatically as soon as a connection was lost */ +const int WIFI_MANAGER_STA_DISCONNECT_BIT = BIT4; + +/* @brief When set, means the wifi manager attempts to restore a previously saved connection at startup. */ +const int WIFI_MANAGER_REQUEST_RESTORE_STA_BIT = BIT5; + +/* @brief When set, means a client requested to disconnect from currently connected AP. */ +const int WIFI_MANAGER_REQUEST_WIFI_DISCONNECT_BIT = BIT6; + +/* @brief When set, means a scan is in progress */ +const int WIFI_MANAGER_SCAN_BIT = BIT7; + +/* @brief When set, means user requested for a disconnect */ +const int WIFI_MANAGER_REQUEST_DISCONNECT_BIT = BIT8; + + + +void wifi_manager_timer_retry_cb( TimerHandle_t xTimer ){ + + ESP_LOGI(TAG, "Retry Timer Tick! Sending ORDER_CONNECT_STA with reason CONNECTION_REQUEST_AUTO_RECONNECT"); + + /* stop the timer */ + xTimerStop( xTimer, (TickType_t) 0 ); + + /* Attempt to reconnect */ + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_AUTO_RECONNECT); + +} + +void wifi_manager_timer_shutdown_ap_cb( TimerHandle_t xTimer){ + + /* stop the timer */ + xTimerStop( xTimer, (TickType_t) 0 ); + + /* Attempt to shutdown AP */ + wifi_manager_send_message(WM_ORDER_STOP_AP, NULL); +} + +void wifi_manager_scan_async(){ + wifi_manager_send_message(WM_ORDER_START_WIFI_SCAN, NULL); +} + +void wifi_manager_disconnect_async(){ + wifi_manager_send_message(WM_ORDER_DISCONNECT_STA, NULL); +} + + +void wifi_manager_start(){ + + /* disable the default wifi logging */ + esp_log_level_set("wifi", ESP_LOG_NONE); + + /* initialize flash memory */ + nvs_flash_init(); + ESP_ERROR_CHECK(nvs_sync_create()); /* semaphore for thread synchronization on NVS memory */ + + /* memory allocation */ + wifi_manager_queue = xQueueCreate( 3, sizeof( queue_message) ); + wifi_manager_json_mutex = xSemaphoreCreateMutex(); + accessp_records = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * MAX_AP_NUM); + accessp_json = (char*)malloc(MAX_AP_NUM * JSON_ONE_APP_SIZE + 4); /* 4 bytes for json encapsulation of "[\n" and "]\0" */ + wifi_manager_clear_access_points_json(); + ip_info_json = (char*)malloc(sizeof(char) * JSON_IP_INFO_SIZE); + wifi_manager_clear_ip_info_json(); + wifi_manager_config_sta = (wifi_config_t*)malloc(sizeof(wifi_config_t)); + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + memset(&wifi_settings.sta_static_ip_config, 0x00, sizeof(esp_netif_ip_info_t)); + cb_ptr_arr = malloc(sizeof(void (*)(void*)) * WM_MESSAGE_CODE_COUNT); + for(int i=0; ista.ssid) != 0){ + /* different ssid or ssid does not exist in flash: save new ssid */ + esp_err = nvs_set_blob(handle, "ssid", wifi_manager_config_sta->sta.ssid, 32); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + ESP_LOGI(TAG, "wifi_manager_wrote wifi_sta_config: ssid:%s",wifi_manager_config_sta->sta.ssid); + + } + + sz = sizeof(tmp_conf.sta.password); + esp_err = nvs_get_blob(handle, "password", tmp_conf.sta.password, &sz); + if( (esp_err == ESP_OK || esp_err == ESP_ERR_NVS_NOT_FOUND) && strcmp( (char*)tmp_conf.sta.password, (char*)wifi_manager_config_sta->sta.password) != 0){ + /* different password or password does not exist in flash: save new password */ + esp_err = nvs_set_blob(handle, "password", wifi_manager_config_sta->sta.password, 64); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + ESP_LOGI(TAG, "wifi_manager_wrote wifi_sta_config: password:%s",wifi_manager_config_sta->sta.password); + } + + sz = sizeof(tmp_settings); + esp_err = nvs_get_blob(handle, "settings", &tmp_settings, &sz); + if( (esp_err == ESP_OK || esp_err == ESP_ERR_NVS_NOT_FOUND) && + ( + strcmp( (char*)tmp_settings.ap_ssid, (char*)wifi_settings.ap_ssid) != 0 || + strcmp( (char*)tmp_settings.ap_pwd, (char*)wifi_settings.ap_pwd) != 0 || + tmp_settings.ap_ssid_hidden != wifi_settings.ap_ssid_hidden || + tmp_settings.ap_bandwidth != wifi_settings.ap_bandwidth || + tmp_settings.sta_only != wifi_settings.sta_only || + tmp_settings.sta_power_save != wifi_settings.sta_power_save || + tmp_settings.ap_channel != wifi_settings.ap_channel + ) + ){ + esp_err = nvs_set_blob(handle, "settings", &wifi_settings, sizeof(wifi_settings)); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_ssid: %s",wifi_settings.ap_ssid); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_pwd: %s",wifi_settings.ap_pwd); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_channel: %i",wifi_settings.ap_channel); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_hidden (1 = yes): %i",wifi_settings.ap_ssid_hidden); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_bandwidth (1 = 20MHz, 2 = 40MHz): %i",wifi_settings.ap_bandwidth); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: sta_only (0 = APSTA, 1 = STA when connected): %i",wifi_settings.sta_only); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: sta_power_save (1 = yes): %i",wifi_settings.sta_power_save); + } + + if(change){ + esp_err = nvs_commit(handle); + } + else{ + ESP_LOGI(TAG, "Wifi config was not saved to flash because no change has been detected."); + } + + if (esp_err != ESP_OK) return esp_err; + + nvs_close(handle); + nvs_sync_unlock(); + + } + else{ + ESP_LOGE(TAG, "wifi_manager_save_sta_config failed to acquire nvs_sync mutex"); + } + + return ESP_OK; +} + +bool wifi_manager_fetch_wifi_sta_config(){ + + nvs_handle handle; + esp_err_t esp_err; + if(nvs_sync_lock( portMAX_DELAY )){ + + esp_err = nvs_open(wifi_manager_nvs_namespace, NVS_READONLY, &handle); + + if(esp_err != ESP_OK){ + nvs_sync_unlock(); + return false; + } + + if(wifi_manager_config_sta == NULL){ + wifi_manager_config_sta = (wifi_config_t*)malloc(sizeof(wifi_config_t)); + } + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + + /* allocate buffer */ + size_t sz = sizeof(wifi_settings); + uint8_t *buff = (uint8_t*)malloc(sizeof(uint8_t) * sz); + memset(buff, 0x00, sizeof(sz)); + + /* ssid */ + sz = sizeof(wifi_manager_config_sta->sta.ssid); + esp_err = nvs_get_blob(handle, "ssid", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(wifi_manager_config_sta->sta.ssid, buff, sz); + + /* password */ + sz = sizeof(wifi_manager_config_sta->sta.password); + esp_err = nvs_get_blob(handle, "password", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(wifi_manager_config_sta->sta.password, buff, sz); + + /* settings */ + sz = sizeof(wifi_settings); + esp_err = nvs_get_blob(handle, "settings", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(&wifi_settings, buff, sz); + + free(buff); + nvs_close(handle); + nvs_sync_unlock(); + + + ESP_LOGI(TAG, "wifi_manager_fetch_wifi_sta_config: ssid:%s password:%s",wifi_manager_config_sta->sta.ssid,wifi_manager_config_sta->sta.password); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_ssid:%s",wifi_settings.ap_ssid); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_pwd:%s",wifi_settings.ap_pwd); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_channel:%i",wifi_settings.ap_channel); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_hidden (1 = yes):%i",wifi_settings.ap_ssid_hidden); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_bandwidth (1 = 20MHz, 2 = 40MHz)%i",wifi_settings.ap_bandwidth); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_only (0 = APSTA, 1 = STA when connected):%i",wifi_settings.sta_only); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_power_save (1 = yes):%i",wifi_settings.sta_power_save); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_static_ip (0 = dhcp client, 1 = static ip):%i",wifi_settings.sta_static_ip); + + return wifi_manager_config_sta->sta.ssid[0] != '\0'; + + + } + else{ + return false; + } + +} + + +void wifi_manager_clear_ip_info_json(){ + strcpy(ip_info_json, "{}\n"); +} + + +void wifi_manager_generate_ip_info_json(update_reason_code_t update_reason_code){ + + wifi_config_t *config = wifi_manager_get_wifi_sta_config(); + if(config){ + + const char *ip_info_json_format = ",\"ip\":\"%s\",\"netmask\":\"%s\",\"gw\":\"%s\",\"urc\":%d}\n"; + + memset(ip_info_json, 0x00, JSON_IP_INFO_SIZE); + + /* to avoid declaring a new buffer we copy the data directly into the buffer at its correct address */ + strcpy(ip_info_json, "{\"ssid\":"); + json_print_string(config->sta.ssid, (unsigned char*)(ip_info_json+strlen(ip_info_json)) ); + + size_t ip_info_json_len = strlen(ip_info_json); + size_t remaining = JSON_IP_INFO_SIZE - ip_info_json_len; + if(update_reason_code == UPDATE_CONNECTION_OK){ + /* rest of the information is copied after the ssid */ + esp_netif_ip_info_t ip_info; + ESP_ERROR_CHECK(esp_netif_get_ip_info(esp_netif_sta, &ip_info)); + + char ip[IP4ADDR_STRLEN_MAX]; /* note: IP4ADDR_STRLEN_MAX is defined in lwip */ + char gw[IP4ADDR_STRLEN_MAX]; + char netmask[IP4ADDR_STRLEN_MAX]; + + esp_ip4addr_ntoa(&ip_info.ip, ip, IP4ADDR_STRLEN_MAX); + esp_ip4addr_ntoa(&ip_info.gw, gw, IP4ADDR_STRLEN_MAX); + esp_ip4addr_ntoa(&ip_info.netmask, netmask, IP4ADDR_STRLEN_MAX); + + + snprintf( (ip_info_json + ip_info_json_len), remaining, ip_info_json_format, + ip, + netmask, + gw, + (int)update_reason_code); + } + else{ + /* notify in the json output the reason code why this was updated without a connection */ + snprintf( (ip_info_json + ip_info_json_len), remaining, ip_info_json_format, + "0", + "0", + "0", + (int)update_reason_code); + } + } + else{ + wifi_manager_clear_ip_info_json(); + } + + +} + + +void wifi_manager_clear_access_points_json(){ + strcpy(accessp_json, "[]\n"); +} +void wifi_manager_generate_acess_points_json(){ + + strcpy(accessp_json, "["); + + + const char oneap_str[] = ",\"chan\":%d,\"rssi\":%d,\"auth\":%d}%c\n"; + + /* stack buffer to hold on to one AP until it's copied over to accessp_json */ + char one_ap[JSON_ONE_APP_SIZE]; + for(int i=0; i and + * . + * + * Another thing deserves our attention is that the default behavior of LwIP is to abort all TCP socket connections on + * receiving the disconnect. Most of time it is not a problem. However, for some special application, this may not be + * what they want, consider following scenarios: + * + * The application creates a TCP connection to maintain the application-level keep-alive data that is sent out + * every 60 seconds. + * + * Due to certain reasons, the Wi-Fi connection is cut off, and the is raised. + * According to the current implementation, all TCP connections will be removed and the keep-alive socket will be + * in a wrong status. However, since the application designer believes that the network layer should NOT care about + * this error at the Wi-Fi layer, the application does not close the socket. + * + * Five seconds later, the Wi-Fi connection is restored because esp_wifi_connect() is called in the application + * event callback function. Moreover, the station connects to the same AP and gets the same IPV4 address as before. + * + * Sixty seconds later, when the application sends out data with the keep-alive socket, the socket returns an error + * and the application closes the socket and re-creates it when necessary. + * + * In above scenario, ideally, the application sockets and the network layer should not be affected, since the Wi-Fi + * connection only fails temporarily and recovers very quickly. The application can enable “Keep TCP connections when + * IP changed” via LwIP menuconfig.*/ + case WIFI_EVENT_STA_DISCONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); + + wifi_event_sta_disconnected_t* wifi_event_sta_disconnected = (wifi_event_sta_disconnected_t*)malloc(sizeof(wifi_event_sta_disconnected_t)); + *wifi_event_sta_disconnected = *( (wifi_event_sta_disconnected_t*)event_data ); + + /* if a DISCONNECT message is posted while a scan is in progress this scan will NEVER end, causing scan to never work again. For this reason SCAN_BIT is cleared too */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_WIFI_CONNECTED_BIT | WIFI_MANAGER_SCAN_BIT); + + /* post disconnect event with reason code */ + wifi_manager_send_message(WM_EVENT_STA_DISCONNECTED, (void*)wifi_event_sta_disconnected ); + break; + + /* This event arises when the AP to which the station is connected changes its authentication mode, e.g., from no auth + * to WPA. Upon receiving this event, the event task will do nothing. Generally, the application event callback does + * not need to handle this either. */ + case WIFI_EVENT_STA_AUTHMODE_CHANGE: + ESP_LOGI(TAG, "WIFI_EVENT_STA_AUTHMODE_CHANGE"); + break; + + case WIFI_EVENT_AP_START: + ESP_LOGI(TAG, "WIFI_EVENT_AP_START"); + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_AP_STARTED_BIT); + break; + + case WIFI_EVENT_AP_STOP: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STOP"); + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_AP_STARTED_BIT); + break; + + /* Every time a station is connected to ESP32 AP, the will arise. Upon receiving this + * event, the event task will do nothing, and the application callback can also ignore it. However, you may want + * to do something, for example, to get the info of the connected STA, etc. */ + case WIFI_EVENT_AP_STACONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STACONNECTED"); + break; + + /* This event can happen in the following scenarios: + * The application calls esp_wifi_disconnect(), or esp_wifi_deauth_sta(), to manually disconnect the station. + * The Wi-Fi driver kicks off the station, e.g. because the AP has not received any packets in the past five minutes, etc. + * The station kicks off the AP. + * When this event happens, the event task will do nothing, but the application event callback needs to do + * something, e.g., close the socket which is related to this station, etc. */ + case WIFI_EVENT_AP_STADISCONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STADISCONNECTED"); + break; + + /* This event is disabled by default. The application can enable it via API esp_wifi_set_event_mask(). + * When this event is enabled, it will be raised each time the AP receives a probe request. */ + case WIFI_EVENT_AP_PROBEREQRECVED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_PROBEREQRECVED"); + break; + + } /* end switch */ + } + else if(event_base == IP_EVENT){ + + switch(event_id){ + + /* This event arises when the DHCP client successfully gets the IPV4 address from the DHCP server, + * or when the IPV4 address is changed. The event means that everything is ready and the application can begin + * its tasks (e.g., creating sockets). + * The IPV4 may be changed because of the following reasons: + * The DHCP client fails to renew/rebind the IPV4 address, and the station’s IPV4 is reset to 0. + * The DHCP client rebinds to a different address. + * The static-configured IPV4 address is changed. + * Whether the IPV4 address is changed or NOT is indicated by field ip_change of ip_event_got_ip_t. + * The socket is based on the IPV4 address, which means that, if the IPV4 changes, all sockets relating to this + * IPV4 will become abnormal. Upon receiving this event, the application needs to close all sockets and recreate + * the application when the IPV4 changes to a valid one. */ + case IP_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP"); + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_WIFI_CONNECTED_BIT); + ip_event_got_ip_t* ip_event_got_ip = (ip_event_got_ip_t*)malloc(sizeof(ip_event_got_ip_t)); + *ip_event_got_ip = *( (ip_event_got_ip_t*)event_data ); + wifi_manager_send_message(WM_EVENT_STA_GOT_IP, (void*)(ip_event_got_ip) ); + break; + + /* This event arises when the IPV6 SLAAC support auto-configures an address for the ESP32, or when this address changes. + * The event means that everything is ready and the application can begin its tasks (e.g., creating sockets). */ + case IP_EVENT_GOT_IP6: + ESP_LOGI(TAG, "IP_EVENT_GOT_IP6"); + break; + + /* This event arises when the IPV4 address become invalid. + * IP_STA_LOST_IP doesn’t arise immediately after the WiFi disconnects, instead it starts an IPV4 address lost timer, + * if the IPV4 address is got before ip lost timer expires, IP_EVENT_STA_LOST_IP doesn’t happen. Otherwise, the event + * arises when IPV4 address lost timer expires. + * Generally the application don’t need to care about this event, it is just a debug event to let the application + * know that the IPV4 address is lost. */ + case IP_EVENT_STA_LOST_IP: + ESP_LOGI(TAG, "IP_EVENT_STA_LOST_IP"); + break; + + } + } + +} + + +wifi_config_t* wifi_manager_get_wifi_sta_config(){ + return wifi_manager_config_sta; +} + + +void wifi_manager_connect_async(){ + /* in order to avoid a false positive on the front end app we need to quickly flush the ip json + * There'se a risk the front end sees an IP or a password error when in fact + * it's a remnant from a previous connection + */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_clear_ip_info_json(); + wifi_manager_unlock_json_buffer(); + } + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_USER); +} + + +char* wifi_manager_get_ip_info_json(){ + return ip_info_json; +} + + +void wifi_manager_destroy(){ + + vTaskDelete(task_wifi_manager); + task_wifi_manager = NULL; + + /* heap buffers */ + free(accessp_records); + accessp_records = NULL; + free(accessp_json); + accessp_json = NULL; + free(ip_info_json); + ip_info_json = NULL; + free(wifi_manager_sta_ip); + wifi_manager_sta_ip = NULL; + if(wifi_manager_config_sta){ + free(wifi_manager_config_sta); + wifi_manager_config_sta = NULL; + } + + /* RTOS objects */ + vSemaphoreDelete(wifi_manager_json_mutex); + wifi_manager_json_mutex = NULL; + vSemaphoreDelete(wifi_manager_sta_ip_mutex); + wifi_manager_sta_ip_mutex = NULL; + vEventGroupDelete(wifi_manager_event_group); + wifi_manager_event_group = NULL; + vQueueDelete(wifi_manager_queue); + wifi_manager_queue = NULL; + + +} + + +void wifi_manager_filter_unique( wifi_ap_record_t * aplist, uint16_t * aps) { + int total_unique; + wifi_ap_record_t * first_free; + total_unique=*aps; + + first_free=NULL; + + for(int i=0; i<*aps-1;i++) { + wifi_ap_record_t * ap = &aplist[i]; + + /* skip the previously removed APs */ + if (ap->ssid[0] == 0) continue; + + /* remove the identical SSID+authmodes */ + for(int j=i+1; j<*aps;j++) { + wifi_ap_record_t * ap1 = &aplist[j]; + if ( (strcmp((const char *)ap->ssid, (const char *)ap1->ssid)==0) && + (ap->authmode == ap1->authmode) ) { /* same SSID, different auth mode is skipped */ + /* save the rssi for the display */ + if ((ap1->rssi) > (ap->rssi)) ap->rssi=ap1->rssi; + /* clearing the record */ + memset(ap1,0, sizeof(wifi_ap_record_t)); + } + } + } + /* reorder the list so APs follow each other in the list */ + for(int i=0; i<*aps;i++) { + wifi_ap_record_t * ap = &aplist[i]; + /* skipping all that has no name */ + if (ap->ssid[0] == 0) { + /* mark the first free slot */ + if (first_free==NULL) first_free=ap; + total_unique--; + continue; + } + if (first_free!=NULL) { + memcpy(first_free, ap, sizeof(wifi_ap_record_t)); + memset(ap,0, sizeof(wifi_ap_record_t)); + /* find the next free slot */ + for(int j=0; j<*aps;j++) { + if (aplist[j].ssid[0]==0) { + first_free=&aplist[j]; + break; + } + } + } + } + /* update the length of the list */ + *aps = total_unique; +} + + +BaseType_t wifi_manager_send_message_to_front(message_code_t code, void *param){ + queue_message msg; + msg.code = code; + msg.param = param; + return xQueueSendToFront( wifi_manager_queue, &msg, portMAX_DELAY); +} + +BaseType_t wifi_manager_send_message(message_code_t code, void *param){ + queue_message msg; + msg.code = code; + msg.param = param; + return xQueueSend( wifi_manager_queue, &msg, portMAX_DELAY); +} + + +void wifi_manager_set_callback(message_code_t message_code, void (*func_ptr)(void*) ){ + + if(cb_ptr_arr && message_code < WM_MESSAGE_CODE_COUNT){ + cb_ptr_arr[message_code] = func_ptr; + } +} + +esp_netif_t* wifi_manager_get_esp_netif_ap(){ + return esp_netif_ap; +} + +esp_netif_t* wifi_manager_get_esp_netif_sta(){ + return esp_netif_sta; +} + +void wifi_manager( void * pvParameters ){ + + + queue_message msg; + BaseType_t xStatus; + EventBits_t uxBits; + uint8_t retries = 0; + + + /* initialize the tcp stack */ + ESP_ERROR_CHECK(esp_netif_init()); + + /* event loop for the wifi driver */ + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + esp_netif_sta = esp_netif_create_default_wifi_sta(); + esp_netif_ap = esp_netif_create_default_wifi_ap(); + + + /* default wifi config */ + wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&wifi_init_config)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + + /* event handler for the connection */ + esp_event_handler_instance_t instance_wifi_event; + esp_event_handler_instance_t instance_ip_event; + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_manager_event_handler, NULL,&instance_wifi_event)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_manager_event_handler, NULL,&instance_ip_event)); + + + /* SoftAP - Wifi Access Point configuration setup */ + wifi_config_t ap_config = { + .ap = { + .ssid_len = 0, + .channel = wifi_settings.ap_channel, + .ssid_hidden = wifi_settings.ap_ssid_hidden, + .max_connection = DEFAULT_AP_MAX_CONNECTIONS, + .beacon_interval = DEFAULT_AP_BEACON_INTERVAL, + }, + }; + memcpy(ap_config.ap.ssid, wifi_settings.ap_ssid , sizeof(wifi_settings.ap_ssid)); + + /* if the password lenght is under 8 char which is the minium for WPA2, the access point starts as open */ + if(strlen( (char*)wifi_settings.ap_pwd) < WPA2_MINIMUM_PASSWORD_LENGTH){ + ap_config.ap.authmode = WIFI_AUTH_OPEN; + memset( ap_config.ap.password, 0x00, sizeof(ap_config.ap.password) ); + } + else{ + ap_config.ap.authmode = WIFI_AUTH_WPA2_PSK; + memcpy(ap_config.ap.password, wifi_settings.ap_pwd, sizeof(wifi_settings.ap_pwd)); + } + + + /* DHCP AP configuration */ + esp_netif_dhcps_stop(esp_netif_ap); /* DHCP client/server must be stopped before setting new IP information. */ + esp_netif_ip_info_t ap_ip_info; + memset(&ap_ip_info, 0x00, sizeof(ap_ip_info)); + inet_pton(AF_INET, DEFAULT_AP_IP, &ap_ip_info.ip); + inet_pton(AF_INET, DEFAULT_AP_GATEWAY, &ap_ip_info.gw); + inet_pton(AF_INET, DEFAULT_AP_NETMASK, &ap_ip_info.netmask); + ESP_ERROR_CHECK(esp_netif_set_ip_info(esp_netif_ap, &ap_ip_info)); + ESP_ERROR_CHECK(esp_netif_dhcps_start(esp_netif_ap)); + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &ap_config)); + ESP_ERROR_CHECK(esp_wifi_set_bandwidth(WIFI_IF_AP, wifi_settings.ap_bandwidth)); + ESP_ERROR_CHECK(esp_wifi_set_ps(wifi_settings.sta_power_save)); + + + /* by default the mode is STA because wifi_manager will not start the access point unless it has to! */ + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); + + /* start http server */ + http_app_start(false); + + /* wifi scanner config */ + wifi_scan_config_t scan_config = { + .ssid = 0, + .bssid = 0, + .channel = 0, + .show_hidden = true + }; + + /* enqueue first event: load previous config */ + wifi_manager_send_message(WM_ORDER_LOAD_AND_RESTORE_STA, NULL); + + + /* main processing loop */ + for(;;){ + xStatus = xQueueReceive( wifi_manager_queue, &msg, portMAX_DELAY ); + + if( xStatus == pdPASS ){ + switch(msg.code){ + + case WM_EVENT_SCAN_DONE:{ + wifi_event_sta_scan_done_t *evt_scan_done = (wifi_event_sta_scan_done_t*)msg.param; + /* only check for AP if the scan is succesful */ + if(evt_scan_done->status == 0){ + /* As input param, it stores max AP number ap_records can hold. As output param, it receives the actual AP number this API returns. + * As a consequence, ap_num MUST be reset to MAX_AP_NUM at every scan */ + ap_num = MAX_AP_NUM; + ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_num, accessp_records)); + /* make sure the http server isn't trying to access the list while it gets refreshed */ + if(wifi_manager_lock_json_buffer( pdMS_TO_TICKS(1000) )){ + /* Will remove the duplicate SSIDs from the list and update ap_num */ + wifi_manager_filter_unique(accessp_records, &ap_num); + wifi_manager_generate_acess_points_json(); + wifi_manager_unlock_json_buffer(); + } + else{ + ESP_LOGE(TAG, "could not get access to json mutex in wifi_scan"); + } + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(evt_scan_done); + } + break; + + case WM_ORDER_START_WIFI_SCAN: + ESP_LOGD(TAG, "MESSAGE: ORDER_START_WIFI_SCAN"); + + /* if a scan is already in progress this message is simply ignored thanks to the WIFI_MANAGER_SCAN_BIT uxBit */ + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if(! (uxBits & WIFI_MANAGER_SCAN_BIT) ){ + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_SCAN_BIT); + ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, false)); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_LOAD_AND_RESTORE_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_LOAD_AND_RESTORE_STA"); + if(wifi_manager_fetch_wifi_sta_config()){ + ESP_LOGI(TAG, "Saved wifi found on startup. Will attempt to connect."); + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_RESTORE_CONNECTION); + } + else{ + /* no wifi saved: start soft AP! This is what should happen during a first run */ + ESP_LOGI(TAG, "No saved wifi found on startup. Starting access point."); + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_CONNECT_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_CONNECT_STA"); + + /* very important: precise that this connection attempt is specifically requested. + * Param in that case is a boolean indicating if the request was made automatically + * by the wifi_manager. + * */ + if((BaseType_t)msg.param == CONNECTION_REQUEST_USER) { + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + } + else if((BaseType_t)msg.param == CONNECTION_REQUEST_RESTORE_CONNECTION) { + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + } + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if( ! (uxBits & WIFI_MANAGER_WIFI_CONNECTED_BIT) ){ + /* update config to latest and attempt connection */ + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, wifi_manager_get_wifi_sta_config())); + + /* if there is a wifi scan in progress abort it first + Calling esp_wifi_scan_stop will trigger a SCAN_DONE event which will reset this bit */ + if(uxBits & WIFI_MANAGER_SCAN_BIT){ + esp_wifi_scan_stop(); + } + ESP_ERROR_CHECK(esp_wifi_connect()); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_EVENT_STA_DISCONNECTED: + ;wifi_event_sta_disconnected_t* wifi_event_sta_disconnected = (wifi_event_sta_disconnected_t*)msg.param; + ESP_LOGI(TAG, "MESSAGE: EVENT_STA_DISCONNECTED with Reason code: %d", wifi_event_sta_disconnected->reason); + + /* this even can be posted in numerous different conditions + * + * 1. SSID password is wrong + * 2. Manual disconnection ordered + * 3. Connection lost + * + * Having clear understand as to WHY the event was posted is key to having an efficient wifi manager + * + * With wifi_manager, we determine: + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT is set, We consider it's a client that requested the connection. + * When SYSTEM_EVENT_STA_DISCONNECTED is posted, it's probably a password/something went wrong with the handshake. + * + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT is set, it's a disconnection that was ASKED by the client (clicking disconnect in the app) + * When SYSTEM_EVENT_STA_DISCONNECTED is posted, saved wifi is erased from the NVS memory. + * + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT and WIFI_MANAGER_REQUEST_STA_CONNECT_BIT are NOT set, it's a lost connection + * + * In this version of the software, reason codes are not used. They are indicated here for potential future usage. + * + * REASON CODE: + * 1 UNSPECIFIED + * 2 AUTH_EXPIRE auth no longer valid, this smells like someone changed a password on the AP + * 3 AUTH_LEAVE + * 4 ASSOC_EXPIRE + * 5 ASSOC_TOOMANY too many devices already connected to the AP => AP fails to respond + * 6 NOT_AUTHED + * 7 NOT_ASSOCED + * 8 ASSOC_LEAVE tested as manual disconnect by user OR in the wireless MAC blacklist + * 9 ASSOC_NOT_AUTHED + * 10 DISASSOC_PWRCAP_BAD + * 11 DISASSOC_SUPCHAN_BAD + * 12 + * 13 IE_INVALID + * 14 MIC_FAILURE + * 15 4WAY_HANDSHAKE_TIMEOUT wrong password! This was personnaly tested on my home wifi with a wrong password. + * 16 GROUP_KEY_UPDATE_TIMEOUT + * 17 IE_IN_4WAY_DIFFERS + * 18 GROUP_CIPHER_INVALID + * 19 PAIRWISE_CIPHER_INVALID + * 20 AKMP_INVALID + * 21 UNSUPP_RSN_IE_VERSION + * 22 INVALID_RSN_IE_CAP + * 23 802_1X_AUTH_FAILED wrong password? + * 24 CIPHER_SUITE_REJECTED + * 200 BEACON_TIMEOUT + * 201 NO_AP_FOUND + * 202 AUTH_FAIL + * 203 ASSOC_FAIL + * 204 HANDSHAKE_TIMEOUT + * + * */ + + /* reset saved sta IP */ + wifi_manager_safe_update_sta_ip_string((uint32_t)0); + + /* if there was a timer on to stop the AP, well now it's time to cancel that since connection was lost! */ + if(xTimerIsTimerActive(wifi_manager_shutdown_ap_timer) == pdTRUE ){ + xTimerStop( wifi_manager_shutdown_ap_timer, (TickType_t)0 ); + } + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if( uxBits & WIFI_MANAGER_REQUEST_STA_CONNECT_BIT ){ + /* there are no retries when it's a user requested connection by design. This avoids a user hanging too much + * in case they typed a wrong password for instance. Here we simply clear the request bit and move on */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_FAILED_ATTEMPT ); + wifi_manager_unlock_json_buffer(); + } + + } + else if (uxBits & WIFI_MANAGER_REQUEST_DISCONNECT_BIT){ + /* user manually requested a disconnect so the lost connection is a normal event. Clear the flag and restart the AP */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_DISCONNECT_BIT); + + /* erase configuration */ + if(wifi_manager_config_sta){ + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + } + + /* regenerate json status */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_USER_DISCONNECT ); + wifi_manager_unlock_json_buffer(); + } + + /* save NVS memory */ + wifi_manager_save_sta_config(); + + /* start SoftAP */ + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + else{ + /* lost connection ? */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_LOST_CONNECTION ); + wifi_manager_unlock_json_buffer(); + } + + /* Start the timer that will try to restore the saved config */ + xTimerStart( wifi_manager_retry_timer, (TickType_t)0 ); + + /* if it was a restore attempt connection, we clear the bit */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + + /* if the AP is not started, we check if we have reached the threshold of failed attempt to start it */ + if(! (uxBits & WIFI_MANAGER_AP_STARTED_BIT) ){ + + /* if the nunber of retries is below the threshold to start the AP, a reconnection attempt is made + * This way we avoid restarting the AP directly in case the connection is mementarily lost */ + if(retries < WIFI_MANAGER_MAX_RETRY_START_AP){ + retries++; + } + else{ + /* In this scenario the connection was lost beyond repair: kick start the AP! */ + retries = 0; + + /* start SoftAP */ + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + } + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(wifi_event_sta_disconnected); + + break; + + case WM_ORDER_START_AP: + ESP_LOGI(TAG, "MESSAGE: ORDER_START_AP"); + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + + /* restart HTTP daemon */ + http_app_stop(); + http_app_start(true); + + /* start DNS */ + dns_server_start(); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_STOP_AP: + ESP_LOGI(TAG, "MESSAGE: ORDER_STOP_AP"); + + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + + /* before stopping the AP, we check that we are still connected. There's a chance that once the timer + * kicks in, for whatever reason the esp32 is already disconnected. + */ + if(uxBits & WIFI_MANAGER_WIFI_CONNECTED_BIT){ + + /* set to STA only */ + esp_wifi_set_mode(WIFI_MODE_STA); + + /* stop DNS */ + dns_server_stop(); + + /* restart HTTP daemon */ + http_app_stop(); + http_app_start(false); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + } + + break; + + case WM_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "WM_EVENT_STA_GOT_IP"); + ip_event_got_ip_t* ip_event_got_ip = (ip_event_got_ip_t*)msg.param; + uxBits = xEventGroupGetBits(wifi_manager_event_group); + + /* reset connection requests bits -- doesn't matter if it was set or not */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + + /* save IP as a string for the HTTP server host */ + wifi_manager_safe_update_sta_ip_string(ip_event_got_ip->ip_info.ip.addr); + + /* save wifi config in NVS if it wasn't a restored of a connection */ + if(uxBits & WIFI_MANAGER_REQUEST_RESTORE_STA_BIT){ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + } + else{ + wifi_manager_save_sta_config(); + } + + /* reset number of retries */ + retries = 0; + + /* refresh JSON with the new IP */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + /* generate the connection info with success */ + wifi_manager_generate_ip_info_json( UPDATE_CONNECTION_OK ); + wifi_manager_unlock_json_buffer(); + } + else { abort(); } + + /* bring down DNS hijack */ + dns_server_stop(); + + /* start the timer that will eventually shutdown the access point + * We check first that it's actually running because in case of a boot and restore connection + * the AP is not even started to begin with. + */ + if(uxBits & WIFI_MANAGER_AP_STARTED_BIT){ + TickType_t t = pdMS_TO_TICKS( WIFI_MANAGER_SHUTDOWN_AP_TIMER ); + + /* if for whatever reason user configured the shutdown timer to be less than 1 tick, the AP is stopped straight away */ + if(t > 0){ + xTimerStart( wifi_manager_shutdown_ap_timer, (TickType_t)0 ); + } + else{ + wifi_manager_send_message(WM_ORDER_STOP_AP, (void*)NULL); + } + + } + + /* callback and free memory allocated for the void* param */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(ip_event_got_ip); + + break; + + case WM_ORDER_DISCONNECT_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_DISCONNECT_STA"); + + /* precise this is coming from a user request */ + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_DISCONNECT_BIT); + + /* order wifi discconect */ + ESP_ERROR_CHECK(esp_wifi_disconnect()); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + default: + break; + + } /* end of switch/case */ + } /* end of if status=pdPASS */ + } /* end of for loop */ + + vTaskDelete( NULL ); + +} + + diff --git a/dist/esp32-wifi-manager_0.0.3/src/wifi_manager.h b/dist/esp32-wifi-manager_0.0.3/src/wifi_manager.h new file mode 100644 index 00000000..cba3b989 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.3/src/wifi_manager.h @@ -0,0 +1,417 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file wifi_manager.h +@author Tony Pottier +@brief Defines all functions necessary for esp32 to connect to a wifi/scan wifis + +Contains the freeRTOS task and all necessary support + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#ifndef WIFI_MANAGER_H_INCLUDED +#define WIFI_MANAGER_H_INCLUDED + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Defines the maximum size of a SSID name. 32 is IEEE standard. + * @warning limit is also hard coded in wifi_config_t. Never extend this value. + */ +#define MAX_SSID_SIZE 32 + +/** + * @brief Defines the maximum size of a WPA2 passkey. 64 is IEEE standard. + * @warning limit is also hard coded in wifi_config_t. Never extend this value. + */ +#define MAX_PASSWORD_SIZE 64 + + +/** + * @brief Defines the maximum number of access points that can be scanned. + * + * To save memory and avoid nasty out of memory errors, + * we can limit the number of APs detected in a wifi scan. + */ +#define MAX_AP_NUM 15 + + +/** + * @brief Defines the maximum number of failed retries allowed before the WiFi manager starts its own access point. + * Setting it to 2 for instance means there will be 3 attempts in total (original request + 2 retries) + */ +#define WIFI_MANAGER_MAX_RETRY_START_AP CONFIG_WIFI_MANAGER_MAX_RETRY_START_AP + +/** + * @brief Time (in ms) between each retry attempt + * Defines the time to wait before an attempt to re-connect to a saved wifi is made after connection is lost or another unsuccesful attempt is made. + */ +#define WIFI_MANAGER_RETRY_TIMER CONFIG_WIFI_MANAGER_RETRY_TIMER + + +/** + * @brief Time (in ms) to wait before shutting down the AP + * Defines the time (in ms) to wait after a succesful connection before shutting down the access point. + */ +#define WIFI_MANAGER_SHUTDOWN_AP_TIMER CONFIG_WIFI_MANAGER_SHUTDOWN_AP_TIMER + + +/** @brief Defines the task priority of the wifi_manager. + * + * Tasks spawn by the manager will have a priority of WIFI_MANAGER_TASK_PRIORITY-1. + * For this particular reason, minimum task priority is 1. It it highly not recommended to set + * it to 1 though as the sub-tasks will now have a priority of 0 which is the priority + * of freeRTOS' idle task. + */ +#define WIFI_MANAGER_TASK_PRIORITY CONFIG_WIFI_MANAGER_TASK_PRIORITY + +/** @brief Defines the auth mode as an access point + * Value must be of type wifi_auth_mode_t + * @see esp_wifi_types.h + * @warning if set to WIFI_AUTH_OPEN, passowrd me be empty. See DEFAULT_AP_PASSWORD. + */ +#define AP_AUTHMODE WIFI_AUTH_WPA2_PSK + +/** @brief Defines visibility of the access point. 0: visible AP. 1: hidden */ +#define DEFAULT_AP_SSID_HIDDEN 0 + +/** @brief Defines access point's name. Default value: esp32. Run 'make menuconfig' to setup your own value or replace here by a string */ +#define DEFAULT_AP_SSID CONFIG_DEFAULT_AP_SSID + +/** @brief Defines access point's password. + * @warning In the case of an open access point, the password must be a null string "" or "\0" if you want to be verbose but waste one byte. + * In addition, the AP_AUTHMODE must be WIFI_AUTH_OPEN + */ +#define DEFAULT_AP_PASSWORD CONFIG_DEFAULT_AP_PASSWORD + +/** @brief Defines the hostname broadcasted by mDNS */ +#define DEFAULT_HOSTNAME "esp32" + +/** @brief Defines access point's bandwidth. + * Value: WIFI_BW_HT20 for 20 MHz or WIFI_BW_HT40 for 40 MHz + * 20 MHz minimize channel interference but is not suitable for + * applications with high data speeds + */ +#define DEFAULT_AP_BANDWIDTH WIFI_BW_HT20 + +/** @brief Defines access point's channel. + * Channel selection is only effective when not connected to another AP. + * Good practice for minimal channel interference to use + * For 20 MHz: 1, 6 or 11 in USA and 1, 5, 9 or 13 in most parts of the world + * For 40 MHz: 3 in USA and 3 or 11 in most parts of the world + */ +#define DEFAULT_AP_CHANNEL CONFIG_DEFAULT_AP_CHANNEL + + + +/** @brief Defines the access point's default IP address. Default: "10.10.0.1 */ +#define DEFAULT_AP_IP CONFIG_DEFAULT_AP_IP + +/** @brief Defines the access point's gateway. This should be the same as your IP. Default: "10.10.0.1" */ +#define DEFAULT_AP_GATEWAY CONFIG_DEFAULT_AP_GATEWAY + +/** @brief Defines the access point's netmask. Default: "255.255.255.0" */ +#define DEFAULT_AP_NETMASK CONFIG_DEFAULT_AP_NETMASK + +/** @brief Defines access point's maximum number of clients. Default: 4 */ +#define DEFAULT_AP_MAX_CONNECTIONS CONFIG_DEFAULT_AP_MAX_CONNECTIONS + +/** @brief Defines access point's beacon interval. 100ms is the recommended default. */ +#define DEFAULT_AP_BEACON_INTERVAL CONFIG_DEFAULT_AP_BEACON_INTERVAL + +/** @brief Defines if esp32 shall run both AP + STA when connected to another AP. + * Value: 0 will have the own AP always on (APSTA mode) + * Value: 1 will turn off own AP when connected to another AP (STA only mode when connected) + * Turning off own AP when connected to another AP minimize channel interference and increase throughput + */ +#define DEFAULT_STA_ONLY 1 + +/** @brief Defines if wifi power save shall be enabled. + * Value: WIFI_PS_NONE for full power (wifi modem always on) + * Value: WIFI_PS_MODEM for power save (wifi modem sleep periodically) + * Note: Power save is only effective when in STA only mode + */ +#define DEFAULT_STA_POWER_SAVE WIFI_PS_NONE + +/** + * @brief Defines the maximum length in bytes of a JSON representation of an access point. + * + * maximum ap string length with full 32 char ssid: 75 + \\n + \0 = 77\n + * example: {"ssid":"abcdefghijklmnopqrstuvwxyz012345","chan":12,"rssi":-100,"auth":4},\n + * BUT: we need to escape JSON. Imagine a ssid full of \" ? so it's 32 more bytes hence 77 + 32 = 99.\n + * this is an edge case but I don't think we should crash in a catastrophic manner just because + * someone decided to have a funny wifi name. + */ +#define JSON_ONE_APP_SIZE 99 + +/** + * @brief Defines the maximum length in bytes of a JSON representation of the IP information + * assuming all ips are 4*3 digits, and all characters in the ssid require to be escaped. + * example: {"ssid":"abcdefghijklmnopqrstuvwxyz012345","ip":"192.168.1.119","netmask":"255.255.255.0","gw":"192.168.1.1","urc":99} + * Run this JS (browser console is easiest) to come to the conclusion that 159 is the worst case. + * ``` + * var a = {"ssid":"abcdefghijklmnopqrstuvwxyz012345","ip":"255.255.255.255","netmask":"255.255.255.255","gw":"255.255.255.255","urc":99}; + * // Replace all ssid characters with a double quote which will have to be escaped + * a.ssid = a.ssid.split('').map(() => '"').join(''); + * console.log(JSON.stringify(a).length); // => 158 +1 for null + * console.log(JSON.stringify(a)); // print it + * ``` + */ +#define JSON_IP_INFO_SIZE 159 + + +/** + * @brief defines the minimum length of an access point password running on WPA2 + */ +#define WPA2_MINIMUM_PASSWORD_LENGTH 8 + + +/** + * @brief Defines the complete list of all messages that the wifi_manager can process. + * + * Some of these message are events ("EVENT"), and some of them are action ("ORDER") + * Each of these messages can trigger a callback function and each callback function is stored + * in a function pointer array for convenience. Because of this behavior, it is extremely important + * to maintain a strict sequence and the top level special element 'MESSAGE_CODE_COUNT' + * + * @see wifi_manager_set_callback + */ +typedef enum message_code_t { + NONE = 0, + WM_ORDER_START_HTTP_SERVER = 1, + WM_ORDER_STOP_HTTP_SERVER = 2, + WM_ORDER_START_DNS_SERVICE = 3, + WM_ORDER_STOP_DNS_SERVICE = 4, + WM_ORDER_START_WIFI_SCAN = 5, + WM_ORDER_LOAD_AND_RESTORE_STA = 6, + WM_ORDER_CONNECT_STA = 7, + WM_ORDER_DISCONNECT_STA = 8, + WM_ORDER_START_AP = 9, + WM_EVENT_STA_DISCONNECTED = 10, + WM_EVENT_SCAN_DONE = 11, + WM_EVENT_STA_GOT_IP = 12, + WM_ORDER_STOP_AP = 13, + WM_MESSAGE_CODE_COUNT = 14 /* important for the callback array */ + +}message_code_t; + +/** + * @brief simplified reason codes for a lost connection. + * + * esp-idf maintains a big list of reason codes which in practice are useless for most typical application. + */ +typedef enum update_reason_code_t { + UPDATE_CONNECTION_OK = 0, + UPDATE_FAILED_ATTEMPT = 1, + UPDATE_USER_DISCONNECT = 2, + UPDATE_LOST_CONNECTION = 3 +}update_reason_code_t; + +typedef enum connection_request_made_by_code_t{ + CONNECTION_REQUEST_NONE = 0, + CONNECTION_REQUEST_USER = 1, + CONNECTION_REQUEST_AUTO_RECONNECT = 2, + CONNECTION_REQUEST_RESTORE_CONNECTION = 3, + CONNECTION_REQUEST_MAX = 0x7fffffff /*force the creation of this enum as a 32 bit int */ +}connection_request_made_by_code_t; + +/** + * The actual WiFi settings in use + */ +struct wifi_settings_t{ + uint8_t ap_ssid[MAX_SSID_SIZE]; + uint8_t ap_pwd[MAX_PASSWORD_SIZE]; + uint8_t ap_channel; + uint8_t ap_ssid_hidden; + wifi_bandwidth_t ap_bandwidth; + bool sta_only; + wifi_ps_type_t sta_power_save; + bool sta_static_ip; + esp_netif_ip_info_t sta_static_ip_config; +}; +extern struct wifi_settings_t wifi_settings; + + +/** + * @brief Structure used to store one message in the queue. + */ +typedef struct{ + message_code_t code; + void *param; +} queue_message; + + +/** + * @brief returns the current esp_netif object for the STAtion + */ +esp_netif_t* wifi_manager_get_esp_netif_sta(); + +/** + * @brief returns the current esp_netif object for the Access Point + */ +esp_netif_t* wifi_manager_get_esp_netif_ap(); + + +/** + * Allocate heap memory for the wifi manager and start the wifi_manager RTOS task + */ +void wifi_manager_start(); + +/** + * Frees up all memory allocated by the wifi_manager and kill the task. + */ +void wifi_manager_destroy(); + +/** + * Filters the AP scan list to unique SSIDs + */ +void filter_unique( wifi_ap_record_t * aplist, uint16_t * ap_num); + +/** + * Main task for the wifi_manager + */ +void wifi_manager( void * pvParameters ); + + +char* wifi_manager_get_ap_list_json(); +char* wifi_manager_get_ip_info_json(); + + +void wifi_manager_scan_async(); + + +/** + * @brief saves the current STA wifi config to flash ram storage. + */ +esp_err_t wifi_manager_save_sta_config(); + +/** + * @brief fetch a previously STA wifi config in the flash ram storage. + * @return true if a previously saved config was found, false otherwise. + */ +bool wifi_manager_fetch_wifi_sta_config(); + +wifi_config_t* wifi_manager_get_wifi_sta_config(); + + +/** + * @brief requests a connection to an access point that will be process in the main task thread. + */ +void wifi_manager_connect_async(); + +/** + * @brief requests a wifi scan + */ +void wifi_manager_scan_awifi_manager_send_messagesync(); + +/** + * @brief requests to disconnect and forget about the access point. + */ +void wifi_manager_disconnect_async(); + +/** + * @brief Tries to get access to json buffer mutex. + * + * The HTTP server can try to access the json to serve clients while the wifi manager thread can try + * to update it. These two tasks are synchronized through a mutex. + * + * The mutex is used by both the access point list json and the connection status json.\n + * These two resources should technically have their own mutex but we lose some flexibility to save + * on memory. + * + * This is a simple wrapper around freeRTOS function xSemaphoreTake. + * + * @param xTicksToWait The time in ticks to wait for the semaphore to become available. + * @return true in success, false otherwise. + */ +bool wifi_manager_lock_json_buffer(TickType_t xTicksToWait); + +/** + * @brief Releases the json buffer mutex. + */ +void wifi_manager_unlock_json_buffer(); + +/** + * @brief Generates the connection status json: ssid and IP addresses. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_generate_ip_info_json(update_reason_code_t update_reason_code); +/** + * @brief Clears the connection status json. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_clear_ip_info_json(); + +/** + * @brief Generates the list of access points after a wifi scan. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_generate_acess_points_json(); + +/** + * @brief Clear the list of access points. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_clear_access_points_json(); + + +/** + * @brief Start the mDNS service + */ +void wifi_manager_initialise_mdns(); + + +bool wifi_manager_lock_sta_ip_string(TickType_t xTicksToWait); +void wifi_manager_unlock_sta_ip_string(); + +/** + * @brief gets the string representation of the STA IP address, e.g.: "192.168.1.69" + */ +char* wifi_manager_get_sta_ip_string(); + +/** + * @brief thread safe char representation of the STA IP update + */ +void wifi_manager_safe_update_sta_ip_string(uint32_t ip); + + +/** + * @brief Register a callback to a custom function when specific event message_code happens. + */ +void wifi_manager_set_callback(message_code_t message_code, void (*func_ptr)(void*) ); + + +BaseType_t wifi_manager_send_message(message_code_t code, void *param); +BaseType_t wifi_manager_send_message_to_front(message_code_t code, void *param); + +#ifdef __cplusplus +} +#endif + +#endif /* WIFI_MANAGER_H_INCLUDED */ diff --git a/dist/esp32-wifi-manager_0.0.4.tgz b/dist/esp32-wifi-manager_0.0.4.tgz new file mode 100644 index 0000000000000000000000000000000000000000..223b62aedd359cf6d61bb8c123863a209db18dca GIT binary patch literal 44333 zcmYhiV{j%+6Ez&$8{4*R+qO5hZQHgtwry+UY>bV~>-zS7KD|{_{bN{D-Bo=~pBbW9 z7#P#gAWJZyg{u=AD}(1x%byJPCJrW67A{82jLeMejBZvwAXk01X4{<2%sUNB4Z`UT zbX{of_Y6%(SG4UZ=c^rLGzzZW{;g>+GIca5a;p8URJV?SR!|_Ys%h6{(c3M>TN{|L z2vFe04%i5B5le3qUEbe0xtPfPAM}M4Z*`x$b`B1dfk%&;ExR`UWAy_*K?|&LvmiOZ z%bBlM&rQ;1tgoWBZJ+MPCmCPQ8LzFa&7Cb;e+@stqfk7wH8|Gu%?&5O;o5=gd4CSW zP&n}R#;$$9bQ`$~Ahyt({=NGQ0G?00$=v~eX#n;a-+-ZaKq_9~zP)3IXZ&d@)iu?1 zz9@(vD3O_^)XVHM7{L1-I6uG63)tU+uwd!~=xyir7&V=W)dQK*fI^mMpvZ3nnI6_s z{PgzNn$=HP`SF1#sZk2{kp6)4e#bS*=9=^0dq}6VT~7LH810-1aI!& z=2opOkb$%B9GZNGMP$#-`X&AZ{t2ET@Ho_wi);~F(px_}SdMNy z9d1F%pBkpcSsErX07JA}K*TGD)PO}~0_jeJ03qwuF^*oIg5EL}Z=XF3ecWj>5`n#2OgiFcd41xJ$8;gBSfZs&(1%IGIXyiXTw zm4;vBuo1GyMbqX7)D=XmLz)^dqzu4x#K(_&{p8aG2cosrp(1sixp0MGGTgZSKD>dD z#D;QnQSq_!{>{a9lacU;%Y${d>_h>#9O4z5>HIU%qm!M0Gb$tF8nl{$c*KG`ct2iL z8{@;M82Ih{?fCa1VQI^bt3&VFRK_3nuMpg<93Q&_=6qo}@Y=z_;ydtH{_dP8&^Id4 zth25E#M(ugrPE0w3A$zoE6{mkx?C&3dk2h!CJ zi_m+vSxmiETm-ymiOAQ^hPS9CiirYhOqeN-fA!;z&dmK~?zUk;vHKPegal1i>!XXM zc#BTy-ejcXJ4LGKGHMzPy)zsK@`XbX%zsHoSD#~aSMHuFfakXGTkF+{8`tBbp?f5h z75RJ4!~5j<&w1ohY{1#m-Lr_dNk{=kTCpQ+=S0UKxSUUAuP!OF`BW?YA_A(wN+Lu? zKTlWCJ?xJ>t5lh<*R?pQ`^<3fp(CcEukKU}l!r!#`6#2#Tx{Z-wp&P4yn=iJqW+sFMJcI1fQSwvfNP*~>bqY)9gxY~T6tyF zt{Oss-{_^h!SorGd)aI!zvkuc#m#%`Y4j4nOKl{zgE-gix@&W@I`lzvrX_Rdx5;-y zXl}dq)8@u*O;57Np8c=V_VZ?HHBYvK!sRv;KT8k>PRss7@KG4gb)FmY^zIPPq0lAr z0@Rzf@rZNyZU+_nf>0-w^Do}nA*#}sDjK4;++UR+ z?2=e3n&gl9>XQ^p`3)o76Pb-vp<%sY7}|q40bwg-6NL)?>2!hjXnl|d1EPH`xUs{M z+aF8`t~(wSDkiPGfQh9XvgV&bk=uUvWa6c?XmlS1%CgCRhdGILI>RC&hRNbQ=wSZ* z!Ogrol~-QPv~l9=g=h_G+Y&rsEG2ec9R#lV0LF)yN7-8G9U2K>1gVAk>N~#!fhESl zb)m-3k|u_a-6NV!yaI< z!n)H#N)*kO6st0Nj#j7&HAck!p)AuI{*8y=M37Sp|M|i4T~k_r!%*6o=44zH`9xzK zT5?)K&5ApNS}R|dr**aMn0?gjGYJw}1gdl_mtp9_op4+GQ6|WLh2MXZVCV*AkhidZ z-ojJ4bMWr{Neq|Ki@p%%ipCbFuMBEbv{IB7MWkuOoFptS3wchP0u%V&0z$7z?Q!br z$;{m^RW3~Q7O_`?Y9IwxPIf!(&c6b-d7i_3e*~Kd23e7C5=?pQgX)IBEu*>TH+Q+> z?rMakMRW5ZNEWGF(ekk=H5hG`VKy2Ybeeur*HGSKXuMSy@QqhXRL^*kc#&On0eemX zOA>5kkkc+^L)wgiPoizvXe}QZ9>rEAOfxPMhjUiPY4O@WG~s)AACu*H90QlPQLf4j zYgeqlrqd|x3BU37>D3oG6J$LZlqyF>^#G#%UNjcQ`Nxr*pQJVLhy{7Wl!_?{+$p4O z8VbRF@5wN~;JHV2SjK@E|KS7;BP)w!LB;$^+g=1#Kivp>dta%bG$W~3XJ1u7Q%z%Y zB1)ju7LxM;GwX%@qMRvnT)~FqigI1pjA#G*;_ht@mHxFQ^|58;=E4aiWluuL07Ulk z*`en-je#=2_p!T8Z{BoX%$3yF;pg}IXhmLgP-uMfO>WGO=I$|0+%&R(zEE~ zSgqgtUVoy0bL(#Xh0z6}hq=Mk&#u~tAwp4G>gILsyl6f{Y?Q*iS$1ySl)N<=g6&~l z;=58_jfp*sL9Ad$i{JRc(4P`SU!DVH8-wLgHw@H!3yl2+9;UwXh@V_xtLrQW1M|KE z-5QU=v6h2gjzWh_U<@YU;GAzNt4dcb)jRkj3hzF|`Jp61B-9CP}+`i{k z^o8VX7g#FdOeqE%Sm<#Kq$ERLYNv@(yC1KkM$Es26ftUgN z--o}CkE=2kwKP$8;k?zOpI1?zBsr2_yItMbQ>u0}uhiUykY6U;v(%}0%%`ZqU^{Jx z(5oCU}eVk4t9Q@{5~^F5+QntK?$d&1{5Z8Y`A6wMb)G`aWa@g#mt8J%}8B+$lBufo^oNB9f*yO~YV#DppO5p*%s{uSj zU#i`J3sm<27(4V*wN28xW|qWyZciL8DpzY_IEO483BLoWljYLRGv;l%MDc~hx3nGi z)|rST(nN!7kI~T=UN!?rkbQ_=B!F*I#xNgGKa%1xQ@S|{4 zvBI9eB7C5F-3KnL2M2B!ql*^b9J_)SU*tW4*d8B6sD(3og;~Grl)lqoj=G1d)Gk%d z_ytaLx3c~U7-jutC%3eoZl-DbzD!hw&WjvPYJQf4!^Dv92e_Z>9x0OVII>SVjeFo zj^EybQp=Z%sQoN)4G`oNMjm_kM+gLnP<^H71iT?GXVN?oj%?qn!$G4k zrZ}3;CMv!|kgml88Z0@T1aaXfNJ@XQ*GCcB4i*==ZxNsz7d|?QTpS`uT8&SNkBoG9 zC2Odm%yd{X*NSnjvdDTyHT=n{#>m#a+5F}d=M%cQ(Oco}rp~W=ePxyPk)hOs=C4N& z{?1?geC0lPuSQzb<s(#5@DViuU-Dg^^q8IL*F6~IvUHQ*Q0a4<=S~mb@xkim$btAq6mT9^$%726JSTK&vI_pI zA<3^GFEk7TnNy!IMPjTi;6ee#{&Nw({|V zHuVw$)d(4cuPl=s9Ubv5jUJV*L8g00*s`7@CzYXXD+J*NN3v|KDwZ=MgfJn*+g-MO zmac`>O7|`rw}DM94%vMmZD71xuEBo|bC7C-n5XEuk_2T^@9ag`f0W5m6UTzn-VwMD z4WH9}355eI?||rlLZ&0v9lomqr0((IkCiW>>(h0Qb>Pa@PfS1|^d|86D>vxrcDihO ziO|T1b>5xVUVUgN8)B=w7 zJ9^)k0Uq}mqXYuJyx(_8@Ii$C2o?6>2&`ul=2I!?rS&8MG&>*BF2?F_2tL#R$X{>L zi_$67wYxPQ^0CHM)dJak^g`AO6c$PQqlZcFO{DBCa|;m+`D`1t~V-}dD9G;rDnR?8DOzDI|0kh|C;0|>X zGfCfH&kKj2LH$5g6|010=uJ{)qOaX?T6wbGyl;cN8$OhYf?6VkQe9g6I>FsR#jK+8 z&`ZC`<0&c3iSBAPIIS-=T~vgL?C)ARjS_>1^Ht zP*D&UvsJuDCXykTicZ_4RZFN0lqH`V9SBDt45>`Q;GS1<>WAe#M^5~sCX7x};3}7% ziYaPRwz!o9)UG(*@Qt>VK|^!)eB%LwO!kgAqBKq3b(|ee6HAYzCN)uFj#0gln?;Mt za@QPEB^q1qbGvTwQ=S|hRIXc-pw=mWR>LARV(y@P!O1!-)mkZ$3@9XtNLh>|yL?M^ zN)|0`&ly%N?cm8k2XjWEz%Ni5>dBhwN*U_3TRRarL44oNv`8<|7}G#z zw~S~Ltqx@b(3Pw|J=(2NvASG41$Yv+IaK|Deh>D<-I zA}0vkhPW5KXXf$UjR)Vl3s(w;-Y7CZnwD1ixMdX z;)9e$$*xMk;wr0v@tqk)xZH)mf<_vAN!&?*wS;Sd1>}oK##SWplRGe3LM_tjCh*+v z%Jr}ecmO6G>=PX^_2Mh;YlUjZ%Ny$JRm&S@tyzJeC&8NKvOjm7RC8QZGhCVm*AVLl z@s5&3;Ww7TsG0}c9q{>U15U{Hho|BYb+@ua6jyY=T|V1<+|z zbe!QMYz26wYi19{t7o3vwP_K9RqmwCEl|U4ylY3sRw{kVRRt{N@$h0e7{22X@4?84 zqeN8%>{4rC@}0a&2u`;lyHc#V*vn!V2TawFh6Pc0jL`~ym%MHS8Q*wCF)1YqJEAoz z7!P`#W@f}+?1{U|;vX3tG)KIkB$1HHmc2$B#|sk%q1OyH!|~hlpbFAh_tGY?ioym& zQ=x%`o!l9hmB>gg*_3;@&v7>LZsdgaj?*X`n4tRbse~E&v`Zu?uJLT6vX~v9S+FCN zjMK=Ki^XG9{GfmFmOhTXzSEbg%CQaXWXpngV?DZK1@R$)IrclO7xcO zKsWA<#7;gsGtOK^F=GIp8v;#Ci8N)K>5j|fwHxY1NWxTT^FwSnN1_VKZJnFW2>D=LmPe;aEb?|sNXoBZ-jhHY9w?W``>bHsf{+fu_X-g1h?8{bvbKf?R5o!(m*5s8 zrs0Z8)tC-_x%ohw8CvzTVrD>E&{+?z$}vq6#0nwuk{+nxRA->16^1tRdoG8gS`*6? zyG^LqI}h|SKZ3s$~bV6cJ9 z?xANu#1(jIq$W9!r<8E8`16J_ZRA@EQ>)^W**op3b*0gU7b{`@s9B0A1|v+$5H?x@ z$^VmX0uw_tg9}J+6A9CfUyx>{dt;8w4B$WK_1OrdplA`Xtxy)$nCWx1j43J*NJpeV zibSn_T0a}m8lU@2nbuKR(ioZW|JIZ9EfqL<%~?Lndx zQK^O7SodIJHxNZ?!3#R?=j-BZyHP*1SA77HW;NL2WGfM|Y6ELdc{dK3_fTdkFmzQg z+_-DUUdX;aSS9q6^V^mLF0~R3GA-=(7zX}=jQuOgt1exhylX9K{g$j8qnjAcPWy=2 zXnaPx{=G{eH*~Mi51#sU%1RZ!HbU z;SM?2(Y1GF{KHn8{G#+~Yhv*cCN|c37G3BQ4B{~ab})@)S9E-RYX)7Vab0i*yQ?v7 z1&z37^_du!8`+F>lml;ZrJA8~Sly`{o3LDRT{-dWsGaIKXkvw6tTd=HiOx^9MY$_B z8p$KgRKwOH)&m;!ekGD&2zt8R1;k{uNMR&`Ki4-JW>7-iMH-

I6I)daAo@%PKHY`nb|#Flc(yupmz#!Mm6;} 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

j$p!JN6nBsQ6K5#%sDnP5b-S`H_A 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 literal 0 HcmV?d00001 diff --git a/dist/esp32-wifi-manager_0.0.4/.gitignore b/dist/esp32-wifi-manager_0.0.4/.gitignore new file mode 100644 index 00000000..5448b48b --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/.gitignore @@ -0,0 +1,75 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Eclipse +.metadata/ +RemoteSystemsTempFiles/.project +.settings/ +*.a +*.o +*.d +wifi_manager/.cproject +wifi_manager/.project +sdkconfig +sdkconfig.old +**/build/ +**/managed_components/ +dependencies.lock +#doxygen +Doxyfile +wifi_manager/doc/ +.project +.cproject + +# Visual Studio Code +.vscode/ diff --git a/dist/esp32-wifi-manager_0.0.4/.travis.yml b/dist/esp32-wifi-manager_0.0.4/.travis.yml new file mode 100644 index 00000000..37c55458 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/.travis.yml @@ -0,0 +1,122 @@ +language: bash + +# when you suspects issues in cache, use the following line to disable cache. +# cache: false +cache: + directories: + - ${HOME}/distfiles + - ${HOME}/.ccache + - ${HOME}/.cache/pip +os: + - linux + +matrix: + include: + - env: + - PROJECT_TARGET="esp32" + - PROJECT_SDK_BRANCH="master" +# - env: +# - PROJECT_TARGET="esp8266" +# - PROJECT_SDK_BRANCH="master" + +addons: + apt: + packages: + - gcc + - wget + - make + - libncurses-dev + - flex + - bison + - python + - python-pip + - gperf + - ccache + +before_install: + # Save path to the git respository + - PROJECT_PATH=$(pwd) + +install: + - export TOOLCHAIN_DIR="${HOME}/${PROJECT_TARGET}" + - | + if [ ${PROJECT_TARGET} == "esp8266" ]; then + export PROJECT_GCC_PREFIX="xtensa-lx106-elf" + export PROJECT_TOOLCHAIN_FILE=xtensa-lx106-elf-linux64-1.22.0-92-g8facf4c-5.2.0.tar.gz + export PROJECT_SDK_NAME="ESP8266_RTOS_SDK" + else + export PROJECT_GCC_PREFIX="xtensa-esp32-elf" + export PROJECT_TOOLCHAIN_FILE=xtensa-esp32-elf-gcc8_2_0-esp32-2019r1-linux-amd64.tar.gz + export PROJECT_SDK_NAME="esp-idf" + fi + - export PROJECT_GCC_FILE="${PROJECT_GCC_PREFIX}-gcc" + - export PROJECT_DISTFILE_DIR="${HOME}/distfiles" + - export IDF_PATH=${TOOLCHAIN_DIR}/${PROJECT_SDK_NAME} + - export PROJECT_LOG="${HOME}/build.log" + - export PROJECT_EXAMPLE_DIR="${PROJECT_PATH}/examples" + # Install ESP32 toochain following steps as desribed + # in http://esp-idf.readthedocs.io/en/latest/linux-setup.html + + # Prepare directory for the toolchain + - mkdir -p ${TOOLCHAIN_DIR} ${PROJECT_DISTFILE_DIR} + # Get SDK from github + - git clone --branch ${PROJECT_SDK_BRANCH} --recursive https://github.com/espressif/${PROJECT_SDK_NAME}.git ${IDF_PATH} + + # Setup ccache to build faster + # XXX when the entire build process exceeds 50 min, th job will be killed + # https://docs.travis-ci.com/user/customizing-the-build/#build-timeouts + - ccache --version + - mkdir ${HOME}/ccache_bin + - (cd ${HOME}/ccache_bin && ln -s /usr/bin/ccache ${PROJECT_GCC_FILE}) + - export CCACHE_BASEDIR=$PROJECT_PATH + - export CCACHE_CPP2=true + + # Get Python requirements + - python -m pip install --user --upgrade pyOpenSSL + - python -m pip install --user -r ${IDF_PATH}/requirements.txt + + # Download binary toolchain if it does not exist + - | + if [ ! -f ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} ]; then + wget -O ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} https://dl.espressif.com/dl/${PROJECT_TOOLCHAIN_FILE} + fi + - tar -xz -C ${TOOLCHAIN_DIR} -f ${PROJECT_DISTFILE_DIR}/${PROJECT_TOOLCHAIN_FILE} + + # Make toolchains available for all terminal sessions + - export PATH=$HOME/ccache_bin:$PATH:$HOME/${PROJECT_TARGET}/${PROJECT_GCC_PREFIX}/bin + +script: + - rm -f ${PROJECT_LOG} + # XXX surpress log output where possible. when the size exceeds 4 MB, the + # job will be killed. + - | + IGNORE_FILE="travis-ignore" + + case ${PROJECT_TARGET} in + esp32) + ;; + esp8266) + IGNORE_FILE="travis-ignore-esp8266" + # these drivers do not compile for ESP8266 yet + export EXCLUDE_COMPONENTS="encoder max7219 mcp23x17" + ;; + esac + + cd ${PROJECT_EXAMPLE_DIR} + for i in $(ls -d */); do + if [ ! -e ${PROJECT_EXAMPLE_DIR}/${i}/${IGNORE_FILE} ]; then + echo "Building ${i}..." + cd ${PROJECT_EXAMPLE_DIR}/${i} + make defconfig + make -j2 >> ${PROJECT_LOG} + if [ $? -ne 0 ]; then + # when failed, show last 100 lines for debugging, and exit with + # non-zero exit code + tail -n 100 ${PROJECT_LOG} + exit 1 + fi + make clean >/dev/null + # make sure the directory is clean + rm -rf ${i}/sdkconfig ${i}/build + fi + done diff --git a/dist/esp32-wifi-manager_0.0.4/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.4/CMakeLists.txt new file mode 100644 index 00000000..83e0077c --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/CMakeLists.txt @@ -0,0 +1,12 @@ +if(IDF_VERSION_MAJOR GREATER_EQUAL 4) + idf_component_register(SRC_DIRS src + REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server + INCLUDE_DIRS src + EMBED_FILES src/style.css src/code.js src/index.html) +else() + set(COMPONENT_SRCDIRS src) + set(COMPONENT_ADD_INCLUDEDIRS src) + set(COMPONENT_REQUIRES esp_wifi log nvs_flash mdns wpa_supplicant lwip esp_http_server) + set(COMPONENT_EMBED_FILES src/style.css src/code.js src/index.html) + register_component() +endif() diff --git a/dist/esp32-wifi-manager_0.0.4/Kconfig b/dist/esp32-wifi-manager_0.0.4/Kconfig new file mode 100644 index 00000000..59c5a9a8 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/Kconfig @@ -0,0 +1,81 @@ +menu "Wifi Manager Configuration" + +config WIFI_MANAGER_TASK_PRIORITY + int "RTOS Task Priority for the wifi_manager" + default 5 + help + Tasks spawn by the manager will have a priority of WIFI_MANAGER_TASK_PRIORITY-1. For this particular reason, minimum recommended task priority is 2. + +config WIFI_MANAGER_RETRY_TIMER + int "Time (in ms) between each retry attempt" + default 5000 + help + Defines the time to wait before an attempt to re-connect to a saved wifi is made after connection is lost or another unsuccesful attempt is made. + +config WIFI_MANAGER_MAX_RETRY_START_AP + int "Max Retry before starting the AP" + default 3 + help + Defines the maximum number of failed retries allowed before the WiFi manager starts its own access point. + +config WIFI_MANAGER_SHUTDOWN_AP_TIMER + int "Time (in ms) to wait before shutting down the AP" + default 60000 + help + Defines the time (in ms) to wait after a succesful connection before shutting down the access point. + +config WEBAPP_LOCATION + string "Defines the URL where the wifi manager is located" + default "/" + help + This parameter helps you relocate the wifimanager to another URL, for instance /wifimanager/ The trailing slash is important and should be included + +config DEFAULT_AP_SSID + string "Access Point SSID" + default "esp32" + help + SSID (network name) the the esp32 will broadcast. + +config DEFAULT_AP_PASSWORD + string "Access Point Password" + default "esp32pwd" + help + Password used for the Access Point. Leave empty and set AUTH MODE to WIFI_AUTH_OPEN for no password. + +config DEFAULT_AP_CHANNEL + int "Access Point WiFi Channel" + default 1 + help + Be careful you might not see the access point if you use a channel not allowed in your country. + +config DEFAULT_AP_IP + string "Access Point IP Address" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_GATEWAY + string "Access Point IP Gateway" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_NETMASK + string "Access Point Netmask" + default "255.255.255.0" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_MAX_CONNECTIONS + int "Access Point Max Connections" + default 4 + help + Max is 4. + +config DEFAULT_AP_BEACON_INTERVAL + int "Access Point Beacon Interval (ms)" + default 100 + help + 100ms is the recommended default. + +endmenu diff --git a/dist/esp32-wifi-manager_0.0.4/LICENSE.md b/dist/esp32-wifi-manager_0.0.4/LICENSE.md new file mode 100644 index 00000000..5f2ac0bc --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2017-2019 Tony Pottier + +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. diff --git a/dist/esp32-wifi-manager_0.0.4/README.md b/dist/esp32-wifi-manager_0.0.4/README.md new file mode 100644 index 00000000..43248f3d --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/README.md @@ -0,0 +1,225 @@ +# What is esp32-wifi-manager? + +### Build status [![Build Status](https://travis-ci.com/tonyp7/esp32-wifi-manager.svg?branch=master)](https://travis-ci.com/tonyp7/esp32-wifi-manager) + +*esp32-wifi-manager* is a pure C esp-idf component for ESP32 that enables easy management of wifi networks through a web portal. + +*esp32-wifi-manager* is is an all in one wifi scanner, http server & dns daemon living in the least amount of RAM possible. + +*esp32-wifi-manager* will automatically attempt to re-connect to a previously saved network on boot, and if it cannot find a saved wifi it will start its own access point through which you can manage and connect to wifi networks. Upon a succesful connection, the software will shutdown the access point automatically after some time (1 minute by default). + +*esp32-wifi-manager* compiles with esp-idf 4.2 and above. See [Getting Started](#getting-started) to guide you through your first setup. + +# Content + - [Demo](#demo) + - [Look And Feel](#look-and-feel) + - [Getting Started](#getting-started) + - [Requirements](#requirements) + - [Hello World](#hello-world) + - [Configuring the Wifi Manager](#configuring-the-wifi-manager) + - [Adding esp32-wifi-manager to your code](#adding-esp32-wifi-manager-to-your-code) + - [Interacting with the manager](#interacting-with-the-manager) + - [Interacting with the http server](#interacting-with-the-http-server) + - [Thread safety and access to NVS](#thread-safety-and-access-to-nvs) + - [License](#license) + + +# Demo +[![esp32-wifi-manager demo](http://img.youtube.com/vi/hxlZi15bym4/0.jpg)](http://www.youtube.com/watch?v=hxlZi15bym4) + +# Look and Feel +![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-password.png "esp32-wifi-manager") ![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-connected-to.png "esp32-wifi-manager") + +# Getting Started + +## Requirements + +To get you started, esp32-wifi-manager needs: + +- esp-idf **4.2 and up** +- esp32 or esp32-s2 + +There are breaking changes and new features in esp-idf 4.1 and 4.2 which makes esp32-wifi-manager incompatible with anything lower than 4.2. This includes esp_netif (introduced in 4.1) and esp_event_handler_instance_t (introduced in 4.2). It is recommended to compile esp32-wifi-manager with the master tree to avoid any compatibility issue. + +## Hello World + +Clone the repository where you want it to be. If you are unfamiliar with Git, you can use Github Desktop on Windows: + +```bash +git clone https://github.com/tonyp7/esp32-wifi-manager.git +``` + +Navigate under the included example: + +```bash +cd esp32-wifi-manager/examples/default_demo +``` + +Compile the code and load it on your esp32: + +```bash +idf.py build flash monitor +``` + +_Note: while it is encouraged to use the newer build system with idf.py and cmake, esp32-wifi-manager still supports the legacy build system. If you are using make on Linux or make using MSYS2 on Windows, you can still use "make build flash monitor" if you prefer_ + +Now, using any wifi capable device, you will see a new wifi access point named *esp32*. Connect to it using the default password *esp32pwd*. If the captive portal does not pop up on your device, you can access the wifi manager at its default IP address: http://10.10.0.1. + +## Configuring the Wifi Manager + +esp32-wifi-manager can be configured without touching its code. At the project level use: + +```bash +idf.py menuconfig +``` + +Navigate in "Component config" then pick "Wifi Manager Configuration". You will be greeted by the following screen: + +![esp32-wifi-manager-menuconfig](https://idyl.io/wp-content/uploads/2020/08/wifi-manager-menuconfig-800px.png "menuconfig screen") + +You can change the ssid and password of the access point at your convenience, but it is highly recommended to keep default values. Your password should be between 8 and 63 characters long, to comply with the WPA2 standard. If the password is set to an empty value or is less than 8 characters long, esp32-wifi-manager will create its access point as an open wifi network. + +You can also change the values for various timers, for instance how long it takes for the access point to shutdown once a connection is established (default: 60000). While it could be tempting to set this timer to 0, just be warned that in that case the user will never get the feedback that a connection is succesful. Shutting down the AP will instantly kill the current navigating session on the captive portal. + +Finally, you can choose to relocate esp32-wifi-manager to a different URL by changing the default value of "/" to something else, for instance "/wifimanager/". Please note that the trailing slash does matter. This feature is particularly useful in case you want your own webapp to co-exist with esp32-wifi-manager's own web pages. + +# Adding esp32-wifi-manager to your code + +In order to use esp32-wifi-manager effectively in your esp-idf projects, copy the whole esp32-wifi-manager repository (or git clone) into a components subfolder. + +Your project should look like this: + + - project_folder + - build + - components + - esp32-wifi-manager + - main + - main.c + +Under eclipse, this is what a typical project looks like: + +![eclipse project with esp32-wifi-manager](https://idyl.io/wp-content/uploads/2020/07/eclipse-idf-project.png "eclipse project with esp32-wifi-manager") + +Once this is done, you need to edit the CMakeLists.txt file at the root of your project to register the components folder. This is done by adding the following line: + +```cmake +set(EXTRA_COMPONENTS_DIRS components/) +``` + +A typical CmakeLists.txt file should look like this: + +```cmake +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS components/) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(name_of_your_project) +``` + +If you are using the old build system with make instead, you should edit the Makefile instead such as: + +```make +PROJECT_NAME := name_of_your_project +EXTRA_COMPONENT_DIRS := components/ +include $(IDF_PATH)/make/project.mk +``` + +Once this is done, you can now in your user code add the header: + +```c +#include "wifi_manager.h" +``` + +All you need to do now is to call wifi_manager_start(); in your code. See [examples/default_demo](examples/default_demo) if you are uncertain. + + +## Interacting with the manager + +Ther are effectively three different ways you can embed esp32-wifi-manager with your code: +* Just forget about it and poll in your code for wifi connectivity status +* Use event callbacks +* Modify esp32-wifi-manager code directly to fit your needs + +**Event callbacks** are the cleanest way to use the wifi manager and that's the recommended way to do it. A typical use-case would be to get notified when wifi manager finally gets a connection to an access point. In order to do this you can simply define a callback function: + +```c +void cb_connection_ok(void *pvParameter){ + ESP_LOGI(TAG, "I have a connection!"); +} +``` + +Then just register it by calling: + +```c +wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok); +``` + +That's it! Now everytime the event is triggered it will call this function. The [examples/default_demo](examples/default_demo) contains sample code using callbacks. + +### List of events + +The list of possible events you can add a callback to are defined by message_code_t in wifi_manager.h. They are as following: + +* WM_ORDER_START_HTTP_SERVER +* WM_ORDER_STOP_HTTP_SERVER +* WM_ORDER_START_DNS_SERVICE +* WM_ORDER_STOP_DNS_SERVICE +* WM_ORDER_START_WIFI_SCAN +* WM_ORDER_LOAD_AND_RESTORE_STA +* WM_ORDER_CONNECT_STA +* WM_ORDER_DISCONNECT_STA +* WM_ORDER_START_AP +* WM_EVENT_STA_DISCONNECTED +* WM_EVENT_SCAN_DONE +* WM_EVENT_STA_GOT_IP +* WM_ORDER_STOP_AP + +In practice, keeping track of WM_EVENT_STA_GOT_IP and WM_EVENT_STA_DISCONNECTED is key to know whether or not your esp32 has a connection. The other messages can mostly be ignored in a typical application using esp32-wifi-manager. + +### Events parameters + +Callback signature includes a void* pointer. For most events, this additional parameter is empty and sent as a NULL value. A few select events have additional data which can be leveraged by user code. They are listed below: + +* WM_EVENT_SCAN_DONE is sent with a wifi_event_sta_scan_done_t* object. +* WM_EVENT_STA_DISCONNECTED is sent with a wifi_event_sta_disconnected_t* object. +* WM_EVENT_STA_GOT_IP is sent with a ip_event_got_ip_t* object. + +These objects are standard esp-idf structures, and are documented as such in the [official pages](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html). + +The [examples/default_demo](examples/default_demo) demonstrates how you can read a ip_event_got_ip_t object to access the IP address assigned to the esp32. + +## Interacting with the http server + +Because esp32-wifi-manager spawns its own http server, you might want to extend this server to serve your own pages in your application. It is possible to do so by registering your own URL handler using the standard esp_http_server signature: + +```c +esp_err_t my_custom_handler(httpd_req_t *req){ +``` + +And then registering the handler by doing + +```c +http_app_set_handler_hook(HTTP_GET, &my_custom_handler); +``` + +The [examples/http_hook](examples/http_hook) contains an example where a web page is registered at /helloworld + +## Thread safety and access to NVS + +esp32-wifi-manager accesses the non-volatile storage to store and loads its configuration into a dedicated namespace "espwifimgr". If you want to make sure there will never be a conflict with concurrent access to the NVS, you can include nvs_sync.h and use calls to nvs_sync_lock and nvs_sync_unlock. + +```c +nvs_handle handle; + +if(nvs_sync_lock( portMAX_DELAY )){ + if(nvs_open(wifi_manager_nvs_namespace, NVS_READWRITE, &handle) == ESP_OK){ + /* do something with NVS */ + nvs_close(handle); + } + nvs_sync_unlock(); +} +``` +nvs_sync_lock waits for the number of ticks sent to it as a parameter to acquire a mutex. It is recommended to use portMAX_DELAY. In practice, nvs_sync_lock will almost never wait. + + +# License +*esp32-wifi-manager* is MIT licensed. As such, it can be included in any project, commercial or not, as long as you retain original copyright. Please make sure to read the license file. diff --git a/dist/esp32-wifi-manager_0.0.4/component.mk b/dist/esp32-wifi-manager_0.0.4/component.mk new file mode 100644 index 00000000..75e34089 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/component.mk @@ -0,0 +1,4 @@ +COMPONENT_ADD_INCLUDEDIRS = src +COMPONENT_SRCDIRS = src +COMPONENT_DEPENDS = log esp_http_server +COMPONENT_EMBED_FILES := src/style.css src/code.js src/index.html diff --git a/dist/esp32-wifi-manager_0.0.4/examples/default_demo/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/CMakeLists.txt new file mode 100644 index 00000000..6458baae --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS ../../) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(default_demo) diff --git a/dist/esp32-wifi-manager_0.0.4/examples/default_demo/Makefile b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/Makefile new file mode 100644 index 00000000..fb186183 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := wifi_manager + +EXTRA_COMPONENT_DIRS := ../../ + +include $(IDF_PATH)/make/project.mk + diff --git a/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/CMakeLists.txt new file mode 100644 index 00000000..31addfc4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/CMakeLists.txt @@ -0,0 +1,3 @@ +set(COMPONENT_SRCS "user_main.c") + +register_component() diff --git a/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/component.mk b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/component.mk new file mode 100644 index 00000000..a98f634e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/idf_component.yml b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true diff --git a/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/user_main.c b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/user_main.c new file mode 100644 index 00000000..19af535e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/main/user_main.c @@ -0,0 +1,80 @@ +/* +Copyright (c) 2017-2019 Tony Pottier + +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. + +@file main.c +@author Tony Pottier +@brief Entry point for the ESP32 application. +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" + +#include "wifi_manager.h" + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "main"; + +/** + * @brief RTOS task that periodically prints the heap memory available. + * @note Pure debug information, should not be ever started on production code! This is an example on how you can integrate your code with wifi-manager + */ +void monitoring_task(void *pvParameter) +{ + for(;;){ + ESP_LOGI(TAG, "free heap: %lu",esp_get_free_heap_size()); + vTaskDelay( pdMS_TO_TICKS(10000) ); + } +} + + +/** + * @brief this is an exemple of a callback that you can setup in your own app to get notified of wifi manager event. + */ +void cb_connection_ok(void *pvParameter){ + ip_event_got_ip_t* param = (ip_event_got_ip_t*)pvParameter; + + /* transform IP to human readable string */ + char str_ip[16]; + esp_ip4addr_ntoa(¶m->ip_info.ip, str_ip, IP4ADDR_STRLEN_MAX); + + ESP_LOGI(TAG, "I have a connection and my IP is %s!", str_ip); +} + +void app_main() +{ + /* start the wifi manager */ + wifi_manager_start(); + + /* register a callback as an example to how you can integrate your code with the wifi manager */ + wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok); +#if CONFIG_SOC_CPU_CORES_NUM > 1 + /* your code should go here. Here we simply create a task on core 2 that monitors free heap memory */ + xTaskCreatePinnedToCore(&monitoring_task, "monitoring_task", 2048, NULL, 1, NULL, 1); +#endif +} diff --git a/dist/esp32-wifi-manager_0.0.4/examples/default_demo/sdkconfig.defaults b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/sdkconfig.defaults new file mode 100644 index 00000000..f44c06f4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/default_demo/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LWIP_IPV6=y diff --git a/dist/esp32-wifi-manager_0.0.4/examples/http_hook/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/CMakeLists.txt new file mode 100644 index 00000000..377f86fe --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +set(EXTRA_COMPONENT_DIRS ../../) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(http_hook) diff --git a/dist/esp32-wifi-manager_0.0.4/examples/http_hook/Makefile b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/Makefile new file mode 100644 index 00000000..fb186183 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := wifi_manager + +EXTRA_COMPONENT_DIRS := ../../ + +include $(IDF_PATH)/make/project.mk + diff --git a/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/CMakeLists.txt b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/CMakeLists.txt new file mode 100644 index 00000000..31addfc4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/CMakeLists.txt @@ -0,0 +1,3 @@ +set(COMPONENT_SRCS "user_main.c") + +register_component() diff --git a/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/component.mk b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/component.mk new file mode 100644 index 00000000..a98f634e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/idf_component.yml b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/idf_component.yml new file mode 100644 index 00000000..8826a619 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/mdns: "^1.3.2" + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true diff --git a/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/user_main.c b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/user_main.c new file mode 100644 index 00000000..2446f269 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/main/user_main.c @@ -0,0 +1,76 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file user_main.c +@author Tony Pottier +@brief Entry point for the ESP32 application. +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" + +#include "wifi_manager.h" +#include "http_app.h" + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "main"; + + +static esp_err_t my_get_handler(httpd_req_t *req){ + + /* our custom page sits at /helloworld in this example */ + if(strcmp(req->uri, "/helloworld") == 0){ + + ESP_LOGI(TAG, "Serving page /helloworld"); + + const char* response = "

Hello World!

"; + + httpd_resp_set_status(req, "200 OK"); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, response, strlen(response)); + } + else{ + /* send a 404 otherwise */ + httpd_resp_send_404(req); + } + + return ESP_OK; +} + + +void app_main() +{ + /* start the wifi manager */ + wifi_manager_start(); + + /* set custom handler for the http server + * Now navigate to /helloworld to see the custom page + * */ + http_app_set_handler_hook(HTTP_GET, &my_get_handler); + +} diff --git a/dist/esp32-wifi-manager_0.0.4/examples/http_hook/sdkconfig.defaults b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/sdkconfig.defaults new file mode 100644 index 00000000..f44c06f4 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/examples/http_hook/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LWIP_IPV6=y diff --git a/dist/esp32-wifi-manager_0.0.4/idf_component.yml b/dist/esp32-wifi-manager_0.0.4/idf_component.yml new file mode 100644 index 00000000..7b9cc72d --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/idf_component.yml @@ -0,0 +1,9 @@ +dependencies: + idf: + version: '>=4.2' +description: esp32-wifi-manager is a pure C esp-idf component for ESP32 that enables + easy management of wifi networks through a web portal. +license: MIT +repository: https://github.com/ankayca/esp32-wifi-manager/tree/update_build_system +url: https://github.com/ankayca/esp32-wifi-manager/tree/update_build_system +version: 0.0.4 diff --git a/dist/esp32-wifi-manager_0.0.4/src/ap.json b/dist/esp32-wifi-manager_0.0.4/src/ap.json new file mode 100644 index 00000000..de61f86a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/ap.json @@ -0,0 +1,12 @@ +[ +{"ssid":"Pantum-AP-A6D49F","chan":11,"rssi":-55,"auth":4}, +{"ssid":"a0308","chan":1,"rssi":-56,"auth":3}, +{"ssid":"dlink-D9D8","chan":11,"rssi":-82,"auth":4}, +{"ssid":"Linksys06730","chan":7,"rssi":-85,"auth":3}, +{"ssid":"SINGTEL-5171","chan":9,"rssi":-88,"auth":4}, +{"ssid":"1126-1","chan":11,"rssi":-89,"auth":4}, +{"ssid":"The Shah 5GHz-2","chan":1,"rssi":-90,"auth":3}, +{"ssid":"SINGTEL-1D28 (2G)","chan":11,"rssi":-91,"auth":3}, +{"ssid":"dlink-F864","chan":1,"rssi":-92,"auth":4}, +{"ssid":"dlink-74F0","chan":1,"rssi":-93,"auth":4} +] \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/code.js b/dist/esp32-wifi-manager_0.0.4/src/code.js new file mode 100644 index 00000000..934595a9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/code.js @@ -0,0 +1,352 @@ +// save some bytes +const gel = (e) => document.getElementById(e); + +const wifi_div = gel("wifi"); +const connect_div = gel("connect"); +const connect_manual_div = gel("connect_manual"); +const connect_wait_div = gel("connect-wait"); +const connect_details_div = gel("connect-details"); + +function docReady(fn) { + // see if DOM is already available + if ( + document.readyState === "complete" || + document.readyState === "interactive" + ) { + // call on next available tick + setTimeout(fn, 1); + } else { + document.addEventListener("DOMContentLoaded", fn); + } +} + +var selectedSSID = ""; +var refreshAPInterval = null; +var checkStatusInterval = null; + +function stopCheckStatusInterval() { + if (checkStatusInterval != null) { + clearInterval(checkStatusInterval); + checkStatusInterval = null; + } +} + +function stopRefreshAPInterval() { + if (refreshAPInterval != null) { + clearInterval(refreshAPInterval); + refreshAPInterval = null; + } +} + +function startCheckStatusInterval() { + checkStatusInterval = setInterval(checkStatus, 950); +} + +function startRefreshAPInterval() { + refreshAPInterval = setInterval(refreshAP, 3800); +} + +docReady(async function () { + gel("wifi-status").addEventListener( + "click", + () => { + wifi_div.style.display = "none"; + document.getElementById("connect-details").style.display = "block"; + }, + false + ); + + gel("manual_add").addEventListener( + "click", + (e) => { + selectedSSID = e.target.innerText; + + gel("ssid-pwd").textContent = selectedSSID; + wifi_div.style.display = "none"; + connect_manual_div.style.display = "block"; + connect_div.style.display = "none"; + + gel("connect-success").display = "none"; + gel("connect-fail").display = "none"; + }, + false + ); + + gel("wifi-list").addEventListener( + "click", + (e) => { + selectedSSID = e.target.innerText; + gel("ssid-pwd").textContent = selectedSSID; + connect_div.style.display = "block"; + wifi_div.style.display = "none"; + // init_cancel(); + }, + false + ); + + function cancel() { + selectedSSID = ""; + connect_div.style.display = "none"; + connect_manual_div.style.display = "none"; + wifi_div.style.display = "block"; + } + + gel("cancel").addEventListener("click", cancel, false); + + gel("manual_cancel").addEventListener("click", cancel, false); + + gel("join").addEventListener("click", performConnect, false); + + gel("manual_join").addEventListener( + "click", + (e) => { + performConnect("manual"); + }, + false + ); + + gel("ok-details").addEventListener( + "click", + () => { + connect_details_div.style.display = "none"; + wifi_div.style.display = "block"; + }, + false + ); + + gel("ok-credits").addEventListener( + "click", + () => { + gel("credits").style.display = "none"; + gel("app").style.display = "block"; + }, + false + ); + + gel("acredits").addEventListener( + "click", + () => { + event.preventDefault(); + gel("app").style.display = "none"; + gel("credits").style.display = "block"; + }, + false + ); + + gel("ok-connect").addEventListener( + "click", + () => { + connect_wait_div.style.display = "none"; + wifi_div.style.display = "block"; + }, + false + ); + + gel("disconnect").addEventListener( + "click", + () => { + gel("diag-disconnect").style.display = "block"; + gel("connect-details-wrap").classList.add("blur"); + }, + false + ); + + gel("no-disconnect").addEventListener( + "click", + () => { + gel("diag-disconnect").style.display = "none"; + gel("connect-details-wrap").classList.remove("blur"); + }, + false + ); + + gel("yes-disconnect").addEventListener("click", async () => { + stopCheckStatusInterval(); + selectedSSID = ""; + + document.getElementById("diag-disconnect").style.display = "none"; + gel("connect-details-wrap").classList.remove("blur"); + + await fetch("connect.json", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: { timestamp: Date.now() }, + }); + + startCheckStatusInterval(); + + connect_details_div.style.display = "none"; + wifi_div.style.display = "block"; + }); + + //first time the page loads: attempt get the connection status and start the wifi scan + await refreshAP(); + startCheckStatusInterval(); + startRefreshAPInterval(); +}); + +async function performConnect(conntype) { + //stop the status refresh. This prevents a race condition where a status + //request would be refreshed with wrong ip info from a previous connection + //and the request would automatically shows as succesful. + stopCheckStatusInterval(); + + //stop refreshing wifi list + stopRefreshAPInterval(); + + var pwd; + if (conntype == "manual") { + //Grab the manual SSID and PWD + selectedSSID = gel("manual_ssid").value; + pwd = gel("manual_pwd").value; + } else { + pwd = gel("pwd").value; + } + //reset connection + gel("loading").style.display = "block"; + gel("connect-success").style.display = "none"; + gel("connect-fail").style.display = "none"; + + gel("ok-connect").disabled = true; + gel("ssid-wait").textContent = selectedSSID; + connect_div.style.display = "none"; + connect_manual_div.style.display = "none"; + connect_wait_div.style.display = "block"; + + await fetch("connect.json", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Custom-ssid": selectedSSID, + "X-Custom-pwd": pwd, + }, + body: { timestamp: Date.now() }, + }); + + //now we can re-set the intervals regardless of result + startCheckStatusInterval(); + startRefreshAPInterval(); +} + +function rssiToIcon(rssi) { + if (rssi >= -60) { + return "w0"; + } else if (rssi >= -67) { + return "w1"; + } else if (rssi >= -75) { + return "w2"; + } else { + return "w3"; + } +} + +async function refreshAP(url = "ap.json") { + try { + var res = await fetch(url); + var access_points = await res.json(); + if (access_points.length > 0) { + //sort by signal strength + access_points.sort((a, b) => { + var x = a["rssi"]; + var y = b["rssi"]; + return x < y ? 1 : x > y ? -1 : 0; + }); + refreshAPHTML(access_points); + } + } catch (e) { + console.info("Access points returned empty from /ap.json!"); + } +} + +function refreshAPHTML(data) { + var h = ""; + data.forEach(function (e, idx, array) { + let ap_class = idx === array.length - 1 ? "" : " brdb"; + let rssicon = rssiToIcon(e.rssi); + let auth = e.auth == 0 ? "" : "pw"; + h += `
${e.ssid}
\n`; + }); + + gel("wifi-list").innerHTML = h; +} + +async function checkStatus(url = "status.json") { + try { + var response = await fetch(url); + var data = await response.json(); + if (data && data.hasOwnProperty("ssid") && data["ssid"] != "") { + if (data["ssid"] === selectedSSID) { + // Attempting connection + switch (data["urc"]) { + case 0: + console.info("Got connection!"); + document.querySelector( + "#connected-to div div div span" + ).textContent = data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = data["ip"]; + gel("netmask").textContent = data["netmask"]; + gel("gw").textContent = data["gw"]; + gel("wifi-status").style.display = "block"; + + //unlock the wait screen if needed + gel("ok-connect").disabled = false; + + //update wait screen + gel("loading").style.display = "none"; + gel("connect-success").style.display = "block"; + gel("connect-fail").style.display = "none"; + break; + case 1: + console.info("Connection attempt failed!"); + document.querySelector( + "#connected-to div div div span" + ).textContent = data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = "0.0.0.0"; + gel("netmask").textContent = "0.0.0.0"; + gel("gw").textContent = "0.0.0.0"; + + //don't show any connection + gel("wifi-status").display = "none"; + + //unlock the wait screen + gel("ok-connect").disabled = false; + + //update wait screen + gel("loading").display = "none"; + gel("connect-fail").style.display = "block"; + gel("connect-success").style.display = "none"; + break; + } + } else if (data.hasOwnProperty("urc") && data["urc"] === 0) { + console.info("Connection established"); + //ESP32 is already connected to a wifi without having the user do anything + if ( + gel("wifi-status").style.display == "" || + gel("wifi-status").style.display == "none" + ) { + document.querySelector("#connected-to div div div span").textContent = + data["ssid"]; + document.querySelector("#connect-details h1").textContent = + data["ssid"]; + gel("ip").textContent = data["ip"]; + gel("netmask").textContent = data["netmask"]; + gel("gw").textContent = data["gw"]; + gel("wifi-status").style.display = "block"; + } + } + } else if (data.hasOwnProperty("urc") && data["urc"] === 2) { + console.log("Manual disconnect requested..."); + if (gel("wifi-status").style.display == "block") { + gel("wifi-status").style.display = "none"; + } + } + } catch (e) { + console.info("Was not able to fetch /status.json"); + } +} diff --git a/dist/esp32-wifi-manager_0.0.4/src/component.mk b/dist/esp32-wifi-manager_0.0.4/src/component.mk new file mode 100644 index 00000000..0b9d7585 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/dist/esp32-wifi-manager_0.0.4/src/compress.bat b/dist/esp32-wifi-manager_0.0.4/src/compress.bat new file mode 100644 index 00000000..b9caad22 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/compress.bat @@ -0,0 +1,2 @@ +gzip index.html style.css --best --keep --force +pause \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/connect b/dist/esp32-wifi-manager_0.0.4/src/connect new file mode 100644 index 00000000..8c7fe211 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/connect @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/dns_server.c b/dist/esp32-wifi-manager_0.0.4/src/dns_server.c new file mode 100644 index 00000000..6693913a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/dns_server.c @@ -0,0 +1,184 @@ +/* +Copyright (c) 2019 Tony Pottier + +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. + +@file dns_server.c +@author Tony Pottier +@brief Defines an extremely basic DNS server for captive portal functionality. +It's basically a DNS hijack that replies to the esp's address no matter which +request is sent to it. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wifi_manager.h" +#include "dns_server.h" + +static const char TAG[] = "dns_server"; +static TaskHandle_t task_dns_server = NULL; +int socket_fd; + +void dns_server_start() { + if(task_dns_server == NULL){ + xTaskCreate(&dns_server, "dns_server", 3072, NULL, WIFI_MANAGER_TASK_PRIORITY-1, &task_dns_server); + } +} + +void dns_server_stop(){ + if(task_dns_server){ + vTaskDelete(task_dns_server); + close(socket_fd); + task_dns_server = NULL; + } + +} + + + +void dns_server(void *pvParameters) { + + + + struct sockaddr_in ra; + + /* Set redirection DNS hijack to the access point IP */ + ip4_addr_t ip_resolved; + inet_pton(AF_INET, DEFAULT_AP_IP, &ip_resolved); + + + /* Create UDP socket */ + socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (socket_fd < 0){ + ESP_LOGE(TAG, "Failed to create socket"); + exit(0); + } + + /* Bind to port 53 (typical DNS Server port) */ + esp_netif_ip_info_t ip; + esp_netif_t* netif_sta = wifi_manager_get_esp_netif_sta(); + ESP_ERROR_CHECK(esp_netif_get_ip_info(netif_sta, &ip)); + ra.sin_family = AF_INET; + ra.sin_addr.s_addr = ip.ip.addr; + ra.sin_port = htons(53); + if (bind(socket_fd, (struct sockaddr *)&ra, sizeof(struct sockaddr_in)) == -1) { + ESP_LOGE(TAG, "Failed to bind to 53/udp"); + close(socket_fd); + exit(1); + } + + struct sockaddr_in client; + socklen_t client_len; + client_len = sizeof(client); + int length; + uint8_t data[DNS_QUERY_MAX_SIZE]; /* dns query buffer */ + uint8_t response[DNS_ANSWER_MAX_SIZE]; /* dns response buffer */ + char ip_address[INET_ADDRSTRLEN]; /* buffer to store IPs as text. This is only used for debug and serves no other purpose */ + char *domain; /* This is only used for debug and serves no other purpose */ + int err; + + ESP_LOGI(TAG, "DNS Server listening on 53/udp"); + + /* Start loop to process DNS requests */ + for(;;) { + + memset(data, 0x00, sizeof(data)); /* reset buffer */ + length = recvfrom(socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client, &client_len); /* read udp request */ + + /*if the query is bigger than the buffer size we simply ignore it. This case should only happen in case of multiple + * queries within the same DNS packet and is not supported by this simple DNS hijack. */ + if ( length > 0 && ((length + sizeof(dns_answer_t)-1) < DNS_ANSWER_MAX_SIZE) ) { + + data[length] = '\0'; /*in case there's a bogus domain name that isn't null terminated */ + + /* Generate header message */ + memcpy(response, data, sizeof(dns_header_t)); + dns_header_t *dns_header = (dns_header_t*)response; + dns_header->QR = 1; /*response bit */ + dns_header->OPCode = DNS_OPCODE_QUERY; /* no support for other type of response */ + dns_header->AA = 1; /*authoritative answer */ + dns_header->RCode = DNS_REPLY_CODE_NO_ERROR; /* no error */ + dns_header->TC = 0; /*no truncation */ + dns_header->RD = 0; /*no recursion */ + dns_header->ANCount = dns_header->QDCount; /* set answer count = question count -- duhh! */ + dns_header->NSCount = 0x0000; /* name server resource records = 0 */ + dns_header->ARCount = 0x0000; /* resource records = 0 */ + + + /* copy the rest of the query in the response */ + memcpy(response + sizeof(dns_header_t), data + sizeof(dns_header_t), length - sizeof(dns_header_t)); + + + /* extract domain name and request IP for debug */ + inet_ntop(AF_INET, &(client.sin_addr), ip_address, INET_ADDRSTRLEN); + domain = (char*) &data[sizeof(dns_header_t) + 1]; + for(char* c=domain; *c != '\0'; c++){ + if(*c < ' ' || *c > 'z') *c = '.'; /* technically we should test if the first two bits are 00 (e.g. if( (*c & 0xC0) == 0x00) *c = '.') but this makes the code a lot more readable */ + } + ESP_LOGI(TAG, "Replying to DNS request for %s from %s", domain, ip_address); + + + /* create DNS answer at the end of the query*/ + dns_answer_t *dns_answer = (dns_answer_t*)&response[length]; + dns_answer->NAME = __bswap_16(0xC00C); /* This is a pointer to the beginning of the question. As per DNS standard, first two bits must be set to 11 for some odd reason hence 0xC0 */ + dns_answer->TYPE = __bswap_16(DNS_ANSWER_TYPE_A); + dns_answer->CLASS = __bswap_16(DNS_ANSWER_CLASS_IN); + dns_answer->TTL = (uint32_t)0x00000000; /* no caching. Avoids DNS poisoning since this is a DNS hijack */ + dns_answer->RDLENGTH = __bswap_16(0x0004); /* 4 byte => size of an ipv4 address */ + dns_answer->RDATA = ip_resolved.addr; + + err = sendto(socket_fd, response, length+sizeof(dns_answer_t), 0, (struct sockaddr *)&client, client_len); + if (err < 0) { + ESP_LOGE(TAG, "UDP sendto failed: %d", err); + } + } + + taskYIELD(); /* allows the freeRTOS scheduler to take over if needed. DNS daemon should not be taxing on the system */ + + } + close(socket_fd); + + vTaskDelete ( NULL ); +} + + + + diff --git a/dist/esp32-wifi-manager_0.0.4/src/dns_server.h b/dist/esp32-wifi-manager_0.0.4/src/dns_server.h new file mode 100644 index 00000000..d1da1f8c --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/dns_server.h @@ -0,0 +1,137 @@ +/* +Copyright (c) 2019 Tony Pottier + +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. + +@file dns_server.h +@author Tony Pottier +@brief Defines an extremly basic DNS server for captive portal functionality. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +@see http://www.zytrax.com/books/dns/ch15 +*/ + +#ifndef MAIN_DNS_SERVER_H_ +#define MAIN_DNS_SERVER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + + +/** 12 byte header, 64 byte domain name, 4 byte qtype/qclass. This NOT compliant with the RFC, but it's good enough for a captive portal + * if a DNS query is too big it just wont be processed. */ +#define DNS_QUERY_MAX_SIZE 80 + +/** Query + 2 byte ptr, 2 byte type, 2 byte class, 4 byte TTL, 2 byte len, 4 byte data */ +#define DNS_ANSWER_MAX_SIZE (DNS_QUERY_MAX_SIZE+16) + + +/** + * @brief RCODE values used in a DNS header message + */ +typedef enum dns_reply_code_t { + DNS_REPLY_CODE_NO_ERROR = 0, + DNS_REPLY_CODE_FORM_ERROR = 1, + DNS_REPLY_CODE_SERVER_FAILURE = 2, + DNS_REPLY_CODE_NON_EXISTANT_DOMAIN = 3, + DNS_REPLY_CODE_NOT_IMPLEMENTED = 4, + DNS_REPLY_CODE_REFUSED = 5, + DNS_REPLY_CODE_YXDOMAIN = 6, + DNS_REPLY_CODE_YXRRSET = 7, + DNS_REPLY_CODE_NXRRSET = 8 +}dns_reply_code_t; + + + +/** + * @brief OPCODE values used in a DNS header message + */ +typedef enum dns_opcode_code_t { + DNS_OPCODE_QUERY = 0, + DNS_OPCODE_IQUERY = 1, + DNS_OPCODE_STATUS = 2 +}dns_opcode_code_t; + + + +/** + * @brief Represents a 12 byte DNS header. + * __packed__ is needed to prevent potential unwanted memory alignments + */ +typedef struct __attribute__((__packed__)) dns_header_t{ + uint16_t ID; // identification number + uint8_t RD : 1; // recursion desired + uint8_t TC : 1; // truncated message + uint8_t AA : 1; // authoritive answer + uint8_t OPCode : 4; // message_type + uint8_t QR : 1; // query/response flag + uint8_t RCode : 4; // response code + uint8_t Z : 3; // its z! reserved + uint8_t RA : 1; // recursion available + uint16_t QDCount; // number of question entries + uint16_t ANCount; // number of answer entries + uint16_t NSCount; // number of authority entries + uint16_t ARCount; // number of resource entries +}dns_header_t; + + + +typedef enum dns_answer_type_t { + DNS_ANSWER_TYPE_A = 1, + DNS_ANSWER_TYPE_NS = 2, + DNS_ANSWER_TYPE_CNAME = 5, + DNS_ANSWER_TYPE_SOA = 6, + DNS_ANSWER_TYPE_WKS = 11, + DNS_ANSWER_TYPE_PTR = 12, + DNS_ANSWER_TYPE_MX = 15, + DNS_ANSWER_TYPE_SRV = 33, + DNS_ANSWER_TYPE_AAAA = 28 +}dns_answer_type_t; + +typedef enum dns_answer_class_t { + DNS_ANSWER_CLASS_IN = 1 +}dns_answer_class_t; + + + +typedef struct __attribute__((__packed__)) dns_answer_t{ + uint16_t NAME; /* for the sake of simplicity only 16 bit pointers are supported */ + uint16_t TYPE; /* Unsigned 16 bit value. The resource record types - determines the content of the RDATA field. */ + uint16_t CLASS; /* Class of response. */ + uint32_t TTL; /* The time in seconds that the record may be cached. A value of 0 indicates the record should not be cached. */ + uint16_t RDLENGTH; /* Unsigned 16-bit value that defines the length in bytes of the RDATA record. */ + uint32_t RDATA; /* For the sake of simplicity only ipv4 is supported, and as such it's a unsigned 32 bit */ +}dns_answer_t; + +void dns_server(void *pvParameters); +void dns_server_start(); +void dns_server_stop(); + + + +#ifdef __cplusplus +} +#endif + + +#endif /* MAIN_DNS_SERVER_H_ */ diff --git a/dist/esp32-wifi-manager_0.0.4/src/http_app.c b/dist/esp32-wifi-manager_0.0.4/src/http_app.c new file mode 100644 index 00000000..8dbc40ef --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/http_app.c @@ -0,0 +1,479 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file http_app.c +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include "esp_netif.h" +#include + +#include "wifi_manager.h" +#include "http_app.h" + + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "http_server"; + +/* @brief the HTTP server handle */ +static httpd_handle_t httpd_handle = NULL; + +/* function pointers to URI handlers that can be user made */ +esp_err_t (*custom_get_httpd_uri_handler)(httpd_req_t *r) = NULL; +esp_err_t (*custom_post_httpd_uri_handler)(httpd_req_t *r) = NULL; + +/* strings holding the URLs of the wifi manager */ +static char* http_root_url = NULL; +static char* http_redirect_url = NULL; +static char* http_js_url = NULL; +static char* http_css_url = NULL; +static char* http_connect_url = NULL; +static char* http_ap_url = NULL; +static char* http_status_url = NULL; + +/** + * @brief embedded binary data. + * @see file "component.mk" + * @see https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html#embedding-binary-data + */ +extern const uint8_t style_css_start[] asm("_binary_style_css_start"); +extern const uint8_t style_css_end[] asm("_binary_style_css_end"); +extern const uint8_t code_js_start[] asm("_binary_code_js_start"); +extern const uint8_t code_js_end[] asm("_binary_code_js_end"); +extern const uint8_t index_html_start[] asm("_binary_index_html_start"); +extern const uint8_t index_html_end[] asm("_binary_index_html_end"); + + +/* const httpd related values stored in ROM */ +const static char http_200_hdr[] = "200 OK"; +const static char http_302_hdr[] = "302 Found"; +const static char http_400_hdr[] = "400 Bad Request"; +const static char http_404_hdr[] = "404 Not Found"; +const static char http_503_hdr[] = "503 Service Unavailable"; +const static char http_location_hdr[] = "Location"; +const static char http_content_type_html[] = "text/html"; +const static char http_content_type_js[] = "text/javascript"; +const static char http_content_type_css[] = "text/css"; +const static char http_content_type_json[] = "application/json"; +const static char http_cache_control_hdr[] = "Cache-Control"; +const static char http_cache_control_no_cache[] = "no-store, no-cache, must-revalidate, max-age=0"; +const static char http_cache_control_cache[] = "public, max-age=31536000"; +const static char http_pragma_hdr[] = "Pragma"; +const static char http_pragma_no_cache[] = "no-cache"; + + + +esp_err_t http_app_set_handler_hook( httpd_method_t method, esp_err_t (*handler)(httpd_req_t *r) ){ + + if(method == HTTP_GET){ + custom_get_httpd_uri_handler = handler; + return ESP_OK; + } + else if(method == HTTP_POST){ + custom_post_httpd_uri_handler = handler; + return ESP_OK; + } + else{ + return ESP_ERR_INVALID_ARG; + } + +} + + +static esp_err_t http_server_delete_handler(httpd_req_t *req){ + + ESP_LOGI(TAG, "DELETE %s", req->uri); + + /* DELETE /connect.json */ + if(strcmp(req->uri, http_connect_url) == 0){ + wifi_manager_disconnect_async(); + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, NULL, 0); + } + else{ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + + return ESP_OK; +} + + +static esp_err_t http_server_post_handler(httpd_req_t *req){ + + + esp_err_t ret = ESP_OK; + + ESP_LOGI(TAG, "POST %s", req->uri); + + /* POST /connect.json */ + if(strcmp(req->uri, http_connect_url) == 0){ + + + /* buffers for the headers */ + size_t ssid_len = 0, password_len = 0; + char *ssid = NULL, *password = NULL; + + /* len of values provided */ + ssid_len = httpd_req_get_hdr_value_len(req, "X-Custom-ssid"); + password_len = httpd_req_get_hdr_value_len(req, "X-Custom-pwd"); + + + if(ssid_len && ssid_len <= MAX_SSID_SIZE && password_len && password_len <= MAX_PASSWORD_SIZE){ + + /* get the actual value of the headers */ + ssid = malloc(sizeof(char) * (ssid_len + 1)); + password = malloc(sizeof(char) * (password_len + 1)); + httpd_req_get_hdr_value_str(req, "X-Custom-ssid", ssid, ssid_len+1); + httpd_req_get_hdr_value_str(req, "X-Custom-pwd", password, password_len+1); + + wifi_config_t* config = wifi_manager_get_wifi_sta_config(); + memset(config, 0x00, sizeof(wifi_config_t)); + memcpy(config->sta.ssid, ssid, ssid_len); + memcpy(config->sta.password, password, password_len); + ESP_LOGI(TAG, "ssid: %s, password: %s", ssid, password); + ESP_LOGD(TAG, "http_server_post_handler: wifi_manager_connect_async() call"); + wifi_manager_connect_async(); + + /* free memory */ + free(ssid); + free(password); + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, NULL, 0); + + } + else{ + /* bad request the authentification header is not complete/not the correct format */ + httpd_resp_set_status(req, http_400_hdr); + httpd_resp_send(req, NULL, 0); + } + + } + else{ + + if(custom_post_httpd_uri_handler == NULL){ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + else{ + + /* if there's a hook, run it */ + ret = (*custom_post_httpd_uri_handler)(req); + } + } + + return ret; +} + + +static esp_err_t http_server_get_handler(httpd_req_t *req){ + + char* host = NULL; + size_t buf_len; + esp_err_t ret = ESP_OK; + + ESP_LOGD(TAG, "GET %s", req->uri); + + /* Get header value string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1; + if (buf_len > 1) { + host = malloc(buf_len); + if(httpd_req_get_hdr_value_str(req, "Host", host, buf_len) != ESP_OK){ + /* if something is wrong we just 0 the whole memory */ + memset(host, 0x00, buf_len); + } + } + + /* determine if Host is from the STA IP address */ + wifi_manager_lock_sta_ip_string(portMAX_DELAY); + bool access_from_sta_ip = host != NULL?strstr(host, wifi_manager_get_sta_ip_string()):false; + wifi_manager_unlock_sta_ip_string(); + + + if (host != NULL && !strstr(host, DEFAULT_AP_IP) && !access_from_sta_ip) { + + /* Captive Portal functionality */ + /* 302 Redirect to IP of the access point */ + httpd_resp_set_status(req, http_302_hdr); + httpd_resp_set_hdr(req, http_location_hdr, http_redirect_url); + httpd_resp_send(req, NULL, 0); + + } + else{ + + /* GET / */ + if(strcmp(req->uri, http_root_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_html); + httpd_resp_send(req, (char*)index_html_start, index_html_end - index_html_start); + } + /* GET /code.js */ + else if(strcmp(req->uri, http_js_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_js); + httpd_resp_send(req, (char*)code_js_start, code_js_end - code_js_start); + } + /* GET /style.css */ + else if(strcmp(req->uri, http_css_url) == 0){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_css); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_cache); + httpd_resp_send(req, (char*)style_css_start, style_css_end - style_css_start); + } + /* GET /ap.json */ + else if(strcmp(req->uri, http_ap_url) == 0){ + + /* if we can get the mutex, write the last version of the AP list */ + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + char* ap_buf = wifi_manager_get_ap_list_json(); + httpd_resp_send(req, ap_buf, strlen(ap_buf)); + wifi_manager_unlock_json_buffer(); + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /ap.json failed to obtain mutex"); + } + + /* request a wifi scan */ + wifi_manager_scan_async(); + } + /* GET /status.json */ + else if(strcmp(req->uri, http_status_url) == 0){ + + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + char *buff = wifi_manager_get_ip_info_json(); + if(buff){ + httpd_resp_set_status(req, http_200_hdr); + httpd_resp_set_type(req, http_content_type_json); + httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache); + httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache); + httpd_resp_send(req, buff, strlen(buff)); + wifi_manager_unlock_json_buffer(); + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + } + } + else{ + httpd_resp_set_status(req, http_503_hdr); + httpd_resp_send(req, NULL, 0); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /status.json failed to obtain mutex"); + } + } + else{ + + if(custom_get_httpd_uri_handler == NULL){ + httpd_resp_set_status(req, http_404_hdr); + httpd_resp_send(req, NULL, 0); + } + else{ + + /* if there's a hook, run it */ + ret = (*custom_get_httpd_uri_handler)(req); + } + } + + } + + /* memory clean up */ + if(host != NULL){ + free(host); + } + + return ret; + +} + +/* URI wild card for any GET request */ +static const httpd_uri_t http_server_get_request = { + .uri = "*", + .method = HTTP_GET, + .handler = http_server_get_handler +}; + +static const httpd_uri_t http_server_post_request = { + .uri = "*", + .method = HTTP_POST, + .handler = http_server_post_handler +}; + +static const httpd_uri_t http_server_delete_request = { + .uri = "*", + .method = HTTP_DELETE, + .handler = http_server_delete_handler +}; + + +void http_app_stop(){ + + if(httpd_handle != NULL){ + + + /* dealloc URLs */ + if(http_root_url) { + free(http_root_url); + http_root_url = NULL; + } + if(http_redirect_url){ + free(http_redirect_url); + http_redirect_url = NULL; + } + if(http_js_url){ + free(http_js_url); + http_js_url = NULL; + } + if(http_css_url){ + free(http_css_url); + http_css_url = NULL; + } + if(http_connect_url){ + free(http_connect_url); + http_connect_url = NULL; + } + if(http_ap_url){ + free(http_ap_url); + http_ap_url = NULL; + } + if(http_status_url){ + free(http_status_url); + http_status_url = NULL; + } + + /* stop server */ + httpd_stop(httpd_handle); + httpd_handle = NULL; + } +} + + +/** + * @brief helper to generate URLs of the wifi manager + */ +static char* http_app_generate_url(const char* page){ + + char* ret; + + int root_len = strlen(WEBAPP_LOCATION); + const size_t url_sz = sizeof(char) * ( (root_len+1) + ( strlen(page) + 1) ); + + ret = malloc(url_sz); + memset(ret, 0x00, url_sz); + strcpy(ret, WEBAPP_LOCATION); + ret = strcat(ret, page); + + return ret; +} + +void http_app_start(bool lru_purge_enable){ + + esp_err_t err; + + if(httpd_handle == NULL){ + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + /* this is an important option that isn't set up by default. + * We could register all URLs one by one, but this would not work while the fake DNS is active */ + config.uri_match_fn = httpd_uri_match_wildcard; + config.lru_purge_enable = lru_purge_enable; + + /* generate the URLs */ + if(http_root_url == NULL){ + int root_len = strlen(WEBAPP_LOCATION); + + /* all the pages */ + const char page_js[] = "code.js"; + const char page_css[] = "style.css"; + const char page_connect[] = "connect.json"; + const char page_ap[] = "ap.json"; + const char page_status[] = "status.json"; + + /* root url, eg "/" */ + const size_t http_root_url_sz = sizeof(char) * (root_len+1); + http_root_url = malloc(http_root_url_sz); + memset(http_root_url, 0x00, http_root_url_sz); + strcpy(http_root_url, WEBAPP_LOCATION); + + /* redirect url */ + size_t redirect_sz = 22 + root_len + 1; /* strlen(http://255.255.255.255) + strlen("/") + 1 for \0 */ + http_redirect_url = malloc(sizeof(char) * redirect_sz); + *http_redirect_url = '\0'; + + if(root_len == 1){ + snprintf(http_redirect_url, redirect_sz, "http://%s", DEFAULT_AP_IP); + } + else{ + snprintf(http_redirect_url, redirect_sz, "http://%s%s", DEFAULT_AP_IP, WEBAPP_LOCATION); + } + + /* generate the other pages URLs*/ + http_js_url = http_app_generate_url(page_js); + http_css_url = http_app_generate_url(page_css); + http_connect_url = http_app_generate_url(page_connect); + http_ap_url = http_app_generate_url(page_ap); + http_status_url = http_app_generate_url(page_status); + + } + + err = httpd_start(&httpd_handle, &config); + + if (err == ESP_OK) { + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(httpd_handle, &http_server_get_request); + httpd_register_uri_handler(httpd_handle, &http_server_post_request); + httpd_register_uri_handler(httpd_handle, &http_server_delete_request); + } + } + +} diff --git a/dist/esp32-wifi-manager_0.0.4/src/http_app.h b/dist/esp32-wifi-manager_0.0.4/src/http_app.h new file mode 100644 index 00000000..53f9472a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/http_app.h @@ -0,0 +1,73 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file http_app.h +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#ifndef HTTP_APP_H_INCLUDED +#define HTTP_APP_H_INCLUDED + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/** @brief Defines the URL where the wifi manager is located + * By default it is at the server root (ie "/"). If you wish to add your own webpages + * you may want to relocate the wifi manager to another URL, for instance /wifimanager + */ +#define WEBAPP_LOCATION CONFIG_WEBAPP_LOCATION + + +/** + * @brief spawns the http server + */ +void http_app_start(bool lru_purge_enable); + +/** + * @brief stops the http server + */ +void http_app_stop(); + +/** + * @brief sets a hook into the wifi manager URI handlers. Setting the handler to NULL disables the hook. + * @return ESP_OK in case of success, ESP_ERR_INVALID_ARG if the method is unsupported. + */ +esp_err_t http_app_set_handler_hook( httpd_method_t method, esp_err_t (*handler)(httpd_req_t *r) ); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/dist/esp32-wifi-manager_0.0.4/src/index.html b/dist/esp32-wifi-manager_0.0.4/src/index.html new file mode 100644 index 00000000..ce4bf146 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/index.html @@ -0,0 +1,136 @@ + + + + + + + + + esp32-wifi-manager + + +
+
+
+
+

Wi-Fi

+
+
+

Connected to:

+
+
+
+
+

Manual connect

+
+
ADD (HIDDEN) SSID
+
+

or choose a network...

+
+
+
Powered by esp32-wifi-manager.
+
+
+
+

Enter Details

+
+

Manual Connection

+
+ + +
+
+ + +
+
+
+
+

Enter Password

+
+

Password for

+
+ +
+
+ + +
+
+
+
+

Please wait...

+
+

Connecting to

+
+
+
+

You may lose wifi access while the esp32 recalibrates its radio. Please wait until your device automatically reconnects. This can take up to 30s.

+
+
+

Success!

+
+
+

Connection failed

+

Please double-check wifi password if any and make sure the access point has good signal.

+
+
+
+ +
+
+
+
+
+

+
+

+
+
+ +
+
+

IP Address

+
+
IP Address:
+
Subnet Mask:
+
Default Gateway:
+
+
+ +
+
+
+
+

Are you sure you would like to disconnect from this wifi?

+
+ + +
+
+
+
+
+
+
+
+

About this app...

+
+

+
+

esp32-wifi-manager, © 2017-2020, Tony Pottier
Licended under the MIT License.

+

+ This app would not be possible without the following libraries: +

+
    +
  • SpinKit, © 2015, Tobias Ahlin. Licensed under the MIT License.
  • +
  • jQuery, The jQuery Foundation. Licensed under the MIT License.
  • +
  • cJSON, © 2009-2017, Dave Gamble and cJSON contributors. Licensed under the MIT License.
  • +
+
+
+ +
+
+ + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/json.c b/dist/esp32-wifi-manager_0.0.4/src/json.c new file mode 100644 index 00000000..d448711a --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/json.c @@ -0,0 +1,143 @@ +/* +@file json.c +@brief handles very basic JSON with a minimal footprint on the system + +This code is a lightly modified version of cJSON 1.4.7. cJSON is licensed under the MIT license: +Copyright (c) 2009 Dave Gamble + +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. + +@see https://github.com/DaveGamble/cJSON +*/ + +#include +#include +#include +#include +#include "json.h" + + +bool json_print_string(const unsigned char *input, unsigned char *output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + //output = ensure(output_buffer, sizeof("\"\""), hooks); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + if (strchr("\"\\\b\f\n\r\t", *input_pointer)) + { + /* one character escape sequence */ + escape_characters++; + } + else if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + /* in the original cJSON it is possible to realloc here in case output buffer is too small. + * This is overkill for an embedded system. */ + output = output_buffer; + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + diff --git a/dist/esp32-wifi-manager_0.0.4/src/json.h b/dist/esp32-wifi-manager_0.0.4/src/json.h new file mode 100644 index 00000000..4c1c8c75 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/json.h @@ -0,0 +1,47 @@ +/* +@file json.h +@brief handles very basic JSON with a minimal footprint on the system + +This code is a lightly modified version of cJSON 1.4.7. cJSON is licensed under the MIT license: +Copyright (c) 2009 Dave Gamble + +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. + +@see https://github.com/DaveGamble/cJSON +*/ + +#ifndef JSON_H_INCLUDED +#define JSON_H_INCLUDED + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Render the cstring provided to a JSON escaped version that can be printed. + * @param input the input buffer to be escaped. + * @param output_buffer the output buffer to write to. You must ensure it is big enough to contain the final string. + * @see cJSON equivlaent static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) + */ +bool json_print_string(const unsigned char *input, unsigned char *output_buffer); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_H_INCLUDED */ diff --git a/dist/esp32-wifi-manager_0.0.4/src/lock.svg b/dist/esp32-wifi-manager_0.0.4/src/lock.svg new file mode 100644 index 00000000..f7ebc7e9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/lock.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/nvs_sync.c b/dist/esp32-wifi-manager_0.0.4/src/nvs_sync.c new file mode 100644 index 00000000..4ceed8bd --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/nvs_sync.c @@ -0,0 +1,79 @@ +/** +Copyright (c) 2020 Tony Pottier + +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. + +@file nvs_sync.c +@author Tony Pottier +@brief Exposes a simple API to synchronize NVS memory read and writes + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include "nvs_sync.h" + + +static SemaphoreHandle_t nvs_sync_mutex = NULL; + +esp_err_t nvs_sync_create(){ + if(nvs_sync_mutex == NULL){ + + nvs_sync_mutex = xSemaphoreCreateMutex(); + + if(nvs_sync_mutex){ + return ESP_OK; + } + else{ + return ESP_FAIL; + } + } + else{ + return ESP_OK; + } +} + +void nvs_sync_free(){ + if(nvs_sync_mutex != NULL){ + vSemaphoreDelete( nvs_sync_mutex ); + nvs_sync_mutex = NULL; + } +} + +bool nvs_sync_lock(TickType_t xTicksToWait){ + if(nvs_sync_mutex){ + if( xSemaphoreTake( nvs_sync_mutex, xTicksToWait ) == pdTRUE ) { + return true; + } + else{ + return false; + } + } + else{ + return false; + } +} + +void nvs_sync_unlock(){ + xSemaphoreGive( nvs_sync_mutex ); +} \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/nvs_sync.h b/dist/esp32-wifi-manager_0.0.4/src/nvs_sync.h new file mode 100644 index 00000000..e03396e5 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/nvs_sync.h @@ -0,0 +1,76 @@ +/** +Copyright (c) 2020 Tony Pottier + +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. + +@file nvs_sync.h +@author Tony Pottier +@brief Exposes a simple API to synchronize NVS memory read and writes + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + + + +#ifndef WIFI_MANAGER_NVS_SYNC_H_INCLUDED +#define WIFI_MANAGER_NVS_SYNC_H_INCLUDED + +#include /* for type bool */ +#include /* for TickType_t */ +#include /* for esp_err_t */ + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Attempts to get hold of the NVS semaphore for a set amount of ticks. + * @note If you are uncertain about the number of ticks to wait use portMAX_DELAY. + * @return true on a succesful lock, false otherwise + */ +bool nvs_sync_lock(TickType_t xTicksToWait); + + +/** + * @brief Releases the NVS semaphore + */ +void nvs_sync_unlock(); + + +/** + * @brief Create the NVS semaphore + * @return ESP_OK: success or if the semaphore already exists + * ESP_FAIL: failure + */ +esp_err_t nvs_sync_create(); + +/** + * @brief Frees memory associated with the NVS semaphore + * @warning Do not delete a semaphore that has tasks blocked on it (tasks that are in the Blocked state waiting for the semaphore to become available). + */ +void nvs_sync_free(); + + +#ifdef __cplusplus +} +#endif + +#endif /* WIFI_MANAGER_NVS_SYNC_H_INCLUDED */ \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/settings.svg b/dist/esp32-wifi-manager_0.0.4/src/settings.svg new file mode 100644 index 00000000..19cacede --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/settings.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/status b/dist/esp32-wifi-manager_0.0.4/src/status new file mode 100644 index 00000000..3824a535 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/status @@ -0,0 +1 @@ +{"ssid":"zodmgbbq","ip":"192.168.1.119","netmask":"255.255.255.0","gw":"192.168.1.1","urc":0} \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/style.css b/dist/esp32-wifi-manager_0.0.4/src/style.css new file mode 100644 index 00000000..5c83cc2f --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/style.css @@ -0,0 +1,248 @@ +body { + background-color: #eee; + border: 0; + margin: 0; + font: 1.1em tahoma, arial, sans-serif; +} +a { + color: darkblue; + transition: color .2s ease-out; + text-decoration: none +} +a:hover { + color: red; +} +input { + display: none; + font: 1.1em tahoma, arial, sans-serif; +} +input:focus, +select:focus, +textarea:focus, +button:focus { + outline: none; +} +input[type="button"] { + width: 100px; + padding: 5px; + text-align: center; + display: block; +} +p { + padding: 10px; +} +#credits { + display: none; +} +#app {} #app-wrap {} #disconnect { + width: 150px; +} +.diag-box { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + height: 100%; + width: 100%; + display: none; +} +.diag-box-win { + position: absolute; + left: 10%; + width: 80%; + text-align: center; + border: 2px outset #888; + background-color: #fff; + border-radius: 10px; + top: 20%; +} +.blur { + -webkit-filter: blur(2px); + -moz-filter: blur(2px); + -ms-filter: blur(2px); + -o-filter: blur(2px); + filter: blur(2px); +} +.ape { + margin-left: 20px; + padding: 10px 0px 10px 10px; +} +.ape:hover { + cursor: pointer; +} +.brdb { + border-bottom: 1px solid #888; +} +header { + background-color: #fff; + border-bottom: 1px solid #888; +} +section { + background-color: #fff; + border-bottom: 1px solid #888; + border-top: 1px solid #888; +} +h1 { + display: block; + text-align: center; + margin: 0; + padding: 15px; + font-size: 1.4em +} +h2 { + margin: 0; + margin-top: 20px; + padding: 10px; + text-transform: uppercase; + color: #888; + font-size: 1.0em +} +h3 { + margin: 0; + text-align: center; + padding: 20px 0px 20px 0px; +} +.gr { + color: green; +} +.rd { + color: red; +} +#wifi-status { + display: none; +} +#connect { + display: none; +} +#connect_manual { + display: none; +} +#manual_ssid { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +#manual_pwd { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +#pwd { + border: none; + width: 80%; + margin-left: 35px; + padding: 10px 0px 10px 10px; + display: block +} +.buttons { + padding: 15px; +} +#join { + float: right; +} +#manual_join { + float: right; +} +#yes-disconnect { + display: inline-block; + margin-left: 20px; +} +#no-disconnect { + display: inline-block; +} +.ctr { + margin: 0 auto; +} +.tctr { + text-align: center; +} +#connect-wait { + display: none; +} +#connect-success { + display: none; +} +#connect-fail { + display: none; +} +#connect-details { + display: none; +} +.fr { + float: right; + margin-right: 20px; +} +.w0 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPHBhdGggZD0iTTUsMTNMNywxNUM5Ljc2LDEyLjI0IDE0LjI0LDEyLjI0IDE3LDE1TDE5LDEzQzE1LjE0LDkuMTQgOC44Nyw5LjE0IDUsMTNaIiBzdHlsZT0iZmlsbDogYmxhY2s7Ii8+CjxwYXRoIGQ9Ik05LDE3TDEyLDIwTDE1LDE3QzEzLjM1LDE1LjM0IDEwLjY2LDE1LjM0IDksMTdaIiBzdHlsZT0iZmlsbDogYmxhY2s7Ii8+Cjwvc3ZnPgo=') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w1 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPHBhdGggZD0iTTksMTdMMTIsMjBMMTUsMTdDMTMuMzUsMTUuMzQgMTAuNjYsMTUuMzQgOSwxN1oiIHN0eWxlPSJmaWxsOiBibGFjazsiLz4KPC9zdmc+Cg==') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w2 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNOSwxN0wxMiwyMEwxNSwxN0MxMy4zNSwxNS4zNCAxMC42NiwxNS4zNCA5LDE3WiIgc3R5bGU9ImZpbGw6IGJsYWNrOyIvPgo8L3N2Zz4K') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.w3 { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIGQ9Ik0xLDlMMywxMUM3Ljk3LDYuMDMgMTYuMDMsNi4wMyAyMSwxMUwyMyw5QzE2LjkzLDIuOTMgNy4wOCwyLjkzIDEsOVoiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNNSwxM0w3LDE1QzkuNzYsMTIuMjQgMTQuMjQsMTIuMjQgMTcsMTVMMTksMTNDMTUuMTQsOS4xNCA4Ljg3LDkuMTQgNSwxM1oiIHN0eWxlPSJmaWxsOiBncmF5OyIvPgo8cGF0aCBkPSJNOSwxN0wxMiwyMEwxNSwxN0MxMy4zNSwxNS4zNCAxMC42NiwxNS4zNCA5LDE3WiIgc3R5bGU9ImZpbGw6IGdyYXk7Ii8+Cjwvc3ZnPgo=') no-repeat right top; + height: 24px; + margin-right: 20px; +} +.pw { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIHN0eWxlPSJmaWxsOiBibGFjazsiIGQ9Ik0xOCA4aC0xVjZjMC0yLjc2LTIuMjQtNS01LTVTNyAzLjI0IDcgNnYySDZjLTEuMSAwLTIgLjktMiAydjEwYzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJWMTBjMC0xLjEtLjktMi0yLTJ6bS02IDljLTEuMSAwLTItLjktMi0ycy45LTIgMi0yIDIgLjkgMiAyLS45IDItMiAyem0zLjEtOUg4LjlWNmMwLTEuNzEgMS4zOS0zLjEgMy4xLTMuMSAxLjcxIDAgMy4xIDEuMzkgMy4xIDMuMXYyeiI+PC9wYXRoPgo8L3N2Zz4=') no-repeat right top; + height: 24px; + margin-right: 30px; +} +/* SpinKit is licensed under the MIT License. Copyright (c) 2015 Tobias Ahlin */ + +.spinner { + width: 40px; + height: 40px; + position: relative; + margin: 100px auto; +} +.double-bounce1, +.double-bounce2 { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #333; + opacity: 0.6; + position: absolute; + top: 0; + left: 0; + -webkit-animation: sk-bounce 2.0s infinite ease-in-out; + animation: sk-bounce 2.0s infinite ease-in-out; +} +.double-bounce2 { + -webkit-animation-delay: -1.0s; + animation-delay: -1.0s; +} +@-webkit-keyframes sk-bounce { + 0%, 100% { + -webkit-transform: scale(0.0) + } + 50% { + -webkit-transform: scale(1.0) + } +} +@keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + -webkit-transform: scale(0.0); + } + 50% { + transform: scale(1.0); + -webkit-transform: scale(1.0); + } +} +/* end of SpinKit */ \ No newline at end of file diff --git a/dist/esp32-wifi-manager_0.0.4/src/wifi0.svg b/dist/esp32-wifi-manager_0.0.4/src/wifi0.svg new file mode 100644 index 00000000..394a6495 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/wifi0.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.4/src/wifi1.svg b/dist/esp32-wifi-manager_0.0.4/src/wifi1.svg new file mode 100644 index 00000000..c64211bf --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/wifi1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.4/src/wifi2.svg b/dist/esp32-wifi-manager_0.0.4/src/wifi2.svg new file mode 100644 index 00000000..033b9de9 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/wifi2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.4/src/wifi3.svg b/dist/esp32-wifi-manager_0.0.4/src/wifi3.svg new file mode 100644 index 00000000..7cd5667e --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/wifi3.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dist/esp32-wifi-manager_0.0.4/src/wifi_manager.c b/dist/esp32-wifi-manager_0.0.4/src/wifi_manager.c new file mode 100644 index 00000000..3d11a940 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/wifi_manager.c @@ -0,0 +1,1336 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file wifi_manager.c +@author Tony Pottier +@brief Defines all functions necessary for esp32 to connect to a wifi/scan wifis + +Contains the freeRTOS task and all necessary support + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include +#include +#include +#include +#include +#include "esp_system.h" +#include +#include +#include +#include +#include +#include +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_wifi_types.h" +#include "esp_log.h" +#include "nvs.h" +#include "nvs_flash.h" +#include "mdns.h" +#include "lwip/api.h" +#include "lwip/err.h" +#include "lwip/netdb.h" +#include "lwip/ip4_addr.h" + + +#include "json.h" +#include "dns_server.h" +#include "nvs_sync.h" +#include "wifi_manager.h" + + + +/* objects used to manipulate the main queue of events */ +QueueHandle_t wifi_manager_queue; + +/* @brief software timer to wait between each connection retry. + * There is no point hogging a hardware timer for a functionality like this which only needs to be 'accurate enough' */ +TimerHandle_t wifi_manager_retry_timer = NULL; + +/* @brief software timer that will trigger shutdown of the AP after a succesful STA connection + * There is no point hogging a hardware timer for a functionality like this which only needs to be 'accurate enough' */ +TimerHandle_t wifi_manager_shutdown_ap_timer = NULL; + +SemaphoreHandle_t wifi_manager_json_mutex = NULL; +SemaphoreHandle_t wifi_manager_sta_ip_mutex = NULL; +char *wifi_manager_sta_ip = NULL; +uint16_t ap_num = MAX_AP_NUM; +wifi_ap_record_t *accessp_records; +char *accessp_json = NULL; +char *ip_info_json = NULL; +wifi_config_t* wifi_manager_config_sta = NULL; + +/* @brief Array of callback function pointers */ +void (**cb_ptr_arr)(void*) = NULL; + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "wifi_manager"; + +/* @brief task handle for the main wifi_manager task */ +static TaskHandle_t task_wifi_manager = NULL; + +/* @brief netif object for the STATION */ +static esp_netif_t* esp_netif_sta = NULL; + +/* @brief netif object for the ACCESS POINT */ +static esp_netif_t* esp_netif_ap = NULL; + +/** + * The actual WiFi settings in use + */ +struct wifi_settings_t wifi_settings = { + .ap_ssid = DEFAULT_AP_SSID, + .ap_pwd = DEFAULT_AP_PASSWORD, + .ap_channel = DEFAULT_AP_CHANNEL, + .ap_ssid_hidden = DEFAULT_AP_SSID_HIDDEN, + .ap_bandwidth = DEFAULT_AP_BANDWIDTH, + .sta_only = DEFAULT_STA_ONLY, + .sta_power_save = DEFAULT_STA_POWER_SAVE, + .sta_static_ip = 0, +}; + +const char wifi_manager_nvs_namespace[] = "espwifimgr"; + +static EventGroupHandle_t wifi_manager_event_group; + +/* @brief indicate that the ESP32 is currently connected. */ +const int WIFI_MANAGER_WIFI_CONNECTED_BIT = BIT0; + +const int WIFI_MANAGER_AP_STA_CONNECTED_BIT = BIT1; + +/* @brief Set automatically once the SoftAP is started */ +const int WIFI_MANAGER_AP_STARTED_BIT = BIT2; + +/* @brief When set, means a client requested to connect to an access point.*/ +const int WIFI_MANAGER_REQUEST_STA_CONNECT_BIT = BIT3; + +/* @brief This bit is set automatically as soon as a connection was lost */ +const int WIFI_MANAGER_STA_DISCONNECT_BIT = BIT4; + +/* @brief When set, means the wifi manager attempts to restore a previously saved connection at startup. */ +const int WIFI_MANAGER_REQUEST_RESTORE_STA_BIT = BIT5; + +/* @brief When set, means a client requested to disconnect from currently connected AP. */ +const int WIFI_MANAGER_REQUEST_WIFI_DISCONNECT_BIT = BIT6; + +/* @brief When set, means a scan is in progress */ +const int WIFI_MANAGER_SCAN_BIT = BIT7; + +/* @brief When set, means user requested for a disconnect */ +const int WIFI_MANAGER_REQUEST_DISCONNECT_BIT = BIT8; + + + +void wifi_manager_timer_retry_cb( TimerHandle_t xTimer ){ + + ESP_LOGI(TAG, "Retry Timer Tick! Sending ORDER_CONNECT_STA with reason CONNECTION_REQUEST_AUTO_RECONNECT"); + + /* stop the timer */ + xTimerStop( xTimer, (TickType_t) 0 ); + + /* Attempt to reconnect */ + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_AUTO_RECONNECT); + +} + +void wifi_manager_timer_shutdown_ap_cb( TimerHandle_t xTimer){ + + /* stop the timer */ + xTimerStop( xTimer, (TickType_t) 0 ); + + /* Attempt to shutdown AP */ + wifi_manager_send_message(WM_ORDER_STOP_AP, NULL); +} + +void wifi_manager_scan_async(){ + wifi_manager_send_message(WM_ORDER_START_WIFI_SCAN, NULL); +} + +void wifi_manager_disconnect_async(){ + wifi_manager_send_message(WM_ORDER_DISCONNECT_STA, NULL); +} + + +void wifi_manager_start(){ + + /* disable the default wifi logging */ + esp_log_level_set("wifi", ESP_LOG_NONE); + + /* initialize flash memory */ + nvs_flash_init(); + ESP_ERROR_CHECK(nvs_sync_create()); /* semaphore for thread synchronization on NVS memory */ + + /* memory allocation */ + wifi_manager_queue = xQueueCreate( 3, sizeof( queue_message) ); + wifi_manager_json_mutex = xSemaphoreCreateMutex(); + accessp_records = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * MAX_AP_NUM); + accessp_json = (char*)malloc(MAX_AP_NUM * JSON_ONE_APP_SIZE + 4); /* 4 bytes for json encapsulation of "[\n" and "]\0" */ + wifi_manager_clear_access_points_json(); + ip_info_json = (char*)malloc(sizeof(char) * JSON_IP_INFO_SIZE); + wifi_manager_clear_ip_info_json(); + wifi_manager_config_sta = (wifi_config_t*)malloc(sizeof(wifi_config_t)); + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + memset(&wifi_settings.sta_static_ip_config, 0x00, sizeof(esp_netif_ip_info_t)); + cb_ptr_arr = malloc(sizeof(void (*)(void*)) * WM_MESSAGE_CODE_COUNT); + for(int i=0; ista.ssid) != 0){ + /* different ssid or ssid does not exist in flash: save new ssid */ + esp_err = nvs_set_blob(handle, "ssid", wifi_manager_config_sta->sta.ssid, 32); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + ESP_LOGI(TAG, "wifi_manager_wrote wifi_sta_config: ssid:%s",wifi_manager_config_sta->sta.ssid); + + } + + sz = sizeof(tmp_conf.sta.password); + esp_err = nvs_get_blob(handle, "password", tmp_conf.sta.password, &sz); + if( (esp_err == ESP_OK || esp_err == ESP_ERR_NVS_NOT_FOUND) && strcmp( (char*)tmp_conf.sta.password, (char*)wifi_manager_config_sta->sta.password) != 0){ + /* different password or password does not exist in flash: save new password */ + esp_err = nvs_set_blob(handle, "password", wifi_manager_config_sta->sta.password, 64); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + ESP_LOGI(TAG, "wifi_manager_wrote wifi_sta_config: password:%s",wifi_manager_config_sta->sta.password); + } + + sz = sizeof(tmp_settings); + esp_err = nvs_get_blob(handle, "settings", &tmp_settings, &sz); + if( (esp_err == ESP_OK || esp_err == ESP_ERR_NVS_NOT_FOUND) && + ( + strcmp( (char*)tmp_settings.ap_ssid, (char*)wifi_settings.ap_ssid) != 0 || + strcmp( (char*)tmp_settings.ap_pwd, (char*)wifi_settings.ap_pwd) != 0 || + tmp_settings.ap_ssid_hidden != wifi_settings.ap_ssid_hidden || + tmp_settings.ap_bandwidth != wifi_settings.ap_bandwidth || + tmp_settings.sta_only != wifi_settings.sta_only || + tmp_settings.sta_power_save != wifi_settings.sta_power_save || + tmp_settings.ap_channel != wifi_settings.ap_channel + ) + ){ + esp_err = nvs_set_blob(handle, "settings", &wifi_settings, sizeof(wifi_settings)); + if (esp_err != ESP_OK){ + nvs_sync_unlock(); + return esp_err; + } + change = true; + + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_ssid: %s",wifi_settings.ap_ssid); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_pwd: %s",wifi_settings.ap_pwd); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_channel: %i",wifi_settings.ap_channel); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_hidden (1 = yes): %i",wifi_settings.ap_ssid_hidden); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: SoftAP_bandwidth (1 = 20MHz, 2 = 40MHz): %i",wifi_settings.ap_bandwidth); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: sta_only (0 = APSTA, 1 = STA when connected): %i",wifi_settings.sta_only); + ESP_LOGD(TAG, "wifi_manager_wrote wifi_settings: sta_power_save (1 = yes): %i",wifi_settings.sta_power_save); + } + + if(change){ + esp_err = nvs_commit(handle); + } + else{ + ESP_LOGI(TAG, "Wifi config was not saved to flash because no change has been detected."); + } + + if (esp_err != ESP_OK) return esp_err; + + nvs_close(handle); + nvs_sync_unlock(); + + } + else{ + ESP_LOGE(TAG, "wifi_manager_save_sta_config failed to acquire nvs_sync mutex"); + } + + return ESP_OK; +} + +bool wifi_manager_fetch_wifi_sta_config(){ + + nvs_handle handle; + esp_err_t esp_err; + if(nvs_sync_lock( portMAX_DELAY )){ + + esp_err = nvs_open(wifi_manager_nvs_namespace, NVS_READONLY, &handle); + + if(esp_err != ESP_OK){ + nvs_sync_unlock(); + return false; + } + + if(wifi_manager_config_sta == NULL){ + wifi_manager_config_sta = (wifi_config_t*)malloc(sizeof(wifi_config_t)); + } + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + + /* allocate buffer */ + size_t sz = sizeof(wifi_settings); + uint8_t *buff = (uint8_t*)malloc(sizeof(uint8_t) * sz); + memset(buff, 0x00, sizeof(sz)); + + /* ssid */ + sz = sizeof(wifi_manager_config_sta->sta.ssid); + esp_err = nvs_get_blob(handle, "ssid", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(wifi_manager_config_sta->sta.ssid, buff, sz); + + /* password */ + sz = sizeof(wifi_manager_config_sta->sta.password); + esp_err = nvs_get_blob(handle, "password", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(wifi_manager_config_sta->sta.password, buff, sz); + + /* settings */ + sz = sizeof(wifi_settings); + esp_err = nvs_get_blob(handle, "settings", buff, &sz); + if(esp_err != ESP_OK){ + free(buff); + nvs_sync_unlock(); + return false; + } + memcpy(&wifi_settings, buff, sz); + + free(buff); + nvs_close(handle); + nvs_sync_unlock(); + + + ESP_LOGI(TAG, "wifi_manager_fetch_wifi_sta_config: ssid:%s password:%s",wifi_manager_config_sta->sta.ssid,wifi_manager_config_sta->sta.password); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_ssid:%s",wifi_settings.ap_ssid); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_pwd:%s",wifi_settings.ap_pwd); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_channel:%i",wifi_settings.ap_channel); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_hidden (1 = yes):%i",wifi_settings.ap_ssid_hidden); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: SoftAP_bandwidth (1 = 20MHz, 2 = 40MHz)%i",wifi_settings.ap_bandwidth); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_only (0 = APSTA, 1 = STA when connected):%i",wifi_settings.sta_only); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_power_save (1 = yes):%i",wifi_settings.sta_power_save); + ESP_LOGD(TAG, "wifi_manager_fetch_wifi_settings: sta_static_ip (0 = dhcp client, 1 = static ip):%i",wifi_settings.sta_static_ip); + + return wifi_manager_config_sta->sta.ssid[0] != '\0'; + + + } + else{ + return false; + } + +} + + +void wifi_manager_clear_ip_info_json(){ + strcpy(ip_info_json, "{}\n"); +} + + +void wifi_manager_generate_ip_info_json(update_reason_code_t update_reason_code){ + + wifi_config_t *config = wifi_manager_get_wifi_sta_config(); + if(config){ + + const char *ip_info_json_format = ",\"ip\":\"%s\",\"netmask\":\"%s\",\"gw\":\"%s\",\"urc\":%d}\n"; + + memset(ip_info_json, 0x00, JSON_IP_INFO_SIZE); + + /* to avoid declaring a new buffer we copy the data directly into the buffer at its correct address */ + strcpy(ip_info_json, "{\"ssid\":"); + json_print_string(config->sta.ssid, (unsigned char*)(ip_info_json+strlen(ip_info_json)) ); + + size_t ip_info_json_len = strlen(ip_info_json); + size_t remaining = JSON_IP_INFO_SIZE - ip_info_json_len; + if(update_reason_code == UPDATE_CONNECTION_OK){ + /* rest of the information is copied after the ssid */ + esp_netif_ip_info_t ip_info; + ESP_ERROR_CHECK(esp_netif_get_ip_info(esp_netif_sta, &ip_info)); + + char ip[IP4ADDR_STRLEN_MAX]; /* note: IP4ADDR_STRLEN_MAX is defined in lwip */ + char gw[IP4ADDR_STRLEN_MAX]; + char netmask[IP4ADDR_STRLEN_MAX]; + + esp_ip4addr_ntoa(&ip_info.ip, ip, IP4ADDR_STRLEN_MAX); + esp_ip4addr_ntoa(&ip_info.gw, gw, IP4ADDR_STRLEN_MAX); + esp_ip4addr_ntoa(&ip_info.netmask, netmask, IP4ADDR_STRLEN_MAX); + + + snprintf( (ip_info_json + ip_info_json_len), remaining, ip_info_json_format, + ip, + netmask, + gw, + (int)update_reason_code); + } + else{ + /* notify in the json output the reason code why this was updated without a connection */ + snprintf( (ip_info_json + ip_info_json_len), remaining, ip_info_json_format, + "0", + "0", + "0", + (int)update_reason_code); + } + } + else{ + wifi_manager_clear_ip_info_json(); + } + + +} + + +void wifi_manager_clear_access_points_json(){ + strcpy(accessp_json, "[]\n"); +} +void wifi_manager_generate_acess_points_json(){ + + strcpy(accessp_json, "["); + + + const char oneap_str[] = ",\"chan\":%d,\"rssi\":%d,\"auth\":%d}%c\n"; + + /* stack buffer to hold on to one AP until it's copied over to accessp_json */ + char one_ap[JSON_ONE_APP_SIZE]; + for(int i=0; i and + * . + * + * Another thing deserves our attention is that the default behavior of LwIP is to abort all TCP socket connections on + * receiving the disconnect. Most of time it is not a problem. However, for some special application, this may not be + * what they want, consider following scenarios: + * + * The application creates a TCP connection to maintain the application-level keep-alive data that is sent out + * every 60 seconds. + * + * Due to certain reasons, the Wi-Fi connection is cut off, and the is raised. + * According to the current implementation, all TCP connections will be removed and the keep-alive socket will be + * in a wrong status. However, since the application designer believes that the network layer should NOT care about + * this error at the Wi-Fi layer, the application does not close the socket. + * + * Five seconds later, the Wi-Fi connection is restored because esp_wifi_connect() is called in the application + * event callback function. Moreover, the station connects to the same AP and gets the same IPV4 address as before. + * + * Sixty seconds later, when the application sends out data with the keep-alive socket, the socket returns an error + * and the application closes the socket and re-creates it when necessary. + * + * In above scenario, ideally, the application sockets and the network layer should not be affected, since the Wi-Fi + * connection only fails temporarily and recovers very quickly. The application can enable “Keep TCP connections when + * IP changed” via LwIP menuconfig.*/ + case WIFI_EVENT_STA_DISCONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); + + wifi_event_sta_disconnected_t* wifi_event_sta_disconnected = (wifi_event_sta_disconnected_t*)malloc(sizeof(wifi_event_sta_disconnected_t)); + *wifi_event_sta_disconnected = *( (wifi_event_sta_disconnected_t*)event_data ); + + /* if a DISCONNECT message is posted while a scan is in progress this scan will NEVER end, causing scan to never work again. For this reason SCAN_BIT is cleared too */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_WIFI_CONNECTED_BIT | WIFI_MANAGER_SCAN_BIT); + + /* post disconnect event with reason code */ + wifi_manager_send_message(WM_EVENT_STA_DISCONNECTED, (void*)wifi_event_sta_disconnected ); + break; + + /* This event arises when the AP to which the station is connected changes its authentication mode, e.g., from no auth + * to WPA. Upon receiving this event, the event task will do nothing. Generally, the application event callback does + * not need to handle this either. */ + case WIFI_EVENT_STA_AUTHMODE_CHANGE: + ESP_LOGI(TAG, "WIFI_EVENT_STA_AUTHMODE_CHANGE"); + break; + + case WIFI_EVENT_AP_START: + ESP_LOGI(TAG, "WIFI_EVENT_AP_START"); + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_AP_STARTED_BIT); + break; + + case WIFI_EVENT_AP_STOP: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STOP"); + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_AP_STARTED_BIT); + break; + + /* Every time a station is connected to ESP32 AP, the will arise. Upon receiving this + * event, the event task will do nothing, and the application callback can also ignore it. However, you may want + * to do something, for example, to get the info of the connected STA, etc. */ + case WIFI_EVENT_AP_STACONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STACONNECTED"); + break; + + /* This event can happen in the following scenarios: + * The application calls esp_wifi_disconnect(), or esp_wifi_deauth_sta(), to manually disconnect the station. + * The Wi-Fi driver kicks off the station, e.g. because the AP has not received any packets in the past five minutes, etc. + * The station kicks off the AP. + * When this event happens, the event task will do nothing, but the application event callback needs to do + * something, e.g., close the socket which is related to this station, etc. */ + case WIFI_EVENT_AP_STADISCONNECTED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_STADISCONNECTED"); + break; + + /* This event is disabled by default. The application can enable it via API esp_wifi_set_event_mask(). + * When this event is enabled, it will be raised each time the AP receives a probe request. */ + case WIFI_EVENT_AP_PROBEREQRECVED: + ESP_LOGI(TAG, "WIFI_EVENT_AP_PROBEREQRECVED"); + break; + + } /* end switch */ + } + else if(event_base == IP_EVENT){ + + switch(event_id){ + + /* This event arises when the DHCP client successfully gets the IPV4 address from the DHCP server, + * or when the IPV4 address is changed. The event means that everything is ready and the application can begin + * its tasks (e.g., creating sockets). + * The IPV4 may be changed because of the following reasons: + * The DHCP client fails to renew/rebind the IPV4 address, and the station’s IPV4 is reset to 0. + * The DHCP client rebinds to a different address. + * The static-configured IPV4 address is changed. + * Whether the IPV4 address is changed or NOT is indicated by field ip_change of ip_event_got_ip_t. + * The socket is based on the IPV4 address, which means that, if the IPV4 changes, all sockets relating to this + * IPV4 will become abnormal. Upon receiving this event, the application needs to close all sockets and recreate + * the application when the IPV4 changes to a valid one. */ + case IP_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP"); + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_WIFI_CONNECTED_BIT); + ip_event_got_ip_t* ip_event_got_ip = (ip_event_got_ip_t*)malloc(sizeof(ip_event_got_ip_t)); + *ip_event_got_ip = *( (ip_event_got_ip_t*)event_data ); + wifi_manager_send_message(WM_EVENT_STA_GOT_IP, (void*)(ip_event_got_ip) ); + break; + + /* This event arises when the IPV6 SLAAC support auto-configures an address for the ESP32, or when this address changes. + * The event means that everything is ready and the application can begin its tasks (e.g., creating sockets). */ + case IP_EVENT_GOT_IP6: + ESP_LOGI(TAG, "IP_EVENT_GOT_IP6"); + break; + + /* This event arises when the IPV4 address become invalid. + * IP_STA_LOST_IP doesn’t arise immediately after the WiFi disconnects, instead it starts an IPV4 address lost timer, + * if the IPV4 address is got before ip lost timer expires, IP_EVENT_STA_LOST_IP doesn’t happen. Otherwise, the event + * arises when IPV4 address lost timer expires. + * Generally the application don’t need to care about this event, it is just a debug event to let the application + * know that the IPV4 address is lost. */ + case IP_EVENT_STA_LOST_IP: + ESP_LOGI(TAG, "IP_EVENT_STA_LOST_IP"); + break; + + } + } + +} + + +wifi_config_t* wifi_manager_get_wifi_sta_config(){ + return wifi_manager_config_sta; +} + + +void wifi_manager_connect_async(){ + /* in order to avoid a false positive on the front end app we need to quickly flush the ip json + * There'se a risk the front end sees an IP or a password error when in fact + * it's a remnant from a previous connection + */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_clear_ip_info_json(); + wifi_manager_unlock_json_buffer(); + } + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_USER); +} + + +char* wifi_manager_get_ip_info_json(){ + return ip_info_json; +} + + +void wifi_manager_destroy(){ + + vTaskDelete(task_wifi_manager); + task_wifi_manager = NULL; + + /* heap buffers */ + free(accessp_records); + accessp_records = NULL; + free(accessp_json); + accessp_json = NULL; + free(ip_info_json); + ip_info_json = NULL; + free(wifi_manager_sta_ip); + wifi_manager_sta_ip = NULL; + if(wifi_manager_config_sta){ + free(wifi_manager_config_sta); + wifi_manager_config_sta = NULL; + } + + /* RTOS objects */ + vSemaphoreDelete(wifi_manager_json_mutex); + wifi_manager_json_mutex = NULL; + vSemaphoreDelete(wifi_manager_sta_ip_mutex); + wifi_manager_sta_ip_mutex = NULL; + vEventGroupDelete(wifi_manager_event_group); + wifi_manager_event_group = NULL; + vQueueDelete(wifi_manager_queue); + wifi_manager_queue = NULL; + + +} + + +void wifi_manager_filter_unique( wifi_ap_record_t * aplist, uint16_t * aps) { + int total_unique; + wifi_ap_record_t * first_free; + total_unique=*aps; + + first_free=NULL; + + for(int i=0; i<*aps-1;i++) { + wifi_ap_record_t * ap = &aplist[i]; + + /* skip the previously removed APs */ + if (ap->ssid[0] == 0) continue; + + /* remove the identical SSID+authmodes */ + for(int j=i+1; j<*aps;j++) { + wifi_ap_record_t * ap1 = &aplist[j]; + if ( (strcmp((const char *)ap->ssid, (const char *)ap1->ssid)==0) && + (ap->authmode == ap1->authmode) ) { /* same SSID, different auth mode is skipped */ + /* save the rssi for the display */ + if ((ap1->rssi) > (ap->rssi)) ap->rssi=ap1->rssi; + /* clearing the record */ + memset(ap1,0, sizeof(wifi_ap_record_t)); + } + } + } + /* reorder the list so APs follow each other in the list */ + for(int i=0; i<*aps;i++) { + wifi_ap_record_t * ap = &aplist[i]; + /* skipping all that has no name */ + if (ap->ssid[0] == 0) { + /* mark the first free slot */ + if (first_free==NULL) first_free=ap; + total_unique--; + continue; + } + if (first_free!=NULL) { + memcpy(first_free, ap, sizeof(wifi_ap_record_t)); + memset(ap,0, sizeof(wifi_ap_record_t)); + /* find the next free slot */ + for(int j=0; j<*aps;j++) { + if (aplist[j].ssid[0]==0) { + first_free=&aplist[j]; + break; + } + } + } + } + /* update the length of the list */ + *aps = total_unique; +} + + +BaseType_t wifi_manager_send_message_to_front(message_code_t code, void *param){ + queue_message msg; + msg.code = code; + msg.param = param; + return xQueueSendToFront( wifi_manager_queue, &msg, portMAX_DELAY); +} + +BaseType_t wifi_manager_send_message(message_code_t code, void *param){ + queue_message msg; + msg.code = code; + msg.param = param; + return xQueueSend( wifi_manager_queue, &msg, portMAX_DELAY); +} + + +void wifi_manager_set_callback(message_code_t message_code, void (*func_ptr)(void*) ){ + + if(cb_ptr_arr && message_code < WM_MESSAGE_CODE_COUNT){ + cb_ptr_arr[message_code] = func_ptr; + } +} + +esp_netif_t* wifi_manager_get_esp_netif_ap(){ + return esp_netif_ap; +} + +esp_netif_t* wifi_manager_get_esp_netif_sta(){ + return esp_netif_sta; +} + +void wifi_manager( void * pvParameters ){ + + + queue_message msg; + BaseType_t xStatus; + EventBits_t uxBits; + uint8_t retries = 0; + + + /* initialize the tcp stack */ + ESP_ERROR_CHECK(esp_netif_init()); + + /* event loop for the wifi driver */ + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + esp_netif_sta = esp_netif_create_default_wifi_sta(); + esp_netif_ap = esp_netif_create_default_wifi_ap(); + + + /* default wifi config */ + wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&wifi_init_config)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + + /* event handler for the connection */ + esp_event_handler_instance_t instance_wifi_event; + esp_event_handler_instance_t instance_ip_event; + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_manager_event_handler, NULL,&instance_wifi_event)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_manager_event_handler, NULL,&instance_ip_event)); + + + /* SoftAP - Wifi Access Point configuration setup */ + wifi_config_t ap_config = { + .ap = { + .ssid_len = 0, + .channel = wifi_settings.ap_channel, + .ssid_hidden = wifi_settings.ap_ssid_hidden, + .max_connection = DEFAULT_AP_MAX_CONNECTIONS, + .beacon_interval = DEFAULT_AP_BEACON_INTERVAL, + }, + }; + memcpy(ap_config.ap.ssid, wifi_settings.ap_ssid , sizeof(wifi_settings.ap_ssid)); + + /* if the password lenght is under 8 char which is the minium for WPA2, the access point starts as open */ + if(strlen( (char*)wifi_settings.ap_pwd) < WPA2_MINIMUM_PASSWORD_LENGTH){ + ap_config.ap.authmode = WIFI_AUTH_OPEN; + memset( ap_config.ap.password, 0x00, sizeof(ap_config.ap.password) ); + } + else{ + ap_config.ap.authmode = WIFI_AUTH_WPA2_PSK; + memcpy(ap_config.ap.password, wifi_settings.ap_pwd, sizeof(wifi_settings.ap_pwd)); + } + + + /* DHCP AP configuration */ + esp_netif_dhcps_stop(esp_netif_ap); /* DHCP client/server must be stopped before setting new IP information. */ + esp_netif_ip_info_t ap_ip_info; + memset(&ap_ip_info, 0x00, sizeof(ap_ip_info)); + inet_pton(AF_INET, DEFAULT_AP_IP, &ap_ip_info.ip); + inet_pton(AF_INET, DEFAULT_AP_GATEWAY, &ap_ip_info.gw); + inet_pton(AF_INET, DEFAULT_AP_NETMASK, &ap_ip_info.netmask); + ESP_ERROR_CHECK(esp_netif_set_ip_info(esp_netif_ap, &ap_ip_info)); + ESP_ERROR_CHECK(esp_netif_dhcps_start(esp_netif_ap)); + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &ap_config)); + ESP_ERROR_CHECK(esp_wifi_set_bandwidth(WIFI_IF_AP, wifi_settings.ap_bandwidth)); + ESP_ERROR_CHECK(esp_wifi_set_ps(wifi_settings.sta_power_save)); + + + /* by default the mode is STA because wifi_manager will not start the access point unless it has to! */ + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); + + /* start http server */ + http_app_start(false); + + /* wifi scanner config */ + wifi_scan_config_t scan_config = { + .ssid = 0, + .bssid = 0, + .channel = 0, + .show_hidden = true + }; + + /* enqueue first event: load previous config */ + wifi_manager_send_message(WM_ORDER_LOAD_AND_RESTORE_STA, NULL); + + + /* main processing loop */ + for(;;){ + xStatus = xQueueReceive( wifi_manager_queue, &msg, portMAX_DELAY ); + + if( xStatus == pdPASS ){ + switch(msg.code){ + + case WM_EVENT_SCAN_DONE:{ + wifi_event_sta_scan_done_t *evt_scan_done = (wifi_event_sta_scan_done_t*)msg.param; + /* only check for AP if the scan is succesful */ + if(evt_scan_done->status == 0){ + /* As input param, it stores max AP number ap_records can hold. As output param, it receives the actual AP number this API returns. + * As a consequence, ap_num MUST be reset to MAX_AP_NUM at every scan */ + ap_num = MAX_AP_NUM; + ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_num, accessp_records)); + /* make sure the http server isn't trying to access the list while it gets refreshed */ + if(wifi_manager_lock_json_buffer( pdMS_TO_TICKS(1000) )){ + /* Will remove the duplicate SSIDs from the list and update ap_num */ + wifi_manager_filter_unique(accessp_records, &ap_num); + wifi_manager_generate_acess_points_json(); + wifi_manager_unlock_json_buffer(); + } + else{ + ESP_LOGE(TAG, "could not get access to json mutex in wifi_scan"); + } + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(evt_scan_done); + } + break; + + case WM_ORDER_START_WIFI_SCAN: + ESP_LOGD(TAG, "MESSAGE: ORDER_START_WIFI_SCAN"); + + /* if a scan is already in progress this message is simply ignored thanks to the WIFI_MANAGER_SCAN_BIT uxBit */ + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if(! (uxBits & WIFI_MANAGER_SCAN_BIT) ){ + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_SCAN_BIT); + ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, false)); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_LOAD_AND_RESTORE_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_LOAD_AND_RESTORE_STA"); + if(wifi_manager_fetch_wifi_sta_config()){ + ESP_LOGI(TAG, "Saved wifi found on startup. Will attempt to connect."); + wifi_manager_send_message(WM_ORDER_CONNECT_STA, (void*)CONNECTION_REQUEST_RESTORE_CONNECTION); + } + else{ + /* no wifi saved: start soft AP! This is what should happen during a first run */ + ESP_LOGI(TAG, "No saved wifi found on startup. Starting access point."); + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_CONNECT_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_CONNECT_STA"); + + /* very important: precise that this connection attempt is specifically requested. + * Param in that case is a boolean indicating if the request was made automatically + * by the wifi_manager. + * */ + if((BaseType_t)msg.param == CONNECTION_REQUEST_USER) { + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + } + else if((BaseType_t)msg.param == CONNECTION_REQUEST_RESTORE_CONNECTION) { + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + } + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if( ! (uxBits & WIFI_MANAGER_WIFI_CONNECTED_BIT) ){ + /* update config to latest and attempt connection */ + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, wifi_manager_get_wifi_sta_config())); + + /* if there is a wifi scan in progress abort it first + Calling esp_wifi_scan_stop will trigger a SCAN_DONE event which will reset this bit */ + if(uxBits & WIFI_MANAGER_SCAN_BIT){ + esp_wifi_scan_stop(); + } + ESP_ERROR_CHECK(esp_wifi_connect()); + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_EVENT_STA_DISCONNECTED: + ;wifi_event_sta_disconnected_t* wifi_event_sta_disconnected = (wifi_event_sta_disconnected_t*)msg.param; + ESP_LOGI(TAG, "MESSAGE: EVENT_STA_DISCONNECTED with Reason code: %d", wifi_event_sta_disconnected->reason); + + /* this even can be posted in numerous different conditions + * + * 1. SSID password is wrong + * 2. Manual disconnection ordered + * 3. Connection lost + * + * Having clear understand as to WHY the event was posted is key to having an efficient wifi manager + * + * With wifi_manager, we determine: + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT is set, We consider it's a client that requested the connection. + * When SYSTEM_EVENT_STA_DISCONNECTED is posted, it's probably a password/something went wrong with the handshake. + * + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT is set, it's a disconnection that was ASKED by the client (clicking disconnect in the app) + * When SYSTEM_EVENT_STA_DISCONNECTED is posted, saved wifi is erased from the NVS memory. + * + * If WIFI_MANAGER_REQUEST_STA_CONNECT_BIT and WIFI_MANAGER_REQUEST_STA_CONNECT_BIT are NOT set, it's a lost connection + * + * In this version of the software, reason codes are not used. They are indicated here for potential future usage. + * + * REASON CODE: + * 1 UNSPECIFIED + * 2 AUTH_EXPIRE auth no longer valid, this smells like someone changed a password on the AP + * 3 AUTH_LEAVE + * 4 ASSOC_EXPIRE + * 5 ASSOC_TOOMANY too many devices already connected to the AP => AP fails to respond + * 6 NOT_AUTHED + * 7 NOT_ASSOCED + * 8 ASSOC_LEAVE tested as manual disconnect by user OR in the wireless MAC blacklist + * 9 ASSOC_NOT_AUTHED + * 10 DISASSOC_PWRCAP_BAD + * 11 DISASSOC_SUPCHAN_BAD + * 12 + * 13 IE_INVALID + * 14 MIC_FAILURE + * 15 4WAY_HANDSHAKE_TIMEOUT wrong password! This was personnaly tested on my home wifi with a wrong password. + * 16 GROUP_KEY_UPDATE_TIMEOUT + * 17 IE_IN_4WAY_DIFFERS + * 18 GROUP_CIPHER_INVALID + * 19 PAIRWISE_CIPHER_INVALID + * 20 AKMP_INVALID + * 21 UNSUPP_RSN_IE_VERSION + * 22 INVALID_RSN_IE_CAP + * 23 802_1X_AUTH_FAILED wrong password? + * 24 CIPHER_SUITE_REJECTED + * 200 BEACON_TIMEOUT + * 201 NO_AP_FOUND + * 202 AUTH_FAIL + * 203 ASSOC_FAIL + * 204 HANDSHAKE_TIMEOUT + * + * */ + + /* reset saved sta IP */ + wifi_manager_safe_update_sta_ip_string((uint32_t)0); + + /* if there was a timer on to stop the AP, well now it's time to cancel that since connection was lost! */ + if(xTimerIsTimerActive(wifi_manager_shutdown_ap_timer) == pdTRUE ){ + xTimerStop( wifi_manager_shutdown_ap_timer, (TickType_t)0 ); + } + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + if( uxBits & WIFI_MANAGER_REQUEST_STA_CONNECT_BIT ){ + /* there are no retries when it's a user requested connection by design. This avoids a user hanging too much + * in case they typed a wrong password for instance. Here we simply clear the request bit and move on */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_FAILED_ATTEMPT ); + wifi_manager_unlock_json_buffer(); + } + + } + else if (uxBits & WIFI_MANAGER_REQUEST_DISCONNECT_BIT){ + /* user manually requested a disconnect so the lost connection is a normal event. Clear the flag and restart the AP */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_DISCONNECT_BIT); + + /* erase configuration */ + if(wifi_manager_config_sta){ + memset(wifi_manager_config_sta, 0x00, sizeof(wifi_config_t)); + } + + /* regenerate json status */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_USER_DISCONNECT ); + wifi_manager_unlock_json_buffer(); + } + + /* save NVS memory */ + wifi_manager_save_sta_config(); + + /* start SoftAP */ + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + else{ + /* lost connection ? */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + wifi_manager_generate_ip_info_json( UPDATE_LOST_CONNECTION ); + wifi_manager_unlock_json_buffer(); + } + + /* Start the timer that will try to restore the saved config */ + xTimerStart( wifi_manager_retry_timer, (TickType_t)0 ); + + /* if it was a restore attempt connection, we clear the bit */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + + /* if the AP is not started, we check if we have reached the threshold of failed attempt to start it */ + if(! (uxBits & WIFI_MANAGER_AP_STARTED_BIT) ){ + + /* if the nunber of retries is below the threshold to start the AP, a reconnection attempt is made + * This way we avoid restarting the AP directly in case the connection is mementarily lost */ + if(retries < WIFI_MANAGER_MAX_RETRY_START_AP){ + retries++; + } + else{ + /* In this scenario the connection was lost beyond repair: kick start the AP! */ + retries = 0; + + /* start SoftAP */ + wifi_manager_send_message(WM_ORDER_START_AP, NULL); + } + } + } + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(wifi_event_sta_disconnected); + + break; + + case WM_ORDER_START_AP: + ESP_LOGI(TAG, "MESSAGE: ORDER_START_AP"); + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + + /* restart HTTP daemon */ + http_app_stop(); + http_app_start(true); + + /* start DNS */ + dns_server_start(); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + case WM_ORDER_STOP_AP: + ESP_LOGI(TAG, "MESSAGE: ORDER_STOP_AP"); + + + uxBits = xEventGroupGetBits(wifi_manager_event_group); + + /* before stopping the AP, we check that we are still connected. There's a chance that once the timer + * kicks in, for whatever reason the esp32 is already disconnected. + */ + if(uxBits & WIFI_MANAGER_WIFI_CONNECTED_BIT){ + + /* set to STA only */ + esp_wifi_set_mode(WIFI_MODE_STA); + + /* stop DNS */ + dns_server_stop(); + + /* restart HTTP daemon */ + http_app_stop(); + http_app_start(false); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + } + + break; + + case WM_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "WM_EVENT_STA_GOT_IP"); + ip_event_got_ip_t* ip_event_got_ip = (ip_event_got_ip_t*)msg.param; + uxBits = xEventGroupGetBits(wifi_manager_event_group); + + /* reset connection requests bits -- doesn't matter if it was set or not */ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_STA_CONNECT_BIT); + + /* save IP as a string for the HTTP server host */ + wifi_manager_safe_update_sta_ip_string(ip_event_got_ip->ip_info.ip.addr); + + /* save wifi config in NVS if it wasn't a restored of a connection */ + if(uxBits & WIFI_MANAGER_REQUEST_RESTORE_STA_BIT){ + xEventGroupClearBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_RESTORE_STA_BIT); + } + else{ + wifi_manager_save_sta_config(); + } + + /* reset number of retries */ + retries = 0; + + /* refresh JSON with the new IP */ + if(wifi_manager_lock_json_buffer( portMAX_DELAY )){ + /* generate the connection info with success */ + wifi_manager_generate_ip_info_json( UPDATE_CONNECTION_OK ); + wifi_manager_unlock_json_buffer(); + } + else { abort(); } + + /* bring down DNS hijack */ + dns_server_stop(); + + /* start the timer that will eventually shutdown the access point + * We check first that it's actually running because in case of a boot and restore connection + * the AP is not even started to begin with. + */ + if(uxBits & WIFI_MANAGER_AP_STARTED_BIT){ + TickType_t t = pdMS_TO_TICKS( WIFI_MANAGER_SHUTDOWN_AP_TIMER ); + + /* if for whatever reason user configured the shutdown timer to be less than 1 tick, the AP is stopped straight away */ + if(t > 0){ + xTimerStart( wifi_manager_shutdown_ap_timer, (TickType_t)0 ); + } + else{ + wifi_manager_send_message(WM_ORDER_STOP_AP, (void*)NULL); + } + + } + + /* callback and free memory allocated for the void* param */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])( msg.param ); + free(ip_event_got_ip); + + break; + + case WM_ORDER_DISCONNECT_STA: + ESP_LOGI(TAG, "MESSAGE: ORDER_DISCONNECT_STA"); + + /* precise this is coming from a user request */ + xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_REQUEST_DISCONNECT_BIT); + + /* order wifi discconect */ + ESP_ERROR_CHECK(esp_wifi_disconnect()); + + /* callback */ + if(cb_ptr_arr[msg.code]) (*cb_ptr_arr[msg.code])(NULL); + + break; + + default: + break; + + } /* end of switch/case */ + } /* end of if status=pdPASS */ + } /* end of for loop */ + + vTaskDelete( NULL ); + +} + + diff --git a/dist/esp32-wifi-manager_0.0.4/src/wifi_manager.h b/dist/esp32-wifi-manager_0.0.4/src/wifi_manager.h new file mode 100644 index 00000000..cba3b989 --- /dev/null +++ b/dist/esp32-wifi-manager_0.0.4/src/wifi_manager.h @@ -0,0 +1,417 @@ +/* +Copyright (c) 2017-2020 Tony Pottier + +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. + +@file wifi_manager.h +@author Tony Pottier +@brief Defines all functions necessary for esp32 to connect to a wifi/scan wifis + +Contains the freeRTOS task and all necessary support + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#ifndef WIFI_MANAGER_H_INCLUDED +#define WIFI_MANAGER_H_INCLUDED + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * @brief Defines the maximum size of a SSID name. 32 is IEEE standard. + * @warning limit is also hard coded in wifi_config_t. Never extend this value. + */ +#define MAX_SSID_SIZE 32 + +/** + * @brief Defines the maximum size of a WPA2 passkey. 64 is IEEE standard. + * @warning limit is also hard coded in wifi_config_t. Never extend this value. + */ +#define MAX_PASSWORD_SIZE 64 + + +/** + * @brief Defines the maximum number of access points that can be scanned. + * + * To save memory and avoid nasty out of memory errors, + * we can limit the number of APs detected in a wifi scan. + */ +#define MAX_AP_NUM 15 + + +/** + * @brief Defines the maximum number of failed retries allowed before the WiFi manager starts its own access point. + * Setting it to 2 for instance means there will be 3 attempts in total (original request + 2 retries) + */ +#define WIFI_MANAGER_MAX_RETRY_START_AP CONFIG_WIFI_MANAGER_MAX_RETRY_START_AP + +/** + * @brief Time (in ms) between each retry attempt + * Defines the time to wait before an attempt to re-connect to a saved wifi is made after connection is lost or another unsuccesful attempt is made. + */ +#define WIFI_MANAGER_RETRY_TIMER CONFIG_WIFI_MANAGER_RETRY_TIMER + + +/** + * @brief Time (in ms) to wait before shutting down the AP + * Defines the time (in ms) to wait after a succesful connection before shutting down the access point. + */ +#define WIFI_MANAGER_SHUTDOWN_AP_TIMER CONFIG_WIFI_MANAGER_SHUTDOWN_AP_TIMER + + +/** @brief Defines the task priority of the wifi_manager. + * + * Tasks spawn by the manager will have a priority of WIFI_MANAGER_TASK_PRIORITY-1. + * For this particular reason, minimum task priority is 1. It it highly not recommended to set + * it to 1 though as the sub-tasks will now have a priority of 0 which is the priority + * of freeRTOS' idle task. + */ +#define WIFI_MANAGER_TASK_PRIORITY CONFIG_WIFI_MANAGER_TASK_PRIORITY + +/** @brief Defines the auth mode as an access point + * Value must be of type wifi_auth_mode_t + * @see esp_wifi_types.h + * @warning if set to WIFI_AUTH_OPEN, passowrd me be empty. See DEFAULT_AP_PASSWORD. + */ +#define AP_AUTHMODE WIFI_AUTH_WPA2_PSK + +/** @brief Defines visibility of the access point. 0: visible AP. 1: hidden */ +#define DEFAULT_AP_SSID_HIDDEN 0 + +/** @brief Defines access point's name. Default value: esp32. Run 'make menuconfig' to setup your own value or replace here by a string */ +#define DEFAULT_AP_SSID CONFIG_DEFAULT_AP_SSID + +/** @brief Defines access point's password. + * @warning In the case of an open access point, the password must be a null string "" or "\0" if you want to be verbose but waste one byte. + * In addition, the AP_AUTHMODE must be WIFI_AUTH_OPEN + */ +#define DEFAULT_AP_PASSWORD CONFIG_DEFAULT_AP_PASSWORD + +/** @brief Defines the hostname broadcasted by mDNS */ +#define DEFAULT_HOSTNAME "esp32" + +/** @brief Defines access point's bandwidth. + * Value: WIFI_BW_HT20 for 20 MHz or WIFI_BW_HT40 for 40 MHz + * 20 MHz minimize channel interference but is not suitable for + * applications with high data speeds + */ +#define DEFAULT_AP_BANDWIDTH WIFI_BW_HT20 + +/** @brief Defines access point's channel. + * Channel selection is only effective when not connected to another AP. + * Good practice for minimal channel interference to use + * For 20 MHz: 1, 6 or 11 in USA and 1, 5, 9 or 13 in most parts of the world + * For 40 MHz: 3 in USA and 3 or 11 in most parts of the world + */ +#define DEFAULT_AP_CHANNEL CONFIG_DEFAULT_AP_CHANNEL + + + +/** @brief Defines the access point's default IP address. Default: "10.10.0.1 */ +#define DEFAULT_AP_IP CONFIG_DEFAULT_AP_IP + +/** @brief Defines the access point's gateway. This should be the same as your IP. Default: "10.10.0.1" */ +#define DEFAULT_AP_GATEWAY CONFIG_DEFAULT_AP_GATEWAY + +/** @brief Defines the access point's netmask. Default: "255.255.255.0" */ +#define DEFAULT_AP_NETMASK CONFIG_DEFAULT_AP_NETMASK + +/** @brief Defines access point's maximum number of clients. Default: 4 */ +#define DEFAULT_AP_MAX_CONNECTIONS CONFIG_DEFAULT_AP_MAX_CONNECTIONS + +/** @brief Defines access point's beacon interval. 100ms is the recommended default. */ +#define DEFAULT_AP_BEACON_INTERVAL CONFIG_DEFAULT_AP_BEACON_INTERVAL + +/** @brief Defines if esp32 shall run both AP + STA when connected to another AP. + * Value: 0 will have the own AP always on (APSTA mode) + * Value: 1 will turn off own AP when connected to another AP (STA only mode when connected) + * Turning off own AP when connected to another AP minimize channel interference and increase throughput + */ +#define DEFAULT_STA_ONLY 1 + +/** @brief Defines if wifi power save shall be enabled. + * Value: WIFI_PS_NONE for full power (wifi modem always on) + * Value: WIFI_PS_MODEM for power save (wifi modem sleep periodically) + * Note: Power save is only effective when in STA only mode + */ +#define DEFAULT_STA_POWER_SAVE WIFI_PS_NONE + +/** + * @brief Defines the maximum length in bytes of a JSON representation of an access point. + * + * maximum ap string length with full 32 char ssid: 75 + \\n + \0 = 77\n + * example: {"ssid":"abcdefghijklmnopqrstuvwxyz012345","chan":12,"rssi":-100,"auth":4},\n + * BUT: we need to escape JSON. Imagine a ssid full of \" ? so it's 32 more bytes hence 77 + 32 = 99.\n + * this is an edge case but I don't think we should crash in a catastrophic manner just because + * someone decided to have a funny wifi name. + */ +#define JSON_ONE_APP_SIZE 99 + +/** + * @brief Defines the maximum length in bytes of a JSON representation of the IP information + * assuming all ips are 4*3 digits, and all characters in the ssid require to be escaped. + * example: {"ssid":"abcdefghijklmnopqrstuvwxyz012345","ip":"192.168.1.119","netmask":"255.255.255.0","gw":"192.168.1.1","urc":99} + * Run this JS (browser console is easiest) to come to the conclusion that 159 is the worst case. + * ``` + * var a = {"ssid":"abcdefghijklmnopqrstuvwxyz012345","ip":"255.255.255.255","netmask":"255.255.255.255","gw":"255.255.255.255","urc":99}; + * // Replace all ssid characters with a double quote which will have to be escaped + * a.ssid = a.ssid.split('').map(() => '"').join(''); + * console.log(JSON.stringify(a).length); // => 158 +1 for null + * console.log(JSON.stringify(a)); // print it + * ``` + */ +#define JSON_IP_INFO_SIZE 159 + + +/** + * @brief defines the minimum length of an access point password running on WPA2 + */ +#define WPA2_MINIMUM_PASSWORD_LENGTH 8 + + +/** + * @brief Defines the complete list of all messages that the wifi_manager can process. + * + * Some of these message are events ("EVENT"), and some of them are action ("ORDER") + * Each of these messages can trigger a callback function and each callback function is stored + * in a function pointer array for convenience. Because of this behavior, it is extremely important + * to maintain a strict sequence and the top level special element 'MESSAGE_CODE_COUNT' + * + * @see wifi_manager_set_callback + */ +typedef enum message_code_t { + NONE = 0, + WM_ORDER_START_HTTP_SERVER = 1, + WM_ORDER_STOP_HTTP_SERVER = 2, + WM_ORDER_START_DNS_SERVICE = 3, + WM_ORDER_STOP_DNS_SERVICE = 4, + WM_ORDER_START_WIFI_SCAN = 5, + WM_ORDER_LOAD_AND_RESTORE_STA = 6, + WM_ORDER_CONNECT_STA = 7, + WM_ORDER_DISCONNECT_STA = 8, + WM_ORDER_START_AP = 9, + WM_EVENT_STA_DISCONNECTED = 10, + WM_EVENT_SCAN_DONE = 11, + WM_EVENT_STA_GOT_IP = 12, + WM_ORDER_STOP_AP = 13, + WM_MESSAGE_CODE_COUNT = 14 /* important for the callback array */ + +}message_code_t; + +/** + * @brief simplified reason codes for a lost connection. + * + * esp-idf maintains a big list of reason codes which in practice are useless for most typical application. + */ +typedef enum update_reason_code_t { + UPDATE_CONNECTION_OK = 0, + UPDATE_FAILED_ATTEMPT = 1, + UPDATE_USER_DISCONNECT = 2, + UPDATE_LOST_CONNECTION = 3 +}update_reason_code_t; + +typedef enum connection_request_made_by_code_t{ + CONNECTION_REQUEST_NONE = 0, + CONNECTION_REQUEST_USER = 1, + CONNECTION_REQUEST_AUTO_RECONNECT = 2, + CONNECTION_REQUEST_RESTORE_CONNECTION = 3, + CONNECTION_REQUEST_MAX = 0x7fffffff /*force the creation of this enum as a 32 bit int */ +}connection_request_made_by_code_t; + +/** + * The actual WiFi settings in use + */ +struct wifi_settings_t{ + uint8_t ap_ssid[MAX_SSID_SIZE]; + uint8_t ap_pwd[MAX_PASSWORD_SIZE]; + uint8_t ap_channel; + uint8_t ap_ssid_hidden; + wifi_bandwidth_t ap_bandwidth; + bool sta_only; + wifi_ps_type_t sta_power_save; + bool sta_static_ip; + esp_netif_ip_info_t sta_static_ip_config; +}; +extern struct wifi_settings_t wifi_settings; + + +/** + * @brief Structure used to store one message in the queue. + */ +typedef struct{ + message_code_t code; + void *param; +} queue_message; + + +/** + * @brief returns the current esp_netif object for the STAtion + */ +esp_netif_t* wifi_manager_get_esp_netif_sta(); + +/** + * @brief returns the current esp_netif object for the Access Point + */ +esp_netif_t* wifi_manager_get_esp_netif_ap(); + + +/** + * Allocate heap memory for the wifi manager and start the wifi_manager RTOS task + */ +void wifi_manager_start(); + +/** + * Frees up all memory allocated by the wifi_manager and kill the task. + */ +void wifi_manager_destroy(); + +/** + * Filters the AP scan list to unique SSIDs + */ +void filter_unique( wifi_ap_record_t * aplist, uint16_t * ap_num); + +/** + * Main task for the wifi_manager + */ +void wifi_manager( void * pvParameters ); + + +char* wifi_manager_get_ap_list_json(); +char* wifi_manager_get_ip_info_json(); + + +void wifi_manager_scan_async(); + + +/** + * @brief saves the current STA wifi config to flash ram storage. + */ +esp_err_t wifi_manager_save_sta_config(); + +/** + * @brief fetch a previously STA wifi config in the flash ram storage. + * @return true if a previously saved config was found, false otherwise. + */ +bool wifi_manager_fetch_wifi_sta_config(); + +wifi_config_t* wifi_manager_get_wifi_sta_config(); + + +/** + * @brief requests a connection to an access point that will be process in the main task thread. + */ +void wifi_manager_connect_async(); + +/** + * @brief requests a wifi scan + */ +void wifi_manager_scan_awifi_manager_send_messagesync(); + +/** + * @brief requests to disconnect and forget about the access point. + */ +void wifi_manager_disconnect_async(); + +/** + * @brief Tries to get access to json buffer mutex. + * + * The HTTP server can try to access the json to serve clients while the wifi manager thread can try + * to update it. These two tasks are synchronized through a mutex. + * + * The mutex is used by both the access point list json and the connection status json.\n + * These two resources should technically have their own mutex but we lose some flexibility to save + * on memory. + * + * This is a simple wrapper around freeRTOS function xSemaphoreTake. + * + * @param xTicksToWait The time in ticks to wait for the semaphore to become available. + * @return true in success, false otherwise. + */ +bool wifi_manager_lock_json_buffer(TickType_t xTicksToWait); + +/** + * @brief Releases the json buffer mutex. + */ +void wifi_manager_unlock_json_buffer(); + +/** + * @brief Generates the connection status json: ssid and IP addresses. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_generate_ip_info_json(update_reason_code_t update_reason_code); +/** + * @brief Clears the connection status json. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_clear_ip_info_json(); + +/** + * @brief Generates the list of access points after a wifi scan. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_generate_acess_points_json(); + +/** + * @brief Clear the list of access points. + * @note This is not thread-safe and should be called only if wifi_manager_lock_json_buffer call is successful. + */ +void wifi_manager_clear_access_points_json(); + + +/** + * @brief Start the mDNS service + */ +void wifi_manager_initialise_mdns(); + + +bool wifi_manager_lock_sta_ip_string(TickType_t xTicksToWait); +void wifi_manager_unlock_sta_ip_string(); + +/** + * @brief gets the string representation of the STA IP address, e.g.: "192.168.1.69" + */ +char* wifi_manager_get_sta_ip_string(); + +/** + * @brief thread safe char representation of the STA IP update + */ +void wifi_manager_safe_update_sta_ip_string(uint32_t ip); + + +/** + * @brief Register a callback to a custom function when specific event message_code happens. + */ +void wifi_manager_set_callback(message_code_t message_code, void (*func_ptr)(void*) ); + + +BaseType_t wifi_manager_send_message(message_code_t code, void *param); +BaseType_t wifi_manager_send_message_to_front(message_code_t code, void *param); + +#ifdef __cplusplus +} +#endif + +#endif /* WIFI_MANAGER_H_INCLUDED */ diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 00000000..563b4c5b --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,8 @@ +dependencies: + idf: + version: '>=4.2' +description: esp32-wifi-manager is a pure C esp-idf component for ESP32 that enables easy management of wifi networks through a web portal. +url: https://github.com/ankayca/esp32-wifi-manager/tree/update_build_system +repository: https://github.com/ankayca/esp32-wifi-manager/tree/update_build_system +version: 0.0.4 +license: "MIT" \ No newline at end of file From ae880c154abc2c5e2f3d7ec878468b1e9fea0deb Mon Sep 17 00:00:00 2001 From: ankayca Date: Sun, 30 Jun 2024 01:06:13 +0300 Subject: [PATCH 11/11] fix crashed when scan wifi --- src/wifi_manager.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wifi_manager.c b/src/wifi_manager.c index 3d11a940..d934563f 100644 --- a/src/wifi_manager.c +++ b/src/wifi_manager.c @@ -1018,6 +1018,8 @@ void wifi_manager( void * pvParameters ){ if(! (uxBits & WIFI_MANAGER_SCAN_BIT) ){ xEventGroupSetBits(wifi_manager_event_group, WIFI_MANAGER_SCAN_BIT); ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, false)); + ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, false)); + esp_wifi_scan_start(&scan_config, false); } /* callback */
+
+ +
+
+

Enter Details

+
+

Manual Connection

+
+ + +
+
+ + +
+
+
+
+

Enter Password

+
+

Password for

+
+ +
+
+ + +
+
+
+
+

Please wait...

+
+

Connecting to

+
+
+
+

You may lose wifi access while the esp32 recalibrates its radio. Please wait until your device automatically reconnects. This can take up to 30s.

+
+
+

Success!

+
+
+

Connection failed

+

Please double-check wifi password if any and make sure the access point has good signal.

+
+
+
+ +
+
+
+
+
+

+
+

+
+
+ +
+
+

IP Address

+
+
IP Address:
+
Subnet Mask:
+
Default Gateway:
+
+
+ +
+
+
+
+

Are you sure you would like to disconnect from this wifi?

+
+ + +
+
+
+
+
+