From ca9c2feebce5aac2ac7ac2eaeac343d8cdbe7e04 Mon Sep 17 00:00:00 2001 From: iranl Date: Mon, 26 Aug 2024 21:47:10 +0200 Subject: [PATCH 01/30] Switch HTTP Server --- lib/ESPAsyncWebServer/LICENSE | 165 --- lib/ESPAsyncWebServer/README.md | 126 -- lib/ESPAsyncWebServer/docs/_config.yml | 8 - lib/ESPAsyncWebServer/docs/index.md | 126 -- .../examples/CaptivePortal/CaptivePortal.ino | 57 - .../examples/Draft/Draft.ino | 37 - .../examples/Filters/Filters.ino | 111 -- .../examples/SimpleServer/SimpleServer.ino | 134 -- .../examples/StreamFiles/StreamConcat.h | 37 - .../examples/StreamFiles/StreamFiles.ino | 84 -- .../examples/StreamFiles/StreamString.h | 40 - .../examples/issues/Issue14/Issue14.ino | 107 -- lib/ESPAsyncWebServer/library.json | 64 - lib/ESPAsyncWebServer/library.properties | 10 - lib/ESPAsyncWebServer/platformio.ini | 80 -- .../src/AsyncEventSource.cpp | 405 ------ lib/ESPAsyncWebServer/src/AsyncEventSource.h | 158 --- lib/ESPAsyncWebServer/src/AsyncJson.h | 255 ---- lib/ESPAsyncWebServer/src/AsyncMessagePack.h | 145 -- lib/ESPAsyncWebServer/src/AsyncWebSocket.cpp | 1207 ----------------- lib/ESPAsyncWebServer/src/AsyncWebSocket.h | 379 ------ lib/ESPAsyncWebServer/src/ChunkPrint.h | 32 - lib/ESPAsyncWebServer/src/ESPAsyncWebServer.h | 714 ---------- .../src/WebAuthentication.cpp | 249 ---- lib/ESPAsyncWebServer/src/WebAuthentication.h | 39 - lib/ESPAsyncWebServer/src/WebHandlerImpl.h | 155 --- lib/ESPAsyncWebServer/src/WebHandlers.cpp | 250 ---- lib/ESPAsyncWebServer/src/WebRequest.cpp | 985 -------------- lib/ESPAsyncWebServer/src/WebResponseImpl.h | 157 --- lib/ESPAsyncWebServer/src/WebResponses.cpp | 849 ------------ lib/ESPAsyncWebServer/src/WebServer.cpp | 227 ---- lib/ESPAsyncWebServer/src/literals.h | 345 ----- .../src/port/SHA1Builder.cpp | 284 ---- lib/ESPAsyncWebServer/src/port/SHA1Builder.h | 39 - lib/PsychicHttp/CHANGELOG.md | 34 + .../CMakeLists.txt | 4 +- lib/PsychicHttp/LICENSE | 7 + lib/PsychicHttp/README.md | 826 +++++++++++ lib/PsychicHttp/RELEASE.md | 6 + lib/PsychicHttp/assets/handler-callbacks.svg | 4 + lib/PsychicHttp/assets/request-flow.svg | 4 + .../benchmark/arduinomongoose/.gitignore | 6 + .../arduinomongoose/data/www/alien.png | Bin 0 -> 28598 bytes .../benchmark/arduinomongoose/include/README | 39 + .../benchmark/arduinomongoose/lib/README | 46 + .../benchmark/arduinomongoose/platformio.ini | 22 + .../benchmark/arduinomongoose/src/main.cpp | 234 ++++ .../benchmark/arduinomongoose/test/README | 11 + lib/PsychicHttp/benchmark/comparison.ods | Bin 0 -> 82456 bytes .../benchmark/espasyncwebserver/.gitignore | 6 + .../espasyncwebserver/data/www/alien.png | Bin 0 -> 28598 bytes .../espasyncwebserver/include/README | 39 + .../benchmark/espasyncwebserver/lib/README | 46 + .../espasyncwebserver/platformio.ini | 22 + .../benchmark/espasyncwebserver/src/main.cpp | 276 ++++ .../benchmark/espasyncwebserver/test/README | 11 + .../benchmark/eventsource-client-test.js | 39 + lib/PsychicHttp/benchmark/http-client-test.js | 43 + lib/PsychicHttp/benchmark/latency.png | Bin 0 -> 50143 bytes lib/PsychicHttp/benchmark/loadtest-http.sh | 37 + .../benchmark/loadtest-websocket.sh | 31 + lib/PsychicHttp/benchmark/package.json | 7 + lib/PsychicHttp/benchmark/performance.png | Bin 0 -> 49201 bytes .../benchmark/psychichttp/.gitignore | 6 + .../benchmark/psychichttp/data/www/alien.png | Bin 0 -> 28598 bytes .../benchmark/psychichttp/include/README | 39 + .../benchmark/psychichttp/lib/README | 46 + .../benchmark/psychichttp/platformio.ini | 22 + .../benchmark/psychichttp/src/main.cpp | 228 ++++ .../benchmark/psychichttp/src/secret.h | 2 + .../benchmark/psychichttp/test/README | 11 + .../benchmark/psychichttps/.gitignore | 6 + .../benchmark/psychichttps/data/server.crt | 19 + .../benchmark/psychichttps/data/server.key | 28 + .../benchmark/psychichttps/data/www/alien.png | Bin 0 -> 28598 bytes .../benchmark/psychichttps/include/README | 39 + .../benchmark/psychichttps/lib/README | 46 + .../benchmark/psychichttps/platformio.ini | 22 + .../benchmark/psychichttps/src/main.cpp | 240 ++++ .../benchmark/psychichttps/src/secret.h | 2 + .../benchmark/psychichttps/test/README | 11 + .../results/arduinomongoose-http-loadtest.log | 1172 ++++++++++++++++ .../arduinomongoose-websocket-loadtest.log | 246 ++++ .../results/espasync-http-loadtest.log | 1165 ++++++++++++++++ .../results/espasync-websocket-loadtest.log | 252 ++++ .../results/psychic-http-loadtest.log | 1172 ++++++++++++++++ .../results/psychic-ssl-http-loadtest.log | 1194 ++++++++++++++++ .../psychic-ssl-websocket-loadtest.log | 246 ++++ .../results/psychic-v1.1-http-loadtest.log | 1179 ++++++++++++++++ .../psychic-v1.1-websocket-loadtest.log | 246 ++++ .../results/psychic-websocket-loadtest.log | 246 ++++ .../benchmark/websocket-client-test.js | 36 + lib/PsychicHttp/component.mk | 3 + lib/PsychicHttp/examples/arduino/.gitignore | 6 + lib/PsychicHttp/examples/arduino/arduino.ino | 497 +++++++ .../arduino/arduino_captive_portal/README.md | 60 + .../arduino_captive_portal.ino | 144 ++ .../images/accesspoint.png | Bin 0 -> 15389 bytes .../arduino_captive_portal/images/station.png | Bin 0 -> 10008 bytes .../examples/arduino/arduino_ota/README.md | 138 ++ .../arduino/arduino_ota/arduino_ota.ino | 219 +++ .../examples/arduino/arduino_ota/code.bin | Bin 0 -> 867072 bytes .../arduino/arduino_ota/data/update.html | 166 +++ .../arduino/arduino_ota/images/otaupdate1.png | Bin 0 -> 60478 bytes .../arduino/arduino_ota/images/otaupdate2.png | Bin 0 -> 60711 bytes .../arduino/arduino_ota/images/otaupdate3.png | Bin 0 -> 60047 bytes .../arduino/arduino_ota/images/otaupdate4.png | Bin 0 -> 59009 bytes .../arduino/arduino_ota/images/otaupdate5.png | Bin 0 -> 60143 bytes .../examples/arduino/arduino_ota/littlefs.bin | Bin 0 -> 1572864 bytes .../examples/arduino/data/custom.txt | 1 + .../arduino/data/img/request_flow.png | Bin 0 -> 30509 bytes .../examples/arduino/data/server.crt | 19 + .../examples/arduino/data/server.key | 28 + .../examples/arduino/data/www-ap/index.html | 15 + .../examples/arduino/data/www/alien.png | Bin 0 -> 28598 bytes .../examples/arduino/data/www/favicon.ico | Bin 0 -> 67646 bytes .../examples/arduino/data/www/index.html | 236 ++++ .../examples/arduino/data/www/text.txt | 1 + lib/PsychicHttp/examples/arduino/secret.h | 2 + lib/PsychicHttp/examples/esp-idf/.gitignore | 4 + .../examples/esp-idf/CMakeLists.txt | 19 + lib/PsychicHttp/examples/esp-idf/README.md | 7 + .../examples/esp-idf/data/custom.txt | 1 + .../esp-idf/data/img/request_flow.png | Bin 0 -> 30509 bytes .../examples/esp-idf/data/server.crt | 19 + .../examples/esp-idf/data/server.key | 28 + .../examples/esp-idf/data/www-ap/index.html | 15 + .../examples/esp-idf/data/www/alien.png | Bin 0 -> 28598 bytes .../examples/esp-idf/data/www/favicon.ico | Bin 0 -> 67646 bytes .../examples/esp-idf/data/www/index.html | 236 ++++ .../examples/esp-idf/data/www/text.txt | 1 + .../examples/esp-idf/include/README | 39 + lib/PsychicHttp/examples/esp-idf/lib/README | 46 + .../examples/esp-idf/main/CMakeLists.txt | 8 + .../examples/esp-idf/main/main.cpp | 510 +++++++ .../examples/esp-idf/main/secret.h | 2 + .../examples/esp-idf/partitions_custom.csv | 7 + .../examples/esp-idf/sdkconfig.defaults | 64 + .../old/esp_ota_http_server/.gitignore | 39 + .../old/esp_ota_http_server/include/README | 39 + .../old/esp_ota_http_server/lib/README | 46 + .../old/esp_ota_http_server/platformio.ini | 74 + .../src/esp_ota_http_server.cpp | 229 ++++ .../old/esp_ota_http_server/test/README | 11 + .../old/simple_http_server/.gitignore | 39 + .../old/simple_http_server/include/README | 39 + .../old/simple_http_server/lib/README | 46 + .../old/simple_http_server/platformio.ini | 64 + .../src/simple_http_server.cpp | 211 +++ .../old/simple_http_server/test/README | 11 + .../old/simple_http_server/test/tests.rest | 35 + .../old/simplest_web_server_esp/.gitignore | 39 + .../old/simplest_web_server_esp/.travis.yml | 67 + .../simplest_web_server_esp/include/README | 39 + .../old/simplest_web_server_esp/lib/README | 46 + .../simplest_web_server_esp/platformio.ini | 40 + .../src/simplest_web_server.cpp | 97 ++ .../old/simplest_web_server_esp/test/README | 11 + .../examples/old/websocket_chat/.gitignore | 39 + .../old/websocket_chat/include/README | 39 + .../examples/old/websocket_chat/lib/README | 46 + .../old/websocket_chat/platformio.ini | 64 + .../old/websocket_chat/src/websocket_chat.cpp | 235 ++++ .../examples/old/websocket_chat/test/README | 11 + .../old/websocket_chat/test/tests.rest | 35 + .../examples/platformio/.gitignore | 6 + .../examples/platformio/data/custom.txt | 1 + .../platformio/data/img/request_flow.png | Bin 0 -> 30509 bytes .../examples/platformio/data/server.crt | 19 + .../examples/platformio/data/server.key | 28 + .../platformio/data/www-ap/index.html | 15 + .../examples/platformio/data/www/alien.png | Bin 0 -> 28598 bytes .../examples/platformio/data/www/favicon.ico | Bin 0 -> 67646 bytes .../examples/platformio/data/www/index.html | 236 ++++ .../examples/platformio/data/www/text.txt | 1 + .../examples/platformio/include/README | 39 + .../examples/platformio/lib/README | 46 + .../examples/platformio/platformio.ini | 30 + .../examples/platformio/src/main.cpp | 583 ++++++++ .../examples/platformio/src/secret.h | 2 + .../examples/platformio/test/README | 11 + .../examples/websockets/.gitignore | 8 + .../examples/websockets/data/www/favicon.ico | Bin 0 -> 67646 bytes .../examples/websockets/data/www/index.html | 76 ++ .../examples/websockets/include/README | 39 + .../examples/websockets/lib/README | 46 + .../examples/websockets/platformio.ini | 25 + .../examples/websockets/src/main.cpp | 225 +++ .../examples/websockets/src/secret.h | 2 + .../examples/websockets/test/README | 11 + lib/PsychicHttp/library.json | 53 + lib/PsychicHttp/library.properties | 11 + lib/PsychicHttp/request flow.drawio | 1 + lib/PsychicHttp/src/ChunkPrinter.cpp | 85 ++ lib/PsychicHttp/src/ChunkPrinter.h | 27 + lib/PsychicHttp/src/PsychicClient.cpp | 72 + lib/PsychicHttp/src/PsychicClient.h | 35 + lib/PsychicHttp/src/PsychicCore.h | 107 ++ lib/PsychicHttp/src/PsychicEndpoint.cpp | 90 ++ lib/PsychicHttp/src/PsychicEndpoint.h | 37 + lib/PsychicHttp/src/PsychicEventSource.cpp | 225 +++ lib/PsychicHttp/src/PsychicEventSource.h | 82 ++ lib/PsychicHttp/src/PsychicFileResponse.cpp | 159 +++ lib/PsychicHttp/src/PsychicFileResponse.h | 23 + lib/PsychicHttp/src/PsychicHandler.cpp | 111 ++ lib/PsychicHttp/src/PsychicHandler.h | 66 + lib/PsychicHttp/src/PsychicHttp.h | 24 + lib/PsychicHttp/src/PsychicHttpServer.cpp | 366 +++++ lib/PsychicHttp/src/PsychicHttpServer.h | 81 ++ lib/PsychicHttp/src/PsychicHttpsServer.cpp | 61 + lib/PsychicHttp/src/PsychicHttpsServer.h | 39 + lib/PsychicHttp/src/PsychicJson.cpp | 133 ++ lib/PsychicHttp/src/PsychicJson.h | 89 ++ lib/PsychicHttp/src/PsychicRequest.cpp | 542 ++++++++ lib/PsychicHttp/src/PsychicRequest.h | 98 ++ lib/PsychicHttp/src/PsychicResponse.cpp | 162 +++ lib/PsychicHttp/src/PsychicResponse.h | 46 + .../src/PsychicStaticFileHander.cpp | 181 +++ .../src/PsychicStaticFileHandler.h | 41 + lib/PsychicHttp/src/PsychicStreamResponse.cpp | 94 ++ lib/PsychicHttp/src/PsychicStreamResponse.h | 35 + lib/PsychicHttp/src/PsychicUploadHandler.cpp | 395 ++++++ lib/PsychicHttp/src/PsychicUploadHandler.h | 68 + lib/PsychicHttp/src/PsychicWebHandler.cpp | 74 + lib/PsychicHttp/src/PsychicWebHandler.h | 34 + lib/PsychicHttp/src/PsychicWebParameter.h | 25 + lib/PsychicHttp/src/PsychicWebSocket.cpp | 258 ++++ lib/PsychicHttp/src/PsychicWebSocket.h | 70 + lib/PsychicHttp/src/TemplatePrinter.cpp | 90 ++ lib/PsychicHttp/src/TemplatePrinter.h | 51 + lib/PsychicHttp/src/async_worker.cpp | 203 +++ lib/PsychicHttp/src/async_worker.h | 36 + lib/PsychicHttp/src/http_status.cpp | 194 +++ lib/PsychicHttp/src/http_status.h | 15 + 234 files changed, 20090 insertions(+), 8061 deletions(-) delete mode 100644 lib/ESPAsyncWebServer/LICENSE delete mode 100644 lib/ESPAsyncWebServer/README.md delete mode 100644 lib/ESPAsyncWebServer/docs/_config.yml delete mode 100644 lib/ESPAsyncWebServer/docs/index.md delete mode 100644 lib/ESPAsyncWebServer/examples/CaptivePortal/CaptivePortal.ino delete mode 100644 lib/ESPAsyncWebServer/examples/Draft/Draft.ino delete mode 100644 lib/ESPAsyncWebServer/examples/Filters/Filters.ino delete mode 100644 lib/ESPAsyncWebServer/examples/SimpleServer/SimpleServer.ino delete mode 100644 lib/ESPAsyncWebServer/examples/StreamFiles/StreamConcat.h delete mode 100644 lib/ESPAsyncWebServer/examples/StreamFiles/StreamFiles.ino delete mode 100644 lib/ESPAsyncWebServer/examples/StreamFiles/StreamString.h delete mode 100644 lib/ESPAsyncWebServer/examples/issues/Issue14/Issue14.ino delete mode 100644 lib/ESPAsyncWebServer/library.json delete mode 100644 lib/ESPAsyncWebServer/library.properties delete mode 100644 lib/ESPAsyncWebServer/platformio.ini delete mode 100644 lib/ESPAsyncWebServer/src/AsyncEventSource.cpp delete mode 100644 lib/ESPAsyncWebServer/src/AsyncEventSource.h delete mode 100644 lib/ESPAsyncWebServer/src/AsyncJson.h delete mode 100644 lib/ESPAsyncWebServer/src/AsyncMessagePack.h delete mode 100644 lib/ESPAsyncWebServer/src/AsyncWebSocket.cpp delete mode 100644 lib/ESPAsyncWebServer/src/AsyncWebSocket.h delete mode 100644 lib/ESPAsyncWebServer/src/ChunkPrint.h delete mode 100644 lib/ESPAsyncWebServer/src/ESPAsyncWebServer.h delete mode 100644 lib/ESPAsyncWebServer/src/WebAuthentication.cpp delete mode 100644 lib/ESPAsyncWebServer/src/WebAuthentication.h delete mode 100644 lib/ESPAsyncWebServer/src/WebHandlerImpl.h delete mode 100644 lib/ESPAsyncWebServer/src/WebHandlers.cpp delete mode 100644 lib/ESPAsyncWebServer/src/WebRequest.cpp delete mode 100644 lib/ESPAsyncWebServer/src/WebResponseImpl.h delete mode 100644 lib/ESPAsyncWebServer/src/WebResponses.cpp delete mode 100644 lib/ESPAsyncWebServer/src/WebServer.cpp delete mode 100644 lib/ESPAsyncWebServer/src/literals.h delete mode 100644 lib/ESPAsyncWebServer/src/port/SHA1Builder.cpp delete mode 100644 lib/ESPAsyncWebServer/src/port/SHA1Builder.h create mode 100644 lib/PsychicHttp/CHANGELOG.md rename lib/{ESPAsyncWebServer => PsychicHttp}/CMakeLists.txt (82%) create mode 100644 lib/PsychicHttp/LICENSE create mode 100644 lib/PsychicHttp/README.md create mode 100644 lib/PsychicHttp/RELEASE.md create mode 100644 lib/PsychicHttp/assets/handler-callbacks.svg create mode 100644 lib/PsychicHttp/assets/request-flow.svg create mode 100644 lib/PsychicHttp/benchmark/arduinomongoose/.gitignore create mode 100644 lib/PsychicHttp/benchmark/arduinomongoose/data/www/alien.png create mode 100644 lib/PsychicHttp/benchmark/arduinomongoose/include/README create mode 100644 lib/PsychicHttp/benchmark/arduinomongoose/lib/README create mode 100644 lib/PsychicHttp/benchmark/arduinomongoose/platformio.ini create mode 100644 lib/PsychicHttp/benchmark/arduinomongoose/src/main.cpp create mode 100644 lib/PsychicHttp/benchmark/arduinomongoose/test/README create mode 100644 lib/PsychicHttp/benchmark/comparison.ods create mode 100644 lib/PsychicHttp/benchmark/espasyncwebserver/.gitignore create mode 100644 lib/PsychicHttp/benchmark/espasyncwebserver/data/www/alien.png create mode 100644 lib/PsychicHttp/benchmark/espasyncwebserver/include/README create mode 100644 lib/PsychicHttp/benchmark/espasyncwebserver/lib/README create mode 100644 lib/PsychicHttp/benchmark/espasyncwebserver/platformio.ini create mode 100644 lib/PsychicHttp/benchmark/espasyncwebserver/src/main.cpp create mode 100644 lib/PsychicHttp/benchmark/espasyncwebserver/test/README create mode 100644 lib/PsychicHttp/benchmark/eventsource-client-test.js create mode 100644 lib/PsychicHttp/benchmark/http-client-test.js create mode 100644 lib/PsychicHttp/benchmark/latency.png create mode 100644 lib/PsychicHttp/benchmark/loadtest-http.sh create mode 100644 lib/PsychicHttp/benchmark/loadtest-websocket.sh create mode 100644 lib/PsychicHttp/benchmark/package.json create mode 100644 lib/PsychicHttp/benchmark/performance.png create mode 100644 lib/PsychicHttp/benchmark/psychichttp/.gitignore create mode 100644 lib/PsychicHttp/benchmark/psychichttp/data/www/alien.png create mode 100644 lib/PsychicHttp/benchmark/psychichttp/include/README create mode 100644 lib/PsychicHttp/benchmark/psychichttp/lib/README create mode 100644 lib/PsychicHttp/benchmark/psychichttp/platformio.ini create mode 100644 lib/PsychicHttp/benchmark/psychichttp/src/main.cpp create mode 100644 lib/PsychicHttp/benchmark/psychichttp/src/secret.h create mode 100644 lib/PsychicHttp/benchmark/psychichttp/test/README create mode 100644 lib/PsychicHttp/benchmark/psychichttps/.gitignore create mode 100644 lib/PsychicHttp/benchmark/psychichttps/data/server.crt create mode 100644 lib/PsychicHttp/benchmark/psychichttps/data/server.key create mode 100644 lib/PsychicHttp/benchmark/psychichttps/data/www/alien.png create mode 100644 lib/PsychicHttp/benchmark/psychichttps/include/README create mode 100644 lib/PsychicHttp/benchmark/psychichttps/lib/README create mode 100644 lib/PsychicHttp/benchmark/psychichttps/platformio.ini create mode 100644 lib/PsychicHttp/benchmark/psychichttps/src/main.cpp create mode 100644 lib/PsychicHttp/benchmark/psychichttps/src/secret.h create mode 100644 lib/PsychicHttp/benchmark/psychichttps/test/README create mode 100644 lib/PsychicHttp/benchmark/results/arduinomongoose-http-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/arduinomongoose-websocket-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/espasync-http-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/espasync-websocket-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/psychic-http-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/psychic-ssl-http-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/psychic-ssl-websocket-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/psychic-v1.1-http-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/psychic-v1.1-websocket-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/results/psychic-websocket-loadtest.log create mode 100644 lib/PsychicHttp/benchmark/websocket-client-test.js create mode 100644 lib/PsychicHttp/component.mk create mode 100644 lib/PsychicHttp/examples/arduino/.gitignore create mode 100644 lib/PsychicHttp/examples/arduino/arduino.ino create mode 100644 lib/PsychicHttp/examples/arduino/arduino_captive_portal/README.md create mode 100644 lib/PsychicHttp/examples/arduino/arduino_captive_portal/arduino_captive_portal.ino create mode 100644 lib/PsychicHttp/examples/arduino/arduino_captive_portal/images/accesspoint.png create mode 100644 lib/PsychicHttp/examples/arduino/arduino_captive_portal/images/station.png create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/README.md create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/arduino_ota.ino create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/code.bin create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/data/update.html create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate1.png create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate2.png create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate3.png create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate4.png create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate5.png create mode 100644 lib/PsychicHttp/examples/arduino/arduino_ota/littlefs.bin create mode 100644 lib/PsychicHttp/examples/arduino/data/custom.txt create mode 100644 lib/PsychicHttp/examples/arduino/data/img/request_flow.png create mode 100644 lib/PsychicHttp/examples/arduino/data/server.crt create mode 100644 lib/PsychicHttp/examples/arduino/data/server.key create mode 100644 lib/PsychicHttp/examples/arduino/data/www-ap/index.html create mode 100644 lib/PsychicHttp/examples/arduino/data/www/alien.png create mode 100644 lib/PsychicHttp/examples/arduino/data/www/favicon.ico create mode 100644 lib/PsychicHttp/examples/arduino/data/www/index.html create mode 100644 lib/PsychicHttp/examples/arduino/data/www/text.txt create mode 100644 lib/PsychicHttp/examples/arduino/secret.h create mode 100644 lib/PsychicHttp/examples/esp-idf/.gitignore create mode 100644 lib/PsychicHttp/examples/esp-idf/CMakeLists.txt create mode 100644 lib/PsychicHttp/examples/esp-idf/README.md create mode 100644 lib/PsychicHttp/examples/esp-idf/data/custom.txt create mode 100644 lib/PsychicHttp/examples/esp-idf/data/img/request_flow.png create mode 100644 lib/PsychicHttp/examples/esp-idf/data/server.crt create mode 100644 lib/PsychicHttp/examples/esp-idf/data/server.key create mode 100644 lib/PsychicHttp/examples/esp-idf/data/www-ap/index.html create mode 100644 lib/PsychicHttp/examples/esp-idf/data/www/alien.png create mode 100644 lib/PsychicHttp/examples/esp-idf/data/www/favicon.ico create mode 100644 lib/PsychicHttp/examples/esp-idf/data/www/index.html create mode 100644 lib/PsychicHttp/examples/esp-idf/data/www/text.txt create mode 100644 lib/PsychicHttp/examples/esp-idf/include/README create mode 100644 lib/PsychicHttp/examples/esp-idf/lib/README create mode 100644 lib/PsychicHttp/examples/esp-idf/main/CMakeLists.txt create mode 100644 lib/PsychicHttp/examples/esp-idf/main/main.cpp create mode 100644 lib/PsychicHttp/examples/esp-idf/main/secret.h create mode 100644 lib/PsychicHttp/examples/esp-idf/partitions_custom.csv create mode 100644 lib/PsychicHttp/examples/esp-idf/sdkconfig.defaults create mode 100644 lib/PsychicHttp/examples/old/esp_ota_http_server/.gitignore create mode 100644 lib/PsychicHttp/examples/old/esp_ota_http_server/include/README create mode 100644 lib/PsychicHttp/examples/old/esp_ota_http_server/lib/README create mode 100644 lib/PsychicHttp/examples/old/esp_ota_http_server/platformio.ini create mode 100644 lib/PsychicHttp/examples/old/esp_ota_http_server/src/esp_ota_http_server.cpp create mode 100644 lib/PsychicHttp/examples/old/esp_ota_http_server/test/README create mode 100644 lib/PsychicHttp/examples/old/simple_http_server/.gitignore create mode 100644 lib/PsychicHttp/examples/old/simple_http_server/include/README create mode 100644 lib/PsychicHttp/examples/old/simple_http_server/lib/README create mode 100644 lib/PsychicHttp/examples/old/simple_http_server/platformio.ini create mode 100644 lib/PsychicHttp/examples/old/simple_http_server/src/simple_http_server.cpp create mode 100644 lib/PsychicHttp/examples/old/simple_http_server/test/README create mode 100644 lib/PsychicHttp/examples/old/simple_http_server/test/tests.rest create mode 100644 lib/PsychicHttp/examples/old/simplest_web_server_esp/.gitignore create mode 100644 lib/PsychicHttp/examples/old/simplest_web_server_esp/.travis.yml create mode 100644 lib/PsychicHttp/examples/old/simplest_web_server_esp/include/README create mode 100644 lib/PsychicHttp/examples/old/simplest_web_server_esp/lib/README create mode 100644 lib/PsychicHttp/examples/old/simplest_web_server_esp/platformio.ini create mode 100644 lib/PsychicHttp/examples/old/simplest_web_server_esp/src/simplest_web_server.cpp create mode 100644 lib/PsychicHttp/examples/old/simplest_web_server_esp/test/README create mode 100644 lib/PsychicHttp/examples/old/websocket_chat/.gitignore create mode 100644 lib/PsychicHttp/examples/old/websocket_chat/include/README create mode 100644 lib/PsychicHttp/examples/old/websocket_chat/lib/README create mode 100644 lib/PsychicHttp/examples/old/websocket_chat/platformio.ini create mode 100644 lib/PsychicHttp/examples/old/websocket_chat/src/websocket_chat.cpp create mode 100644 lib/PsychicHttp/examples/old/websocket_chat/test/README create mode 100644 lib/PsychicHttp/examples/old/websocket_chat/test/tests.rest create mode 100644 lib/PsychicHttp/examples/platformio/.gitignore create mode 100644 lib/PsychicHttp/examples/platformio/data/custom.txt create mode 100644 lib/PsychicHttp/examples/platformio/data/img/request_flow.png create mode 100644 lib/PsychicHttp/examples/platformio/data/server.crt create mode 100644 lib/PsychicHttp/examples/platformio/data/server.key create mode 100644 lib/PsychicHttp/examples/platformio/data/www-ap/index.html create mode 100644 lib/PsychicHttp/examples/platformio/data/www/alien.png create mode 100644 lib/PsychicHttp/examples/platformio/data/www/favicon.ico create mode 100644 lib/PsychicHttp/examples/platformio/data/www/index.html create mode 100644 lib/PsychicHttp/examples/platformio/data/www/text.txt create mode 100644 lib/PsychicHttp/examples/platformio/include/README create mode 100644 lib/PsychicHttp/examples/platformio/lib/README create mode 100644 lib/PsychicHttp/examples/platformio/platformio.ini create mode 100644 lib/PsychicHttp/examples/platformio/src/main.cpp create mode 100644 lib/PsychicHttp/examples/platformio/src/secret.h create mode 100644 lib/PsychicHttp/examples/platformio/test/README create mode 100644 lib/PsychicHttp/examples/websockets/.gitignore create mode 100644 lib/PsychicHttp/examples/websockets/data/www/favicon.ico create mode 100644 lib/PsychicHttp/examples/websockets/data/www/index.html create mode 100644 lib/PsychicHttp/examples/websockets/include/README create mode 100644 lib/PsychicHttp/examples/websockets/lib/README create mode 100644 lib/PsychicHttp/examples/websockets/platformio.ini create mode 100644 lib/PsychicHttp/examples/websockets/src/main.cpp create mode 100644 lib/PsychicHttp/examples/websockets/src/secret.h create mode 100644 lib/PsychicHttp/examples/websockets/test/README create mode 100644 lib/PsychicHttp/library.json create mode 100644 lib/PsychicHttp/library.properties create mode 100644 lib/PsychicHttp/request flow.drawio create mode 100644 lib/PsychicHttp/src/ChunkPrinter.cpp create mode 100644 lib/PsychicHttp/src/ChunkPrinter.h create mode 100644 lib/PsychicHttp/src/PsychicClient.cpp create mode 100644 lib/PsychicHttp/src/PsychicClient.h create mode 100644 lib/PsychicHttp/src/PsychicCore.h create mode 100644 lib/PsychicHttp/src/PsychicEndpoint.cpp create mode 100644 lib/PsychicHttp/src/PsychicEndpoint.h create mode 100644 lib/PsychicHttp/src/PsychicEventSource.cpp create mode 100644 lib/PsychicHttp/src/PsychicEventSource.h create mode 100644 lib/PsychicHttp/src/PsychicFileResponse.cpp create mode 100644 lib/PsychicHttp/src/PsychicFileResponse.h create mode 100644 lib/PsychicHttp/src/PsychicHandler.cpp create mode 100644 lib/PsychicHttp/src/PsychicHandler.h create mode 100644 lib/PsychicHttp/src/PsychicHttp.h create mode 100644 lib/PsychicHttp/src/PsychicHttpServer.cpp create mode 100644 lib/PsychicHttp/src/PsychicHttpServer.h create mode 100644 lib/PsychicHttp/src/PsychicHttpsServer.cpp create mode 100644 lib/PsychicHttp/src/PsychicHttpsServer.h create mode 100644 lib/PsychicHttp/src/PsychicJson.cpp create mode 100644 lib/PsychicHttp/src/PsychicJson.h create mode 100644 lib/PsychicHttp/src/PsychicRequest.cpp create mode 100644 lib/PsychicHttp/src/PsychicRequest.h create mode 100644 lib/PsychicHttp/src/PsychicResponse.cpp create mode 100644 lib/PsychicHttp/src/PsychicResponse.h create mode 100644 lib/PsychicHttp/src/PsychicStaticFileHander.cpp create mode 100644 lib/PsychicHttp/src/PsychicStaticFileHandler.h create mode 100644 lib/PsychicHttp/src/PsychicStreamResponse.cpp create mode 100644 lib/PsychicHttp/src/PsychicStreamResponse.h create mode 100644 lib/PsychicHttp/src/PsychicUploadHandler.cpp create mode 100644 lib/PsychicHttp/src/PsychicUploadHandler.h create mode 100644 lib/PsychicHttp/src/PsychicWebHandler.cpp create mode 100644 lib/PsychicHttp/src/PsychicWebHandler.h create mode 100644 lib/PsychicHttp/src/PsychicWebParameter.h create mode 100644 lib/PsychicHttp/src/PsychicWebSocket.cpp create mode 100644 lib/PsychicHttp/src/PsychicWebSocket.h create mode 100644 lib/PsychicHttp/src/TemplatePrinter.cpp create mode 100644 lib/PsychicHttp/src/TemplatePrinter.h create mode 100644 lib/PsychicHttp/src/async_worker.cpp create mode 100644 lib/PsychicHttp/src/async_worker.h create mode 100644 lib/PsychicHttp/src/http_status.cpp create mode 100644 lib/PsychicHttp/src/http_status.h diff --git a/lib/ESPAsyncWebServer/LICENSE b/lib/ESPAsyncWebServer/LICENSE deleted file mode 100644 index 153d416..0000000 --- a/lib/ESPAsyncWebServer/LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file diff --git a/lib/ESPAsyncWebServer/README.md b/lib/ESPAsyncWebServer/README.md deleted file mode 100644 index 6ca1d1e..0000000 --- a/lib/ESPAsyncWebServer/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# ESPAsyncWebServer - -[![License: LGPL 3.0](https://img.shields.io/badge/License-LGPL%203.0-yellow.svg)](https://opensource.org/license/lgpl-3-0/) -[![Continuous Integration](https://github.com/mathieucarbou/ESPAsyncWebServer/actions/workflows/ci.yml/badge.svg)](https://github.com/mathieucarbou/ESPAsyncWebServer/actions/workflows/ci.yml) -[![PlatformIO Registry](https://badges.registry.platformio.org/packages/mathieucarbou/library/ESPAsyncWebServer.svg)](https://registry.platformio.org/libraries/mathieucarbou/ESPAsyncWebServer) - -Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 -Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc. - -This fork is based on [yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) and includes all the concurrency fixes. - -## Coordinate and dependencies: - -**WARNING** The library name was changed from `ESP Async WebServer` to `ESPAsyncWebServer` as per the Arduino Lint recommendations. - -``` -mathieucarbou/ESPAsyncWebServer @ 3.1.5 -``` - -Dependency: - -- **ESP32**: `mathieucarbou/AsyncTCP @ 3.2.4` (Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.4](https://github.com/mathieucarbou/AsyncTCP/releases/tag/v3.2.0)) -- **ESP8266**: `esphome/ESPAsyncTCP-esphome @ 2.0.0` (Arduino IDE: [https://github.com/mathieucarbou/esphome-ESPAsyncTCP#v2.0.0](https://github.com/mathieucarbou/esphome-ESPAsyncTCP/releases/tag/v2.0.0)) -- **RP2040**: `khoih-prog/AsyncTCP_RP2040W @ 1.2.0` (Arduino IDE: [https://github.com/khoih-prog/AsyncTCP_RP2040W#v1.2.0](https://github.com/khoih-prog/AsyncTCP_RP2040W/releases/tag/v1.2.0)) - -## Changes in this fork - -- [@ayushsharma82](https://github.com/ayushsharma82) and [@mathieucarbou](https://github.com/mathieucarbou): Add RP2040 support ([#31](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/31)) -- [@mathieucarbou](https://github.com/mathieucarbou): `SSE_MAX_QUEUED_MESSAGES` to control the maximum number of messages that can be queued for a SSE client -- [@mathieucarbou](https://github.com/mathieucarbou): `write()` function public in `AsyncEventSource.h` -- [@mathieucarbou](https://github.com/mathieucarbou): `WS_MAX_QUEUED_MESSAGES`: control the maximum number of messages that can be queued for a Websocket client -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setAuthentication(const String& username, const String& password)` -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setCloseClientOnQueueFull(bool)` which can be set on a client to either close the connection or discard messages but not close the connection when the queue is full -- [@mathieucarbou](https://github.com/mathieucarbou): Added `StreamConcat` example to show how to stream multiple files in one response -- [@mathieucarbou](https://github.com/mathieucarbou): Added all flavors of `binary()`, `text()`, `binaryAll()` and `textAll()` in `AsyncWebSocket` -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino 3 / ESP-IDF 5.1 compatibility -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino Json 7 compatibility and backward compatible with 6 and 6 (changes in `AsyncJson.h`). The API to use Json has not changed. These are only internal changes. -- [@mathieucarbou](https://github.com/mathieucarbou): CI -- [@mathieucarbou](https://github.com/mathieucarbou): Depends on `mathieucarbou/AsyncTCP @ 3.2.4` -- [@mathieucarbou](https://github.com/mathieucarbou): Deployed in PlatformIO registry and Arduino IDE library manager -- [@mathieucarbou](https://github.com/mathieucarbou): Firmware size optimization: remove mbedtls dependency (accounts for 33KB in firmware) -- [@mathieucarbou](https://github.com/mathieucarbou): Made DEFAULT_MAX_SSE_CLIENTS customizable -- [@mathieucarbou](https://github.com/mathieucarbou): Made DEFAULT_MAX_WS_CLIENTS customizable -- [@mathieucarbou](https://github.com/mathieucarbou): MessagePack Support ([#62](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/62)) -- [@mathieucarbou](https://github.com/mathieucarbou): Remove filename after inline in Content-Disposition header according to RFC2183 -- [@mathieucarbou](https://github.com/mathieucarbou): Removed SPIFFSEditor to reduce library size and maintainance. SPIFF si also deprecated. If you need it, please copy the files from the original repository in your project. This fork focus on maintaining the server part and the SPIFFEditor is an application which has nothing to do inside a server library. -- [@mathieucarbou](https://github.com/mathieucarbou): Resurrected `AsyncWebSocketMessageBuffer` and `makeBuffer()` in order to make the fork API-compatible with the original library from me-no-dev regarding WebSocket. -- [@mathieucarbou](https://github.com/mathieucarbou): Some code cleanup -- [@mathieucarbou](https://github.com/mathieucarbou): Use `-D DEFAULT_MAX_WS_CLIENTS` to change the number of allows WebSocket clients and use `cleanupClients()` to help cleanup resources about dead clients -- [@nilo85](https://github.com/nilo85): Add support for Auth & GET requests in AsyncCallbackJsonWebHandler ([#14](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/14)) -- [@p0p-x](https://github.com/p0p-x): ESP IDF Compatibility (added back CMakeLists.txt) ([#32](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/32)) -- [@tueddy](https://github.com/tueddy): Compile with Arduino 3 (ESP-IDF 5.1) ([#13](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/13)) -- [@vortigont](https://github.com/vortigont): Set real "Last-Modified" header based on file's LastWrite time ([#5](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/5)) -- [@vortigont](https://github.com/vortigont): Some websocket code cleanup ([#29](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/29)) -- [@vortigont](https://github.com/vortigont): Refactor code - replace DYI structs with STL objects ([#39](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/39)) - -## Documentation - -Usage and API stays the same as the original library. -Please look at the original libraries for more examples and documentation. - -- [https://github.com/me-no-dev/ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) (original library) -- [https://github.com/yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) (fork of the original library) - -## `AsyncWebSocketMessageBuffer` and `makeBuffer()` - -The fork from `yubox-node-org` introduces some breaking API changes compared to the original library, especially regarding the use of `std::shared_ptr>` for WebSocket. - -This fork is compatible with the original library from `me-no-dev` regarding WebSocket, and wraps the optimizations done by `yubox-node-org` in the `AsyncWebSocketMessageBuffer` class. -So you have the choice of which API to use. - -Here are examples for serializing a Json document in a websocket message buffer: - -```cpp -void send(JsonDocument& doc) { - const size_t len = measureJson(doc); - - // original API from me-no-dev - AsyncWebSocketMessageBuffer* buffer = _ws->makeBuffer(len); - assert(buffer); // up to you to keep or remove this - serializeJson(doc, buffer->get(), len); - _ws->textAll(buffer); -} -``` - -```cpp -void send(JsonDocument& doc) { - const size_t len = measureJson(doc); - - // this fork (originally from yubox-node-org), uses another API with shared pointer - auto buffer = std::make_shared>(len); - assert(buffer); // up to you to keep or remove this - serializeJson(doc, buffer->data(), len); - _ws->textAll(std::move(buffer)); -} -``` - -I recommend to use the official API `AsyncWebSocketMessageBuffer` to retain further compatibility. - -## Important recommendations - -Most of the crashes are caused by improper configuration of the library for the project. -Here are some recommendations to avoid them. - -1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` -2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`. - The default value of `16384` might be too much for your project. - You can look at the [MycilaTaskMonitor](https://oss.carbou.me/MycilaTaskMonitor) project to monitor the stack usage. -3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`. - Default is `10`. -4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`. - Default is `64`. -5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`. - Default is `5000`. - -I personally use the following configuration in my projects because my WS messages can be big (up to 4k). -If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128. - -```c++ - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 - -D WS_MAX_QUEUED_MESSAGES=64 -``` diff --git a/lib/ESPAsyncWebServer/docs/_config.yml b/lib/ESPAsyncWebServer/docs/_config.yml deleted file mode 100644 index 3636597..0000000 --- a/lib/ESPAsyncWebServer/docs/_config.yml +++ /dev/null @@ -1,8 +0,0 @@ -# bundle exec jekyll serve --host=0.0.0.0 - -title: ESPAsyncWebServer -description: "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040" -remote_theme: pages-themes/cayman@v0.2.0 -plugins: - - jekyll-remote-theme - \ No newline at end of file diff --git a/lib/ESPAsyncWebServer/docs/index.md b/lib/ESPAsyncWebServer/docs/index.md deleted file mode 100644 index 6ca1d1e..0000000 --- a/lib/ESPAsyncWebServer/docs/index.md +++ /dev/null @@ -1,126 +0,0 @@ -# ESPAsyncWebServer - -[![License: LGPL 3.0](https://img.shields.io/badge/License-LGPL%203.0-yellow.svg)](https://opensource.org/license/lgpl-3-0/) -[![Continuous Integration](https://github.com/mathieucarbou/ESPAsyncWebServer/actions/workflows/ci.yml/badge.svg)](https://github.com/mathieucarbou/ESPAsyncWebServer/actions/workflows/ci.yml) -[![PlatformIO Registry](https://badges.registry.platformio.org/packages/mathieucarbou/library/ESPAsyncWebServer.svg)](https://registry.platformio.org/libraries/mathieucarbou/ESPAsyncWebServer) - -Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 -Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc. - -This fork is based on [yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) and includes all the concurrency fixes. - -## Coordinate and dependencies: - -**WARNING** The library name was changed from `ESP Async WebServer` to `ESPAsyncWebServer` as per the Arduino Lint recommendations. - -``` -mathieucarbou/ESPAsyncWebServer @ 3.1.5 -``` - -Dependency: - -- **ESP32**: `mathieucarbou/AsyncTCP @ 3.2.4` (Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.4](https://github.com/mathieucarbou/AsyncTCP/releases/tag/v3.2.0)) -- **ESP8266**: `esphome/ESPAsyncTCP-esphome @ 2.0.0` (Arduino IDE: [https://github.com/mathieucarbou/esphome-ESPAsyncTCP#v2.0.0](https://github.com/mathieucarbou/esphome-ESPAsyncTCP/releases/tag/v2.0.0)) -- **RP2040**: `khoih-prog/AsyncTCP_RP2040W @ 1.2.0` (Arduino IDE: [https://github.com/khoih-prog/AsyncTCP_RP2040W#v1.2.0](https://github.com/khoih-prog/AsyncTCP_RP2040W/releases/tag/v1.2.0)) - -## Changes in this fork - -- [@ayushsharma82](https://github.com/ayushsharma82) and [@mathieucarbou](https://github.com/mathieucarbou): Add RP2040 support ([#31](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/31)) -- [@mathieucarbou](https://github.com/mathieucarbou): `SSE_MAX_QUEUED_MESSAGES` to control the maximum number of messages that can be queued for a SSE client -- [@mathieucarbou](https://github.com/mathieucarbou): `write()` function public in `AsyncEventSource.h` -- [@mathieucarbou](https://github.com/mathieucarbou): `WS_MAX_QUEUED_MESSAGES`: control the maximum number of messages that can be queued for a Websocket client -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setAuthentication(const String& username, const String& password)` -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setCloseClientOnQueueFull(bool)` which can be set on a client to either close the connection or discard messages but not close the connection when the queue is full -- [@mathieucarbou](https://github.com/mathieucarbou): Added `StreamConcat` example to show how to stream multiple files in one response -- [@mathieucarbou](https://github.com/mathieucarbou): Added all flavors of `binary()`, `text()`, `binaryAll()` and `textAll()` in `AsyncWebSocket` -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino 3 / ESP-IDF 5.1 compatibility -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino Json 7 compatibility and backward compatible with 6 and 6 (changes in `AsyncJson.h`). The API to use Json has not changed. These are only internal changes. -- [@mathieucarbou](https://github.com/mathieucarbou): CI -- [@mathieucarbou](https://github.com/mathieucarbou): Depends on `mathieucarbou/AsyncTCP @ 3.2.4` -- [@mathieucarbou](https://github.com/mathieucarbou): Deployed in PlatformIO registry and Arduino IDE library manager -- [@mathieucarbou](https://github.com/mathieucarbou): Firmware size optimization: remove mbedtls dependency (accounts for 33KB in firmware) -- [@mathieucarbou](https://github.com/mathieucarbou): Made DEFAULT_MAX_SSE_CLIENTS customizable -- [@mathieucarbou](https://github.com/mathieucarbou): Made DEFAULT_MAX_WS_CLIENTS customizable -- [@mathieucarbou](https://github.com/mathieucarbou): MessagePack Support ([#62](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/62)) -- [@mathieucarbou](https://github.com/mathieucarbou): Remove filename after inline in Content-Disposition header according to RFC2183 -- [@mathieucarbou](https://github.com/mathieucarbou): Removed SPIFFSEditor to reduce library size and maintainance. SPIFF si also deprecated. If you need it, please copy the files from the original repository in your project. This fork focus on maintaining the server part and the SPIFFEditor is an application which has nothing to do inside a server library. -- [@mathieucarbou](https://github.com/mathieucarbou): Resurrected `AsyncWebSocketMessageBuffer` and `makeBuffer()` in order to make the fork API-compatible with the original library from me-no-dev regarding WebSocket. -- [@mathieucarbou](https://github.com/mathieucarbou): Some code cleanup -- [@mathieucarbou](https://github.com/mathieucarbou): Use `-D DEFAULT_MAX_WS_CLIENTS` to change the number of allows WebSocket clients and use `cleanupClients()` to help cleanup resources about dead clients -- [@nilo85](https://github.com/nilo85): Add support for Auth & GET requests in AsyncCallbackJsonWebHandler ([#14](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/14)) -- [@p0p-x](https://github.com/p0p-x): ESP IDF Compatibility (added back CMakeLists.txt) ([#32](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/32)) -- [@tueddy](https://github.com/tueddy): Compile with Arduino 3 (ESP-IDF 5.1) ([#13](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/13)) -- [@vortigont](https://github.com/vortigont): Set real "Last-Modified" header based on file's LastWrite time ([#5](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/5)) -- [@vortigont](https://github.com/vortigont): Some websocket code cleanup ([#29](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/29)) -- [@vortigont](https://github.com/vortigont): Refactor code - replace DYI structs with STL objects ([#39](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/39)) - -## Documentation - -Usage and API stays the same as the original library. -Please look at the original libraries for more examples and documentation. - -- [https://github.com/me-no-dev/ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) (original library) -- [https://github.com/yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) (fork of the original library) - -## `AsyncWebSocketMessageBuffer` and `makeBuffer()` - -The fork from `yubox-node-org` introduces some breaking API changes compared to the original library, especially regarding the use of `std::shared_ptr>` for WebSocket. - -This fork is compatible with the original library from `me-no-dev` regarding WebSocket, and wraps the optimizations done by `yubox-node-org` in the `AsyncWebSocketMessageBuffer` class. -So you have the choice of which API to use. - -Here are examples for serializing a Json document in a websocket message buffer: - -```cpp -void send(JsonDocument& doc) { - const size_t len = measureJson(doc); - - // original API from me-no-dev - AsyncWebSocketMessageBuffer* buffer = _ws->makeBuffer(len); - assert(buffer); // up to you to keep or remove this - serializeJson(doc, buffer->get(), len); - _ws->textAll(buffer); -} -``` - -```cpp -void send(JsonDocument& doc) { - const size_t len = measureJson(doc); - - // this fork (originally from yubox-node-org), uses another API with shared pointer - auto buffer = std::make_shared>(len); - assert(buffer); // up to you to keep or remove this - serializeJson(doc, buffer->data(), len); - _ws->textAll(std::move(buffer)); -} -``` - -I recommend to use the official API `AsyncWebSocketMessageBuffer` to retain further compatibility. - -## Important recommendations - -Most of the crashes are caused by improper configuration of the library for the project. -Here are some recommendations to avoid them. - -1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` -2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`. - The default value of `16384` might be too much for your project. - You can look at the [MycilaTaskMonitor](https://oss.carbou.me/MycilaTaskMonitor) project to monitor the stack usage. -3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`. - Default is `10`. -4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`. - Default is `64`. -5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`. - Default is `5000`. - -I personally use the following configuration in my projects because my WS messages can be big (up to 4k). -If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128. - -```c++ - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 - -D WS_MAX_QUEUED_MESSAGES=64 -``` diff --git a/lib/ESPAsyncWebServer/examples/CaptivePortal/CaptivePortal.ino b/lib/ESPAsyncWebServer/examples/CaptivePortal/CaptivePortal.ino deleted file mode 100644 index 2d0de89..0000000 --- a/lib/ESPAsyncWebServer/examples/CaptivePortal/CaptivePortal.ino +++ /dev/null @@ -1,57 +0,0 @@ -#include -#ifdef ESP32 - #include - #include -#elif defined(ESP8266) - #include - #include -#elif defined(TARGET_RP2040) - #include - #include -#endif -#include "ESPAsyncWebServer.h" - -DNSServer dnsServer; -AsyncWebServer server(80); - -class CaptiveRequestHandler : public AsyncWebHandler { - public: - CaptiveRequestHandler() {} - virtual ~CaptiveRequestHandler() {} - - bool canHandle(__unused AsyncWebServerRequest* request) { - // request->addInterestingHeader("ANY"); - return true; - } - - void handleRequest(AsyncWebServerRequest* request) { - AsyncResponseStream* response = request->beginResponseStream("text/html"); - response->print("Captive Portal"); - response->print("

This is out captive portal front page.

"); - response->printf("

You were trying to reach: http://%s%s

", request->host().c_str(), request->url().c_str()); - response->printf("

Try opening this link instead

", WiFi.softAPIP().toString().c_str()); - response->print(""); - request->send(response); - } -}; - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println("Configuring access point..."); - - if (!WiFi.softAP("esp-captive")) { - Serial.println("Soft AP creation failed."); - while (1) - ; - } - - dnsServer.start(53, "*", WiFi.softAPIP()); - server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); // only when requested from AP - // more handlers... - server.begin(); -} - -void loop() { - dnsServer.processNextRequest(); -} diff --git a/lib/ESPAsyncWebServer/examples/Draft/Draft.ino b/lib/ESPAsyncWebServer/examples/Draft/Draft.ino deleted file mode 100644 index f10a9e7..0000000 --- a/lib/ESPAsyncWebServer/examples/Draft/Draft.ino +++ /dev/null @@ -1,37 +0,0 @@ -#include "mbedtls/md5.h" -#include -#include - -void setup() { - Serial.begin(115200); - delay(2000); - - const char* data = "Hello World"; - - { - uint8_t md5[16]; - mbedtls_md5_context _ctx; - mbedtls_md5_init(&_ctx); - mbedtls_md5_starts(&_ctx); - mbedtls_md5_update(&_ctx, (const unsigned char*)data, strlen(data)); - mbedtls_md5_finish(&_ctx, md5); - char output[33]; - for (int i = 0; i < 16; i++) { - sprintf_P(output + (i * 2), PSTR("%02x"), md5[i]); - } - Serial.println(String(output)); - } - - { - MD5Builder md5; - md5.begin(); - md5.add(data, strlen(data); - md5.calculate(); - char output[33]; - md5.getChars(output); - Serial.println(String(output)); - } -} - -void loop() { -} diff --git a/lib/ESPAsyncWebServer/examples/Filters/Filters.ino b/lib/ESPAsyncWebServer/examples/Filters/Filters.ino deleted file mode 100644 index f031a1f..0000000 --- a/lib/ESPAsyncWebServer/examples/Filters/Filters.ino +++ /dev/null @@ -1,111 +0,0 @@ -// Reproduced issue https://github.com/mathieucarbou/ESPAsyncWebServer/issues/26 - -#include -#ifdef ESP32 - #include - #include -#elif defined(ESP8266) - #include - #include -#elif defined(TARGET_RP2040) - #include - #include -#endif -#include "ESPAsyncWebServer.h" - -DNSServer dnsServer; -AsyncWebServer server(80); - -class CaptiveRequestHandler : public AsyncWebHandler { - public: - CaptiveRequestHandler() {} - virtual ~CaptiveRequestHandler() {} - - bool canHandle(__unused AsyncWebServerRequest* request) { - // request->addInterestingHeader("ANY"); - return true; - } - - void handleRequest(AsyncWebServerRequest* request) { - AsyncResponseStream* response = request->beginResponseStream("text/html"); - response->print("Captive Portal"); - response->print("

This is out captive portal front page.

"); - response->printf("

You were trying to reach: http://%s%s

", request->host().c_str(), request->url().c_str()); - response->printf("

Try opening this link instead

", WiFi.softAPIP().toString().c_str()); - response->print(""); - request->send(response); - } -}; - -bool hit1 = false; -bool hit2 = false; - -void setup() { - Serial.begin(115200); - - server - .on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - Serial.println("Captive portal request..."); - Serial.println("WiFi.localIP(): " + WiFi.localIP().toString()); - Serial.println("request->client()->localIP(): " + request->client()->localIP().toString()); -#if ESP_IDF_VERSION_MAJOR >= 5 - Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type())); - Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type())); -#endif - Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER"); - Serial.println(WiFi.localIP() == request->client()->localIP()); - Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString()); - request->send(200, "text/plain", "This is the captive portal"); - hit1 = true; - }) - .setFilter(ON_AP_FILTER); - - server - .on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - Serial.println("Website request..."); - Serial.println("WiFi.localIP(): " + WiFi.localIP().toString()); - Serial.println("request->client()->localIP(): " + request->client()->localIP().toString()); -#if ESP_IDF_VERSION_MAJOR >= 5 - Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type())); - Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type())); -#endif - Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER"); - Serial.println(WiFi.localIP() == request->client()->localIP()); - Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString()); - request->send(200, "text/plain", "This is the website"); - hit2 = true; - }) - .setFilter(ON_STA_FILTER); - - // assert(WiFi.softAP("esp-captive-portal")); - // dnsServer.start(53, "*", WiFi.softAPIP()); - // server.begin(); - // Serial.println("Captive portal started!"); - - // while (!hit1) { - // dnsServer.processNextRequest(); - // yield(); - // } - // delay(1000); // Wait for the client to process the response - - // Serial.println("Captive portal opened, stopping it and connecting to WiFi..."); - // dnsServer.stop(); - // WiFi.softAPdisconnect(); - - WiFi.persistent(false); - WiFi.begin("IoT"); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - } - Serial.println("Connected to WiFi with IP address: " + WiFi.localIP().toString()); - server.begin(); - - // while (!hit2) { - // delay(10); - // } - // delay(1000); // Wait for the client to process the response - // ESP.restart(); -} - -void loop() { -} diff --git a/lib/ESPAsyncWebServer/examples/SimpleServer/SimpleServer.ino b/lib/ESPAsyncWebServer/examples/SimpleServer/SimpleServer.ino deleted file mode 100644 index f3a1605..0000000 --- a/lib/ESPAsyncWebServer/examples/SimpleServer/SimpleServer.ino +++ /dev/null @@ -1,134 +0,0 @@ -// -// A simple server implementation showing how to: -// * serve static messages -// * read GET and POST parameters -// * handle missing pages / 404s -// - -#include -#ifdef ESP32 - #include - #include -#elif defined(ESP8266) - #include - #include -#elif defined(TARGET_RP2040) - #include - #include -#endif - -#include - -#include -#include -#include - -AsyncWebServer server(80); - -const char* PARAM_MESSAGE = "message"; - -void notFound(AsyncWebServerRequest* request) { - request->send(404, "text/plain", "Not found"); -} - -AsyncCallbackJsonWebHandler* jsonHandler = new AsyncCallbackJsonWebHandler("/json2"); -AsyncCallbackMessagePackWebHandler* msgPackHandler = new AsyncCallbackMessagePackWebHandler("/msgpack2"); - -void setup() { - - Serial.begin(115200); - - // WiFi.mode(WIFI_STA); - // WiFi.begin("YOUR_SSID", "YOUR_PASSWORD"); - // if (WiFi.waitForConnectResult() != WL_CONNECTED) { - // Serial.printf("WiFi Failed!\n"); - // return; - // } - // Serial.print("IP Address: "); - // Serial.println(WiFi.localIP()); - - WiFi.mode(WIFI_AP); - WiFi.softAP("esp-captive"); - - server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - request->send(200, "text/plain", "Hello, world"); - }); - - // Send a GET request to /get?message= - server.on("/get", HTTP_GET, [](AsyncWebServerRequest* request) { - String message; - if (request->hasParam(PARAM_MESSAGE)) { - message = request->getParam(PARAM_MESSAGE)->value(); - } else { - message = "No message sent"; - } - request->send(200, "text/plain", "Hello, GET: " + message); - }); - - // Send a POST request to /post with a form field message set to - server.on("/post", HTTP_POST, [](AsyncWebServerRequest* request) { - String message; - if (request->hasParam(PARAM_MESSAGE, true)) { - message = request->getParam(PARAM_MESSAGE, true)->value(); - } else { - message = "No message sent"; - } - request->send(200, "text/plain", "Hello, POST: " + message); - }); - - // JSON - - // receives JSON and sends JSON - jsonHandler->onRequest([](AsyncWebServerRequest* request, JsonVariant& json) { - JsonObject jsonObj = json.as(); - // ... - - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot().to(); - root["hello"] = "world"; - response->setLength(); - request->send(response); - }); - - // sends JSON - server.on("/json1", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot().to(); - root["hello"] = "world"; - response->setLength(); - request->send(response); - }); - - // MessagePack - - // receives MessagePack and sends MessagePack - msgPackHandler->onRequest([](AsyncWebServerRequest* request, JsonVariant& json) { - JsonObject jsonObj = json.as(); - // ... - - AsyncMessagePackResponse* response = new AsyncMessagePackResponse(); - JsonObject root = response->getRoot().to(); - root["hello"] = "world"; - response->setLength(); - request->send(response); - }); - - // sends MessagePack - server.on("/msgpack1", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncMessagePackResponse* response = new AsyncMessagePackResponse(); - JsonObject root = response->getRoot().to(); - root["hello"] = "world"; - response->setLength(); - request->send(response); - }); - - server.addHandler(jsonHandler); - server.addHandler(msgPackHandler); - - server.onNotFound(notFound); - - server.begin(); -} - -void loop() { -} \ No newline at end of file diff --git a/lib/ESPAsyncWebServer/examples/StreamFiles/StreamConcat.h b/lib/ESPAsyncWebServer/examples/StreamFiles/StreamConcat.h deleted file mode 100644 index c1e1927..0000000 --- a/lib/ESPAsyncWebServer/examples/StreamFiles/StreamConcat.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include - -class StreamConcat : public Stream { - public: - StreamConcat(Stream* s1, Stream* s2) : _s1(s1), _s2(s2) {} - - size_t write(__unused const uint8_t* p, __unused size_t n) override { return 0; } - size_t write(__unused uint8_t c) override { return 0; } - void flush() override {} - - int available() override { return _s1->available() + _s2->available(); } - - int read() override { - int c = _s1->read(); - return c != -1 ? c : _s2->read(); - } - -#if defined(TARGET_RP2040) - size_t readBytes(char* buffer, size_t length) { -#else - size_t readBytes(char* buffer, size_t length) override { -#endif - size_t count = _s1->readBytes(buffer, length); - return count > 0 ? count : _s2->readBytes(buffer, length); - } - - int peek() override { - int c = _s1->peek(); - return c != -1 ? c : _s2->peek(); - } - - private: - Stream* _s1; - Stream* _s2; -}; diff --git a/lib/ESPAsyncWebServer/examples/StreamFiles/StreamFiles.ino b/lib/ESPAsyncWebServer/examples/StreamFiles/StreamFiles.ino deleted file mode 100644 index 2a2c1b6..0000000 --- a/lib/ESPAsyncWebServer/examples/StreamFiles/StreamFiles.ino +++ /dev/null @@ -1,84 +0,0 @@ -#include -#include -#ifdef ESP32 - #include - #include -#elif defined(ESP8266) - #include - #include -#elif defined(TARGET_RP2040) - #include - #include -#endif -#include "StreamConcat.h" -#include "StreamString.h" -#include -#include - -DNSServer dnsServer; -AsyncWebServer server(80); - -void setup() { - Serial.begin(115200); - - LittleFS.begin(); - - WiFi.mode(WIFI_AP); - WiFi.softAP("esp-captive"); - dnsServer.start(53, "*", WiFi.softAPIP()); - - File file1 = LittleFS.open("/header.html", "w"); - file1.print("ESP Captive Portal"); - file1.close(); - - File file2 = LittleFS.open("/body.html", "w"); - file2.print("

Welcome to ESP Captive Portal

"); - file2.close(); - - File file3 = LittleFS.open("/footer.html", "w"); - file3.print(""); - file3.close(); - - server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - File header = LittleFS.open("/header.html", "r"); - File body = LittleFS.open("/body.html", "r"); - StreamConcat stream1(&header, &body); - - StreamString content; -#if defined(TARGET_RP2040) - content.printf("FreeHeap: %d", rp2040.getFreeHeap()); -#else - content.printf("FreeHeap: %" PRIu32, ESP.getFreeHeap()); -#endif - StreamConcat stream2 = StreamConcat(&stream1, &content); - - File footer = LittleFS.open("/footer.html", "r"); - StreamConcat stream3 = StreamConcat(&stream2, &footer); - - request->send(stream3, "text/html", stream3.available()); - header.close(); - body.close(); - footer.close(); - }); - - server.onNotFound([](AsyncWebServerRequest* request) { - request->send(404, "text/plain", "Not found"); - }); - - server.begin(); -} - -uint32_t last = 0; - -void loop() { - // dnsServer.processNextRequest(); - - if (millis() - last > 2000) { -#if defined(TARGET_RP2040) - Serial.printf("FreeHeap: %d", rp2040.getFreeHeap()); -#else - Serial.printf("FreeHeap: %" PRIu32, ESP.getFreeHeap()); -#endif - last = millis(); - } -} \ No newline at end of file diff --git a/lib/ESPAsyncWebServer/examples/StreamFiles/StreamString.h b/lib/ESPAsyncWebServer/examples/StreamFiles/StreamString.h deleted file mode 100644 index a6e0655..0000000 --- a/lib/ESPAsyncWebServer/examples/StreamFiles/StreamString.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include - -class StreamString : public Stream { - public: - size_t write(const uint8_t* p, size_t n) override { return _buffer.concat(reinterpret_cast(p), n) ? n : 0; } - size_t write(uint8_t c) override { return _buffer.concat(static_cast(c)) ? 1 : 0; } - void flush() override {} - - int available() override { return static_cast(_buffer.length()); } - - int read() override { - if (_buffer.length() == 0) - return -1; - char c = _buffer[0]; - _buffer.remove(0, 1); - return c; - } - -#if defined(TARGET_RP2040) - size_t readBytes(char* buffer, size_t length) { -#else - size_t readBytes(char* buffer, size_t length) override { -#endif - if (length > _buffer.length()) - length = _buffer.length(); - // Don't use _str.ToCharArray() because it inserts a terminator - memcpy(buffer, _buffer.c_str(), length); - _buffer.remove(0, static_cast(length)); - return length; - } - - int peek() override { return _buffer.length() > 0 ? _buffer[0] : -1; } - - const String& buffer() const { return _buffer; } - - private: - String _buffer; -}; diff --git a/lib/ESPAsyncWebServer/examples/issues/Issue14/Issue14.ino b/lib/ESPAsyncWebServer/examples/issues/Issue14/Issue14.ino deleted file mode 100644 index f62084a..0000000 --- a/lib/ESPAsyncWebServer/examples/issues/Issue14/Issue14.ino +++ /dev/null @@ -1,107 +0,0 @@ -#include -#ifdef ESP32 - #include - #include -#elif defined(ESP8266) - #include - #include -#elif defined(TARGET_RP2040) - #include - #include -#endif - -#include "ESPAsyncWebServer.h" - -const char appWebPage[] PROGMEM = R"rawliteral( - - - - -)rawliteral"; - -AsyncWebServer server(80); -AsyncEventSource events("/events"); - -const uint32_t interval = 1000; -const int button1Pin = 4; - -uint32_t lastSend = 0; - -void prepareJson(String& buffer) { - buffer.reserve(512); - buffer.concat("{\"button1\":"); - buffer.concat(digitalRead(button1Pin) == LOW); - buffer.concat(",\"1234567890abcdefghij1234567890abcdefghij1234567890abcdefghij1234567890abcdefghij1234567890abcdefghij1234567890abcdefghij\":"); - buffer.concat(random(0, 999999999)); - buffer.concat("}"); -} - -void setup() { - Serial.begin(115200); -#if ARDUINO_USB_CDC_ON_BOOT - Serial.setTxTimeoutMs(0); - delay(100); -#else - while (!Serial) - yield(); -#endif - - randomSeed(micros()); - - pinMode(button1Pin, OUTPUT); - digitalWrite(button1Pin, HIGH); - - WiFi.softAP("esp-captive"); - - server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - request->send(200, "text/html", appWebPage); - }); - - server.on("/button1", HTTP_GET, [](AsyncWebServerRequest* request) { - request->send(200, "text/plain", "OK"); - digitalWrite(button1Pin, digitalRead(button1Pin) == LOW ? HIGH : LOW); - - String buffer; - prepareJson(buffer); - ESP_LOGI("async_tcp", "Sending from handler..."); - events.send(buffer.c_str(), "state", millis()); - ESP_LOGI("async_tcp", "Sent from handler!"); - }); - - events.onConnect([](AsyncEventSourceClient* client) { - String buffer; - prepareJson(buffer); - ESP_LOGI("async_tcp", "Sending from onConnect..."); - client->send(buffer.c_str(), "state", millis(), 5000); - ESP_LOGI("async_tcp", "Sent from onConnect!"); - }); - - server.addHandler(&events); - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); - - server.begin(); -} - -void loop() { - if (millis() - lastSend >= interval) { - String buffer; - prepareJson(buffer); - ESP_LOGI("loop", "Sending..."); - events.send(buffer.c_str(), "state", millis()); - ESP_LOGI("loop", "Sent!"); - lastSend = millis(); - } -} diff --git a/lib/ESPAsyncWebServer/library.json b/lib/ESPAsyncWebServer/library.json deleted file mode 100644 index eb946c6..0000000 --- a/lib/ESPAsyncWebServer/library.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "ESPAsyncWebServer", - "version": "3.1.5", - "description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.", - "keywords": "http,async,websocket,webserver", - "homepage": "https://github.com/mathieucarbou/ESPAsyncWebServer", - "repository": { - "type": "git", - "url": "https://github.com/mathieucarbou/ESPAsyncWebServer.git" - }, - "authors": [ - { - "name": "Hristo Gochkov" - }, - { - "name": "Mathieu Carbou", - "maintainer": true - } - ], - "license": "LGPL-3.0", - "frameworks": "arduino", - "platforms": [ - "espressif32", - "espressif8266", - "raspberrypi" - ], - "dependencies": [ - { - "owner": "mathieucarbou", - "name": "AsyncTCP", - "version": "^3.2.4", - "platforms": "espressif32" - }, - { - "owner": "esphome", - "name": "ESPAsyncTCP-esphome", - "version": "^2.0.0", - "platforms": "espressif8266" - }, - { - "name": "Hash", - "platforms": "espressif8266" - }, - { - "owner": "khoih-prog", - "name": "AsyncTCP_RP2040W", - "version": "^1.2.0", - "platforms": "raspberrypi" - } - ], - "export": { - "include": [ - "examples", - "src", - "library.json", - "library.properties", - "LICENSE", - "README.md" - ] - }, - "build": { - "libCompatMode": "strict" - } -} \ No newline at end of file diff --git a/lib/ESPAsyncWebServer/library.properties b/lib/ESPAsyncWebServer/library.properties deleted file mode 100644 index 3036044..0000000 --- a/lib/ESPAsyncWebServer/library.properties +++ /dev/null @@ -1,10 +0,0 @@ -name=ESPAsyncWebServer -version=3.1.5 -author=Me-No-Dev -maintainer=Mathieu Carbou -sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 -paragraph=Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc -category=Other -url=https://github.com/mathieucarbou/ESPAsyncWebServer -architectures=* -license=LGPL-3.0 diff --git a/lib/ESPAsyncWebServer/platformio.ini b/lib/ESPAsyncWebServer/platformio.ini deleted file mode 100644 index 6f68d0d..0000000 --- a/lib/ESPAsyncWebServer/platformio.ini +++ /dev/null @@ -1,80 +0,0 @@ -[env] -framework = arduino -build_flags = - -Wall -Wextra - -D CONFIG_ARDUHAL_LOG_COLORS - -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 -upload_protocol = esptool -monitor_speed = 115200 -monitor_filters = esp32_exception_decoder, log2file - -[platformio] -lib_dir = . -; src_dir = examples/CaptivePortal -src_dir = examples/SimpleServer -; src_dir = examples/StreamFiles -; src_dir = examples/Filters -; src_dir = examples/Draft -; src_dir = examples/issues/Issue14 - -[env:arduino] -platform = espressif32 -board = esp32dev -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.4 - -[env:arduino-2] -platform = espressif32@6.8.1 -board = esp32dev -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.4 - -[env:arduino-3] -platform = espressif32 -platform_packages= - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.4 - platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.4/esp32-arduino-libs-3.0.4.zip -board = esp32dev -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.4 - -[env:esp8266] -platform = espressif8266 -board = huzzah -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - esphome/ESPAsyncTCP-esphome @ 2.0.0 - -; PlatformIO support for Raspberry Pi Pico is not official -; https://github.com/platformio/platform-raspberrypi/pull/36 -; https://github.com/earlephilhower/arduino-pico/blob/master/docs/platformio.rst -; board settings: https://github.com/earlephilhower/arduino-pico/blob/master/tools/json/rpipico.json -[env:rpipicow] -upload_protocol = picotool -platform = https://github.com/maxgerhardt/platform-raspberrypi.git -board = rpipicow -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - khoih-prog/AsyncTCP_RP2040W @ 1.2.0 - -[env:pioarduino-esp32dev] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip -board = esp32dev -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.4 - -[env:pioarduino-c6] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip -board = esp32-c6-devkitc-1 -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.4 diff --git a/lib/ESPAsyncWebServer/src/AsyncEventSource.cpp b/lib/ESPAsyncWebServer/src/AsyncEventSource.cpp deleted file mode 100644 index 639fd56..0000000 --- a/lib/ESPAsyncWebServer/src/AsyncEventSource.cpp +++ /dev/null @@ -1,405 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "Arduino.h" -#if defined(ESP32) - #include -#endif -#include "AsyncEventSource.h" -#include "literals.h" - -using namespace asyncsrv; - -static String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect) { - String ev; - - if (reconnect) { - ev += T_retry_; - ev += reconnect; - ev += T_rn; - } - - if (id) { - ev += T_id__; - ev += id; - ev += T_rn; - } - - if (event != NULL) { - ev += T_event_; - ev += event; - ev += T_rn; - } - - if (message != NULL) { - size_t messageLen = strlen(message); - char* lineStart = (char*)message; - char* lineEnd; - do { - char* nextN = strchr(lineStart, '\n'); - char* nextR = strchr(lineStart, '\r'); - if (nextN == NULL && nextR == NULL) { - size_t llen = ((char*)message + messageLen) - lineStart; - char* ldata = (char*)malloc(llen + 1); - if (ldata != NULL) { - memcpy(ldata, lineStart, llen); - ldata[llen] = 0; - ev += T_data_; - ev += ldata; - ev += T_rnrn; - free(ldata); - } - lineStart = (char*)message + messageLen; - } else { - char* nextLine = NULL; - if (nextN != NULL && nextR != NULL) { - if (nextR < nextN) { - lineEnd = nextR; - if (nextN == (nextR + 1)) - nextLine = nextN + 1; - else - nextLine = nextR + 1; - } else { - lineEnd = nextN; - if (nextR == (nextN + 1)) - nextLine = nextR + 1; - else - nextLine = nextN + 1; - } - } else if (nextN != NULL) { - lineEnd = nextN; - nextLine = nextN + 1; - } else { - lineEnd = nextR; - nextLine = nextR + 1; - } - - size_t llen = lineEnd - lineStart; - char* ldata = (char*)malloc(llen + 1); - if (ldata != NULL) { - memcpy(ldata, lineStart, llen); - ldata[llen] = 0; - ev += T_data_; - ev += ldata; - ev += T_rn; - free(ldata); - } - lineStart = nextLine; - if (lineStart == ((char*)message + messageLen)) - ev += T_rn; - } - } while (lineStart < ((char*)message + messageLen)); - } - - return ev; -} - -// Message - -AsyncEventSourceMessage::AsyncEventSourceMessage(const char* data, size_t len) - : _data(nullptr), _len(len), _sent(0), _acked(0) { - _data = (uint8_t*)malloc(_len + 1); - if (_data == nullptr) { - _len = 0; - } else { - memcpy(_data, data, len); - _data[_len] = 0; - } -} - -AsyncEventSourceMessage::~AsyncEventSourceMessage() { - if (_data != NULL) - free(_data); -} - -size_t AsyncEventSourceMessage::ack(size_t len, uint32_t time) { - (void)time; - // If the whole message is now acked... - if (_acked + len > _len) { - // Return the number of extra bytes acked (they will be carried on to the next message) - const size_t extra = _acked + len - _len; - _acked = _len; - return extra; - } - // Return that no extra bytes left. - _acked += len; - return 0; -} - -// This could also return void as the return value is not used. -// Leaving as-is for compatibility... -size_t AsyncEventSourceMessage::send(AsyncClient* client) { - if (_sent >= _len) { - return 0; - } - const size_t len_to_send = _len - _sent; - auto position = reinterpret_cast(_data + _sent); - const size_t sent_now = client->write(position, len_to_send); - _sent += sent_now; - return sent_now; -} - -// Client - -AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server) { - _client = request->client(); - _server = server; - _lastId = 0; - if (request->hasHeader(T_Last_Event_ID)) - _lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str()); - - _client->setRxTimeout(0); - _client->onError(NULL, NULL); - _client->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; ((AsyncEventSourceClient*)(r))->_onAck(len, time); }, this); - _client->onPoll([](void* r, AsyncClient* c) { (void)c; ((AsyncEventSourceClient*)(r))->_onPoll(); }, this); - _client->onData(NULL, NULL); - _client->onTimeout([this](void* r, AsyncClient* c __attribute__((unused)), uint32_t time) { ((AsyncEventSourceClient*)(r))->_onTimeout(time); }, this); - _client->onDisconnect([this](void* r, AsyncClient* c) { ((AsyncEventSourceClient*)(r))->_onDisconnect(); delete c; }, this); - - _server->_addClient(this); - delete request; -} - -AsyncEventSourceClient::~AsyncEventSourceClient() { -#ifdef ESP32 - std::lock_guard lock(_lockmq); -#endif - _messageQueue.clear(); - close(); -} - -void AsyncEventSourceClient::_queueMessage(const char* message, size_t len) { -#ifdef ESP32 - // length() is not thread-safe, thus acquiring the lock before this call.. - std::lock_guard lock(_lockmq); -#endif - - if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) { -#ifdef ESP8266 - ets_printf(String(F("ERROR: Too many messages queued\n")).c_str()); -#elif defined(ESP32) - log_e("Too many messages queued: deleting message"); -#endif - return; - } - - _messageQueue.emplace_back(message, len); - // runqueue trigger when new messages added - if (_client->canSend()) { - _runQueue(); - } -} - -void AsyncEventSourceClient::_onAck(size_t len, uint32_t time) { -#ifdef ESP32 - // Same here, acquiring the lock early - std::lock_guard lock(_lockmq); -#endif - while (len && _messageQueue.size()) { - len = _messageQueue.front().ack(len, time); - if (_messageQueue.front().finished()) - _messageQueue.pop_front(); - } - _runQueue(); -} - -void AsyncEventSourceClient::_onPoll() { -#ifdef ESP32 - // Same here, acquiring the lock early - std::lock_guard lock(_lockmq); -#endif - if (_messageQueue.size()) { - _runQueue(); - } -} - -void AsyncEventSourceClient::_onTimeout(uint32_t time __attribute__((unused))) { - _client->close(true); -} - -void AsyncEventSourceClient::_onDisconnect() { - _client = NULL; - _server->_handleDisconnect(this); -} - -void AsyncEventSourceClient::close() { - if (_client != NULL) - _client->close(); -} - -void AsyncEventSourceClient::write(const char* message, size_t len) { - if (!connected()) - return; - _queueMessage(message, len); -} - -void AsyncEventSourceClient::send(const char* message, const char* event, uint32_t id, uint32_t reconnect) { - if (!connected()) - return; - String ev = generateEventMessage(message, event, id, reconnect); - _queueMessage(ev.c_str(), ev.length()); -} - -size_t AsyncEventSourceClient::packetsWaiting() const { -#ifdef ESP32 - std::lock_guard lock(_lockmq); -#endif - return _messageQueue.size(); -} - -void AsyncEventSourceClient::_runQueue() { - // Calls to this private method now already protected by _lockmq acquisition - // so no extra call of _lockmq.lock() here.. - for (auto& i : _messageQueue) { - if (!i.sent()) - i.send(_client); - } -} - -// Handler -void AsyncEventSource::onConnect(ArEventHandlerFunction cb) { - _connectcb = cb; -} - -void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb) { - _authorizeConnectHandler = cb; -} - -void AsyncEventSource::_addClient(AsyncEventSourceClient* client) { - if (!client) - return; -#ifdef ESP32 - std::lock_guard lock(_client_queue_lock); -#endif - _clients.emplace_back(client); - if (_connectcb) - _connectcb(client); -} - -void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient* client) { -#ifdef ESP32 - std::lock_guard lock(_client_queue_lock); -#endif - for (auto i = _clients.begin(); i != _clients.end(); ++i) { - if (i->get() == client) - _clients.erase(i); - } -} - -void AsyncEventSource::close() { - // While the whole loop is not done, the linked list is locked and so the - // iterator should remain valid even when AsyncEventSource::_handleDisconnect() - // is called very early -#ifdef ESP32 - std::lock_guard lock(_client_queue_lock); -#endif - for (const auto& c : _clients) { - if (c->connected()) - c->close(); - } -} - -// pmb fix -size_t AsyncEventSource::avgPacketsWaiting() const { - size_t aql = 0; - uint32_t nConnectedClients = 0; -#ifdef ESP32 - std::lock_guard lock(_client_queue_lock); -#endif - if (!_clients.size()) - return 0; - - for (const auto& c : _clients) { - if (c->connected()) { - aql += c->packetsWaiting(); - ++nConnectedClients; - } - } - return ((aql) + (nConnectedClients / 2)) / (nConnectedClients); // round up -} - -void AsyncEventSource::send( - const char* message, const char* event, uint32_t id, uint32_t reconnect) { - String ev = generateEventMessage(message, event, id, reconnect); -#ifdef ESP32 - std::lock_guard lock(_client_queue_lock); -#endif - for (const auto& c : _clients) { - if (c->connected()) { - c->write(ev.c_str(), ev.length()); - } - } -} - -size_t AsyncEventSource::count() const { -#ifdef ESP32 - std::lock_guard lock(_client_queue_lock); -#endif - size_t n_clients{0}; - for (const auto& i : _clients) - if (i->connected()) - ++n_clients; - - return n_clients; -} - -bool AsyncEventSource::canHandle(AsyncWebServerRequest* request) { - if (request->method() != HTTP_GET || !request->url().equals(_url)) { - return false; - } - request->addInterestingHeader(T_Last_Event_ID); - request->addInterestingHeader(T_Cookie); - return true; -} - -void AsyncEventSource::handleRequest(AsyncWebServerRequest* request) { - if ((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())) { - return request->requestAuthentication(); - } - if (_authorizeConnectHandler != NULL) { - if (!_authorizeConnectHandler(request)) { - return request->send(401); - } - } - request->send(new AsyncEventSourceResponse(this)); -} - -// Response - -AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource* server) { - _server = server; - _code = 200; - _contentType = T_text_event_stream; - _sendContentLength = false; - addHeader(T_Cache_Control, T_no_cache); - addHeader(T_Connection, T_keep_alive); -} - -void AsyncEventSourceResponse::_respond(AsyncWebServerRequest* request) { - String out = _assembleHead(request->version()); - request->client()->write(out.c_str(), _headLength); - _state = RESPONSE_WAIT_ACK; -} - -size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest* request, size_t len, uint32_t time __attribute__((unused))) { - if (len) { - new AsyncEventSourceClient(request, _server); - } - return 0; -} diff --git a/lib/ESPAsyncWebServer/src/AsyncEventSource.h b/lib/ESPAsyncWebServer/src/AsyncEventSource.h deleted file mode 100644 index 0289ebf..0000000 --- a/lib/ESPAsyncWebServer/src/AsyncEventSource.h +++ /dev/null @@ -1,158 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#ifndef ASYNCEVENTSOURCE_H_ -#define ASYNCEVENTSOURCE_H_ - -#include -#include -#ifdef ESP32 - #include - #include - #ifndef SSE_MAX_QUEUED_MESSAGES - #define SSE_MAX_QUEUED_MESSAGES 32 - #endif -#elif defined(ESP8266) - #include - #ifndef SSE_MAX_QUEUED_MESSAGES - #define SSE_MAX_QUEUED_MESSAGES 8 - #endif -#elif defined(TARGET_RP2040) - #include - #ifndef SSE_MAX_QUEUED_MESSAGES - #define SSE_MAX_QUEUED_MESSAGES 32 - #endif -#endif - -#include - -#ifdef ESP8266 - #include - #ifdef CRYPTO_HASH_h // include Hash.h from espressif framework if the first include was from the crypto library - #include <../src/Hash.h> - #endif -#endif - -#ifndef DEFAULT_MAX_SSE_CLIENTS - #ifdef ESP32 - #define DEFAULT_MAX_SSE_CLIENTS 8 - #else - #define DEFAULT_MAX_SSE_CLIENTS 4 - #endif -#endif - -class AsyncEventSource; -class AsyncEventSourceResponse; -class AsyncEventSourceClient; -using ArEventHandlerFunction = std::function; -using ArAuthorizeConnectHandler = std::function; - -class AsyncEventSourceMessage { - private: - uint8_t* _data; - size_t _len; - size_t _sent; - // size_t _ack; - size_t _acked; - - public: - AsyncEventSourceMessage(const char* data, size_t len); - ~AsyncEventSourceMessage(); - size_t ack(size_t len, uint32_t time __attribute__((unused))); - size_t send(AsyncClient* client); - bool finished() { return _acked == _len; } - bool sent() { return _sent == _len; } -}; - -class AsyncEventSourceClient { - private: - AsyncClient* _client; - AsyncEventSource* _server; - uint32_t _lastId; - std::list _messageQueue; -#ifdef ESP32 - mutable std::mutex _lockmq; -#endif - void _queueMessage(const char* message, size_t len); - void _runQueue(); - - public: - AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server); - ~AsyncEventSourceClient(); - - AsyncClient* client() { return _client; } - void close(); - void write(const char* message, size_t len); - void send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0); - bool connected() const { return (_client != NULL) && _client->connected(); } - uint32_t lastId() const { return _lastId; } - size_t packetsWaiting() const; - - // system callbacks (do not call) - void _onAck(size_t len, uint32_t time); - void _onPoll(); - void _onTimeout(uint32_t time); - void _onDisconnect(); -}; - -class AsyncEventSource : public AsyncWebHandler { - private: - String _url; - std::list> _clients; -#ifdef ESP32 - // Same as for individual messages, protect mutations of _clients list - // since simultaneous access from different tasks is possible - mutable std::mutex _client_queue_lock; -#endif - ArEventHandlerFunction _connectcb{nullptr}; - ArAuthorizeConnectHandler _authorizeConnectHandler; - - public: - AsyncEventSource(const String& url) : _url(url){}; - ~AsyncEventSource() { close(); }; - - const char* url() const { return _url.c_str(); } - void close(); - void onConnect(ArEventHandlerFunction cb); - void authorizeConnect(ArAuthorizeConnectHandler cb); - void send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0); - // number of clients connected - size_t count() const; - size_t avgPacketsWaiting() const; - - // system callbacks (do not call) - void _addClient(AsyncEventSourceClient* client); - void _handleDisconnect(AsyncEventSourceClient* client); - virtual bool canHandle(AsyncWebServerRequest* request) override final; - virtual void handleRequest(AsyncWebServerRequest* request) override final; -}; - -class AsyncEventSourceResponse : public AsyncWebServerResponse { - private: - String _content; - AsyncEventSource* _server; - - public: - AsyncEventSourceResponse(AsyncEventSource* server); - void _respond(AsyncWebServerRequest* request); - size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time); - bool _sourceValid() const { return true; } -}; - -#endif /* ASYNCEVENTSOURCE_H_ */ diff --git a/lib/ESPAsyncWebServer/src/AsyncJson.h b/lib/ESPAsyncWebServer/src/AsyncJson.h deleted file mode 100644 index bca3f24..0000000 --- a/lib/ESPAsyncWebServer/src/AsyncJson.h +++ /dev/null @@ -1,255 +0,0 @@ -// AsyncJson.h -/* - Async Response to use with ArduinoJson and AsyncWebServer - Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon. - - Example of callback in use - - server.on("/json", HTTP_ANY, [](AsyncWebServerRequest * request) { - - AsyncJsonResponse * response = new AsyncJsonResponse(); - JsonObject& root = response->getRoot(); - root["key1"] = "key number one"; - JsonObject& nested = root.createNestedObject("nested"); - nested["key1"] = "key number one"; - - response->setLength(); - request->send(response); - }); - - -------------------- - - Async Request to use with ArduinoJson and AsyncWebServer - Written by Arsène von Wyss (avonwyss) - - Example - - AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler("/rest/endpoint"); - handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) { - JsonObject jsonObj = json.as(); - // ... - }); - server.addHandler(handler); - -*/ -#ifndef ASYNC_JSON_H_ -#define ASYNC_JSON_H_ -#include -#include - -#include "ChunkPrint.h" - -#if ARDUINOJSON_VERSION_MAJOR == 6 - #ifndef DYNAMIC_JSON_DOCUMENT_SIZE - #define DYNAMIC_JSON_DOCUMENT_SIZE 1024 - #endif -#endif - -constexpr const char* JSON_MIMETYPE = "application/json"; - -/* - * Json Response - * */ - -class AsyncJsonResponse : public AsyncAbstractResponse { - protected: -#if ARDUINOJSON_VERSION_MAJOR == 5 - DynamicJsonBuffer _jsonBuffer; -#elif ARDUINOJSON_VERSION_MAJOR == 6 - DynamicJsonDocument _jsonBuffer; -#else - JsonDocument _jsonBuffer; -#endif - - JsonVariant _root; - bool _isValid; - - public: -#if ARDUINOJSON_VERSION_MAJOR == 5 - AsyncJsonResponse(bool isArray = false) : _isValid{false} { - _code = 200; - _contentType = JSON_MIMETYPE; - if (isArray) - _root = _jsonBuffer.createArray(); - else - _root = _jsonBuffer.createObject(); - } -#elif ARDUINOJSON_VERSION_MAJOR == 6 - AsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { - _code = 200; - _contentType = JSON_MIMETYPE; - if (isArray) - _root = _jsonBuffer.createNestedArray(); - else - _root = _jsonBuffer.createNestedObject(); - } -#else - AsyncJsonResponse(bool isArray = false) : _isValid{false} { - _code = 200; - _contentType = JSON_MIMETYPE; - if (isArray) - _root = _jsonBuffer.add(); - else - _root = _jsonBuffer.add(); - } -#endif - - JsonVariant& getRoot() { return _root; } - bool _sourceValid() const { return _isValid; } - size_t setLength() { - -#if ARDUINOJSON_VERSION_MAJOR == 5 - _contentLength = _root.measureLength(); -#else - _contentLength = measureJson(_root); -#endif - - if (_contentLength) { - _isValid = true; - } - return _contentLength; - } - - size_t getSize() const { return _jsonBuffer.size(); } - -#if ARDUINOJSON_VERSION_MAJOR >= 6 - bool overflowed() const { return _jsonBuffer.overflowed(); } -#endif - - size_t _fillBuffer(uint8_t* data, size_t len) { - ChunkPrint dest(data, _sentLength, len); - -#if ARDUINOJSON_VERSION_MAJOR == 5 - _root.printTo(dest); -#else - serializeJson(_root, dest); -#endif - return len; - } -}; - -class PrettyAsyncJsonResponse : public AsyncJsonResponse { - public: -#if ARDUINOJSON_VERSION_MAJOR == 6 - PrettyAsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : AsyncJsonResponse{isArray, maxJsonBufferSize} {} -#else - PrettyAsyncJsonResponse(bool isArray = false) : AsyncJsonResponse{isArray} {} -#endif - size_t setLength() { -#if ARDUINOJSON_VERSION_MAJOR == 5 - _contentLength = _root.measurePrettyLength(); -#else - _contentLength = measureJsonPretty(_root); -#endif - if (_contentLength) { - _isValid = true; - } - return _contentLength; - } - size_t _fillBuffer(uint8_t* data, size_t len) { - ChunkPrint dest(data, _sentLength, len); -#if ARDUINOJSON_VERSION_MAJOR == 5 - _root.prettyPrintTo(dest); -#else - serializeJsonPretty(_root, dest); -#endif - return len; - } -}; - -typedef std::function ArJsonRequestHandlerFunction; - -class AsyncCallbackJsonWebHandler : public AsyncWebHandler { - private: - protected: - const String _uri; - WebRequestMethodComposite _method; - ArJsonRequestHandlerFunction _onRequest; - size_t _contentLength; -#if ARDUINOJSON_VERSION_MAJOR == 6 - const size_t maxJsonBufferSize; -#endif - size_t _maxContentLength; - - public: -#if ARDUINOJSON_VERSION_MAJOR == 6 - AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {} -#else - AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} -#endif - - void setMethod(WebRequestMethodComposite method) { _method = method; } - void setMaxContentLength(int maxContentLength) { _maxContentLength = maxContentLength; } - void onRequest(ArJsonRequestHandlerFunction fn) { _onRequest = fn; } - - virtual bool canHandle(AsyncWebServerRequest* request) override final { - if (!_onRequest) - return false; - - WebRequestMethodComposite request_method = request->method(); - if (!(_method & request_method)) - return false; - - if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) - return false; - - if (request_method != HTTP_GET && !request->contentType().equalsIgnoreCase(JSON_MIMETYPE)) - return false; - - request->addInterestingHeader("ANY"); - return true; - } - - virtual void handleRequest(AsyncWebServerRequest* request) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onRequest) { - if (request->method() == HTTP_GET) { - JsonVariant json; - _onRequest(request, json); - return; - } else if (request->_tempObject != NULL) { - -#if ARDUINOJSON_VERSION_MAJOR == 5 - DynamicJsonBuffer jsonBuffer; - JsonVariant json = jsonBuffer.parse((uint8_t*)(request->_tempObject)); - if (json.success()) { -#elif ARDUINOJSON_VERSION_MAJOR == 6 - DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); -#else - JsonDocument jsonBuffer; - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); -#endif - - _onRequest(request, json); - return; - } - } - request->send(_contentLength > _maxContentLength ? 413 : 400); - } else { - request->send(500); - } - } - virtual void handleUpload(__unused AsyncWebServerRequest* request, __unused const String& filename, __unused size_t index, __unused uint8_t* data, __unused size_t len, __unused bool final) override final { - } - virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final { - if (_onRequest) { - _contentLength = total; - if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { - request->_tempObject = malloc(total); - } - if (request->_tempObject != NULL) { - memcpy((uint8_t*)(request->_tempObject) + index, data, len); - } - } - } - virtual bool isRequestHandlerTrivial() override final { return _onRequest ? false : true; } -}; -#endif diff --git a/lib/ESPAsyncWebServer/src/AsyncMessagePack.h b/lib/ESPAsyncWebServer/src/AsyncMessagePack.h deleted file mode 100644 index 57a8824..0000000 --- a/lib/ESPAsyncWebServer/src/AsyncMessagePack.h +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once - -/* - server.on("/msg_pack", HTTP_ANY, [](AsyncWebServerRequest * request) { - AsyncMessagePackResponse * response = new AsyncMessagePackResponse(); - JsonObject& root = response->getRoot(); - root["key1"] = "key number one"; - JsonObject& nested = root.createNestedObject("nested"); - nested["key1"] = "key number one"; - response->setLength(); - request->send(response); - }); - - -------------------- - - AsyncCallbackMessagePackWebHandler* handler = new AsyncCallbackMessagePackWebHandler("/msg_pack/endpoint"); - handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) { - JsonObject jsonObj = json.as(); - // ... - }); - server.addHandler(handler); -*/ - -#include -#include - -#include "ChunkPrint.h" -#include "literals.h" - -class AsyncMessagePackResponse : public AsyncAbstractResponse { - protected: - JsonDocument _jsonBuffer; - JsonVariant _root; - bool _isValid; - - public: - AsyncMessagePackResponse(bool isArray = false) : _isValid{false} { - _code = 200; - _contentType = asyncsrv::T_application_msgpack; - if (isArray) - _root = _jsonBuffer.add(); - else - _root = _jsonBuffer.add(); - } - - JsonVariant& getRoot() { return _root; } - - bool _sourceValid() const { return _isValid; } - - size_t setLength() { - _contentLength = measureMsgPack(_root); - if (_contentLength) { - _isValid = true; - } - return _contentLength; - } - - size_t getSize() const { return _jsonBuffer.size(); } - - size_t _fillBuffer(uint8_t* data, size_t len) { - ChunkPrint dest(data, _sentLength, len); - serializeMsgPack(_root, dest); - return len; - } -}; - -class AsyncCallbackMessagePackWebHandler : public AsyncWebHandler { - public: - typedef std::function ArJsonRequestHandlerFunction; - - protected: - const String _uri; - WebRequestMethodComposite _method; - ArJsonRequestHandlerFunction _onRequest; - size_t _contentLength; - size_t _maxContentLength; - - public: - AsyncCallbackMessagePackWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} - - void setMethod(WebRequestMethodComposite method) { _method = method; } - void setMaxContentLength(int maxContentLength) { _maxContentLength = maxContentLength; } - void onRequest(ArJsonRequestHandlerFunction fn) { _onRequest = fn; } - - virtual bool canHandle(AsyncWebServerRequest* request) override final { - if (!_onRequest) - return false; - - WebRequestMethodComposite request_method = request->method(); - if (!(_method & request_method)) - return false; - - if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) - return false; - - if (request_method != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack)) - return false; - - request->addInterestingHeader("ANY"); - return true; - } - - virtual void handleRequest(AsyncWebServerRequest* request) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - - if (_onRequest) { - if (request->method() == HTTP_GET) { - JsonVariant json; - _onRequest(request, json); - return; - - } else if (request->_tempObject != NULL) { - JsonDocument jsonBuffer; - DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t*)(request->_tempObject)); - - if (!error) { - JsonVariant json = jsonBuffer.as(); - _onRequest(request, json); - return; - } - } - request->send(_contentLength > _maxContentLength ? 413 : 400); - } else { - request->send(500); - } - } - - virtual void handleUpload(__unused AsyncWebServerRequest* request, __unused const String& filename, __unused size_t index, __unused uint8_t* data, __unused size_t len, __unused bool final) override final {} - - virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final { - if (_onRequest) { - _contentLength = total; - if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { - request->_tempObject = malloc(total); - } - if (request->_tempObject != NULL) { - memcpy((uint8_t*)(request->_tempObject) + index, data, len); - } - } - } - - virtual bool isRequestHandlerTrivial() override final { return _onRequest ? false : true; } -}; diff --git a/lib/ESPAsyncWebServer/src/AsyncWebSocket.cpp b/lib/ESPAsyncWebServer/src/AsyncWebSocket.cpp deleted file mode 100644 index 88d88ec..0000000 --- a/lib/ESPAsyncWebServer/src/AsyncWebSocket.cpp +++ /dev/null @@ -1,1207 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "AsyncWebSocket.h" -#include "Arduino.h" - -#include - -#include - -#if defined(ESP32) - #if ESP_IDF_VERSION_MAJOR < 5 - #include "./port/SHA1Builder.h" - #else - #include - #endif - #include -#elif defined(TARGET_RP2040) || defined(ESP8266) - #include -#endif - -#define MAX_PRINTF_LEN 64 - -using namespace asyncsrv; - - -size_t webSocketSendFrameWindow(AsyncClient* client) { - if (!client->canSend()) - return 0; - size_t space = client->space(); - if (space < 9) - return 0; - return space - 8; -} - -size_t webSocketSendFrame(AsyncClient* client, bool final, uint8_t opcode, bool mask, uint8_t* data, size_t len) { - if (!client->canSend()) { - // Serial.println("SF 1"); - return 0; - } - size_t space = client->space(); - if (space < 2) { - // Serial.println("SF 2"); - return 0; - } - uint8_t mbuf[4] = {0, 0, 0, 0}; - uint8_t headLen = 2; - if (len && mask) { - headLen += 4; - mbuf[0] = rand() % 0xFF; - mbuf[1] = rand() % 0xFF; - mbuf[2] = rand() % 0xFF; - mbuf[3] = rand() % 0xFF; - } - if (len > 125) - headLen += 2; - if (space < headLen) { - // Serial.println("SF 2"); - return 0; - } - space -= headLen; - - if (len > space) - len = space; - - uint8_t* buf = (uint8_t*)malloc(headLen); - if (buf == NULL) { - // os_printf("could not malloc %u bytes for frame header\n", headLen); - // Serial.println("SF 3"); - return 0; - } - - buf[0] = opcode & 0x0F; - if (final) - buf[0] |= 0x80; - if (len < 126) - buf[1] = len & 0x7F; - else { - buf[1] = 126; - buf[2] = (uint8_t)((len >> 8) & 0xFF); - buf[3] = (uint8_t)(len & 0xFF); - } - if (len && mask) { - buf[1] |= 0x80; - memcpy(buf + (headLen - 4), mbuf, 4); - } - if (client->add((const char*)buf, headLen) != headLen) { - // os_printf("error adding %lu header bytes\n", headLen); - free(buf); - // Serial.println("SF 4"); - return 0; - } - free(buf); - - if (len) { - if (len && mask) { - size_t i; - for (i = 0; i < len; i++) - data[i] = data[i] ^ mbuf[i % 4]; - } - if (client->add((const char*)data, len) != len) { - // os_printf("error adding %lu data bytes\n", len); - // Serial.println("SF 5"); - return 0; - } - } - if (!client->send()) { - // os_printf("error sending frame: %lu\n", headLen+len); - // Serial.println("SF 6"); - return 0; - } - // Serial.println("SF"); - return len; -} - -/* - * AsyncWebSocketMessageBuffer - */ - -AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(const uint8_t* data, size_t size) - : _buffer(std::make_shared>(size)) { - if (_buffer->capacity() < size) { - _buffer->reserve(size); - } else { - std::memcpy(_buffer->data(), data, size); - } -} - -AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(size_t size) - : _buffer(std::make_shared>(size)) { - if (_buffer->capacity() < size) { - _buffer->reserve(size); - } -} - -bool AsyncWebSocketMessageBuffer::reserve(size_t size) { - if (_buffer->capacity() >= size) - return true; - _buffer->reserve(size); - return _buffer->capacity() >= size; -} - -/* - * Control Frame - */ - -class AsyncWebSocketControl { - private: - uint8_t _opcode; - uint8_t* _data; - size_t _len; - bool _mask; - bool _finished; - - public: - AsyncWebSocketControl(uint8_t opcode, const uint8_t* data = NULL, size_t len = 0, bool mask = false) - : _opcode(opcode), _len(len), _mask(len && mask), _finished(false) { - if (data == NULL) - _len = 0; - if (_len) { - if (_len > 125) - _len = 125; - - _data = (uint8_t*)malloc(_len); - - if (_data == NULL) - _len = 0; - else - memcpy(_data, data, len); - } else - _data = NULL; - } - - virtual ~AsyncWebSocketControl() { - if (_data != NULL) - free(_data); - } - - virtual bool finished() const { return _finished; } - uint8_t opcode() { return _opcode; } - uint8_t len() { return _len + 2; } - size_t send(AsyncClient* client) { - _finished = true; - return webSocketSendFrame(client, true, _opcode & 0x0F, _mask, _data, _len); - } -}; - -/* - * AsyncWebSocketMessage Message - */ - -AsyncWebSocketMessage::AsyncWebSocketMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode, bool mask) : _WSbuffer{buffer}, - _opcode(opcode & 0x07), - _mask{mask}, - _status{_WSbuffer ? WS_MSG_SENDING : WS_MSG_ERROR} { -} - -void AsyncWebSocketMessage::ack(size_t len, uint32_t time) { - (void)time; - _acked += len; - if (_sent >= _WSbuffer->size() && _acked >= _ack) { - _status = WS_MSG_SENT; - } - // ets_printf("A: %u\n", len); -} - -size_t AsyncWebSocketMessage::send(AsyncClient* client) { - if (_status != WS_MSG_SENDING) - return 0; - if (_acked < _ack) { - return 0; - } - if (_sent == _WSbuffer->size()) { - if (_acked == _ack) - _status = WS_MSG_SENT; - return 0; - } - if (_sent > _WSbuffer->size()) { - _status = WS_MSG_ERROR; - // ets_printf("E: %u > %u\n", _sent, _WSbuffer->length()); - return 0; - } - - size_t toSend = _WSbuffer->size() - _sent; - size_t window = webSocketSendFrameWindow(client); - - if (window < toSend) { - toSend = window; - } - - _sent += toSend; - _ack += toSend + ((toSend < 126) ? 2 : 4) + (_mask * 4); - - // ets_printf("W: %u %u\n", _sent - toSend, toSend); - - bool final = (_sent == _WSbuffer->size()); - uint8_t* dPtr = (uint8_t*)(_WSbuffer->data() + (_sent - toSend)); - uint8_t opCode = (toSend && _sent == toSend) ? _opcode : (uint8_t)WS_CONTINUATION; - - size_t sent = webSocketSendFrame(client, final, opCode, _mask, dPtr, toSend); - _status = WS_MSG_SENDING; - if (toSend && sent != toSend) { - // ets_printf("E: %u != %u\n", toSend, sent); - _sent -= (toSend - sent); - _ack -= (toSend - sent); - } - // ets_printf("S: %u %u\n", _sent, sent); - return sent; -} - -/* - * Async WebSocket Client - */ -const char* AWSC_PING_PAYLOAD = "ESPAsyncWebServer-PING"; -const size_t AWSC_PING_PAYLOAD_LEN = 22; - -AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest* request, AsyncWebSocket* server) - : _tempObject(NULL) { - _client = request->client(); - _server = server; - _clientId = _server->_getNextId(); - _status = WS_CONNECTED; - _pstate = 0; - _lastMessageTime = millis(); - _keepAlivePeriod = 0; - _client->setRxTimeout(0); - _client->onError([](void* r, AsyncClient* c, int8_t error) { (void)c; ((AsyncWebSocketClient*)(r))->_onError(error); }, this); - _client->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; ((AsyncWebSocketClient*)(r))->_onAck(len, time); }, this); - _client->onDisconnect([](void* r, AsyncClient* c) { ((AsyncWebSocketClient*)(r))->_onDisconnect(); delete c; }, this); - _client->onTimeout([](void* r, AsyncClient* c, uint32_t time) { (void)c; ((AsyncWebSocketClient*)(r))->_onTimeout(time); }, this); - _client->onData([](void* r, AsyncClient* c, void* buf, size_t len) { (void)c; ((AsyncWebSocketClient*)(r))->_onData(buf, len); }, this); - _client->onPoll([](void* r, AsyncClient* c) { (void)c; ((AsyncWebSocketClient*)(r))->_onPoll(); }, this); - _server->_handleEvent(this, WS_EVT_CONNECT, request, NULL, 0); - delete request; - memset(&_pinfo, 0, sizeof(_pinfo)); -} - -AsyncWebSocketClient::~AsyncWebSocketClient() { - { -#ifdef ESP32 - std::lock_guard lock(_lock); -#endif - _messageQueue.clear(); - _controlQueue.clear(); - } - _server->_handleEvent(this, WS_EVT_DISCONNECT, NULL, NULL, 0); -} - -void AsyncWebSocketClient::_clearQueue() { - while (!_messageQueue.empty() && _messageQueue.front().finished()) - _messageQueue.pop_front(); -} - -void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { - _lastMessageTime = millis(); - -#ifdef ESP32 - std::lock_guard lock(_lock); -#endif - - if (!_controlQueue.empty()) { - auto& head = _controlQueue.front(); - if (head.finished()) { - len -= head.len(); - if (_status == WS_DISCONNECTING && head.opcode() == WS_DISCONNECT) { - _controlQueue.pop_front(); - _status = WS_DISCONNECTED; - if (_client) - _client->close(true); - return; - } - _controlQueue.pop_front(); - } - } - - if (len && !_messageQueue.empty()) { - _messageQueue.front().ack(len, time); - } - - _clearQueue(); - - _runQueue(); -} - -void AsyncWebSocketClient::_onPoll() { - if (!_client) - return; - -#ifdef ESP32 - std::unique_lock lock(_lock); -#endif - if (_client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) { - _runQueue(); - } else if (_keepAlivePeriod > 0 && (millis() - _lastMessageTime) >= _keepAlivePeriod && (_controlQueue.empty() && _messageQueue.empty())) { -#ifdef ESP32 - lock.unlock(); -#endif - ping((uint8_t*)AWSC_PING_PAYLOAD, AWSC_PING_PAYLOAD_LEN); - } -} - -void AsyncWebSocketClient::_runQueue() { - // all calls to this method MUST be protected by a mutex lock! - if (!_client) - return; - - _clearQueue(); - - if (!_controlQueue.empty() && (_messageQueue.empty() || _messageQueue.front().betweenFrames()) && webSocketSendFrameWindow(_client) > (size_t)(_controlQueue.front().len() - 1)) { - _controlQueue.front().send(_client); - } else if (!_messageQueue.empty() && _messageQueue.front().betweenFrames() && webSocketSendFrameWindow(_client)) { - _messageQueue.front().send(_client); - } -} - -bool AsyncWebSocketClient::queueIsFull() const { -#ifdef ESP32 - std::lock_guard lock(_lock); -#endif - size_t size = _messageQueue.size(); - ; - return (size >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED); -} - -size_t AsyncWebSocketClient::queueLen() const { -#ifdef ESP32 - std::lock_guard lock(_lock); -#endif - - return _messageQueue.size() + _controlQueue.size(); -} - -bool AsyncWebSocketClient::canSend() const { -#ifdef ESP32 - std::lock_guard lock(_lock); -#endif - return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES; -} - -void AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t* data, size_t len, bool mask) { - if (!_client) - return; - - { -#ifdef ESP32 - std::lock_guard lock(_lock); -#endif - _controlQueue.emplace_back(opcode, data, len, mask); - } - - if (_client && _client->canSend()) - _runQueue(); -} - -void AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode, bool mask) { - if (!_client || buffer->size() == 0 || _status != WS_CONNECTED) - return; - -#ifdef ESP32 - std::lock_guard lock(_lock); -#endif - if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) { - if (closeWhenFull) { -#ifdef ESP8266 - ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: closing connection\n"); -#elif defined(ESP32) - log_e("Too many messages queued: closing connection"); -#endif - _status = WS_DISCONNECTED; - if (_client) - _client->close(true); - } else { -#ifdef ESP8266 - ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: discarding new message\n"); -#elif defined(ESP32) - log_e("Too many messages queued: discarding new message"); -#endif - } - return; - } else { - _messageQueue.emplace_back(buffer, opcode, mask); - } - - if (_client && _client->canSend()) - _runQueue(); -} - -void AsyncWebSocketClient::close(uint16_t code, const char* message) { - if (_status != WS_CONNECTED) - return; - - if (code) { - uint8_t packetLen = 2; - if (message != NULL) { - size_t mlen = strlen(message); - if (mlen > 123) - mlen = 123; - packetLen += mlen; - } - char* buf = (char*)malloc(packetLen); - if (buf != NULL) { - buf[0] = (uint8_t)(code >> 8); - buf[1] = (uint8_t)(code & 0xFF); - if (message != NULL) { - memcpy(buf + 2, message, packetLen - 2); - } - _queueControl(WS_DISCONNECT, (uint8_t*)buf, packetLen); - free(buf); - return; - } - } - _queueControl(WS_DISCONNECT); -} - -void AsyncWebSocketClient::ping(const uint8_t* data, size_t len) { - if (_status == WS_CONNECTED) - _queueControl(WS_PING, data, len); -} - -void AsyncWebSocketClient::_onError(int8_t) { - // Serial.println("onErr"); -} - -void AsyncWebSocketClient::_onTimeout(uint32_t time) { - // Serial.println("onTime"); - (void)time; - _client->close(true); -} - -void AsyncWebSocketClient::_onDisconnect() { - // Serial.println("onDis"); - _client = NULL; -} - -void AsyncWebSocketClient::_onData(void* pbuf, size_t plen) { - // Serial.println("onData"); - _lastMessageTime = millis(); - uint8_t* data = (uint8_t*)pbuf; - while (plen > 0) { - if (!_pstate) { - const uint8_t* fdata = data; - _pinfo.index = 0; - _pinfo.final = (fdata[0] & 0x80) != 0; - _pinfo.opcode = fdata[0] & 0x0F; - _pinfo.masked = (fdata[1] & 0x80) != 0; - _pinfo.len = fdata[1] & 0x7F; - data += 2; - plen -= 2; - if (_pinfo.len == 126) { - _pinfo.len = fdata[3] | (uint16_t)(fdata[2]) << 8; - data += 2; - plen -= 2; - } else if (_pinfo.len == 127) { - _pinfo.len = fdata[9] | (uint16_t)(fdata[8]) << 8 | (uint32_t)(fdata[7]) << 16 | (uint32_t)(fdata[6]) << 24 | (uint64_t)(fdata[5]) << 32 | (uint64_t)(fdata[4]) << 40 | (uint64_t)(fdata[3]) << 48 | (uint64_t)(fdata[2]) << 56; - data += 8; - plen -= 8; - } - - if (_pinfo.masked) { - memcpy(_pinfo.mask, data, 4); - data += 4; - plen -= 4; - } - } - - const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen); - const auto datalast = data[datalen]; - - if (_pinfo.masked) { - for (size_t i = 0; i < datalen; i++) - data[i] ^= _pinfo.mask[(_pinfo.index + i) % 4]; - } - - if ((datalen + _pinfo.index) < _pinfo.len) { - _pstate = 1; - - if (_pinfo.index == 0) { - if (_pinfo.opcode) { - _pinfo.message_opcode = _pinfo.opcode; - _pinfo.num = 0; - } - } - if (datalen > 0) - _server->_handleEvent(this, WS_EVT_DATA, (void*)&_pinfo, (uint8_t*)data, datalen); - - _pinfo.index += datalen; - } else if ((datalen + _pinfo.index) == _pinfo.len) { - _pstate = 0; - if (_pinfo.opcode == WS_DISCONNECT) { - if (datalen) { - uint16_t reasonCode = (uint16_t)(data[0] << 8) + data[1]; - char* reasonString = (char*)(data + 2); - if (reasonCode > 1001) { - _server->_handleEvent(this, WS_EVT_ERROR, (void*)&reasonCode, (uint8_t*)reasonString, strlen(reasonString)); - } - } - if (_status == WS_DISCONNECTING) { - _status = WS_DISCONNECTED; - _client->close(true); - } else { - _status = WS_DISCONNECTING; - _client->ackLater(); - _queueControl(WS_DISCONNECT, data, datalen); - } - } else if (_pinfo.opcode == WS_PING) { - _queueControl(WS_PONG, data, datalen); - } else if (_pinfo.opcode == WS_PONG) { - if (datalen != AWSC_PING_PAYLOAD_LEN || memcmp(AWSC_PING_PAYLOAD, data, AWSC_PING_PAYLOAD_LEN) != 0) - _server->_handleEvent(this, WS_EVT_PONG, NULL, data, datalen); - } else if (_pinfo.opcode < 8) { // continuation or text/binary frame - _server->_handleEvent(this, WS_EVT_DATA, (void*)&_pinfo, data, datalen); - if (_pinfo.final) - _pinfo.num = 0; - else - _pinfo.num += 1; - } - } else { - // os_printf("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len); - // what should we do? - break; - } - - // restore byte as _handleEvent may have added a null terminator i.e., data[len] = 0; - if (datalen > 0) - data[datalen] = datalast; - - data += datalen; - plen -= datalen; - } -} - -size_t AsyncWebSocketClient::printf(const char* format, ...) { - va_list arg; - va_start(arg, format); - char* temp = new char[MAX_PRINTF_LEN]; - if (!temp) { - va_end(arg); - return 0; - } - char* buffer = temp; - size_t len = vsnprintf(temp, MAX_PRINTF_LEN, format, arg); - va_end(arg); - - if (len > (MAX_PRINTF_LEN - 1)) { - buffer = new char[len + 1]; - if (!buffer) { - delete[] temp; - return 0; - } - va_start(arg, format); - vsnprintf(buffer, len + 1, format, arg); - va_end(arg); - } - text(buffer, len); - if (buffer != temp) { - delete[] buffer; - } - delete[] temp; - return len; -} - -#ifdef ESP8266 -size_t AsyncWebSocketClient::printf_P(PGM_P formatP, ...) { - va_list arg; - va_start(arg, formatP); - char* temp = new char[MAX_PRINTF_LEN]; - if (!temp) { - va_end(arg); - return 0; - } - char* buffer = temp; - size_t len = vsnprintf_P(temp, MAX_PRINTF_LEN, formatP, arg); - va_end(arg); - - if (len > (MAX_PRINTF_LEN - 1)) { - buffer = new char[len + 1]; - if (!buffer) { - delete[] temp; - return 0; - } - va_start(arg, formatP); - vsnprintf_P(buffer, len + 1, formatP, arg); - va_end(arg); - } - text(buffer, len); - if (buffer != temp) { - delete[] buffer; - } - delete[] temp; - return len; -} -#endif - -namespace { - AsyncWebSocketSharedBuffer makeSharedBuffer(const uint8_t* message, size_t len) { - auto buffer = std::make_shared>(len); - std::memcpy(buffer->data(), message, len); - return buffer; - } -} - -void AsyncWebSocketClient::text(AsyncWebSocketMessageBuffer* buffer) { - if (buffer) { - text(std::move(buffer->_buffer)); - delete buffer; - } -} - -void AsyncWebSocketClient::text(AsyncWebSocketSharedBuffer buffer) { - _queueMessage(buffer); -} - -void AsyncWebSocketClient::text(const uint8_t* message, size_t len) { - text(makeSharedBuffer(message, len)); -} - -void AsyncWebSocketClient::text(const char* message, size_t len) { - text((const uint8_t*)message, len); -} - -void AsyncWebSocketClient::text(const char* message) { - text(message, strlen(message)); -} - -void AsyncWebSocketClient::text(const String& message) { - text(message.c_str(), message.length()); -} - -#ifdef ESP8266 -void AsyncWebSocketClient::text(const __FlashStringHelper* data) { - PGM_P p = reinterpret_cast(data); - - size_t n = 0; - while (1) { - if (pgm_read_byte(p + n) == 0) - break; - n += 1; - } - - char* message = (char*)malloc(n + 1); - if (message) { - memcpy_P(message, p, n); - message[n] = 0; - text(message, n); - free(message); - } -} -#endif // ESP8266 - -void AsyncWebSocketClient::binary(AsyncWebSocketMessageBuffer* buffer) { - if (buffer) { - binary(std::move(buffer->_buffer)); - delete buffer; - } -} - -void AsyncWebSocketClient::binary(AsyncWebSocketSharedBuffer buffer) { - _queueMessage(buffer, WS_BINARY); -} - -void AsyncWebSocketClient::binary(const uint8_t* message, size_t len) { - binary(makeSharedBuffer(message, len)); -} - -void AsyncWebSocketClient::binary(const char* message, size_t len) { - binary((const uint8_t*)message, len); -} - -void AsyncWebSocketClient::binary(const char* message) { - binary(message, strlen(message)); -} - -void AsyncWebSocketClient::binary(const String& message) { - binary(message.c_str(), message.length()); -} - -#ifdef ESP8266 -void AsyncWebSocketClient::binary(const __FlashStringHelper* data, size_t len) { - PGM_P p = reinterpret_cast(data); - char* message = (char*)malloc(len); - if (message) { - memcpy_P(message, p, len); - binary(message, len); - free(message); - } -} -#endif - -IPAddress AsyncWebSocketClient::remoteIP() const { - if (!_client) - return IPAddress((uint32_t)0U); - - return _client->remoteIP(); -} - -uint16_t AsyncWebSocketClient::remotePort() const { - if (!_client) - return 0; - - return _client->remotePort(); -} - -/* - * Async Web Socket - Each separate socket location - */ - -void AsyncWebSocket::_handleEvent(AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { - if (_eventHandler != NULL) { - _eventHandler(this, client, type, arg, data, len); - } -} - -AsyncWebSocketClient* AsyncWebSocket::_newClient(AsyncWebServerRequest* request) { - _clients.emplace_back(request, this); - return &_clients.back(); -} - -bool AsyncWebSocket::availableForWriteAll() { - return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient& c) { return c.queueIsFull(); }); -} - -bool AsyncWebSocket::availableForWrite(uint32_t id) { - const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [id](const AsyncWebSocketClient& c) { return c.id() == id; }); - if (iter == std::end(_clients)) - return true; - return !iter->queueIsFull(); -} - -size_t AsyncWebSocket::count() const { - return std::count_if(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient& c) { return c.status() == WS_CONNECTED; }); -} - -AsyncWebSocketClient* AsyncWebSocket::client(uint32_t id) { - const auto iter = std::find_if(_clients.begin(), _clients.end(), [id](const AsyncWebSocketClient& c) { return c.id() == id && c.status() == WS_CONNECTED; }); - if (iter == std::end(_clients)) - return nullptr; - - return &(*iter); -} - -void AsyncWebSocket::close(uint32_t id, uint16_t code, const char* message) { - if (AsyncWebSocketClient* c = client(id)) - c->close(code, message); -} - -void AsyncWebSocket::closeAll(uint16_t code, const char* message) { - for (auto& c : _clients) - if (c.status() == WS_CONNECTED) - c.close(code, message); -} - -void AsyncWebSocket::cleanupClients(uint16_t maxClients) { - if (count() > maxClients) - _clients.front().close(); - - for (auto iter = std::begin(_clients); iter != std::end(_clients);) { - if (iter->shouldBeDeleted()) - iter = _clients.erase(iter); - else - iter++; - } -} - -void AsyncWebSocket::ping(uint32_t id, const uint8_t* data, size_t len) { - if (AsyncWebSocketClient* c = client(id)) - c->ping(data, len); -} - -void AsyncWebSocket::pingAll(const uint8_t* data, size_t len) { - for (auto& c : _clients) - if (c.status() == WS_CONNECTED) - c.ping(data, len); -} - -void AsyncWebSocket::text(uint32_t id, const uint8_t* message, size_t len) { - if (AsyncWebSocketClient* c = client(id)) - c->text(makeSharedBuffer(message, len)); -} -void AsyncWebSocket::text(uint32_t id, const char* message, size_t len) { - text(id, (const uint8_t*)message, len); -} -void AsyncWebSocket::text(uint32_t id, const char* message) { - text(id, message, strlen(message)); -} -void AsyncWebSocket::text(uint32_t id, const String& message) { - text(id, message.c_str(), message.length()); -} - -#ifdef ESP8266 -void AsyncWebSocket::text(uint32_t id, const __FlashStringHelper* data) { - PGM_P p = reinterpret_cast(data); - - size_t n = 0; - while (true) { - if (pgm_read_byte(p + n) == 0) - break; - n += 1; - } - - char* message = (char*)malloc(n + 1); - if (message) { - memcpy_P(message, p, n); - message[n] = 0; - text(id, message, n); - free(message); - } -} -#endif // ESP8266 - -void AsyncWebSocket::text(uint32_t id, AsyncWebSocketMessageBuffer* buffer) { - if (buffer) { - text(id, std::move(buffer->_buffer)); - delete buffer; - } -} -void AsyncWebSocket::text(uint32_t id, AsyncWebSocketSharedBuffer buffer) { - if (AsyncWebSocketClient* c = client(id)) - c->text(buffer); -} - -void AsyncWebSocket::textAll(const uint8_t* message, size_t len) { - textAll(makeSharedBuffer(message, len)); -} -void AsyncWebSocket::textAll(const char* message, size_t len) { - textAll((const uint8_t*)message, len); -} -void AsyncWebSocket::textAll(const char* message) { - textAll(message, strlen(message)); -} -void AsyncWebSocket::textAll(const String& message) { - textAll(message.c_str(), message.length()); -} -#ifdef ESP8266 -void AsyncWebSocket::textAll(const __FlashStringHelper* data) { - PGM_P p = reinterpret_cast(data); - - size_t n = 0; - while (1) { - if (pgm_read_byte(p + n) == 0) - break; - n += 1; - } - - char* message = (char*)malloc(n + 1); - if (message) { - memcpy_P(message, p, n); - message[n] = 0; - textAll(message, n); - free(message); - } -} -#endif // ESP8266 -void AsyncWebSocket::textAll(AsyncWebSocketMessageBuffer* buffer) { - if (buffer) { - textAll(std::move(buffer->_buffer)); - delete buffer; - } -} - -void AsyncWebSocket::textAll(AsyncWebSocketSharedBuffer buffer) { - for (auto& c : _clients) - if (c.status() == WS_CONNECTED) - c.text(buffer); -} - -void AsyncWebSocket::binary(uint32_t id, const uint8_t* message, size_t len) { - if (AsyncWebSocketClient* c = client(id)) - c->binary(makeSharedBuffer(message, len)); -} -void AsyncWebSocket::binary(uint32_t id, const char* message, size_t len) { - binary(id, (const uint8_t*)message, len); -} -void AsyncWebSocket::binary(uint32_t id, const char* message) { - binary(id, message, strlen(message)); -} -void AsyncWebSocket::binary(uint32_t id, const String& message) { - binary(id, message.c_str(), message.length()); -} - -#ifdef ESP8266 -void AsyncWebSocket::binary(uint32_t id, const __FlashStringHelper* data, size_t len) { - PGM_P p = reinterpret_cast(data); - char* message = (char*)malloc(len); - if (message) { - memcpy_P(message, p, len); - binary(id, message, len); - free(message); - } -} -#endif // ESP8266 - -void AsyncWebSocket::binary(uint32_t id, AsyncWebSocketMessageBuffer* buffer) { - if (buffer) { - binary(id, std::move(buffer->_buffer)); - delete buffer; - } -} -void AsyncWebSocket::binary(uint32_t id, AsyncWebSocketSharedBuffer buffer) { - if (AsyncWebSocketClient* c = client(id)) - c->binary(buffer); -} - -void AsyncWebSocket::binaryAll(const uint8_t* message, size_t len) { - binaryAll(makeSharedBuffer(message, len)); -} -void AsyncWebSocket::binaryAll(const char* message, size_t len) { - binaryAll((const uint8_t*)message, len); -} -void AsyncWebSocket::binaryAll(const char* message) { - binaryAll(message, strlen(message)); -} -void AsyncWebSocket::binaryAll(const String& message) { - binaryAll(message.c_str(), message.length()); -} - -#ifdef ESP8266 -void AsyncWebSocket::binaryAll(const __FlashStringHelper* data, size_t len) { - PGM_P p = reinterpret_cast(data); - char* message = (char*)malloc(len); - if (message) { - memcpy_P(message, p, len); - binaryAll(message, len); - free(message); - } -} -#endif // ESP8266 - -void AsyncWebSocket::binaryAll(AsyncWebSocketMessageBuffer* buffer) { - if (buffer) { - binaryAll(std::move(buffer->_buffer)); - delete buffer; - } -} -void AsyncWebSocket::binaryAll(AsyncWebSocketSharedBuffer buffer) { - for (auto& c : _clients) - if (c.status() == WS_CONNECTED) - c.binary(buffer); -} - -size_t AsyncWebSocket::printf(uint32_t id, const char* format, ...) { - AsyncWebSocketClient* c = client(id); - if (c) { - va_list arg; - va_start(arg, format); - size_t len = c->printf(format, arg); - va_end(arg); - return len; - } - return 0; -} - -size_t AsyncWebSocket::printfAll(const char* format, ...) { - va_list arg; - char* temp = new char[MAX_PRINTF_LEN]; - if (!temp) - return 0; - - va_start(arg, format); - size_t len = vsnprintf(temp, MAX_PRINTF_LEN, format, arg); - va_end(arg); - delete[] temp; - - AsyncWebSocketSharedBuffer buffer = std::make_shared>(len); - - va_start(arg, format); - vsnprintf((char*)buffer->data(), len + 1, format, arg); - va_end(arg); - - textAll(buffer); - return len; -} - -#ifdef ESP8266 -size_t AsyncWebSocket::printf_P(uint32_t id, PGM_P formatP, ...) { - AsyncWebSocketClient* c = client(id); - if (c != NULL) { - va_list arg; - va_start(arg, formatP); - size_t len = c->printf_P(formatP, arg); - va_end(arg); - return len; - } - return 0; -} - -size_t AsyncWebSocket::printfAll_P(PGM_P formatP, ...) { - va_list arg; - char* temp = new char[MAX_PRINTF_LEN]; - if (!temp) - return 0; - - va_start(arg, formatP); - size_t len = vsnprintf_P(temp, MAX_PRINTF_LEN, formatP, arg); - va_end(arg); - delete[] temp; - - AsyncWebSocketSharedBuffer buffer = std::make_shared>(len + 1); - - va_start(arg, formatP); - vsnprintf_P((char*)buffer->data(), len + 1, formatP, arg); - va_end(arg); - - textAll(buffer); - return len; -} -#endif - -const char __WS_STR_CONNECTION[] PROGMEM = {"Connection"}; -const char __WS_STR_UPGRADE[] PROGMEM = {"Upgrade"}; -const char __WS_STR_ORIGIN[] PROGMEM = {"Origin"}; -const char __WS_STR_COOKIE[] PROGMEM = {"Cookie"}; -const char __WS_STR_VERSION[] PROGMEM = {"Sec-WebSocket-Version"}; -const char __WS_STR_KEY[] PROGMEM = {"Sec-WebSocket-Key"}; -const char __WS_STR_PROTOCOL[] PROGMEM = {"Sec-WebSocket-Protocol"}; -const char __WS_STR_ACCEPT[] PROGMEM = {"Sec-WebSocket-Accept"}; -const char __WS_STR_UUID[] PROGMEM = {"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"}; - -#define WS_STR_UUID_LEN 36 - -#define WS_STR_CONNECTION FPSTR(__WS_STR_CONNECTION) -#define WS_STR_UPGRADE FPSTR(__WS_STR_UPGRADE) -#define WS_STR_ORIGIN FPSTR(__WS_STR_ORIGIN) -#define WS_STR_COOKIE FPSTR(__WS_STR_COOKIE) -#define WS_STR_VERSION FPSTR(__WS_STR_VERSION) -#define WS_STR_KEY FPSTR(__WS_STR_KEY) -#define WS_STR_PROTOCOL FPSTR(__WS_STR_PROTOCOL) -#define WS_STR_ACCEPT FPSTR(__WS_STR_ACCEPT) -#define WS_STR_UUID FPSTR(__WS_STR_UUID) - -bool AsyncWebSocket::canHandle(AsyncWebServerRequest* request) { - if (!_enabled) - return false; - - if (request->method() != HTTP_GET || !request->url().equals(_url) || !request->isExpectedRequestedConnType(RCT_WS)) - return false; - - request->addInterestingHeader(WS_STR_CONNECTION); - request->addInterestingHeader(WS_STR_UPGRADE); - request->addInterestingHeader(WS_STR_ORIGIN); - request->addInterestingHeader(WS_STR_COOKIE); - request->addInterestingHeader(WS_STR_VERSION); - request->addInterestingHeader(WS_STR_KEY); - request->addInterestingHeader(WS_STR_PROTOCOL); - return true; -} - -void AsyncWebSocket::handleRequest(AsyncWebServerRequest* request) { - if (!request->hasHeader(WS_STR_VERSION) || !request->hasHeader(WS_STR_KEY)) { - request->send(400); - return; - } - if ((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())) { - return request->requestAuthentication(); - } - if (_handshakeHandler != nullptr) { - if (!_handshakeHandler(request)) { - request->send(401); - return; - } - } - const AsyncWebHeader* version = request->getHeader(WS_STR_VERSION); - if (version->value().toInt() != 13) { - AsyncWebServerResponse* response = request->beginResponse(400); - response->addHeader(WS_STR_VERSION, T_13); - request->send(response); - return; - } - const AsyncWebHeader* key = request->getHeader(WS_STR_KEY); - AsyncWebServerResponse* response = new AsyncWebSocketResponse(key->value(), this); - if (request->hasHeader(WS_STR_PROTOCOL)) { - const AsyncWebHeader* protocol = request->getHeader(WS_STR_PROTOCOL); - // ToDo: check protocol - response->addHeader(WS_STR_PROTOCOL, protocol->value()); - } - request->send(response); -} - -AsyncWebSocketMessageBuffer* AsyncWebSocket::makeBuffer(size_t size) { - AsyncWebSocketMessageBuffer* buffer = new AsyncWebSocketMessageBuffer(size); - if (buffer->length() != size) { - delete buffer; - return nullptr; - } else { - return buffer; - } -} - -AsyncWebSocketMessageBuffer* AsyncWebSocket::makeBuffer(const uint8_t* data, size_t size) { - AsyncWebSocketMessageBuffer* buffer = new AsyncWebSocketMessageBuffer(data, size); - if (buffer->length() != size) { - delete buffer; - return nullptr; - } else { - return buffer; - } -} - -/* - * Response to Web Socket request - sends the authorization and detaches the TCP Client from the web server - * Authentication code from https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSockets.cpp#L480 - */ - -AsyncWebSocketResponse::AsyncWebSocketResponse(const String& key, AsyncWebSocket* server) { - _server = server; - _code = 101; - _sendContentLength = false; - - uint8_t hash[20]; - char buffer[33]; - -#if defined(ESP8266) || defined(TARGET_RP2040) - sha1(key + WS_STR_UUID, hash); -#else - String k; - k.reserve(key.length() + WS_STR_UUID_LEN); - k.concat(key); - k.concat(WS_STR_UUID); - SHA1Builder sha1; - sha1.begin(); - sha1.add((const uint8_t*)k.c_str(), k.length()); - sha1.calculate(); - sha1.getBytes(hash); -#endif - base64_encodestate _state; - base64_init_encodestate(&_state); - int len = base64_encode_block((const char*)hash, 20, buffer, &_state); - len = base64_encode_blockend((buffer + len), &_state); - addHeader(WS_STR_CONNECTION, WS_STR_UPGRADE); - addHeader(WS_STR_UPGRADE, T_WS); - addHeader(WS_STR_ACCEPT, buffer); -} - -void AsyncWebSocketResponse::_respond(AsyncWebServerRequest* request) { - if (_state == RESPONSE_FAILED) { - request->client()->close(true); - return; - } - String out(_assembleHead(request->version())); - request->client()->write(out.c_str(), _headLength); - _state = RESPONSE_WAIT_ACK; -} - -size_t AsyncWebSocketResponse::_ack(AsyncWebServerRequest* request, size_t len, uint32_t time) { - (void)time; - - if (len) - _server->_newClient(request); - - return 0; -} diff --git a/lib/ESPAsyncWebServer/src/AsyncWebSocket.h b/lib/ESPAsyncWebServer/src/AsyncWebSocket.h deleted file mode 100644 index 34256a7..0000000 --- a/lib/ESPAsyncWebServer/src/AsyncWebSocket.h +++ /dev/null @@ -1,379 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#ifndef ASYNCWEBSOCKET_H_ -#define ASYNCWEBSOCKET_H_ - -#include -#ifdef ESP32 - #include - #include - #ifndef WS_MAX_QUEUED_MESSAGES - #define WS_MAX_QUEUED_MESSAGES 32 - #endif -#elif defined(ESP8266) - #include - #ifndef WS_MAX_QUEUED_MESSAGES - #define WS_MAX_QUEUED_MESSAGES 8 - #endif -#elif defined(TARGET_RP2040) - #include - #ifndef WS_MAX_QUEUED_MESSAGES - #define WS_MAX_QUEUED_MESSAGES 32 - #endif -#endif - -#include - -#include -#include -#include - -#ifdef ESP8266 - #include - #ifdef CRYPTO_HASH_h // include Hash.h from espressif framework if the first include was from the crypto library - #include <../src/Hash.h> - #endif -#endif - -#ifndef DEFAULT_MAX_WS_CLIENTS - #ifdef ESP32 - #define DEFAULT_MAX_WS_CLIENTS 8 - #else - #define DEFAULT_MAX_WS_CLIENTS 4 - #endif -#endif - -using AsyncWebSocketSharedBuffer = std::shared_ptr>; - -class AsyncWebSocket; -class AsyncWebSocketResponse; -class AsyncWebSocketClient; -class AsyncWebSocketControl; - -typedef struct { - /** Message type as defined by enum AwsFrameType. - * Note: Applications will only see WS_TEXT and WS_BINARY. - * All other types are handled by the library. */ - uint8_t message_opcode; - /** Frame number of a fragmented message. */ - uint32_t num; - /** Is this the last frame in a fragmented message ?*/ - uint8_t final; - /** Is this frame masked? */ - uint8_t masked; - /** Message type as defined by enum AwsFrameType. - * This value is the same as message_opcode for non-fragmented - * messages, but may also be WS_CONTINUATION in a fragmented message. */ - uint8_t opcode; - /** Length of the current frame. - * This equals the total length of the message if num == 0 && final == true */ - uint64_t len; - /** Mask key */ - uint8_t mask[4]; - /** Offset of the data inside the current frame. */ - uint64_t index; -} AwsFrameInfo; - -typedef enum { WS_DISCONNECTED, - WS_CONNECTED, - WS_DISCONNECTING } AwsClientStatus; -typedef enum { WS_CONTINUATION, - WS_TEXT, - WS_BINARY, - WS_DISCONNECT = 0x08, - WS_PING, - WS_PONG } AwsFrameType; -typedef enum { WS_MSG_SENDING, - WS_MSG_SENT, - WS_MSG_ERROR } AwsMessageStatus; -typedef enum { WS_EVT_CONNECT, - WS_EVT_DISCONNECT, - WS_EVT_PONG, - WS_EVT_ERROR, - WS_EVT_DATA } AwsEventType; - -class AsyncWebSocketMessageBuffer { - friend AsyncWebSocket; - friend AsyncWebSocketClient; - - private: - AsyncWebSocketSharedBuffer _buffer; - - public: - AsyncWebSocketMessageBuffer() {} - explicit AsyncWebSocketMessageBuffer(size_t size); - AsyncWebSocketMessageBuffer(const uint8_t* data, size_t size); - //~AsyncWebSocketMessageBuffer(); - bool reserve(size_t size); - uint8_t* get() { return _buffer->data(); } - size_t length() const { return _buffer->size(); } -}; - -class AsyncWebSocketMessage { - private: - AsyncWebSocketSharedBuffer _WSbuffer; - uint8_t _opcode{WS_TEXT}; - bool _mask{false}; - AwsMessageStatus _status{WS_MSG_ERROR}; - size_t _sent{}; - size_t _ack{}; - size_t _acked{}; - - public: - AsyncWebSocketMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false); - - bool finished() const { return _status != WS_MSG_SENDING; } - bool betweenFrames() const { return _acked == _ack; } - - void ack(size_t len, uint32_t time); - size_t send(AsyncClient* client); -}; - -class AsyncWebSocketClient { - private: - AsyncClient* _client; - AsyncWebSocket* _server; - uint32_t _clientId; - AwsClientStatus _status; -#ifdef ESP32 - mutable std::mutex _lock; -#endif - std::deque _controlQueue; - std::deque _messageQueue; - bool closeWhenFull = true; - - uint8_t _pstate; - AwsFrameInfo _pinfo; - - uint32_t _lastMessageTime; - uint32_t _keepAlivePeriod; - - void _queueControl(uint8_t opcode, const uint8_t* data = NULL, size_t len = 0, bool mask = false); - void _queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false); - void _runQueue(); - void _clearQueue(); - - public: - void* _tempObject; - - AsyncWebSocketClient(AsyncWebServerRequest* request, AsyncWebSocket* server); - ~AsyncWebSocketClient(); - - // client id increments for the given server - uint32_t id() const { return _clientId; } - AwsClientStatus status() const { return _status; } - AsyncClient* client() { return _client; } - const AsyncClient* client() const { return _client; } - AsyncWebSocket* server() { return _server; } - const AsyncWebSocket* server() const { return _server; } - AwsFrameInfo const& pinfo() const { return _pinfo; } - - // - If "true" (default), the connection will be closed if the message queue is full. - // This is the default behavior in yubox-node-org, which is not silently discarding messages but instead closes the connection. - // The big issue with this behavior is that is can cause the UI to automatically re-create a new WS connection, which can be filled again, - // and so on, causing a resource exhaustion. - // - // - If "false", the incoming message will be discarded if the queue is full. - // This is the default behavior in the original ESPAsyncWebServer library from me-no-dev. - // This behavior allows the best performance at the expense of unreliable message delivery in case the queue is full (some messages may be lost). - // - // - In any case, when the queue is full, a message is logged. - // - IT is recommended to use the methods queueIsFull(), availableForWriteAll(), availableForWrite(clientId) to check if the queue is full before sending a message. - // - // Usage: - // - can be set in the onEvent listener when connecting (event type is: WS_EVT_CONNECT) - // - // Use cases:, - // - if using websocket to send logging messages, maybe some loss is acceptable. - // - But if using websocket to send UI update messages, maybe the connection should be closed and the UI redrawn. - void setCloseClientOnQueueFull(bool close) { closeWhenFull = close; } - bool willCloseClientOnQueueFull() const { return closeWhenFull; } - - IPAddress remoteIP() const; - uint16_t remotePort() const; - - bool shouldBeDeleted() const { return !_client; } - - // control frames - void close(uint16_t code = 0, const char* message = NULL); - void ping(const uint8_t* data = NULL, size_t len = 0); - - // set auto-ping period in seconds. disabled if zero (default) - void keepAlivePeriod(uint16_t seconds) { - _keepAlivePeriod = seconds * 1000; - } - uint16_t keepAlivePeriod() { - return (uint16_t)(_keepAlivePeriod / 1000); - } - - // data packets - void message(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false) { _queueMessage(buffer, opcode, mask); } - bool queueIsFull() const; - size_t queueLen() const; - - size_t printf(const char* format, ...) __attribute__((format(printf, 2, 3))); - - void text(AsyncWebSocketSharedBuffer buffer); - void text(const uint8_t* message, size_t len); - void text(const char* message, size_t len); - void text(const char* message); - void text(const String& message); - void text(AsyncWebSocketMessageBuffer* buffer); - - void binary(AsyncWebSocketSharedBuffer buffer); - void binary(const uint8_t* message, size_t len); - void binary(const char* message, size_t len); - void binary(const char* message); - void binary(const String& message); - void binary(AsyncWebSocketMessageBuffer* buffer); - - bool canSend() const; - - // system callbacks (do not call) - void _onAck(size_t len, uint32_t time); - void _onError(int8_t); - void _onPoll(); - void _onTimeout(uint32_t time); - void _onDisconnect(); - void _onData(void* pbuf, size_t plen); - -#ifdef ESP8266 - size_t printf_P(PGM_P formatP, ...) __attribute__((format(printf, 2, 3))); - void text(const __FlashStringHelper* message); - void binary(const __FlashStringHelper* message, size_t len); -#endif -}; - -using AwsHandshakeHandler = std::function; -using AwsEventHandler = std::function; - -// WebServer Handler implementation that plays the role of a socket server -class AsyncWebSocket : public AsyncWebHandler { - private: - String _url; - std::list _clients; - uint32_t _cNextId; - AwsEventHandler _eventHandler{nullptr}; - AwsHandshakeHandler _handshakeHandler; - bool _enabled; -#ifdef ESP32 - mutable std::mutex _lock; -#endif - - public: - explicit AsyncWebSocket(const char* url) : _url(url), _cNextId(1), _enabled(true) {} - AsyncWebSocket(const String& url) : _url(url), _cNextId(1), _enabled(true) {} - ~AsyncWebSocket(){}; - const char* url() const { return _url.c_str(); } - void enable(bool e) { _enabled = e; } - bool enabled() const { return _enabled; } - bool availableForWriteAll(); - bool availableForWrite(uint32_t id); - - size_t count() const; - AsyncWebSocketClient* client(uint32_t id); - bool hasClient(uint32_t id) { return client(id) != nullptr; } - - void close(uint32_t id, uint16_t code = 0, const char* message = NULL); - void closeAll(uint16_t code = 0, const char* message = NULL); - void cleanupClients(uint16_t maxClients = DEFAULT_MAX_WS_CLIENTS); - - void ping(uint32_t id, const uint8_t* data = NULL, size_t len = 0); - void pingAll(const uint8_t* data = NULL, size_t len = 0); // done - - void text(uint32_t id, const uint8_t* message, size_t len); - void text(uint32_t id, const char* message, size_t len); - void text(uint32_t id, const char* message); - void text(uint32_t id, const String& message); - void text(uint32_t id, AsyncWebSocketMessageBuffer* buffer); - void text(uint32_t id, AsyncWebSocketSharedBuffer buffer); - - void textAll(const uint8_t* message, size_t len); - void textAll(const char* message, size_t len); - void textAll(const char* message); - void textAll(const String& message); - void textAll(AsyncWebSocketMessageBuffer* buffer); - void textAll(AsyncWebSocketSharedBuffer buffer); - - void binary(uint32_t id, const uint8_t* message, size_t len); - void binary(uint32_t id, const char* message, size_t len); - void binary(uint32_t id, const char* message); - void binary(uint32_t id, const String& message); - void binary(uint32_t id, AsyncWebSocketMessageBuffer* buffer); - void binary(uint32_t id, AsyncWebSocketSharedBuffer buffer); - - void binaryAll(const uint8_t* message, size_t len); - void binaryAll(const char* message, size_t len); - void binaryAll(const char* message); - void binaryAll(const String& message); - void binaryAll(AsyncWebSocketMessageBuffer* buffer); - void binaryAll(AsyncWebSocketSharedBuffer buffer); - - size_t printf(uint32_t id, const char* format, ...) __attribute__((format(printf, 3, 4))); - size_t printfAll(const char* format, ...) __attribute__((format(printf, 2, 3))); - -#ifdef ESP8266 - void text(uint32_t id, const __FlashStringHelper* message); - void textAll(const __FlashStringHelper* message); - void binary(uint32_t id, const __FlashStringHelper* message, size_t len); - void binaryAll(const __FlashStringHelper* message, size_t len); - size_t printf_P(uint32_t id, PGM_P formatP, ...) __attribute__((format(printf, 3, 4))); - size_t printfAll_P(PGM_P formatP, ...) __attribute__((format(printf, 2, 3))); -#endif - - // event listener - void onEvent(AwsEventHandler handler) { - _eventHandler = handler; - } - - // Handshake Handler - void handleHandshake(AwsHandshakeHandler handler) { - _handshakeHandler = handler; - } - - // system callbacks (do not call) - uint32_t _getNextId() { return _cNextId++; } - AsyncWebSocketClient* _newClient(AsyncWebServerRequest* request); - void _handleEvent(AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); - virtual bool canHandle(AsyncWebServerRequest* request) override final; - virtual void handleRequest(AsyncWebServerRequest* request) override final; - - // messagebuffer functions/objects. - AsyncWebSocketMessageBuffer* makeBuffer(size_t size = 0); - AsyncWebSocketMessageBuffer* makeBuffer(const uint8_t* data, size_t size); - - const std::list& getClients() const { return _clients; } -}; - -// WebServer response to authenticate the socket and detach the tcp client from the web server request -class AsyncWebSocketResponse : public AsyncWebServerResponse { - private: - String _content; - AsyncWebSocket* _server; - - public: - AsyncWebSocketResponse(const String& key, AsyncWebSocket* server); - void _respond(AsyncWebServerRequest* request); - size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time); - bool _sourceValid() const { return true; } -}; - -#endif /* ASYNCWEBSOCKET_H_ */ diff --git a/lib/ESPAsyncWebServer/src/ChunkPrint.h b/lib/ESPAsyncWebServer/src/ChunkPrint.h deleted file mode 100644 index 2f40741..0000000 --- a/lib/ESPAsyncWebServer/src/ChunkPrint.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef CHUNKPRINT_H -#define CHUNKPRINT_H - -#include - -class ChunkPrint : public Print { - private: - uint8_t* _destination; - size_t _to_skip; - size_t _to_write; - size_t _pos; - - public: - ChunkPrint(uint8_t* destination, size_t from, size_t len) - : _destination(destination), _to_skip(from), _to_write(len), _pos{0} {} - virtual ~ChunkPrint() {} - size_t write(uint8_t c) { - if (_to_skip > 0) { - _to_skip--; - return 1; - } else if (_to_write > 0) { - _to_write--; - _destination[_pos++] = c; - return 1; - } - return 0; - } - size_t write(const uint8_t* buffer, size_t size) { - return this->Print::write(buffer, size); - } -}; -#endif diff --git a/lib/ESPAsyncWebServer/src/ESPAsyncWebServer.h b/lib/ESPAsyncWebServer/src/ESPAsyncWebServer.h deleted file mode 100644 index de08bc0..0000000 --- a/lib/ESPAsyncWebServer/src/ESPAsyncWebServer.h +++ /dev/null @@ -1,714 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#ifndef _ESPAsyncWebServer_H_ -#define _ESPAsyncWebServer_H_ - -#include "Arduino.h" - -#include "FS.h" -#include -#include -#include - -#ifdef ESP32 - #include - #include -#elif defined(ESP8266) - #include - #include -#elif defined(TARGET_RP2040) - #include - #include - #include - #include -#else - #error Platform not supported -#endif - -#include "literals.h" - -#define ASYNCWEBSERVER_VERSION "3.1.5" -#define ASYNCWEBSERVER_VERSION_MAJOR 3 -#define ASYNCWEBSERVER_VERSION_MINOR 1 -#define ASYNCWEBSERVER_VERSION_REVISION 5 -#define ASYNCWEBSERVER_FORK_mathieucarbou - -#ifdef ASYNCWEBSERVER_REGEX - #define ASYNCWEBSERVER_REGEX_ATTRIBUTE -#else - #define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined"))) -#endif - -class AsyncWebServer; -class AsyncWebServerRequest; -class AsyncWebServerResponse; -class AsyncWebHeader; -class AsyncWebParameter; -class AsyncWebRewrite; -class AsyncWebHandler; -class AsyncStaticWebHandler; -class AsyncCallbackWebHandler; -class AsyncResponseStream; - -#if defined(TARGET_RP2040) -typedef enum http_method WebRequestMethod; -#else - #ifndef WEBSERVER_H -typedef enum { - HTTP_GET = 0b00000001, - HTTP_POST = 0b00000010, - HTTP_DELETE = 0b00000100, - HTTP_PUT = 0b00001000, - HTTP_PATCH = 0b00010000, - HTTP_HEAD = 0b00100000, - HTTP_OPTIONS = 0b01000000, - HTTP_ANY = 0b01111111, -} WebRequestMethod; - #endif -#endif - -#ifndef HAVE_FS_FILE_OPEN_MODE -namespace fs { - class FileOpenMode { - public: - static const char* read; - static const char* write; - static const char* append; - }; -}; -#else - #include "FileOpenMode.h" -#endif - -// if this value is returned when asked for data, packet will not be sent and you will be asked for data again -#define RESPONSE_TRY_AGAIN 0xFFFFFFFF - -typedef uint8_t WebRequestMethodComposite; -typedef std::function ArDisconnectHandler; - -/* - * PARAMETER :: Chainable object to hold GET/POST and FILE parameters - * */ - -class AsyncWebParameter { - private: - String _name; - String _value; - size_t _size; - bool _isForm; - bool _isFile; - - public: - AsyncWebParameter(const String& name, const String& value, bool form = false, bool file = false, size_t size = 0) : _name(name), _value(value), _size(size), _isForm(form), _isFile(file) {} - const String& name() const { return _name; } - const String& value() const { return _value; } - size_t size() const { return _size; } - bool isPost() const { return _isForm; } - bool isFile() const { return _isFile; } -}; - -/* - * HEADER :: Chainable object to hold the headers - * */ - -class AsyncWebHeader { - private: - String _name; - String _value; - - public: - AsyncWebHeader() = default; - AsyncWebHeader(const AsyncWebHeader&) = default; - - AsyncWebHeader(const char* name, const char* value) : _name(name), _value(value) {} - AsyncWebHeader(const String& name, const String& value) : _name(name), _value(value) {} - AsyncWebHeader(const String& data) { - if (!data) - return; - int index = data.indexOf(':'); - if (index < 0) - return; - _name = data.substring(0, index); - _value = data.substring(index + 2); - } - - AsyncWebHeader& operator=(const AsyncWebHeader&) = default; - - const String& name() const { return _name; } - const String& value() const { return _value; } - String toString() const { - String str = _name; - str.concat((char)0x3a); - str.concat((char)0x20); - str.concat(_value); - str.concat(asyncsrv::T_rn); - return str; - } -}; - -/* - * REQUEST :: Each incoming Client is wrapped inside a Request and both live together until disconnect - * */ - -typedef enum { RCT_NOT_USED = -1, - RCT_DEFAULT = 0, - RCT_HTTP, - RCT_WS, - RCT_EVENT, - RCT_MAX } RequestedConnectionType; - -typedef std::function AwsResponseFiller; -typedef std::function AwsTemplateProcessor; - -class AsyncWebServerRequest { - using File = fs::File; - using FS = fs::FS; - friend class AsyncWebServer; - friend class AsyncCallbackWebHandler; - - private: - AsyncClient* _client; - AsyncWebServer* _server; - AsyncWebHandler* _handler; - AsyncWebServerResponse* _response; - std::vector _interestingHeaders; - ArDisconnectHandler _onDisconnectfn; - - String _temp; - uint8_t _parseState; - - uint8_t _version; - WebRequestMethodComposite _method; - String _url; - String _host; - String _contentType; - String _boundary; - String _authorization; - RequestedConnectionType _reqconntype; - void _removeNotInterestingHeaders(); - bool _isDigest; - bool _isMultipart; - bool _isPlainPost; - bool _expectingContinue; - size_t _contentLength; - size_t _parsedLength; - - std::list _headers; - std::list _params; - std::vector _pathParams; - - uint8_t _multiParseState; - uint8_t _boundaryPosition; - size_t _itemStartIndex; - size_t _itemSize; - String _itemName; - String _itemFilename; - String _itemType; - String _itemValue; - uint8_t* _itemBuffer; - size_t _itemBufferIndex; - bool _itemIsFile; - - void _onPoll(); - void _onAck(size_t len, uint32_t time); - void _onError(int8_t error); - void _onTimeout(uint32_t time); - void _onDisconnect(); - void _onData(void* buf, size_t len); - - void _addPathParam(const char* param); - - bool _parseReqHead(); - bool _parseReqHeader(); - void _parseLine(); - void _parsePlainPostChar(uint8_t data); - void _parseMultipartPostByte(uint8_t data, bool last); - void _addGetParams(const String& params); - - void _handleUploadStart(); - void _handleUploadByte(uint8_t data, bool last); - void _handleUploadEnd(); - - public: - File _tempFile; - void* _tempObject; - - AsyncWebServerRequest(AsyncWebServer*, AsyncClient*); - ~AsyncWebServerRequest(); - - AsyncClient* client() { return _client; } - uint8_t version() const { return _version; } - WebRequestMethodComposite method() const { return _method; } - const String& url() const { return _url; } - const String& host() const { return _host; } - const String& contentType() const { return _contentType; } - size_t contentLength() const { return _contentLength; } - bool multipart() const { return _isMultipart; } - -#ifndef ESP8266 - const char* methodToString() const; - const char* requestedConnTypeToString() const; -#else - const __FlashStringHelper* methodToString() const; - const __FlashStringHelper* requestedConnTypeToString() const; -#endif - - RequestedConnectionType requestedConnType() const { return _reqconntype; } - bool isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2 = RCT_NOT_USED, RequestedConnectionType erct3 = RCT_NOT_USED); - void onDisconnect(ArDisconnectHandler fn); - - // hash is the string representation of: - // base64(user:pass) for basic or - // user:realm:md5(user:realm:pass) for digest - bool authenticate(const char* hash); - bool authenticate(const char* username, const char* password, const char* realm = NULL, bool passwordIsHash = false); - void requestAuthentication(const char* realm = NULL, bool isDigest = true); - - void setHandler(AsyncWebHandler* handler) { _handler = handler; } - - /** - * @brief add header to collect from a response - * - * @param name - */ - void addInterestingHeader(const char* name); - void addInterestingHeader(const String& name) { return addInterestingHeader(name.c_str()); }; - - /** - * @brief issue 302 redirect response - * - * @param url - */ - void redirect(const char* url); - void redirect(const String& url) { return redirect(url.c_str()); }; - - void send(AsyncWebServerResponse* response); - - void send(int code, const char* contentType = asyncsrv::empty, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, callback)); } - void send(int code, const String& contentType, const String& content = emptyString, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, callback)); } - - void send(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, len, callback)); } - void send(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, len, callback)); } - - void send(FS& fs, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) { - if (fs.exists(path) || (!download && fs.exists(path + asyncsrv::T__gz))) { - send(beginResponse(fs, path, contentType, download, callback)); - } else - send(404); - } - void send(FS& fs, const String& path, const String& contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { send(fs, path, contentType.c_str(), download, callback); } - - void send(File content, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) { - if (content) { - send(beginResponse(content, path, contentType, download, callback)); - } else - send(404); - } - void send(File content, const String& path, const String& contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { send(content, path, contentType.c_str(), download, callback); } - - void send(Stream& stream, const char* contentType, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(stream, contentType, len, callback)); } - void send(Stream& stream, const String& contentType, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(stream, contentType, len, callback)); } - - void send(const char* contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginResponse(contentType, len, callback, templateCallback)); } - void send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginResponse(contentType, len, callback, templateCallback)); } - - void sendChunked(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginChunkedResponse(contentType, callback, templateCallback)); } - void sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginChunkedResponse(contentType, callback, templateCallback)); } - - [[deprecated("Replaced by send(...)")]] - void send_P(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { - send(code, contentType, content, len, callback); - } - [[deprecated("Replaced by send(...)")]] - void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) { - send(code, contentType, content, callback); - } - -#ifdef ESP8266 - void send(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, callback)); } -#endif - - AsyncWebServerResponse* beginResponse(int code, const char* contentType = asyncsrv::empty, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse* beginResponse(int code, const String& contentType, const String& content = emptyString, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content.c_str(), callback); } - - AsyncWebServerResponse* beginResponse(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse* beginResponse(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content, len, callback); } - - AsyncWebServerResponse* beginResponse(FS& fs, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse* beginResponse(FS& fs, const String& path, const String& contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { return beginResponse(fs, path, contentType.c_str(), download, callback); } - - AsyncWebServerResponse* beginResponse(File content, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse* beginResponse(File content, const String& path, const String& contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { return beginResponse(content, path, contentType.c_str(), download, callback); } - - AsyncWebServerResponse* beginResponse(Stream& stream, const char* contentType, size_t len, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse* beginResponse(Stream& stream, const String& contentType, size_t len, AwsTemplateProcessor callback = nullptr) { return beginResponse(stream, contentType.c_str(), len, callback); } - - AsyncWebServerResponse* beginResponse(const char* contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - AsyncWebServerResponse* beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { return beginResponse(contentType.c_str(), len, callback, templateCallback); } - - AsyncWebServerResponse* beginChunkedResponse(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - AsyncWebServerResponse* beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - - AsyncResponseStream* beginResponseStream(const char* contentType, size_t bufferSize = 1460); - AsyncResponseStream* beginResponseStream(const String& contentType, size_t bufferSize = 1460) { return beginResponseStream(contentType.c_str(), bufferSize); } - - [[deprecated("Replaced by beginResponse(...)")]] - AsyncWebServerResponse* beginResponse_P(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { - return beginResponse(code, contentType, content, len, callback); - } - [[deprecated("Replaced by beginResponse(...)")]] - AsyncWebServerResponse* beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) { - return beginResponse(code, contentType, content, callback); - } - -#ifdef ESP8266 - AsyncWebServerResponse* beginResponse(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr); -#endif - - size_t headers() const; // get header count - - // check if header exists - bool hasHeader(const char* name) const; - bool hasHeader(const String& name) const { return hasHeader(name.c_str()); }; -#ifdef ESP8266 - bool hasHeader(const __FlashStringHelper* data) const; // check if header exists -#endif - - const AsyncWebHeader* getHeader(const char* name) const; - const AsyncWebHeader* getHeader(const String& name) const { return getHeader(name.c_str()); }; -#ifdef ESP8266 - const AsyncWebHeader* getHeader(const __FlashStringHelper* data) const; -#endif - const AsyncWebHeader* getHeader(size_t num) const; - - size_t params() const; // get arguments count - bool hasParam(const char* name, bool post = false, bool file = false) const; - bool hasParam(const String& name, bool post = false, bool file = false) const { return hasParam(name.c_str(), post, file); }; -#ifdef ESP8266 - bool hasParam(const __FlashStringHelper* data, bool post = false, bool file = false) const { return hasParam(String(data).c_str(), post, file); }; -#endif - - /** - * @brief Get the Request parameter by name - * - * @param name - * @param post - * @param file - * @return const AsyncWebParameter* - */ - const AsyncWebParameter* getParam(const char* name, bool post = false, bool file = false) const; - - const AsyncWebParameter* getParam(const String& name, bool post = false, bool file = false) const { return getParam(name.c_str(), post, file); }; -#ifdef ESP8266 - const AsyncWebParameter* getParam(const __FlashStringHelper* data, bool post, bool file) const; -#endif - - /** - * @brief Get request parameter by number - * i.e., n-th parameter - * @param num - * @return const AsyncWebParameter* - */ - const AsyncWebParameter* getParam(size_t num) const; - - size_t args() const { return params(); } // get arguments count - - // get request argument value by name - const String& arg(const char* name) const; - // get request argument value by name - const String& arg(const String& name) const { return arg(name.c_str()); }; -#ifdef ESP8266 - const String& arg(const __FlashStringHelper* data) const; // get request argument value by F(name) -#endif - const String& arg(size_t i) const; // get request argument value by number - const String& argName(size_t i) const; // get request argument name by number - bool hasArg(const char* name) const; // check if argument exists - bool hasArg(const String& name) const { return hasArg(name.c_str()); }; -#ifdef ESP8266 - bool hasArg(const __FlashStringHelper* data) const; // check if F(argument) exists -#endif - - const String& ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const; - - // get request header value by name - const String& header(const char* name) const; - const String& header(const String& name) const { return header(name.c_str()); }; - -#ifdef ESP8266 - const String& header(const __FlashStringHelper* data) const; // get request header value by F(name) -#endif - - const String& header(size_t i) const; // get request header value by number - const String& headerName(size_t i) const; // get request header name by number - - String urlDecode(const String& text) const; -}; - -/* - * FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server) - * */ - -using ArRequestFilterFunction = std::function; - -bool ON_STA_FILTER(AsyncWebServerRequest* request); - -bool ON_AP_FILTER(AsyncWebServerRequest* request); - -/* - * REWRITE :: One instance can be handle any Request (done by the Server) - * */ - -class AsyncWebRewrite { - protected: - String _from; - String _toUrl; - String _params; - ArRequestFilterFunction _filter{nullptr}; - - public: - AsyncWebRewrite(const char* from, const char* to) : _from(from), _toUrl(to) { - int index = _toUrl.indexOf('?'); - if (index > 0) { - _params = _toUrl.substring(index + 1); - _toUrl = _toUrl.substring(0, index); - } - } - virtual ~AsyncWebRewrite() {} - AsyncWebRewrite& setFilter(ArRequestFilterFunction fn) { - _filter = fn; - return *this; - } - bool filter(AsyncWebServerRequest* request) const { return _filter == NULL || _filter(request); } - const String& from(void) const { return _from; } - const String& toUrl(void) const { return _toUrl; } - const String& params(void) const { return _params; } - virtual bool match(AsyncWebServerRequest* request) { return from() == request->url() && filter(request); } -}; - -/* - * HANDLER :: One instance can be attached to any Request (done by the Server) - * */ - -class AsyncWebHandler { - protected: - ArRequestFilterFunction _filter{nullptr}; - String _username; - String _password; - - public: - AsyncWebHandler() {} - AsyncWebHandler& setFilter(ArRequestFilterFunction fn) { - _filter = fn; - return *this; - } - AsyncWebHandler& setAuthentication(const char* username, const char* password) { - _username = username; - _password = password; - return *this; - }; - AsyncWebHandler& setAuthentication(const String& username, const String& password) { - _username = username; - _password = password; - return *this; - }; - bool filter(AsyncWebServerRequest* request) { return _filter == NULL || _filter(request); } - virtual ~AsyncWebHandler() {} - virtual bool canHandle(AsyncWebServerRequest* request __attribute__((unused))) { - return false; - } - virtual void handleRequest(AsyncWebServerRequest* request __attribute__((unused))) {} - virtual void handleUpload(AsyncWebServerRequest* request __attribute__((unused)), const String& filename __attribute__((unused)), size_t index __attribute__((unused)), uint8_t* data __attribute__((unused)), size_t len __attribute__((unused)), bool final __attribute__((unused))) {} - virtual void handleBody(AsyncWebServerRequest* request __attribute__((unused)), uint8_t* data __attribute__((unused)), size_t len __attribute__((unused)), size_t index __attribute__((unused)), size_t total __attribute__((unused))) {} - virtual bool isRequestHandlerTrivial() { return true; } -}; - -/* - * RESPONSE :: One instance is created for each Request (attached by the Handler) - * */ - -typedef enum { - RESPONSE_SETUP, - RESPONSE_HEADERS, - RESPONSE_CONTENT, - RESPONSE_WAIT_ACK, - RESPONSE_END, - RESPONSE_FAILED -} WebResponseState; - -class AsyncWebServerResponse { - protected: - int _code; - std::list _headers; - String _contentType; - size_t _contentLength; - bool _sendContentLength; - bool _chunked; - size_t _headLength; - size_t _sentLength; - size_t _ackedLength; - size_t _writtenLength; - WebResponseState _state; - - public: -#ifndef ESP8266 - static const char* responseCodeToString(int code); -#else - static const __FlashStringHelper* responseCodeToString(int code); -#endif - - public: - AsyncWebServerResponse(); - virtual ~AsyncWebServerResponse(); - virtual void setCode(int code); - virtual void setContentLength(size_t len); - void setContentType(const String& type) { setContentType(type.c_str()); } - virtual void setContentType(const char* type); - virtual void addHeader(const char* name, const char* value); - void addHeader(const String& name, const String& value) { addHeader(name.c_str(), value.c_str()); } - virtual String _assembleHead(uint8_t version); - virtual bool _started() const; - virtual bool _finished() const; - virtual bool _failed() const; - virtual bool _sourceValid() const; - virtual void _respond(AsyncWebServerRequest* request); - virtual size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time); -}; - -/* - * SERVER :: One instance - * */ - -typedef std::function ArRequestHandlerFunction; -typedef std::function ArUploadHandlerFunction; -typedef std::function ArBodyHandlerFunction; - -class AsyncWebServer { - protected: - AsyncServer _server; - std::list> _rewrites; - std::list> _handlers; - AsyncCallbackWebHandler* _catchAllHandler; - - public: - AsyncWebServer(uint16_t port); - ~AsyncWebServer(); - - void begin(); - void end(); - -#if ASYNC_TCP_SSL_ENABLED - void onSslFileRequest(AcSSlFileHandler cb, void* arg); - void beginSecure(const char* cert, const char* private_key_file, const char* password); -#endif - - AsyncWebRewrite& addRewrite(AsyncWebRewrite* rewrite); - - /** - * @brief (compat) Add url rewrite rule by pointer - * a deep copy of the pounter object will be created, - * it is up to user to manage further lifetime of the object in argument - * - * @param rewrite pointer to rewrite object to copy setting from - * @return AsyncWebRewrite& reference to a newly created rewrite rule - */ - AsyncWebRewrite& addRewrite(std::shared_ptr rewrite); - - /** - * @brief add url rewrite rule - * - * @param from - * @param to - * @return AsyncWebRewrite& - */ - AsyncWebRewrite& rewrite(const char* from, const char* to); - - /** - * @brief (compat) remove rewrite rule via referenced object - * this will NOT deallocate pointed object itself, internal rule with same from/to urls will be removed if any - * it's a compat method, better use `removeRewrite(const char* from, const char* to)` - * @param rewrite - * @return true - * @return false - */ - bool removeRewrite(AsyncWebRewrite* rewrite); - - /** - * @brief remove rewrite rule - * - * @param from - * @param to - * @return true - * @return false - */ - bool removeRewrite(const char* from, const char* to); - - AsyncWebHandler& addHandler(AsyncWebHandler* handler); - bool removeHandler(AsyncWebHandler* handler); - - AsyncCallbackWebHandler& on(const char* uri, ArRequestHandlerFunction onRequest); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody); - - AsyncStaticWebHandler& serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); - - void onNotFound(ArRequestHandlerFunction fn); // called when handler is not assigned - void onFileUpload(ArUploadHandlerFunction fn); // handle file uploads - void onRequestBody(ArBodyHandlerFunction fn); // handle posts with plain body content (JSON often transmitted this way as a request) - - void reset(); // remove all writers and handlers, with onNotFound/onFileUpload/onRequestBody - - void _handleDisconnect(AsyncWebServerRequest* request); - void _attachHandler(AsyncWebServerRequest* request); - void _rewriteRequest(AsyncWebServerRequest* request); -}; - -class DefaultHeaders { - using headers_t = std::list; - headers_t _headers; - - public: - DefaultHeaders() = default; - - using ConstIterator = headers_t::const_iterator; - - void addHeader(const String& name, const String& value) { - _headers.emplace_back(name, value); - } - - ConstIterator begin() const { return _headers.begin(); } - ConstIterator end() const { return _headers.end(); } - - DefaultHeaders(DefaultHeaders const&) = delete; - DefaultHeaders& operator=(DefaultHeaders const&) = delete; - - static DefaultHeaders& Instance() { - static DefaultHeaders instance; - return instance; - } -}; - -#include "AsyncEventSource.h" -#include "AsyncWebSocket.h" -#include "WebHandlerImpl.h" -#include "WebResponseImpl.h" - -#endif /* _AsyncWebServer_H_ */ diff --git a/lib/ESPAsyncWebServer/src/WebAuthentication.cpp b/lib/ESPAsyncWebServer/src/WebAuthentication.cpp deleted file mode 100644 index b5962fc..0000000 --- a/lib/ESPAsyncWebServer/src/WebAuthentication.cpp +++ /dev/null @@ -1,249 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "WebAuthentication.h" -#include -#if defined(ESP32) || defined(TARGET_RP2040) - #include -#else - #include "md5.h" -#endif -#include "literals.h" - -using namespace asyncsrv; - -// Basic Auth hash = base64("username:password") - -bool checkBasicAuthentication(const char* hash, const char* username, const char* password) { - if (username == NULL || password == NULL || hash == NULL) - return false; - - size_t toencodeLen = strlen(username) + strlen(password) + 1; - size_t encodedLen = base64_encode_expected_len(toencodeLen); - if (strlen(hash) != encodedLen) -// Fix from https://github.com/me-no-dev/ESPAsyncWebServer/issues/667 -#ifdef ARDUINO_ARCH_ESP32 - if (strlen(hash) != encodedLen) -#else - if (strlen(hash) != encodedLen - 1) -#endif - return false; - - char* toencode = new char[toencodeLen + 1]; - if (toencode == NULL) { - return false; - } - char* encoded = new char[base64_encode_expected_len(toencodeLen) + 1]; - if (encoded == NULL) { - delete[] toencode; - return false; - } - sprintf_P(toencode, PSTR("%s:%s"), username, password); - if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && memcmp(hash, encoded, encodedLen) == 0) { - delete[] toencode; - delete[] encoded; - return true; - } - delete[] toencode; - delete[] encoded; - return false; -} - -static bool getMD5(uint8_t* data, uint16_t len, char* output) { // 33 bytes or more -#if defined(ESP32) || defined(TARGET_RP2040) - MD5Builder md5; - md5.begin(); - md5.add(data, len); - md5.calculate(); - md5.getChars(output); -#else - md5_context_t _ctx; - - uint8_t* _buf = (uint8_t*)malloc(16); - if (_buf == NULL) - return false; - memset(_buf, 0x00, 16); - - MD5Init(&_ctx); - MD5Update(&_ctx, data, len); - MD5Final(_buf, &_ctx); - - for (uint8_t i = 0; i < 16; i++) { - sprintf_P(output + (i * 2), PSTR("%02x"), _buf[i]); - } - - free(_buf); -#endif - return true; -} - -static String genRandomMD5() { -#ifdef ESP8266 - uint32_t r = RANDOM_REG32; -#else - uint32_t r = rand(); -#endif - char* out = (char*)malloc(33); - if (out == NULL || !getMD5((uint8_t*)(&r), 4, out)) - return emptyString; - String res = String(out); - free(out); - return res; -} - -static String stringMD5(const String& in) { - char* out = (char*)malloc(33); - if (out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) - return emptyString; - String res = String(out); - free(out); - return res; -} - -String generateDigestHash(const char* username, const char* password, const char* realm) { - if (username == NULL || password == NULL || realm == NULL) { - return emptyString; - } - char* out = (char*)malloc(33); - String res = String(username); - res += ':'; - res.concat(realm); - res += ':'; - String in = res; - in.concat(password); - if (out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) - return emptyString; - res.concat(out); - free(out); - return res; -} - -String requestDigestAuthentication(const char* realm) { - String header(T_realm__); - if (realm == NULL) - header.concat(T_asyncesp); - else - header.concat(realm); - header.concat(T_auth_nonce); - header.concat(genRandomMD5()); - header.concat(T__opaque); - header.concat(genRandomMD5()); - header += (char)0x22; // '"' - return header; -} - -#ifndef ESP8266 -bool checkDigestAuthentication(const char* header, const char* method, const char* username, const char* password, const char* realm, bool passwordIsHash, const char* nonce, const char* opaque, const char* uri) -#else -bool checkDigestAuthentication(const char* header, const __FlashStringHelper* method, const char* username, const char* password, const char* realm, bool passwordIsHash, const char* nonce, const char* opaque, const char* uri) -#endif -{ - if (username == NULL || password == NULL || header == NULL || method == NULL) { - // os_printf("AUTH FAIL: missing requred fields\n"); - return false; - } - - String myHeader(header); - int nextBreak = myHeader.indexOf(','); - if (nextBreak < 0) { - // os_printf("AUTH FAIL: no variables\n"); - return false; - } - - String myUsername; - String myRealm; - String myNonce; - String myUri; - String myResponse; - String myQop; - String myNc; - String myCnonce; - - myHeader += (char)0x2c; // ',' - myHeader += (char)0x20; // ' ' - do { - String avLine(myHeader.substring(0, nextBreak)); - avLine.trim(); - myHeader = myHeader.substring(nextBreak + 1); - nextBreak = myHeader.indexOf(','); - - int eqSign = avLine.indexOf('='); - if (eqSign < 0) { - // os_printf("AUTH FAIL: no = sign\n"); - return false; - } - String varName(avLine.substring(0, eqSign)); - avLine = avLine.substring(eqSign + 1); - if (avLine.startsWith(String('"'))) { - avLine = avLine.substring(1, avLine.length() - 1); - } - - if (varName.equals(T_username)) { - if (!avLine.equals(username)) { - // os_printf("AUTH FAIL: username\n"); - return false; - } - myUsername = avLine; - } else if (varName.equals(T_realm)) { - if (realm != NULL && !avLine.equals(realm)) { - // os_printf("AUTH FAIL: realm\n"); - return false; - } - myRealm = avLine; - } else if (varName.equals(T_nonce)) { - if (nonce != NULL && !avLine.equals(nonce)) { - // os_printf("AUTH FAIL: nonce\n"); - return false; - } - myNonce = avLine; - } else if (varName.equals(T_opaque)) { - if (opaque != NULL && !avLine.equals(opaque)) { - // os_printf("AUTH FAIL: opaque\n"); - return false; - } - } else if (varName.equals(T_uri)) { - if (uri != NULL && !avLine.equals(uri)) { - // os_printf("AUTH FAIL: uri\n"); - return false; - } - myUri = avLine; - } else if (varName.equals(T_response)) { - myResponse = avLine; - } else if (varName.equals(T_qop)) { - myQop = avLine; - } else if (varName.equals(T_nc)) { - myNc = avLine; - } else if (varName.equals(T_cnonce)) { - myCnonce = avLine; - } - } while (nextBreak > 0); - - String ha1 = (passwordIsHash) ? String(password) : stringMD5(myUsername + ':' + myRealm + ':' + password); - String ha2 = String(method) + ':' + myUri; - String response = ha1 + ':' + myNonce + ':' + myNc + ':' + myCnonce + ':' + myQop + ':' + stringMD5(ha2); - - if (myResponse.equals(stringMD5(response))) { - // os_printf("AUTH SUCCESS\n"); - return true; - } - - // os_printf("AUTH FAIL: password\n"); - return false; -} diff --git a/lib/ESPAsyncWebServer/src/WebAuthentication.h b/lib/ESPAsyncWebServer/src/WebAuthentication.h deleted file mode 100644 index d519777..0000000 --- a/lib/ESPAsyncWebServer/src/WebAuthentication.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#ifndef WEB_AUTHENTICATION_H_ -#define WEB_AUTHENTICATION_H_ - -#include "Arduino.h" - -bool checkBasicAuthentication(const char* header, const char* username, const char* password); -String requestDigestAuthentication(const char* realm); - -bool checkDigestAuthentication(const char* header, const char* method, const char* username, const char* password, const char* realm, bool passwordIsHash, const char* nonce, const char* opaque, const char* uri); - -#ifdef ESP8266 -bool checkDigestAuthentication(const char* header, const __FlashStringHelper* method, const char* username, const char* password, const char* realm, bool passwordIsHash, const char* nonce, const char* opaque, const char* uri); -#endif - -// for storing hashed versions on the device that can be authenticated against -String generateDigestHash(const char* username, const char* password, const char* realm); - -#endif diff --git a/lib/ESPAsyncWebServer/src/WebHandlerImpl.h b/lib/ESPAsyncWebServer/src/WebHandlerImpl.h deleted file mode 100644 index 22757d7..0000000 --- a/lib/ESPAsyncWebServer/src/WebHandlerImpl.h +++ /dev/null @@ -1,155 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#ifndef ASYNCWEBSERVERHANDLERIMPL_H_ -#define ASYNCWEBSERVERHANDLERIMPL_H_ - -#include -#ifdef ASYNCWEBSERVER_REGEX - #include -#endif - -#include "stddef.h" -#include - -class AsyncStaticWebHandler : public AsyncWebHandler { - using File = fs::File; - using FS = fs::FS; - - private: - bool _getFile(AsyncWebServerRequest* request); - bool _fileExists(AsyncWebServerRequest* request, const String& path); - uint8_t _countBits(const uint8_t value) const; - - protected: - FS _fs; - String _uri; - String _path; - String _default_file; - String _cache_control; - String _last_modified; - AwsTemplateProcessor _callback; - bool _isDir; - bool _gzipFirst; - uint8_t _gzipStats; - - public: - AsyncStaticWebHandler(const char* uri, FS& fs, const char* path, const char* cache_control); - virtual bool canHandle(AsyncWebServerRequest* request) override final; - virtual void handleRequest(AsyncWebServerRequest* request) override final; - AsyncStaticWebHandler& setIsDir(bool isDir); - AsyncStaticWebHandler& setDefaultFile(const char* filename); - AsyncStaticWebHandler& setCacheControl(const char* cache_control); - AsyncStaticWebHandler& setLastModified(const char* last_modified); - AsyncStaticWebHandler& setLastModified(struct tm* last_modified); -#ifdef ESP8266 - AsyncStaticWebHandler& setLastModified(time_t last_modified); - AsyncStaticWebHandler& setLastModified(); // sets to current time. Make sure sntp is runing and time is updated -#endif - AsyncStaticWebHandler& setTemplateProcessor(AwsTemplateProcessor newCallback) { - _callback = newCallback; - return *this; - } -}; - -class AsyncCallbackWebHandler : public AsyncWebHandler { - private: - protected: - String _uri; - WebRequestMethodComposite _method; - ArRequestHandlerFunction _onRequest; - ArUploadHandlerFunction _onUpload; - ArBodyHandlerFunction _onBody; - bool _isRegex; - - public: - AsyncCallbackWebHandler() : _uri(), _method(HTTP_ANY), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {} - void setUri(const String& uri) { - _uri = uri; - _isRegex = uri.startsWith("^") && uri.endsWith("$"); - } - void setMethod(WebRequestMethodComposite method) { _method = method; } - void onRequest(ArRequestHandlerFunction fn) { _onRequest = fn; } - void onUpload(ArUploadHandlerFunction fn) { _onUpload = fn; } - void onBody(ArBodyHandlerFunction fn) { _onBody = fn; } - - virtual bool canHandle(AsyncWebServerRequest* request) override final { - - if (!_onRequest) - return false; - - if (!(_method & request->method())) - return false; - -#ifdef ASYNCWEBSERVER_REGEX - if (_isRegex) { - std::regex pattern(_uri.c_str()); - std::smatch matches; - std::string s(request->url().c_str()); - if (std::regex_search(s, matches, pattern)) { - for (size_t i = 1; i < matches.size(); ++i) { // start from 1 - request->_addPathParam(matches[i].str().c_str()); - } - } else { - return false; - } - } else -#endif - if (_uri.length() && _uri.startsWith("/*.")) { - String uriTemplate = String(_uri); - uriTemplate = uriTemplate.substring(uriTemplate.lastIndexOf(".")); - if (!request->url().endsWith(uriTemplate)) - return false; - } else if (_uri.length() && _uri.endsWith("*")) { - String uriTemplate = String(_uri); - uriTemplate = uriTemplate.substring(0, uriTemplate.length() - 1); - if (!request->url().startsWith(uriTemplate)) - return false; - } else if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) - return false; - - request->addInterestingHeader("ANY"); - return true; - } - - virtual void handleRequest(AsyncWebServerRequest* request) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onRequest) - _onRequest(request); - else - request->send(500); - } - virtual void handleUpload(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onUpload) - _onUpload(request, filename, index, data, len, final); - } - virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onBody) - _onBody(request, data, len, index, total); - } - virtual bool isRequestHandlerTrivial() override final { return _onRequest ? false : true; } -}; - -#endif /* ASYNCWEBSERVERHANDLERIMPL_H_ */ diff --git a/lib/ESPAsyncWebServer/src/WebHandlers.cpp b/lib/ESPAsyncWebServer/src/WebHandlers.cpp deleted file mode 100644 index d904a39..0000000 --- a/lib/ESPAsyncWebServer/src/WebHandlers.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "ESPAsyncWebServer.h" -#include "WebHandlerImpl.h" - -using namespace asyncsrv; - - -AsyncStaticWebHandler::AsyncStaticWebHandler(const char* uri, FS& fs, const char* path, const char* cache_control) - : _fs(fs), _uri(uri), _path(path), _default_file(F("index.htm")), _cache_control(cache_control), _last_modified(), _callback(nullptr) { - // Ensure leading '/' - if (_uri.length() == 0 || _uri[0] != '/') - _uri = String('/') + _uri; - if (_path.length() == 0 || _path[0] != '/') - _path = String('/') + _path; - - // If path ends with '/' we assume a hint that this is a directory to improve performance. - // However - if it does not end with '/' we, can't assume a file, path can still be a directory. - _isDir = _path[_path.length() - 1] == '/'; - - // Remove the trailing '/' so we can handle default file - // Notice that root will be "" not "/" - if (_uri[_uri.length() - 1] == '/') - _uri = _uri.substring(0, _uri.length() - 1); - if (_path[_path.length() - 1] == '/') - _path = _path.substring(0, _path.length() - 1); - - // Reset stats - _gzipFirst = false; - _gzipStats = 0xF8; -} - -AsyncStaticWebHandler& AsyncStaticWebHandler::setIsDir(bool isDir) { - _isDir = isDir; - return *this; -} - -AsyncStaticWebHandler& AsyncStaticWebHandler::setDefaultFile(const char* filename) { - _default_file = String(filename); - return *this; -} - -AsyncStaticWebHandler& AsyncStaticWebHandler::setCacheControl(const char* cache_control) { - _cache_control = String(cache_control); - return *this; -} - -AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified(const char* last_modified) { - _last_modified = last_modified; - return *this; -} - -AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified(struct tm* last_modified) { - auto formatP = PSTR("%a, %d %b %Y %H:%M:%S %Z"); - char format[strlen_P(formatP) + 1]; - strcpy_P(format, formatP); - - char result[30]; - strftime(result, sizeof(result), format, last_modified); - return setLastModified((const char*)result); -} - -#ifdef ESP8266 -AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified(time_t last_modified) { - return setLastModified((struct tm*)gmtime(&last_modified)); -} - -AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified() { - time_t last_modified; - if (time(&last_modified) == 0) // time is not yet set - return *this; - return setLastModified(last_modified); -} -#endif -bool AsyncStaticWebHandler::canHandle(AsyncWebServerRequest* request) { - if (request->method() != HTTP_GET || !request->url().startsWith(_uri) || !request->isExpectedRequestedConnType(RCT_DEFAULT, RCT_HTTP)) { - return false; - } - if (_getFile(request)) { - // We interested in "If-Modified-Since" header to check if file was modified - if (_last_modified.length()) - request->addInterestingHeader(F("If-Modified-Since")); - - if (_cache_control.length()) - request->addInterestingHeader(F("If-None-Match")); - - return true; - } - - return false; -} - -bool AsyncStaticWebHandler::_getFile(AsyncWebServerRequest* request) { - // Remove the found uri - String path = request->url().substring(_uri.length()); - - // We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/' - bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length() - 1] == '/'); - - path = _path + path; - - // Do we have a file or .gz file - if (!canSkipFileCheck && _fileExists(request, path)) - return true; - - // Can't handle if not default file - if (_default_file.length() == 0) - return false; - - // Try to add default file, ensure there is a trailing '/' ot the path. - if (path.length() == 0 || path[path.length() - 1] != '/') - path += String('/'); - path += _default_file; - - return _fileExists(request, path); -} - -#ifdef ESP32 - #define FILE_IS_REAL(f) (f == true && !f.isDirectory()) -#else - #define FILE_IS_REAL(f) (f == true) -#endif - -bool AsyncStaticWebHandler::_fileExists(AsyncWebServerRequest* request, const String& path) { - bool fileFound = false; - bool gzipFound = false; - - String gzip = path + F(".gz"); - - if (_gzipFirst) { - if (_fs.exists(gzip)) { - request->_tempFile = _fs.open(gzip, fs::FileOpenMode::read); - gzipFound = FILE_IS_REAL(request->_tempFile); - } - if (!gzipFound) { - if (_fs.exists(path)) { - request->_tempFile = _fs.open(path, fs::FileOpenMode::read); - fileFound = FILE_IS_REAL(request->_tempFile); - } - } - } else { - if (_fs.exists(path)) { - request->_tempFile = _fs.open(path, fs::FileOpenMode::read); - fileFound = FILE_IS_REAL(request->_tempFile); - } - if (!fileFound) { - if (_fs.exists(gzip)) { - request->_tempFile = _fs.open(gzip, fs::FileOpenMode::read); - gzipFound = FILE_IS_REAL(request->_tempFile); - } - } - } - - bool found = fileFound || gzipFound; - - if (found) { - // Extract the file name from the path and keep it in _tempObject - size_t pathLen = path.length(); - char* _tempPath = (char*)malloc(pathLen + 1); - snprintf_P(_tempPath, pathLen + 1, PSTR("%s"), path.c_str()); - request->_tempObject = (void*)_tempPath; - - // Calculate gzip statistic - _gzipStats = (_gzipStats << 1) + (gzipFound ? 1 : 0); - if (_gzipStats == 0x00) - _gzipFirst = false; // All files are not gzip - else if (_gzipStats == 0xFF) - _gzipFirst = true; // All files are gzip - else - _gzipFirst = _countBits(_gzipStats) > 4; // IF we have more gzip files - try gzip first - } - - return found; -} - -uint8_t AsyncStaticWebHandler::_countBits(const uint8_t value) const { - uint8_t w = value; - uint8_t n; - for (n = 0; w != 0; n++) - w &= w - 1; - return n; -} - -void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest* request) { - // Get the filename from request->_tempObject and free it - String filename = String((char*)request->_tempObject); - free(request->_tempObject); - request->_tempObject = NULL; - if ((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - - if (request->_tempFile == true) { - time_t lw = request->_tempFile.getLastWrite(); // get last file mod time (if supported by FS) - // set etag to lastmod timestamp if available, otherwise to size - String etag; - if (lw) { - setLastModified(gmtime(&lw)); -#if defined(TARGET_RP2040) - // time_t == long long int - const size_t len = 1 + 8 * sizeof(time_t); - char buf[len]; - char* ret = lltoa(lw, buf, len, 10); - etag = ret ? String(ret) : String(request->_tempFile.size()); -#else - etag = String(lw); -#endif - } else { - etag = String(request->_tempFile.size()); - } - if (_last_modified.length() && _last_modified == request->header(T_IMS)) { - request->_tempFile.close(); - request->send(304); // Not modified - } else if (_cache_control.length() && request->hasHeader(T_INM) && request->header(T_INM).equals(etag)) { - request->_tempFile.close(); - AsyncWebServerResponse* response = new AsyncBasicResponse(304); // Not modified - response->addHeader(T_Cache_Control, _cache_control.c_str()); - response->addHeader(T_ETag, etag.c_str()); - request->send(response); - } else { - AsyncWebServerResponse* response = new AsyncFileResponse(request->_tempFile, filename, String(), false, _callback); - if (_last_modified.length()) - response->addHeader(T_Last_Modified, _last_modified.c_str()); - if (_cache_control.length()) { - response->addHeader(T_Cache_Control, _cache_control.c_str()); - response->addHeader(T_ETag, etag.c_str()); - } - request->send(response); - } - } else { - request->send(404); - } -} diff --git a/lib/ESPAsyncWebServer/src/WebRequest.cpp b/lib/ESPAsyncWebServer/src/WebRequest.cpp deleted file mode 100644 index 95fda7e..0000000 --- a/lib/ESPAsyncWebServer/src/WebRequest.cpp +++ /dev/null @@ -1,985 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "ESPAsyncWebServer.h" -#include "WebAuthentication.h" -#include "WebResponseImpl.h" -#include "literals.h" -#include - -#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '=')) - -using namespace asyncsrv; - -enum { PARSE_REQ_START, - PARSE_REQ_HEADERS, - PARSE_REQ_BODY, - PARSE_REQ_END, - PARSE_REQ_FAIL }; - -AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) - : _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(0), _version(0), _method(HTTP_ANY), _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _isDigest(false), _isMultipart(false), _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { - c->onError([](void* r, AsyncClient* c, int8_t error) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this); - c->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onAck(len, time); }, this); - c->onDisconnect([](void* r, AsyncClient* c) { AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onDisconnect(); delete c; }, this); - c->onTimeout([](void* r, AsyncClient* c, uint32_t time) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onTimeout(time); }, this); - c->onData([](void* r, AsyncClient* c, void* buf, size_t len) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onData(buf, len); }, this); - c->onPoll([](void* r, AsyncClient* c) { (void)c; AsyncWebServerRequest *req = ( AsyncWebServerRequest*)r; req->_onPoll(); }, this); -} - -AsyncWebServerRequest::~AsyncWebServerRequest() { - _headers.clear(); - - _pathParams.clear(); - - _interestingHeaders.clear(); - - if (_response != NULL) { - delete _response; - } - - if (_tempObject != NULL) { - free(_tempObject); - } - - if (_tempFile) { - _tempFile.close(); - } - - if (_itemBuffer) { - free(_itemBuffer); - } -} - -void AsyncWebServerRequest::_onData(void* buf, size_t len) { - size_t i = 0; - while (true) { - - if (_parseState < PARSE_REQ_BODY) { - // Find new line in buf - char* str = (char*)buf; - for (i = 0; i < len; i++) { - if (str[i] == '\n') { - break; - } - } - if (i == len) { // No new line, just add the buffer in _temp - char ch = str[len - 1]; - str[len - 1] = 0; - _temp.reserve(_temp.length() + len); - _temp.concat(str); - _temp.concat(ch); - } else { // Found new line - extract it and parse - str[i] = 0; // Terminate the string at the end of the line. - _temp.concat(str); - _temp.trim(); - _parseLine(); - if (++i < len) { - // Still have more buffer to process - buf = str + i; - len -= i; - continue; - } - } - } else if (_parseState == PARSE_REQ_BODY) { - // A handler should be already attached at this point in _parseLine function. - // If handler does nothing (_onRequest is NULL), we don't need to really parse the body. - const bool needParse = _handler && !_handler->isRequestHandlerTrivial(); - if (_isMultipart) { - if (needParse) { - size_t i; - for (i = 0; i < len; i++) { - _parseMultipartPostByte(((uint8_t*)buf)[i], i == len - 1); - _parsedLength++; - } - } else - _parsedLength += len; - } else { - if (_parsedLength == 0) { - if (_contentType.startsWith(T_app_xform_urlencoded)) { - _isPlainPost = true; - } else if (_contentType == T_text_plain && __is_param_char(((char*)buf)[0])) { - size_t i = 0; - while (i < len && __is_param_char(((char*)buf)[i++])) - ; - if (i < len && ((char*)buf)[i - 1] == '=') { - _isPlainPost = true; - } - } - } - if (!_isPlainPost) { - // check if authenticated before calling the body - if (_handler) - _handler->handleBody(this, (uint8_t*)buf, len, _parsedLength, _contentLength); - _parsedLength += len; - } else if (needParse) { - size_t i; - for (i = 0; i < len; i++) { - _parsedLength++; - _parsePlainPostChar(((uint8_t*)buf)[i]); - } - } else { - _parsedLength += len; - } - } - if (_parsedLength == _contentLength) { - _parseState = PARSE_REQ_END; - // check if authenticated before calling handleRequest and request auth instead - if (_handler) - _handler->handleRequest(this); - else - send(501); - } - } - break; - } -} - -void AsyncWebServerRequest::_removeNotInterestingHeaders() { - if (std::any_of(std::begin(_interestingHeaders), std::end(_interestingHeaders), [](const String& str) { return str.equalsIgnoreCase(T_ANY); })) - return; // nothing to do - - for (auto iter = std::begin(_headers); iter != std::end(_headers);) { - const auto name = iter->name(); - - if (std::none_of(std::begin(_interestingHeaders), std::end(_interestingHeaders), [&name](const String& str) { return str.equalsIgnoreCase(name); })) - iter = _headers.erase(iter); - else - iter++; - } -} - -void AsyncWebServerRequest::_onPoll() { - // os_printf("p\n"); - if (_response != NULL && _client != NULL && _client->canSend()) { - if (!_response->_finished()) { - _response->_ack(this, 0, 0); - } else { - AsyncWebServerResponse* r = _response; - _response = NULL; - delete r; - - _client->close(); - } - } -} - -void AsyncWebServerRequest::_onAck(size_t len, uint32_t time) { - // os_printf("a:%u:%u\n", len, time); - if (_response != NULL) { - if (!_response->_finished()) { - _response->_ack(this, len, time); - } else if (_response->_finished()) { - AsyncWebServerResponse* r = _response; - _response = NULL; - delete r; - - _client->close(); - } - } -} - -void AsyncWebServerRequest::_onError(int8_t error) { - (void)error; -} - -void AsyncWebServerRequest::_onTimeout(uint32_t time) { - (void)time; - // os_printf("TIMEOUT: %u, state: %s\n", time, _client->stateToString()); - _client->close(); -} - -void AsyncWebServerRequest::onDisconnect(ArDisconnectHandler fn) { - _onDisconnectfn = fn; -} - -void AsyncWebServerRequest::_onDisconnect() { - // os_printf("d\n"); - if (_onDisconnectfn) { - _onDisconnectfn(); - } - _server->_handleDisconnect(this); -} - -void AsyncWebServerRequest::_addPathParam(const char* p) { - _pathParams.emplace_back(p); -} - -void AsyncWebServerRequest::_addGetParams(const String& params) { - size_t start = 0; - while (start < params.length()) { - int end = params.indexOf('&', start); - if (end < 0) - end = params.length(); - int equal = params.indexOf('=', start); - if (equal < 0 || equal > end) - equal = end; - String name(params.substring(start, equal)); - String value(equal + 1 < end ? params.substring(equal + 1, end) : String()); - _params.emplace_back(urlDecode(name), urlDecode(value)); - start = end + 1; - } -} - -bool AsyncWebServerRequest::_parseReqHead() { - // Split the head into method, url and version - int index = _temp.indexOf(' '); - String m = _temp.substring(0, index); - index = _temp.indexOf(' ', index + 1); - String u = _temp.substring(m.length() + 1, index); - _temp = _temp.substring(index + 1); - - if (m == T_GET) { - _method = HTTP_GET; - } else if (m == T_POST) { - _method = HTTP_POST; - } else if (m == T_DELETE) { - _method = HTTP_DELETE; - } else if (m == T_PUT) { - _method = HTTP_PUT; - } else if (m == T_PATCH) { - _method = HTTP_PATCH; - } else if (m == T_HEAD) { - _method = HTTP_HEAD; - } else if (m == T_OPTIONS) { - _method = HTTP_OPTIONS; - } - - String g; - index = u.indexOf('?'); - if (index > 0) { - g = u.substring(index + 1); - u = u.substring(0, index); - } - _url = urlDecode(u); - _addGetParams(g); - - if (!_temp.startsWith(T_HTTP_1_0)) - _version = 1; - - _temp = emptyString; - return true; -} - -bool AsyncWebServerRequest::_parseReqHeader() { - int index = _temp.indexOf(':'); - if (index) { - String name(_temp.substring(0, index)); - String value(_temp.substring(index + 2)); - if (name.equalsIgnoreCase(T_Host)) { - _host = value; - } else if (name.equalsIgnoreCase(T_Content_Type)) { - _contentType = value.substring(0, value.indexOf(';')); - if (value.startsWith(T_MULTIPART_)) { - _boundary = value.substring(value.indexOf('=') + 1); - _boundary.replace(String('"'), String()); - _isMultipart = true; - } - } else if (name.equalsIgnoreCase(T_Content_Length)) { - _contentLength = atoi(value.c_str()); - } else if (name.equalsIgnoreCase(T_EXPECT) && value == T_100_CONTINUE) { - _expectingContinue = true; - } else if (name.equalsIgnoreCase(T_AUTH)) { - if (value.length() > 5 && value.substring(0, 5).equalsIgnoreCase(T_BASIC)) { - _authorization = value.substring(6); - } else if (value.length() > 6 && value.substring(0, 6).equalsIgnoreCase(T_DIGEST)) { - _isDigest = true; - _authorization = value.substring(7); - } - } else { - if (name.equalsIgnoreCase(T_UPGRADE) && value.equalsIgnoreCase(T_WS)) { - // WebSocket request can be uniquely identified by header: [Upgrade: websocket] - _reqconntype = RCT_WS; - } else if (name.equalsIgnoreCase(T_ACCEPT)) { - String lowcase(value); - lowcase.toLowerCase(); -#ifndef ESP8266 - const char* substr = std::strstr(lowcase.c_str(), T_text_event_stream); -#else - const char* substr = std::strstr(lowcase.c_str(), String(T_text_event_stream).c_str()); -#endif - if (substr != NULL) { - // WebEvent request can be uniquely identified by header: [Accept: text/event-stream] - _reqconntype = RCT_EVENT; - } - } - } - _headers.emplace_back(name, value); - } -#ifndef TARGET_RP2040 - _temp.clear(); -#else - // Ancient PRI core does not have String::clear() method 8-() - _temp = emptyString; -#endif - return true; -} - -void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) { - if (data && (char)data != '&') - _temp += (char)data; - if (!data || (char)data == '&' || _parsedLength == _contentLength) { - String name(T_BODY); - String value(_temp); - if (!(_temp.charAt(0) == '{') && !(_temp.charAt(0) == '[') && _temp.indexOf('=') > 0) { - name = _temp.substring(0, _temp.indexOf('=')); - value = _temp.substring(_temp.indexOf('=') + 1); - } - _params.emplace_back(urlDecode(name), urlDecode(value), true); - -#ifndef TARGET_RP2040 - _temp.clear(); -#else - // Ancient PRI core does not have String::clear() method 8-() - _temp = emptyString; -#endif - } -} - -void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last) { - _itemBuffer[_itemBufferIndex++] = data; - - if (last || _itemBufferIndex == 1460) { - // check if authenticated before calling the upload - if (_handler) - _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false); - _itemBufferIndex = 0; - } -} - -enum { - EXPECT_BOUNDARY, - PARSE_HEADERS, - WAIT_FOR_RETURN1, - EXPECT_FEED1, - EXPECT_DASH1, - EXPECT_DASH2, - BOUNDARY_OR_DATA, - DASH3_OR_RETURN2, - EXPECT_FEED2, - PARSING_FINISHED, - PARSE_ERROR -}; - -void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { -#define itemWriteByte(b) \ - do { \ - _itemSize++; \ - if (_itemIsFile) \ - _handleUploadByte(b, last); \ - else \ - _itemValue += (char)(b); \ - } while (0) - - if (!_parsedLength) { - _multiParseState = EXPECT_BOUNDARY; - _temp = emptyString; - _itemName = emptyString; - _itemFilename = emptyString; - _itemType = emptyString; - } - - if (_multiParseState == WAIT_FOR_RETURN1) { - if (data != '\r') { - itemWriteByte(data); - } else { - _multiParseState = EXPECT_FEED1; - } - } else if (_multiParseState == EXPECT_BOUNDARY) { - if (_parsedLength < 2 && data != '-') { - _multiParseState = PARSE_ERROR; - return; - } else if (_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data) { - _multiParseState = PARSE_ERROR; - return; - } else if (_parsedLength - 2 == _boundary.length() && data != '\r') { - _multiParseState = PARSE_ERROR; - return; - } else if (_parsedLength - 3 == _boundary.length()) { - if (data != '\n') { - _multiParseState = PARSE_ERROR; - return; - } - _multiParseState = PARSE_HEADERS; - _itemIsFile = false; - } - } else if (_multiParseState == PARSE_HEADERS) { - if ((char)data != '\r' && (char)data != '\n') - _temp += (char)data; - if ((char)data == '\n') { - if (_temp.length()) { - if (_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase(T_Content_Type)) { - _itemType = _temp.substring(14); - _itemIsFile = true; - } else if (_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase(T_Content_Disposition)) { - _temp = _temp.substring(_temp.indexOf(';') + 2); - while (_temp.indexOf(';') > 0) { - String name = _temp.substring(0, _temp.indexOf('=')); - String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1); - if (name == T_name) { - _itemName = nameVal; - } else if (name == T_filename) { - _itemFilename = nameVal; - _itemIsFile = true; - } - _temp = _temp.substring(_temp.indexOf(';') + 2); - } - String name = _temp.substring(0, _temp.indexOf('=')); - String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1); - if (name == T_name) { - _itemName = nameVal; - } else if (name == T_filename) { - _itemFilename = nameVal; - _itemIsFile = true; - } - } - _temp = emptyString; - } else { - _multiParseState = WAIT_FOR_RETURN1; - // value starts from here - _itemSize = 0; - _itemStartIndex = _parsedLength; - _itemValue = emptyString; - if (_itemIsFile) { - if (_itemBuffer) - free(_itemBuffer); - _itemBuffer = (uint8_t*)malloc(1460); - if (_itemBuffer == NULL) { - _multiParseState = PARSE_ERROR; - return; - } - _itemBufferIndex = 0; - } - } - } - } else if (_multiParseState == EXPECT_FEED1) { - if (data != '\n') { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); - _parseMultipartPostByte(data, last); - } else { - _multiParseState = EXPECT_DASH1; - } - } else if (_multiParseState == EXPECT_DASH1) { - if (data != '-') { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); - itemWriteByte('\n'); - _parseMultipartPostByte(data, last); - } else { - _multiParseState = EXPECT_DASH2; - } - } else if (_multiParseState == EXPECT_DASH2) { - if (data != '-') { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); - itemWriteByte('\n'); - itemWriteByte('-'); - _parseMultipartPostByte(data, last); - } else { - _multiParseState = BOUNDARY_OR_DATA; - _boundaryPosition = 0; - } - } else if (_multiParseState == BOUNDARY_OR_DATA) { - if (_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data) { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); - itemWriteByte('\n'); - itemWriteByte('-'); - itemWriteByte('-'); - uint8_t i; - for (i = 0; i < _boundaryPosition; i++) - itemWriteByte(_boundary.c_str()[i]); - _parseMultipartPostByte(data, last); - } else if (_boundaryPosition == _boundary.length() - 1) { - _multiParseState = DASH3_OR_RETURN2; - if (!_itemIsFile) { - _params.emplace_back(_itemName, _itemValue, true); - } else { - if (_itemSize) { - // check if authenticated before calling the upload - if (_handler) - _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); - _itemBufferIndex = 0; - _params.emplace_back(_itemName, _itemFilename, true, true, _itemSize); - } - free(_itemBuffer); - _itemBuffer = NULL; - } - - } else { - _boundaryPosition++; - } - } else if (_multiParseState == DASH3_OR_RETURN2) { - if (data == '-' && (_contentLength - _parsedLength - 4) != 0) { - // os_printf("ERROR: The parser got to the end of the POST but is expecting %u bytes more!\nDrop an issue so we can have more info on the matter!\n", _contentLength - _parsedLength - 4); - _contentLength = _parsedLength + 4; // lets close the request gracefully - } - if (data == '\r') { - _multiParseState = EXPECT_FEED2; - } else if (data == '-' && _contentLength == (_parsedLength + 4)) { - _multiParseState = PARSING_FINISHED; - } else { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); - itemWriteByte('\n'); - itemWriteByte('-'); - itemWriteByte('-'); - uint8_t i; - for (i = 0; i < _boundary.length(); i++) - itemWriteByte(_boundary.c_str()[i]); - _parseMultipartPostByte(data, last); - } - } else if (_multiParseState == EXPECT_FEED2) { - if (data == '\n') { - _multiParseState = PARSE_HEADERS; - _itemIsFile = false; - } else { - _multiParseState = WAIT_FOR_RETURN1; - itemWriteByte('\r'); - itemWriteByte('\n'); - itemWriteByte('-'); - itemWriteByte('-'); - uint8_t i; - for (i = 0; i < _boundary.length(); i++) - itemWriteByte(_boundary.c_str()[i]); - itemWriteByte('\r'); - _parseMultipartPostByte(data, last); - } - } -} - -void AsyncWebServerRequest::_parseLine() { - if (_parseState == PARSE_REQ_START) { - if (!_temp.length()) { - _parseState = PARSE_REQ_FAIL; - _client->close(); - } else { - _parseReqHead(); - _parseState = PARSE_REQ_HEADERS; - } - return; - } - - if (_parseState == PARSE_REQ_HEADERS) { - if (!_temp.length()) { - // end of headers - _server->_rewriteRequest(this); - _server->_attachHandler(this); - _removeNotInterestingHeaders(); - if (_expectingContinue) { - String response(T_HTTP_100_CONT); - _client->write(response.c_str(), response.length()); - } - // check handler for authentication - if (_contentLength) { - _parseState = PARSE_REQ_BODY; - } else { - _parseState = PARSE_REQ_END; - if (_handler) - _handler->handleRequest(this); - else - send(501); - } - } else - _parseReqHeader(); - } -} - -size_t AsyncWebServerRequest::headers() const { - return _headers.size(); -} - -bool AsyncWebServerRequest::hasHeader(const char* name) const { - for (const auto& h : _headers) { - if (h.name().equalsIgnoreCase(name)) { - return true; - } - } - return false; -} - -#ifdef ESP8266 -bool AsyncWebServerRequest::hasHeader(const __FlashStringHelper* data) const { - return hasHeader(String(data)); -} -#endif - -const AsyncWebHeader* AsyncWebServerRequest::getHeader(const char* name) const { - auto iter = std::find_if(std::begin(_headers), std::end(_headers), [&name](const AsyncWebHeader& header) { return header.name().equalsIgnoreCase(name); }); - - return (iter == std::end(_headers)) ? nullptr : &(*iter); -} - -#ifdef ESP8266 -const AsyncWebHeader* AsyncWebServerRequest::getHeader(const __FlashStringHelper* data) const { - PGM_P p = reinterpret_cast(data); - size_t n = strlen_P(p); - char* name = (char*)malloc(n + 1); - if (name) { - strcpy_P(name, p); - const AsyncWebHeader* result = getHeader(String(name)); - free(name); - return result; - } else { - return nullptr; - } -} -#endif - -const AsyncWebHeader* AsyncWebServerRequest::getHeader(size_t num) const { - if (num >= _headers.size()) - return nullptr; - return &(*std::next(_headers.cbegin(), num)); -} - -size_t AsyncWebServerRequest::params() const { - return _params.size(); -} - -bool AsyncWebServerRequest::hasParam(const char* name, bool post, bool file) const { - for (const auto& p : _params) { - if (p.name().equals(name) && p.isPost() == post && p.isFile() == file) { - return true; - } - } - return false; -} - -const AsyncWebParameter* AsyncWebServerRequest::getParam(const char* name, bool post, bool file) const { - for (const auto& p : _params) { - if (p.name() == name && p.isPost() == post && p.isFile() == file) { - return &p; - } - } - return nullptr; -} - -#ifdef ESP8266 -const AsyncWebParameter* AsyncWebServerRequest::getParam(const __FlashStringHelper* data, bool post, bool file) const { - return getParam(String(data), post, file); -} -#endif - -const AsyncWebParameter* AsyncWebServerRequest::getParam(size_t num) const { - if (num >= _params.size()) - return nullptr; - return &(*std::next(_params.cbegin(), num)); -} - -void AsyncWebServerRequest::addInterestingHeader(const char* name) { - if (std::none_of(std::begin(_interestingHeaders), std::end(_interestingHeaders), [&name](const String& str) { return str.equalsIgnoreCase(name); })) - _interestingHeaders.emplace_back(name); -} - -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(int code, const char* contentType, const char* content, AwsTemplateProcessor callback) { - if (callback) - return new AsyncProgmemResponse(code, contentType, (const uint8_t*)content, strlen(content), callback); - return new AsyncBasicResponse(code, contentType, content); -} - -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback) { - return new AsyncProgmemResponse(code, contentType, content, len, callback); -} - -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(FS& fs, const String& path, const char* contentType, bool download, AwsTemplateProcessor callback) { - if (fs.exists(path) || (!download && fs.exists(path + T__gz))) - return new AsyncFileResponse(fs, path, contentType, download, callback); - return NULL; -} - -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(File content, const String& path, const char* contentType, bool download, AwsTemplateProcessor callback) { - if (content == true) - return new AsyncFileResponse(content, path, contentType, download, callback); - return NULL; -} - -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(Stream& stream, const char* contentType, size_t len, AwsTemplateProcessor callback) { - return new AsyncStreamResponse(stream, contentType, len, callback); -} - -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(const char* contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback) { - return new AsyncCallbackResponse(contentType, len, callback, templateCallback); -} - -AsyncWebServerResponse* AsyncWebServerRequest::beginChunkedResponse(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback) { - if (_version) - return new AsyncChunkedResponse(contentType, callback, templateCallback); - return new AsyncCallbackResponse(contentType, 0, callback, templateCallback); -} - -AsyncResponseStream* AsyncWebServerRequest::beginResponseStream(const char* contentType, size_t bufferSize) { - return new AsyncResponseStream(contentType, bufferSize); -} - -#ifdef ESP8266 -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback) { - return new AsyncProgmemResponse(code, contentType, (const uint8_t*)content, strlen_P(content), callback); -} -#endif - -void AsyncWebServerRequest::send(AsyncWebServerResponse* response) { - _response = response; - if (_response == NULL) { - _client->close(true); - _onDisconnect(); - return; - } - if (!_response->_sourceValid()) { - delete response; - _response = NULL; - send(500); - } else { - _client->setRxTimeout(0); - _response->_respond(this); - } -} - -void AsyncWebServerRequest::redirect(const char* url) { - AsyncWebServerResponse* response = beginResponse(302); - response->addHeader(T_LOCATION, url); - send(response); -} - -bool AsyncWebServerRequest::authenticate(const char* username, const char* password, const char* realm, bool passwordIsHash) { - if (_authorization.length()) { - if (_isDigest) - return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL); - else if (!passwordIsHash) - return checkBasicAuthentication(_authorization.c_str(), username, password); - else - return _authorization.equals(password); - } - return false; -} - -bool AsyncWebServerRequest::authenticate(const char* hash) { - if (!_authorization.length() || hash == NULL) - return false; - - if (_isDigest) { - String hStr = String(hash); - int separator = hStr.indexOf(':'); - if (separator <= 0) - return false; - String username = hStr.substring(0, separator); - hStr = hStr.substring(separator + 1); - separator = hStr.indexOf(':'); - if (separator <= 0) - return false; - String realm = hStr.substring(0, separator); - hStr = hStr.substring(separator + 1); - return checkDigestAuthentication(_authorization.c_str(), methodToString(), username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); - } - - return (_authorization.equals(hash)); -} - -void AsyncWebServerRequest::requestAuthentication(const char* realm, bool isDigest) { - AsyncWebServerResponse* r = beginResponse(401); - if (!isDigest && realm == NULL) { - r->addHeader(T_WWW_AUTH, T_BASIC_REALM_LOGIN_REQ); - } else if (!isDigest) { - String header(T_BASIC_REALM); - header.concat(realm); - header += '"'; - r->addHeader(T_WWW_AUTH, header.c_str()); - } else { - String header(T_DIGEST_); - header.concat(requestDigestAuthentication(realm)); - r->addHeader(T_WWW_AUTH, header.c_str()); - } - send(r); -} - -bool AsyncWebServerRequest::hasArg(const char* name) const { - for (const auto& arg : _params) { - if (arg.name() == name) { - return true; - } - } - return false; -} - -#ifdef ESP8266 -bool AsyncWebServerRequest::hasArg(const __FlashStringHelper* data) const { - return hasArg(String(data).c_str()); -} -#endif - -const String& AsyncWebServerRequest::arg(const char* name) const { - for (const auto& arg : _params) { - if (arg.name() == name) { - return arg.value(); - } - } - return emptyString; -} - -#ifdef ESP8266 -const String& AsyncWebServerRequest::arg(const __FlashStringHelper* data) const { - return arg(String(data).c_str()); -} -#endif - -const String& AsyncWebServerRequest::arg(size_t i) const { - return getParam(i)->value(); -} - -const String& AsyncWebServerRequest::argName(size_t i) const { - return getParam(i)->name(); -} - -const String& AsyncWebServerRequest::pathArg(size_t i) const { - return i < _pathParams.size() ? _pathParams[i] : emptyString; -} - -const String& AsyncWebServerRequest::header(const char* name) const { - const AsyncWebHeader* h = getHeader(name); - return h ? h->value() : emptyString; -} - -#ifdef ESP8266 -const String& AsyncWebServerRequest::header(const __FlashStringHelper* data) const { - return header(String(data).c_str()); -}; -#endif - -const String& AsyncWebServerRequest::header(size_t i) const { - const AsyncWebHeader* h = getHeader(i); - return h ? h->value() : emptyString; -} - -const String& AsyncWebServerRequest::headerName(size_t i) const { - const AsyncWebHeader* h = getHeader(i); - return h ? h->name() : emptyString; -} - -String AsyncWebServerRequest::urlDecode(const String& text) const { - char temp[] = "0x00"; - unsigned int len = text.length(); - unsigned int i = 0; - String decoded; - decoded.reserve(len); // Allocate the string internal buffer - never longer from source text - while (i < len) { - char decodedChar; - char encodedChar = text.charAt(i++); - if ((encodedChar == '%') && (i + 1 < len)) { - temp[2] = text.charAt(i++); - temp[3] = text.charAt(i++); - decodedChar = strtol(temp, NULL, 16); - } else if (encodedChar == '+') { - decodedChar = ' '; - } else { - decodedChar = encodedChar; // normal ascii char - } - decoded.concat(decodedChar); - } - return decoded; -} - -#ifndef ESP8266 -const char* AsyncWebServerRequest::methodToString() const { - if (_method == HTTP_ANY) - return T_ANY; - if (_method & HTTP_GET) - return T_GET; - if (_method & HTTP_POST) - return T_POST; - if (_method & HTTP_DELETE) - return T_DELETE; - if (_method & HTTP_PUT) - return T_PUT; - if (_method & HTTP_PATCH) - return T_PATCH; - if (_method & HTTP_HEAD) - return T_HEAD; - if (_method & HTTP_OPTIONS) - return T_OPTIONS; - return T_UNKNOWN; -} -#else // ESP8266 -const __FlashStringHelper* AsyncWebServerRequest::methodToString() const { - if (_method == HTTP_ANY) - return FPSTR(T_ANY); - if (_method & HTTP_GET) - return FPSTR(T_GET); - if (_method & HTTP_POST) - return FPSTR(T_POST); - if (_method & HTTP_DELETE) - return FPSTR(T_DELETE); - if (_method & HTTP_PUT) - return FPSTR(T_PUT); - if (_method & HTTP_PATCH) - return FPSTR(T_PATCH); - if (_method & HTTP_HEAD) - return FPSTR(T_HEAD); - if (_method & HTTP_OPTIONS) - return FPSTR(T_OPTIONS); - return FPSTR(T_UNKNOWN); -} -#endif // ESP8266 - -#ifndef ESP8266 -const char* AsyncWebServerRequest::requestedConnTypeToString() const { - switch (_reqconntype) { - case RCT_NOT_USED: - return T_RCT_NOT_USED; - case RCT_DEFAULT: - return T_RCT_DEFAULT; - case RCT_HTTP: - return T_RCT_HTTP; - case RCT_WS: - return T_RCT_WS; - case RCT_EVENT: - return T_RCT_EVENT; - default: - return T_ERROR; - } -} -#else // ESP8266 -const __FlashStringHelper* AsyncWebServerRequest::requestedConnTypeToString() const { - switch (_reqconntype) { - case RCT_NOT_USED: - return FPSTR(T_RCT_NOT_USED); - case RCT_DEFAULT: - return FPSTR(T_RCT_DEFAULT); - case RCT_HTTP: - return FPSTR(T_RCT_HTTP); - case RCT_WS: - return FPSTR(T_RCT_WS); - case RCT_EVENT: - return FPSTR(T_RCT_EVENT); - default: - return FPSTR(T_ERROR); - } -} -#endif // ESP8266 - -bool AsyncWebServerRequest::isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2, RequestedConnectionType erct3) { - bool res = false; - if ((erct1 != RCT_NOT_USED) && (erct1 == _reqconntype)) - res = true; - if ((erct2 != RCT_NOT_USED) && (erct2 == _reqconntype)) - res = true; - if ((erct3 != RCT_NOT_USED) && (erct3 == _reqconntype)) - res = true; - return res; -} diff --git a/lib/ESPAsyncWebServer/src/WebResponseImpl.h b/lib/ESPAsyncWebServer/src/WebResponseImpl.h deleted file mode 100644 index a6f71bb..0000000 --- a/lib/ESPAsyncWebServer/src/WebResponseImpl.h +++ /dev/null @@ -1,157 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#ifndef ASYNCWEBSERVERRESPONSEIMPL_H_ -#define ASYNCWEBSERVERRESPONSEIMPL_H_ - -#ifdef Arduino_h - // arduino is not compatible with std::vector - #undef min - #undef max -#endif -#include -#include -#include "literals.h" - -// It is possible to restore these defines, but one can use _min and _max instead. Or std::min, std::max. - -class AsyncBasicResponse : public AsyncWebServerResponse { - private: - String _content; - - public: - explicit AsyncBasicResponse(int code, const char* contentType = asyncsrv::empty, const char* content = asyncsrv::empty); - AsyncBasicResponse(int code, const String& contentType, const String& content = emptyString) : AsyncBasicResponse(code, contentType.c_str(), content.c_str()) {} - void _respond(AsyncWebServerRequest* request); - size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time); - bool _sourceValid() const { return true; } -}; - -class AsyncAbstractResponse : public AsyncWebServerResponse { - private: - String _head; - // Data is inserted into cache at begin(). - // This is inefficient with vector, but if we use some other container, - // we won't be able to access it as contiguous array of bytes when reading from it, - // so by gaining performance in one place, we'll lose it in another. - std::vector _cache; - size_t _readDataFromCacheOrContent(uint8_t* data, const size_t len); - size_t _fillBufferAndProcessTemplates(uint8_t* buf, size_t maxLen); - - protected: - AwsTemplateProcessor _callback; - - public: - AsyncAbstractResponse(AwsTemplateProcessor callback = nullptr); - void _respond(AsyncWebServerRequest* request); - size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time); - bool _sourceValid() const { return false; } - virtual size_t _fillBuffer(uint8_t* buf __attribute__((unused)), size_t maxLen __attribute__((unused))) { return 0; } -}; - -#ifndef TEMPLATE_PLACEHOLDER - #define TEMPLATE_PLACEHOLDER '%' -#endif - -#define TEMPLATE_PARAM_NAME_LENGTH 32 -class AsyncFileResponse : public AsyncAbstractResponse { - using File = fs::File; - using FS = fs::FS; - - private: - File _content; - String _path; - void _setContentTypeFromPath(const String& path); - - public: - AsyncFileResponse(FS& fs, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); - AsyncFileResponse(FS& fs, const String& path, const String& contentType, bool download = false, AwsTemplateProcessor callback = nullptr) : AsyncFileResponse(fs, path, contentType.c_str(), download, callback) {} - AsyncFileResponse(File content, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); - AsyncFileResponse(File content, const String& path, const String& contentType, bool download = false, AwsTemplateProcessor callack = nullptr) : AsyncFileResponse(content, path, contentType.c_str(), download, callack) {} - ~AsyncFileResponse(); - bool _sourceValid() const { return !!(_content); } - virtual size_t _fillBuffer(uint8_t* buf, size_t maxLen) override; -}; - -class AsyncStreamResponse : public AsyncAbstractResponse { - private: - Stream* _content; - - public: - AsyncStreamResponse(Stream& stream, const char* contentType, size_t len, AwsTemplateProcessor callback = nullptr); - AsyncStreamResponse(Stream& stream, const String& contentType, size_t len, AwsTemplateProcessor callback = nullptr) : AsyncStreamResponse(stream, contentType.c_str(), len, callback) {} - bool _sourceValid() const { return !!(_content); } - virtual size_t _fillBuffer(uint8_t* buf, size_t maxLen) override; -}; - -class AsyncCallbackResponse : public AsyncAbstractResponse { - private: - AwsResponseFiller _content; - size_t _filledLength; - - public: - AsyncCallbackResponse(const char* contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) : AsyncCallbackResponse(contentType.c_str(), len, callback, templateCallback) {} - bool _sourceValid() const { return !!(_content); } - virtual size_t _fillBuffer(uint8_t* buf, size_t maxLen) override; -}; - -class AsyncChunkedResponse : public AsyncAbstractResponse { - private: - AwsResponseFiller _content; - size_t _filledLength; - - public: - AsyncChunkedResponse(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) : AsyncChunkedResponse(contentType.c_str(), callback, templateCallback) {} - bool _sourceValid() const { return !!(_content); } - virtual size_t _fillBuffer(uint8_t* buf, size_t maxLen) override; -}; - -class AsyncProgmemResponse : public AsyncAbstractResponse { - private: - const uint8_t* _content; - size_t _readLength; - - public: - AsyncProgmemResponse(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr); - AsyncProgmemResponse(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) : AsyncProgmemResponse(code, contentType.c_str(), content, len, callback) {} - bool _sourceValid() const { return true; } - virtual size_t _fillBuffer(uint8_t* buf, size_t maxLen) override; -}; - -class cbuf; - -class AsyncResponseStream : public AsyncAbstractResponse, public Print { - private: - std::unique_ptr _content; - - public: - AsyncResponseStream(const char* contentType, size_t bufferSize); - AsyncResponseStream(const String& contentType, size_t bufferSize) : AsyncResponseStream(contentType.c_str(), bufferSize) {} - ~AsyncResponseStream(); - bool _sourceValid() const { return (_state < RESPONSE_END); } - virtual size_t _fillBuffer(uint8_t* buf, size_t maxLen) override; - size_t write(const uint8_t* data, size_t len); - size_t write(uint8_t data); - using Print::write; -}; - -#endif /* ASYNCWEBSERVERRESPONSEIMPL_H_ */ diff --git a/lib/ESPAsyncWebServer/src/WebResponses.cpp b/lib/ESPAsyncWebServer/src/WebResponses.cpp deleted file mode 100644 index f1994b1..0000000 --- a/lib/ESPAsyncWebServer/src/WebResponses.cpp +++ /dev/null @@ -1,849 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "ESPAsyncWebServer.h" -#include "WebResponseImpl.h" -#include "cbuf.h" - -using namespace asyncsrv; - -// Since ESP8266 does not link memchr by default, here's its implementation. -void* memchr(void* ptr, int ch, size_t count) { - unsigned char* p = static_cast(ptr); - while (count--) - if (*p++ == static_cast(ch)) - return --p; - return nullptr; -} - -/* - * Abstract Response - * - */ - -#ifndef ESP8266 -const char* AsyncWebServerResponse::responseCodeToString(int code) { - switch (code) { - case 100: - return T_HTTP_CODE_100; - case 101: - return T_HTTP_CODE_101; - case 200: - return T_HTTP_CODE_200; - case 201: - return T_HTTP_CODE_201; - case 202: - return T_HTTP_CODE_202; - case 203: - return T_HTTP_CODE_203; - case 204: - return T_HTTP_CODE_204; - case 205: - return T_HTTP_CODE_205; - case 206: - return T_HTTP_CODE_206; - case 300: - return T_HTTP_CODE_300; - case 301: - return T_HTTP_CODE_301; - case 302: - return T_HTTP_CODE_302; - case 303: - return T_HTTP_CODE_303; - case 304: - return T_HTTP_CODE_304; - case 305: - return T_HTTP_CODE_305; - case 307: - return T_HTTP_CODE_307; - case 400: - return T_HTTP_CODE_400; - case 401: - return T_HTTP_CODE_401; - case 402: - return T_HTTP_CODE_402; - case 403: - return T_HTTP_CODE_403; - case 404: - return T_HTTP_CODE_404; - case 405: - return T_HTTP_CODE_405; - case 406: - return T_HTTP_CODE_406; - case 407: - return T_HTTP_CODE_407; - case 408: - return T_HTTP_CODE_408; - case 409: - return T_HTTP_CODE_409; - case 410: - return T_HTTP_CODE_410; - case 411: - return T_HTTP_CODE_411; - case 412: - return T_HTTP_CODE_412; - case 413: - return T_HTTP_CODE_413; - case 414: - return T_HTTP_CODE_414; - case 415: - return T_HTTP_CODE_415; - case 416: - return T_HTTP_CODE_416; - case 417: - return T_HTTP_CODE_417; - case 500: - return T_HTTP_CODE_500; - case 501: - return T_HTTP_CODE_501; - case 502: - return T_HTTP_CODE_502; - case 503: - return T_HTTP_CODE_503; - case 504: - return T_HTTP_CODE_504; - case 505: - return T_HTTP_CODE_505; - default: - return T_HTTP_CODE_ANY; - } -} -#else // ESP8266 -const __FlashStringHelper* AsyncWebServerResponse::responseCodeToString(int code) -{ - switch (code) { - case 100: - return FPSTR(T_HTTP_CODE_100); - case 101: - return FPSTR(T_HTTP_CODE_101); - case 200: - return FPSTR(T_HTTP_CODE_200); - case 201: - return FPSTR(T_HTTP_CODE_201); - case 202: - return FPSTR(T_HTTP_CODE_202); - case 203: - return FPSTR(T_HTTP_CODE_203); - case 204: - return FPSTR(T_HTTP_CODE_204); - case 205: - return FPSTR(T_HTTP_CODE_205); - case 206: - return FPSTR(T_HTTP_CODE_206); - case 300: - return FPSTR(T_HTTP_CODE_300); - case 301: - return FPSTR(T_HTTP_CODE_301); - case 302: - return FPSTR(T_HTTP_CODE_302); - case 303: - return FPSTR(T_HTTP_CODE_303); - case 304: - return FPSTR(T_HTTP_CODE_304); - case 305: - return FPSTR(T_HTTP_CODE_305); - case 307: - return FPSTR(T_HTTP_CODE_307); - case 400: - return FPSTR(T_HTTP_CODE_400); - case 401: - return FPSTR(T_HTTP_CODE_401); - case 402: - return FPSTR(T_HTTP_CODE_402); - case 403: - return FPSTR(T_HTTP_CODE_403); - case 404: - return FPSTR(T_HTTP_CODE_404); - case 405: - return FPSTR(T_HTTP_CODE_405); - case 406: - return FPSTR(T_HTTP_CODE_406); - case 407: - return FPSTR(T_HTTP_CODE_407); - case 408: - return FPSTR(T_HTTP_CODE_408); - case 409: - return FPSTR(T_HTTP_CODE_409); - case 410: - return FPSTR(T_HTTP_CODE_410); - case 411: - return FPSTR(T_HTTP_CODE_411); - case 412: - return FPSTR(T_HTTP_CODE_412); - case 413: - return FPSTR(T_HTTP_CODE_413); - case 414: - return FPSTR(T_HTTP_CODE_414); - case 415: - return FPSTR(T_HTTP_CODE_415); - case 416: - return FPSTR(T_HTTP_CODE_416); - case 417: - return FPSTR(T_HTTP_CODE_417); - case 500: - return FPSTR(T_HTTP_CODE_500); - case 501: - return FPSTR(T_HTTP_CODE_501); - case 502: - return FPSTR(T_HTTP_CODE_502); - case 503: - return FPSTR(T_HTTP_CODE_503); - case 504: - return FPSTR(T_HTTP_CODE_504); - case 505: - return FPSTR(T_HTTP_CODE_505); - default: - return FPSTR(T_HTTP_CODE_ANY); - } -} -#endif // ESP8266 - -AsyncWebServerResponse::AsyncWebServerResponse() - : _code(0), _contentType(), _contentLength(0), _sendContentLength(true), _chunked(false), _headLength(0), _sentLength(0), _ackedLength(0), _writtenLength(0), _state(RESPONSE_SETUP) { - for (const auto& header : DefaultHeaders::Instance()) { - _headers.emplace_back(header); - } -} - -AsyncWebServerResponse::~AsyncWebServerResponse() = default; - -void AsyncWebServerResponse::setCode(int code) { - if (_state == RESPONSE_SETUP) - _code = code; -} - -void AsyncWebServerResponse::setContentLength(size_t len) { - if (_state == RESPONSE_SETUP) - _contentLength = len; -} - -void AsyncWebServerResponse::setContentType(const char* type) { - if (_state == RESPONSE_SETUP) - _contentType = type; -} - -void AsyncWebServerResponse::addHeader(const char* name, const char* value) { - _headers.emplace_back(name, value); -} - -String AsyncWebServerResponse::_assembleHead(uint8_t version) { - if (version) { - addHeader(T_Accept_Ranges, T_none); - if (_chunked) - addHeader(Transfer_Encoding, T_chunked); - } - String out; - int bufSize = 300; - char buf[bufSize]; - -#ifndef ESP8266 - snprintf(buf, bufSize, "HTTP/1.%d %d %s\r\n", version, _code, responseCodeToString(_code)); -#else - snprintf_P(buf, bufSize, PSTR("HTTP/1.%d %d %s\r\n"), version, _code, String(responseCodeToString(_code)).c_str()); -#endif - out.concat(buf); - - if (_sendContentLength) { - snprintf_P(buf, bufSize, PSTR("Content-Length: %d\r\n"), _contentLength); - out.concat(buf); - } - if (_contentType.length()) { - snprintf_P(buf, bufSize, PSTR("Content-Type: %s\r\n"), _contentType.c_str()); - out.concat(buf); - } - - for (const auto& header : _headers) { - snprintf_P(buf, bufSize, PSTR("%s: %s\r\n"), header.name().c_str(), header.value().c_str()); - out.concat(buf); - } - _headers.clear(); - - out.concat(T_rn); - _headLength = out.length(); - return out; -} - -bool AsyncWebServerResponse::_started() const { return _state > RESPONSE_SETUP; } -bool AsyncWebServerResponse::_finished() const { return _state > RESPONSE_WAIT_ACK; } -bool AsyncWebServerResponse::_failed() const { return _state == RESPONSE_FAILED; } -bool AsyncWebServerResponse::_sourceValid() const { return false; } -void AsyncWebServerResponse::_respond(AsyncWebServerRequest* request) { - _state = RESPONSE_END; - request->client()->close(); -} -size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest* request, size_t len, uint32_t time) { - (void)request; - (void)len; - (void)time; - return 0; -} - -/* - * String/Code Response - * */ -AsyncBasicResponse::AsyncBasicResponse(int code, const char* contentType, const char* content) { - _code = code; - _content = content; - _contentType = contentType; - if (_content.length()) { - _contentLength = _content.length(); - if (!_contentType.length()) - _contentType = T_text_plain; - } - addHeader(T_Connection, T_close); -} - -void AsyncBasicResponse::_respond(AsyncWebServerRequest* request) { - _state = RESPONSE_HEADERS; - String out = _assembleHead(request->version()); - size_t outLen = out.length(); - size_t space = request->client()->space(); - if (!_contentLength && space >= outLen) { - _writtenLength += request->client()->write(out.c_str(), outLen); - _state = RESPONSE_WAIT_ACK; - } else if (_contentLength && space >= outLen + _contentLength) { - out += _content; - outLen += _contentLength; - _writtenLength += request->client()->write(out.c_str(), outLen); - _state = RESPONSE_WAIT_ACK; - } else if (space && space < outLen) { - String partial = out.substring(0, space); - _content = out.substring(space) + _content; - _contentLength += outLen - space; - _writtenLength += request->client()->write(partial.c_str(), partial.length()); - _state = RESPONSE_CONTENT; - } else if (space > outLen && space < (outLen + _contentLength)) { - size_t shift = space - outLen; - outLen += shift; - _sentLength += shift; - out += _content.substring(0, shift); - _content = _content.substring(shift); - _writtenLength += request->client()->write(out.c_str(), outLen); - _state = RESPONSE_CONTENT; - } else { - _content = out + _content; - _contentLength += outLen; - _state = RESPONSE_CONTENT; - } -} - -size_t AsyncBasicResponse::_ack(AsyncWebServerRequest* request, size_t len, uint32_t time) { - (void)time; - _ackedLength += len; - if (_state == RESPONSE_CONTENT) { - size_t available = _contentLength - _sentLength; - size_t space = request->client()->space(); - // we can fit in this packet - if (space > available) { - _writtenLength += request->client()->write(_content.c_str(), available); - _content = emptyString; - _state = RESPONSE_WAIT_ACK; - return available; - } - // send some data, the rest on ack - String out = _content.substring(0, space); - _content = _content.substring(space); - _sentLength += space; - _writtenLength += request->client()->write(out.c_str(), space); - return space; - } else if (_state == RESPONSE_WAIT_ACK) { - if (_ackedLength >= _writtenLength) { - _state = RESPONSE_END; - } - } - return 0; -} - -/* - * Abstract Response - * */ - -AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback) : _callback(callback) { - // In case of template processing, we're unable to determine real response size - if (callback) { - _contentLength = 0; - _sendContentLength = false; - _chunked = true; - } -} - -void AsyncAbstractResponse::_respond(AsyncWebServerRequest* request) { - addHeader(T_Connection, T_close); - _head = _assembleHead(request->version()); - _state = RESPONSE_HEADERS; - _ack(request, 0, 0); -} - -size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, uint32_t time) { - (void)time; - if (!_sourceValid()) { - _state = RESPONSE_FAILED; - request->client()->close(); - return 0; - } - _ackedLength += len; - size_t space = request->client()->space(); - - size_t headLen = _head.length(); - if (_state == RESPONSE_HEADERS) { - if (space >= headLen) { - _state = RESPONSE_CONTENT; - space -= headLen; - } else { - String out = _head.substring(0, space); - _head = _head.substring(space); - _writtenLength += request->client()->write(out.c_str(), out.length()); - return out.length(); - } - } - - if (_state == RESPONSE_CONTENT) { - size_t outLen; - if (_chunked) { - if (space <= 8) { - return 0; - } - outLen = space; - } else if (!_sendContentLength) { - outLen = space; - } else { - outLen = ((_contentLength - _sentLength) > space) ? space : (_contentLength - _sentLength); - } - - uint8_t* buf = (uint8_t*)malloc(outLen + headLen); - if (!buf) { - // os_printf("_ack malloc %d failed\n", outLen+headLen); - return 0; - } - - if (headLen) { - memcpy(buf, _head.c_str(), _head.length()); - } - - size_t readLen = 0; - - if (_chunked) { - // HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added. - // See RFC2616 sections 2, 3.6.1. - readLen = _fillBufferAndProcessTemplates(buf + headLen + 6, outLen - 8); - if (readLen == RESPONSE_TRY_AGAIN) { - free(buf); - return 0; - } - outLen = sprintf_P((char*)buf + headLen, PSTR("%x"), readLen) + headLen; - while (outLen < headLen + 4) - buf[outLen++] = ' '; - buf[outLen++] = '\r'; - buf[outLen++] = '\n'; - outLen += readLen; - buf[outLen++] = '\r'; - buf[outLen++] = '\n'; - } else { - readLen = _fillBufferAndProcessTemplates(buf + headLen, outLen); - if (readLen == RESPONSE_TRY_AGAIN) { - free(buf); - return 0; - } - outLen = readLen + headLen; - } - - if (headLen) { - _head = emptyString; - } - - if (outLen) { - _writtenLength += request->client()->write((const char*)buf, outLen); - } - - if (_chunked) { - _sentLength += readLen; - } else { - _sentLength += outLen - headLen; - } - - free(buf); - - if ((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)) { - _state = RESPONSE_WAIT_ACK; - } - return outLen; - - } else if (_state == RESPONSE_WAIT_ACK) { - if (!_sendContentLength || _ackedLength >= _writtenLength) { - _state = RESPONSE_END; - if (!_chunked && !_sendContentLength) - request->client()->close(true); - } - } - return 0; -} - -size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t* data, const size_t len) { - // If we have something in cache, copy it to buffer - const size_t readFromCache = std::min(len, _cache.size()); - if (readFromCache) { - memcpy(data, _cache.data(), readFromCache); - _cache.erase(_cache.begin(), _cache.begin() + readFromCache); - } - // If we need to read more... - const size_t needFromFile = len - readFromCache; - const size_t readFromContent = _fillBuffer(data + readFromCache, needFromFile); - return readFromCache + readFromContent; -} - -size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t* data, size_t len) { - if (!_callback) - return _fillBuffer(data, len); - - const size_t originalLen = len; - len = _readDataFromCacheOrContent(data, len); - // Now we've read 'len' bytes, either from cache or from file - // Search for template placeholders - uint8_t* pTemplateStart = data; - while ((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t*)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) { // data[0] ... data[len - 1] - uint8_t* pTemplateEnd = (pTemplateStart < &data[len - 1]) ? (uint8_t*)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr; - // temporary buffer to hold parameter name - uint8_t buf[TEMPLATE_PARAM_NAME_LENGTH + 1]; - String paramName; - // If closing placeholder is found: - if (pTemplateEnd) { - // prepare argument to callback - const size_t paramNameLength = std::min((size_t)sizeof(buf) - 1, (size_t)(pTemplateEnd - pTemplateStart - 1)); - if (paramNameLength) { - memcpy(buf, pTemplateStart + 1, paramNameLength); - buf[paramNameLength] = 0; - paramName = String(reinterpret_cast(buf)); - } else { // double percent sign encountered, this is single percent sign escaped. - // remove the 2nd percent sign - memmove(pTemplateEnd, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); - len += _readDataFromCacheOrContent(&data[len - 1], 1) - 1; - ++pTemplateStart; - } - } else if (&data[len - 1] - pTemplateStart + 1 < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data - memcpy(buf, pTemplateStart + 1, &data[len - 1] - pTemplateStart); - const size_t readFromCacheOrContent = _readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1)); - if (readFromCacheOrContent) { - pTemplateEnd = (uint8_t*)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent); - if (pTemplateEnd) { - // prepare argument to callback - *pTemplateEnd = 0; - paramName = String(reinterpret_cast(buf)); - // Copy remaining read-ahead data into cache - _cache.insert(_cache.begin(), pTemplateEnd + 1, buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); - pTemplateEnd = &data[len - 1]; - } else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position - { - // but first, store read file data in cache - _cache.insert(_cache.begin(), buf + (&data[len - 1] - pTemplateStart), buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); - ++pTemplateStart; - } - } else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position - ++pTemplateStart; - } else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position - ++pTemplateStart; - if (paramName.length()) { - // call callback and replace with result. - // Everything in range [pTemplateStart, pTemplateEnd] can be safely replaced with parameter value. - // Data after pTemplateEnd may need to be moved. - // The first byte of data after placeholder is located at pTemplateEnd + 1. - // It should be located at pTemplateStart + numBytesCopied (to begin right after inserted parameter value). - const String paramValue(_callback(paramName)); - const char* pvstr = paramValue.c_str(); - const unsigned int pvlen = paramValue.length(); - const size_t numBytesCopied = std::min(pvlen, static_cast(&data[originalLen - 1] - pTemplateStart + 1)); - // make room for param value - // 1. move extra data to cache if parameter value is longer than placeholder AND if there is no room to store - if ((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) { - _cache.insert(_cache.begin(), &data[originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1)], &data[len]); - // 2. parameter value is longer than placeholder text, push the data after placeholder which not saved into cache further to the end - memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[originalLen] - pTemplateStart - numBytesCopied); - len = originalLen; // fix issue with truncated data, not sure if it has any side effects - } else if (pTemplateEnd + 1 != pTemplateStart + numBytesCopied) - // 2. Either parameter value is shorter than placeholder text OR there is enough free space in buffer to fit. - // Move the entire data after the placeholder - memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); - // 3. replace placeholder with actual value - memcpy(pTemplateStart, pvstr, numBytesCopied); - // If result is longer than buffer, copy the remainder into cache (this could happen only if placeholder text itself did not fit entirely in buffer) - if (numBytesCopied < pvlen) { - _cache.insert(_cache.begin(), pvstr + numBytesCopied, pvstr + pvlen); - } else if (pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text... - // there is some free room, fill it from cache - const size_t roomFreed = pTemplateEnd + 1 - pTemplateStart - numBytesCopied; - const size_t totalFreeRoom = originalLen - len + roomFreed; - len += _readDataFromCacheOrContent(&data[len - roomFreed], totalFreeRoom) - roomFreed; - } else { // result is copied fully; it is longer than placeholder text - const size_t roomTaken = pTemplateStart + numBytesCopied - pTemplateEnd - 1; - len = std::min(len + roomTaken, originalLen); - } - } - } // while(pTemplateStart) - return len; -} - -/* - * File Response - * */ - -AsyncFileResponse::~AsyncFileResponse() { - if (_content) - _content.close(); -} - -void AsyncFileResponse::_setContentTypeFromPath(const String& path) { -#if HAVE_EXTERN_GET_Content_Type_FUNCTION - #ifndef ESP8266 - extern const char* getContentType(const String& path); - #else - extern const __FlashStringHelper* getContentType(const String& path); - #endif - _contentType = getContentType(path); -#else - if (path.endsWith(T__html)) - _contentType = T_text_html; - else if (path.endsWith(T__htm)) - _contentType = T_text_html; - else if (path.endsWith(T__css)) - _contentType = T_text_css; - else if (path.endsWith(T__json)) - _contentType = T_application_json; - else if (path.endsWith(T__js)) - _contentType = T_application_javascript; - else if (path.endsWith(T__png)) - _contentType = T_image_png; - else if (path.endsWith(T__gif)) - _contentType = T_image_gif; - else if (path.endsWith(T__jpg)) - _contentType = T_image_jpeg; - else if (path.endsWith(T__ico)) - _contentType = T_image_x_icon; - else if (path.endsWith(T__svg)) - _contentType = T_image_svg_xml; - else if (path.endsWith(T__eot)) - _contentType = T_font_eot; - else if (path.endsWith(T__woff)) - _contentType = T_font_woff; - else if (path.endsWith(T__woff2)) - _contentType = T_font_woff2; - else if (path.endsWith(T__ttf)) - _contentType = T_font_ttf; - else if (path.endsWith(T__xml)) - _contentType = T_text_xml; - else if (path.endsWith(T__pdf)) - _contentType = T_application_pdf; - else if (path.endsWith(T__zip)) - _contentType = T_application_zip; - else if (path.endsWith(T__gz)) - _contentType = T_application_x_gzip; - else - _contentType = T_text_plain; -#endif -} - -AsyncFileResponse::AsyncFileResponse(FS& fs, const String& path, const char* contentType, bool download, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { - _code = 200; - _path = path; - - if (!download && !fs.exists(_path) && fs.exists(_path + T__gz)) { - _path = _path + T__gz; - addHeader(T_Content_Encoding, T_gzip); - _callback = nullptr; // Unable to process zipped templates - _sendContentLength = true; - _chunked = false; - } - - _content = fs.open(_path, fs::FileOpenMode::read); - _contentLength = _content.size(); - - if (strlen(contentType) == 0) - _setContentTypeFromPath(path); - else - _contentType = contentType; - - int filenameStart = path.lastIndexOf('/') + 1; - char buf[26 + path.length() - filenameStart]; - char* filename = (char*)path.c_str() + filenameStart; - - if (download) { - // set filename and force download - snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); - } else { - // set filename and force rendering - snprintf_P(buf, sizeof(buf), PSTR("inline")); - } - addHeader(T_Content_Disposition, buf); -} - -AsyncFileResponse::AsyncFileResponse(File content, const String& path, const char* contentType, bool download, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { - _code = 200; - _path = path; - - if (!download && String(content.name()).endsWith(T__gz) && !path.endsWith(T__gz)) { - addHeader(T_Content_Encoding, T_gzip); - _callback = nullptr; // Unable to process gzipped templates - _sendContentLength = true; - _chunked = false; - } - - _content = content; - _contentLength = _content.size(); - - if (strlen(contentType) == 0) - _setContentTypeFromPath(path); - else - _contentType = contentType; - - int filenameStart = path.lastIndexOf('/') + 1; - char buf[26 + path.length() - filenameStart]; - char* filename = (char*)path.c_str() + filenameStart; - - if (download) { - snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); - } else { - snprintf_P(buf, sizeof(buf), PSTR("inline")); - } - addHeader(T_Content_Disposition, buf); -} - -size_t AsyncFileResponse::_fillBuffer(uint8_t* data, size_t len) { - return _content.read(data, len); -} - -/* - * Stream Response - * */ - -AsyncStreamResponse::AsyncStreamResponse(Stream& stream, const char* contentType, size_t len, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { - _code = 200; - _content = &stream; - _contentLength = len; - _contentType = contentType; -} - -size_t AsyncStreamResponse::_fillBuffer(uint8_t* data, size_t len) { - size_t available = _content->available(); - size_t outLen = (available > len) ? len : available; - size_t i; - for (i = 0; i < outLen; i++) - data[i] = _content->read(); - return outLen; -} - -/* - * Callback Response - * */ - -AsyncCallbackResponse::AsyncCallbackResponse(const char* contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback) : AsyncAbstractResponse(templateCallback) { - _code = 200; - _content = callback; - _contentLength = len; - if (!len) - _sendContentLength = false; - _contentType = contentType; - _filledLength = 0; -} - -size_t AsyncCallbackResponse::_fillBuffer(uint8_t* data, size_t len) { - size_t ret = _content(data, len, _filledLength); - if (ret != RESPONSE_TRY_AGAIN) { - _filledLength += ret; - } - return ret; -} - -/* - * Chunked Response - * */ - -AsyncChunkedResponse::AsyncChunkedResponse(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback) : AsyncAbstractResponse(processorCallback) { - _code = 200; - _content = callback; - _contentLength = 0; - _contentType = contentType; - _sendContentLength = false; - _chunked = true; - _filledLength = 0; -} - -size_t AsyncChunkedResponse::_fillBuffer(uint8_t* data, size_t len) { - size_t ret = _content(data, len, _filledLength); - if (ret != RESPONSE_TRY_AGAIN) { - _filledLength += ret; - } - return ret; -} - -/* - * Progmem Response - * */ - -AsyncProgmemResponse::AsyncProgmemResponse(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { - _code = code; - _content = content; - _contentType = contentType; - _contentLength = len; - _readLength = 0; -} - -size_t AsyncProgmemResponse::_fillBuffer(uint8_t* data, size_t len) { - size_t left = _contentLength - _readLength; - if (left > len) { - memcpy_P(data, _content + _readLength, len); - _readLength += len; - return len; - } - memcpy_P(data, _content + _readLength, left); - _readLength += left; - return left; -} - -/* - * Response Stream (You can print/write/printf to it, up to the contentLen bytes) - * */ - -AsyncResponseStream::AsyncResponseStream(const char* contentType, size_t bufferSize) { - _code = 200; - _contentLength = 0; - _contentType = contentType; - _content = std::unique_ptr(new cbuf(bufferSize)); // std::make_unique(bufferSize); -} - -AsyncResponseStream::~AsyncResponseStream() = default; - -size_t AsyncResponseStream::_fillBuffer(uint8_t* buf, size_t maxLen) { - return _content->read((char*)buf, maxLen); -} - -size_t AsyncResponseStream::write(const uint8_t* data, size_t len) { - if (_started()) - return 0; - - if (len > _content->room()) { - size_t needed = len - _content->room(); - _content->resizeAdd(needed); - } - size_t written = _content->write((const char*)data, len); - _contentLength += written; - return written; -} - -size_t AsyncResponseStream::write(uint8_t data) { - return write(&data, 1); -} diff --git a/lib/ESPAsyncWebServer/src/WebServer.cpp b/lib/ESPAsyncWebServer/src/WebServer.cpp deleted file mode 100644 index d7c9a02..0000000 --- a/lib/ESPAsyncWebServer/src/WebServer.cpp +++ /dev/null @@ -1,227 +0,0 @@ -/* - Asynchronous WebServer library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ -#include "ESPAsyncWebServer.h" -#include "WebHandlerImpl.h" - -using namespace asyncsrv; - -bool ON_STA_FILTER(AsyncWebServerRequest* request) { - #ifndef CONFIG_IDF_TARGET_ESP32H2 - return WiFi.localIP() == request->client()->localIP(); - #else - return false; - #endif -} - -bool ON_AP_FILTER(AsyncWebServerRequest* request) { - #ifndef CONFIG_IDF_TARGET_ESP32H2 - return WiFi.localIP() != request->client()->localIP(); - #else - return false; - #endif -} - -#ifndef HAVE_FS_FILE_OPEN_MODE -const char* fs::FileOpenMode::read = "r"; -const char* fs::FileOpenMode::write = "w"; -const char* fs::FileOpenMode::append = "a"; -#endif - -AsyncWebServer::AsyncWebServer(uint16_t port) - : _server(port) { - _catchAllHandler = new AsyncCallbackWebHandler(); - if (_catchAllHandler == NULL) - return; - _server.onClient([](void* s, AsyncClient* c) { - if (c == NULL) - return; - c->setRxTimeout(3); - AsyncWebServerRequest* r = new AsyncWebServerRequest((AsyncWebServer*)s, c); - if (r == NULL) { - c->close(true); - c->free(); - delete c; - } - }, - this); -} - -AsyncWebServer::~AsyncWebServer() { - reset(); - end(); - if (_catchAllHandler) - delete _catchAllHandler; -} - -AsyncWebRewrite& AsyncWebServer::addRewrite(std::shared_ptr rewrite) { - _rewrites.emplace_back(rewrite); - return *_rewrites.back().get(); -} - -AsyncWebRewrite& AsyncWebServer::addRewrite(AsyncWebRewrite* rewrite) { - _rewrites.emplace_back(rewrite); - return *_rewrites.back().get(); -} - -bool AsyncWebServer::removeRewrite(AsyncWebRewrite* rewrite) { - return removeRewrite(rewrite->from().c_str(), rewrite->toUrl().c_str()); -} - -bool AsyncWebServer::removeRewrite(const char* from, const char* to) { - for (auto r = _rewrites.begin(); r != _rewrites.end(); ++r) { - if (r->get()->from() == from && r->get()->toUrl() == to) { - _rewrites.erase(r); - return true; - } - } - return false; -} - -AsyncWebRewrite& AsyncWebServer::rewrite(const char* from, const char* to) { - _rewrites.emplace_back(std::make_shared(from, to)); - return *_rewrites.back().get(); -} - -AsyncWebHandler& AsyncWebServer::addHandler(AsyncWebHandler* handler) { - _handlers.emplace_back(handler); - return *(_handlers.back().get()); -} - -bool AsyncWebServer::removeHandler(AsyncWebHandler* handler) { - for (auto i = _handlers.begin(); i != _handlers.end(); ++i) { - if (i->get() == handler) { - _handlers.erase(i); - return true; - } - } - return false; -} - -void AsyncWebServer::begin() { - _server.setNoDelay(true); - _server.begin(); -} - -void AsyncWebServer::end() { - _server.end(); -} - -#if ASYNC_TCP_SSL_ENABLED -void AsyncWebServer::onSslFileRequest(AcSSlFileHandler cb, void* arg) { - _server.onSslFileRequest(cb, arg); -} - -void AsyncWebServer::beginSecure(const char* cert, const char* key, const char* password) { - _server.beginSecure(cert, key, password); -} -#endif - -void AsyncWebServer::_handleDisconnect(AsyncWebServerRequest* request) { - delete request; -} - -void AsyncWebServer::_rewriteRequest(AsyncWebServerRequest* request) { - for (const auto& r : _rewrites) { - if (r->match(request)) { - request->_url = r->toUrl(); - request->_addGetParams(r->params()); - } - } -} - -void AsyncWebServer::_attachHandler(AsyncWebServerRequest* request) { - for (auto& h : _handlers) { - if (h->filter(request) && h->canHandle(request)) { - request->setHandler(h.get()); - return; - } - } - - request->addInterestingHeader(T_ANY); - request->setHandler(_catchAllHandler); -} - -AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody) { - AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); - handler->setMethod(method); - handler->onRequest(onRequest); - handler->onUpload(onUpload); - handler->onBody(onBody); - addHandler(handler); - return *handler; -} - -AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload) { - AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); - handler->setMethod(method); - handler->onRequest(onRequest); - handler->onUpload(onUpload); - addHandler(handler); - return *handler; -} - -AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest) { - AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); - handler->setMethod(method); - handler->onRequest(onRequest); - addHandler(handler); - return *handler; -} - -AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, ArRequestHandlerFunction onRequest) { - AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); - handler->onRequest(onRequest); - addHandler(handler); - return *handler; -} - -AsyncStaticWebHandler& AsyncWebServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control) { - AsyncStaticWebHandler* handler = new AsyncStaticWebHandler(uri, fs, path, cache_control); - addHandler(handler); - return *handler; -} - -void AsyncWebServer::onNotFound(ArRequestHandlerFunction fn) { - _catchAllHandler->onRequest(fn); -} - -void AsyncWebServer::onFileUpload(ArUploadHandlerFunction fn) { - _catchAllHandler->onUpload(fn); -} - -void AsyncWebServer::onRequestBody(ArBodyHandlerFunction fn) { - _catchAllHandler->onBody(fn); -} - -void AsyncWebServer::reset() { - _rewrites.clear(); - _handlers.clear(); - - if (_catchAllHandler != NULL) { - _catchAllHandler->onRequest(NULL); - _catchAllHandler->onUpload(NULL); - _catchAllHandler->onBody(NULL); - } -} diff --git a/lib/ESPAsyncWebServer/src/literals.h b/lib/ESPAsyncWebServer/src/literals.h deleted file mode 100644 index 8a7293b..0000000 --- a/lib/ESPAsyncWebServer/src/literals.h +++ /dev/null @@ -1,345 +0,0 @@ -#pragma once - -namespace asyncsrv { - -static constexpr const char* empty = ""; - -#ifndef ESP8622 -static constexpr const char* T_100_CONTINUE = "100-continue"; -static constexpr const char* T_ACCEPT = "Accept"; -static constexpr const char* T_Accept_Ranges = "Accept-Ranges"; -static constexpr const char* T_app_xform_urlencoded = "application/x-www-form-urlencoded"; -static constexpr const char* T_AUTH = "Authorization"; -static constexpr const char* T_BASIC = "Basic"; -static constexpr const char* T_BASIC_REALM = "Basic realm=\""; -static constexpr const char* T_BASIC_REALM_LOGIN_REQ = "Basic realm=\"Login Required\""; -static constexpr const char* T_BODY = "body"; -static constexpr const char* T_Cache_Control = "Cache-Control"; -static constexpr const char* T_chunked = "chunked"; -static constexpr const char* T_close = "close"; -static constexpr const char* T_Connection = "Connection"; -static constexpr const char* T_Content_Disposition = "Content-Disposition"; -static constexpr const char* T_Content_Encoding = "Content-Encoding"; -static constexpr const char* T_Content_Length = "Content-Length"; -static constexpr const char* T_Content_Type = "Content-Type"; -static constexpr const char* T_Cookie = "Cookie"; -static constexpr const char* T_DIGEST = "Digest"; -static constexpr const char* T_DIGEST_ = "Digest "; -static constexpr const char* T_ETag = "ETag"; -static constexpr const char* T_EXPECT = "Expect"; -static constexpr const char* T_HTTP_1_0 = "HTTP/1.0"; -static constexpr const char* T_HTTP_100_CONT = "HTTP/1.1 100 Continue\r\n\r\n"; -static constexpr const char* T_IMS = "If-Modified-Since"; -static constexpr const char* T_INM = "If-None-Match"; -static constexpr const char* T_keep_alive = "keep-alive"; -static constexpr const char* T_Last_Event_ID = "Last-Event-ID"; -static constexpr const char* T_Last_Modified = "Last-Modified"; -static constexpr const char* T_LOCATION = "Location"; -static constexpr const char* T_MULTIPART_ = "multipart/"; -static constexpr const char* T_no_cache = "no-cache"; -static constexpr const char* T_none = "none"; -static constexpr const char* T_UPGRADE = "Upgrade"; -static constexpr const char* T_WS = "websocket"; -static constexpr const char* T_WWW_AUTH = "WWW-Authenticate"; -static constexpr const char* Transfer_Encoding = "Transfer-Encoding"; - -// HTTP Methods -static constexpr const char* T_ANY = "ANY"; -static constexpr const char* T_GET = "GET"; -static constexpr const char* T_POST = "POST"; -static constexpr const char* T_PUT = "PUT"; -static constexpr const char* T_DELETE = "DELETE"; -static constexpr const char* T_PATCH = "PATCH"; -static constexpr const char* T_HEAD = "HEAD"; -static constexpr const char* T_OPTIONS = "OPTIONS"; -static constexpr const char* T_UNKNOWN = "UNKNOWN"; - -// Req content types -static constexpr const char* T_RCT_NOT_USED = "RCT_NOT_USED"; -static constexpr const char* T_RCT_DEFAULT = "RCT_DEFAULT"; -static constexpr const char* T_RCT_HTTP = "RCT_HTTP"; -static constexpr const char* T_RCT_WS = "RCT_WS"; -static constexpr const char* T_RCT_EVENT = "RCT_EVENT"; -static constexpr const char* T_ERROR = "ERROR"; - -// extentions & MIME-Types -static constexpr const char* T__css = ".css"; -static constexpr const char* T__eot = ".eot"; -static constexpr const char* T__gif = ".gif"; -static constexpr const char* T__gz = ".gz"; -static constexpr const char* T__htm = ".htm"; -static constexpr const char* T__html = ".html"; -static constexpr const char* T__ico = ".ico"; -static constexpr const char* T__jpg = ".jpg"; -static constexpr const char* T__js = ".js"; -static constexpr const char* T__json = ".json"; -static constexpr const char* T__pdf = ".pdf"; -static constexpr const char* T__png = ".png"; -static constexpr const char* T__svg = ".svg"; -static constexpr const char* T__ttf = ".ttf"; -static constexpr const char* T__woff = ".woff"; -static constexpr const char* T__woff2 = ".woff2"; -static constexpr const char* T__xml = ".xml"; -static constexpr const char* T__zip = ".zip"; -static constexpr const char* T_application_javascript = "application/javascript"; -static constexpr const char* T_application_json = "application/json"; -static constexpr const char* T_application_msgpack = "application/msgpack"; -static constexpr const char* T_application_pdf = "application/pdf"; -static constexpr const char* T_application_x_gzip = "application/x-gzip"; -static constexpr const char* T_application_zip = "application/zip"; -static constexpr const char* T_font_eot = "font/eot"; -static constexpr const char* T_font_ttf = "font/ttf"; -static constexpr const char* T_font_woff = "font/woff"; -static constexpr const char* T_font_woff2 = "font/woff2"; -static constexpr const char* T_image_gif = "image/gif"; -static constexpr const char* T_image_jpeg = "image/jpeg"; -static constexpr const char* T_image_png = "image/png"; -static constexpr const char* T_image_svg_xml = "image/svg+xml"; -static constexpr const char* T_image_x_icon = "image/x-icon"; -static constexpr const char* T_text_css = "text/css"; -static constexpr const char* T_text_event_stream = "text/event-stream"; -static constexpr const char* T_text_html = "text/html"; -static constexpr const char* T_text_plain = "text/plain"; -static constexpr const char* T_text_xml = "text/xml"; - -// Responce codes -static constexpr const char* T_HTTP_CODE_100 = "Continue"; -static constexpr const char* T_HTTP_CODE_101 = "Switching Protocols"; -static constexpr const char* T_HTTP_CODE_200 = "OK"; -static constexpr const char* T_HTTP_CODE_201 = "Created"; -static constexpr const char* T_HTTP_CODE_202 = "Accepted"; -static constexpr const char* T_HTTP_CODE_203 = "Non-Authoritative Information"; -static constexpr const char* T_HTTP_CODE_204 = "No Content"; -static constexpr const char* T_HTTP_CODE_205 = "Reset Content"; -static constexpr const char* T_HTTP_CODE_206 = "Partial Content"; -static constexpr const char* T_HTTP_CODE_300 = "Multiple Choices"; -static constexpr const char* T_HTTP_CODE_301 = "Moved Permanently"; -static constexpr const char* T_HTTP_CODE_302 = "Found"; -static constexpr const char* T_HTTP_CODE_303 = "See Other"; -static constexpr const char* T_HTTP_CODE_304 = "Not Modified"; -static constexpr const char* T_HTTP_CODE_305 = "Use Proxy"; -static constexpr const char* T_HTTP_CODE_307 = "Temporary Redirect"; -static constexpr const char* T_HTTP_CODE_400 = "Bad Request"; -static constexpr const char* T_HTTP_CODE_401 = "Unauthorized"; -static constexpr const char* T_HTTP_CODE_402 = "Payment Required"; -static constexpr const char* T_HTTP_CODE_403 = "Forbidden"; -static constexpr const char* T_HTTP_CODE_404 = "Not Found"; -static constexpr const char* T_HTTP_CODE_405 = "Method Not Allowed"; -static constexpr const char* T_HTTP_CODE_406 = "Not Acceptable"; -static constexpr const char* T_HTTP_CODE_407 = "Proxy Authentication Required"; -static constexpr const char* T_HTTP_CODE_408 = "Request Time-out"; -static constexpr const char* T_HTTP_CODE_409 = "Conflict"; -static constexpr const char* T_HTTP_CODE_410 = "Gone"; -static constexpr const char* T_HTTP_CODE_411 = "Length Required"; -static constexpr const char* T_HTTP_CODE_412 = "Precondition Failed"; -static constexpr const char* T_HTTP_CODE_413 = "Request Entity Too Large"; -static constexpr const char* T_HTTP_CODE_414 = "Request-URI Too Large"; -static constexpr const char* T_HTTP_CODE_415 = "Unsupported Media Type"; -static constexpr const char* T_HTTP_CODE_416 = "Requested range not satisfiable"; -static constexpr const char* T_HTTP_CODE_417 = "Expectation Failed"; -static constexpr const char* T_HTTP_CODE_500 = "Internal Server Error"; -static constexpr const char* T_HTTP_CODE_501 = "Not Implemented"; -static constexpr const char* T_HTTP_CODE_502 = "Bad Gateway"; -static constexpr const char* T_HTTP_CODE_503 = "Service Unavailable"; -static constexpr const char* T_HTTP_CODE_504 = "Gateway Time-out"; -static constexpr const char* T_HTTP_CODE_505 = "HTTP Version not supported"; -static constexpr const char* T_HTTP_CODE_ANY = "Unknown code"; - -// other -static constexpr const char* T__opaque = "\", opaque=\""; -static constexpr const char* T_13 = "13"; -static constexpr const char* T_asyncesp = "asyncesp"; -static constexpr const char* T_auth_nonce = "\", qop=\"auth\", nonce=\""; -static constexpr const char* T_cnonce = "cnonce"; -static constexpr const char* T_data_ = "data: "; -static constexpr const char* T_event_ = "event: "; -static constexpr const char* T_filename = "filename"; -static constexpr const char* T_gzip = "gzip"; -static constexpr const char* T_Host = "Host"; -static constexpr const char* T_id__ = "id: "; -static constexpr const char* T_name = "name"; -static constexpr const char* T_nc = "nc"; -static constexpr const char* T_nonce = "nonce"; -static constexpr const char* T_opaque = "opaque"; -static constexpr const char* T_qop = "qop"; -static constexpr const char* T_realm = "realm"; -static constexpr const char* T_realm__ = "realm=\""; -static constexpr const char* T_response = "response"; -static constexpr const char* T_retry_ = "retry: "; -static constexpr const char* T_rn = "\r\n"; -static constexpr const char* T_rnrn = "\r\n\r\n"; -static constexpr const char* T_uri = "uri"; -static constexpr const char* T_username = "username"; - - -#else // ESP8622 - -static const char T_100_CONTINUE[] PROGMEM = "100-continue"; -static const char T_ACCEPT[] PROGMEM = "Accept"; -static const char T_Accept_Ranges[] PROGMEM = "Accept-Ranges"; -static const char T_app_xform_urlencoded[] PROGMEM = "application/x-www-form-urlencoded"; -static const char T_AUTH[] PROGMEM = "Authorization"; -static const char T_BASIC[] PROGMEM = "Basic"; -static const char T_BASIC_REALM[] PROGMEM = "Basic realm=\""; -static const char T_BASIC_REALM_LOGIN_REQ[] PROGMEM = "Basic realm=\"Login Required\""; -static const char T_BODY[] PROGMEM = "body"; -static const char T_Cache_Control[] PROGMEM = "Cache-Control"; -static const char T_chunked[] PROGMEM = "chunked"; -static const char T_close[] PROGMEM = "close"; -static const char T_Connection[] PROGMEM = "Connection"; -static const char T_Content_Disposition[] PROGMEM = "Content-Disposition"; -static const char T_Content_Encoding[] PROGMEM = "Content-Encoding"; -static const char T_Content_Length[] PROGMEM = "Content-Length"; -static const char T_Content_Type[] PROGMEM = "Content-Type"; -static const char T_Cookie[] PROGMEM = "Cookie"; -static const char T_DIGEST[] PROGMEM = "Digest"; -static const char T_DIGEST_[] PROGMEM = "Digest "; -static const char T_ETag[] PROGMEM = "ETag"; -static const char T_EXPECT[] PROGMEM = "Expect"; -static const char T_HTTP_1_0[] PROGMEM = "HTTP/1.0"; -static const char T_HTTP_100_CONT[] PROGMEM = "HTTP/1.1 100 Continue\r\n\r\n"; -static const char T_IMS[] PROGMEM = "If-Modified-Since"; -static const char T_INM[] PROGMEM = "If-None-Match"; -static const char T_keep_alive[] PROGMEM = "keep-alive"; -static const char T_Last_Event_ID[] PROGMEM = "Last-Event-ID"; -static const char T_Last_Modified[] PROGMEM = "Last-Modified"; -static const char T_LOCATION[] PROGMEM = "Location"; -static const char T_MULTIPART_[] PROGMEM = "multipart/"; -static const char T_no_cache[] PROGMEM = "no-cache"; -static const char T_none[] PROGMEM = "none"; -static const char T_UPGRADE[] PROGMEM = "Upgrade"; -static const char T_WS[] PROGMEM = "websocket"; -static const char T_WWW_AUTH[] PROGMEM = "WWW-Authenticate"; -static const char Transfer_Encoding[] PROGMEM = "Transfer-Encoding"; - -// HTTP Methods -static const char T_ANY[] PROGMEM = "ANY"; -static const char T_GET[] PROGMEM = "GET"; -static const char T_POST[] PROGMEM = "POST"; -static const char T_PUT[] PROGMEM = "PUT"; -static const char T_DELETE[] PROGMEM = "DELETE"; -static const char T_PATCH[] PROGMEM = "PATCH"; -static const char T_HEAD[] PROGMEM = "HEAD"; -static const char T_OPTIONS[] PROGMEM = "OPTIONS"; -static const char T_UNKNOWN[] PROGMEM = "UNKNOWN"; - -// Req content types -static const char T_RCT_NOT_USED[] PROGMEM = "RCT_NOT_USED"; -static const char T_RCT_DEFAULT[] PROGMEM = "RCT_DEFAULT"; -static const char T_RCT_HTTP[] PROGMEM = "RCT_HTTP"; -static const char T_RCT_WS[] PROGMEM = "RCT_WS"; -static const char T_RCT_EVENT[] PROGMEM = "RCT_EVENT"; -static const char T_ERROR[] PROGMEM = "ERROR"; - -// extentions & MIME-Types -static const char T__css[] PROGMEM = ".css"; -static const char T__eot[] PROGMEM = ".eot"; -static const char T__gif[] PROGMEM = ".gif"; -static const char T__gz[] PROGMEM = ".gz"; -static const char T__htm[] PROGMEM = ".htm"; -static const char T__html[] PROGMEM = ".html"; -static const char T__ico[] PROGMEM = ".ico"; -static const char T__jpg[] PROGMEM = ".jpg"; -static const char T__js[] PROGMEM = ".js"; -static const char T__json[] PROGMEM = ".json"; -static const char T__pdf[] PROGMEM = ".pdf"; -static const char T__png[] PROGMEM = ".png"; -static const char T__svg[] PROGMEM = ".svg"; -static const char T__ttf[] PROGMEM = ".ttf"; -static const char T__woff[] PROGMEM = ".woff"; -static const char T__woff2[] PROGMEM = ".woff2"; -static const char T__xml[] PROGMEM = ".xml"; -static const char T__zip[] PROGMEM = ".zip"; -static const char T_application_javascript[] PROGMEM = "application/javascript"; -static const char T_application_json[] PROGMEM = "application/json"; -static const char T_application_msgpack[] PROGMEM = "application/msgpack"; -static const char T_application_pdf[] PROGMEM = "application/pdf"; -static const char T_application_x_gzip[] PROGMEM = "application/x-gzip"; -static const char T_application_zip[] PROGMEM = "application/zip"; -static const char T_font_eot[] PROGMEM = "font/eot"; -static const char T_font_ttf[] PROGMEM = "font/ttf"; -static const char T_font_woff[] PROGMEM = "font/woff"; -static const char T_font_woff2[] PROGMEM = "font/woff2"; -static const char T_image_gif[] PROGMEM = "image/gif"; -static const char T_image_jpeg[] PROGMEM = "image/jpeg"; -static const char T_image_png[] PROGMEM = "image/png"; -static const char T_image_svg_xml[] PROGMEM = "image/svg+xml"; -static const char T_image_x_icon[] PROGMEM = "image/x-icon"; -static const char T_text_css[] PROGMEM = "text/css"; -static const char T_text_event_stream[] PROGMEM = "text/event-stream"; -static const char T_text_html[] PROGMEM = "text/html"; -static const char T_text_plain[] PROGMEM = "text/plain"; -static const char T_text_xml[] PROGMEM = "text/xml"; - -// Responce codes -static const char T_HTTP_CODE_100[] PROGMEM = "Continue"; -static const char T_HTTP_CODE_101[] PROGMEM = "Switching Protocols"; -static const char T_HTTP_CODE_200[] PROGMEM = "OK"; -static const char T_HTTP_CODE_201[] PROGMEM = "Created"; -static const char T_HTTP_CODE_202[] PROGMEM = "Accepted"; -static const char T_HTTP_CODE_203[] PROGMEM = "Non-Authoritative Information"; -static const char T_HTTP_CODE_204[] PROGMEM = "No Content"; -static const char T_HTTP_CODE_205[] PROGMEM = "Reset Content"; -static const char T_HTTP_CODE_206[] PROGMEM = "Partial Content"; -static const char T_HTTP_CODE_300[] PROGMEM = "Multiple Choices"; -static const char T_HTTP_CODE_301[] PROGMEM = "Moved Permanently"; -static const char T_HTTP_CODE_302[] PROGMEM = "Found"; -static const char T_HTTP_CODE_303[] PROGMEM = "See Other"; -static const char T_HTTP_CODE_304[] PROGMEM = "Not Modified"; -static const char T_HTTP_CODE_305[] PROGMEM = "Use Proxy"; -static const char T_HTTP_CODE_307[] PROGMEM = "Temporary Redirect"; -static const char T_HTTP_CODE_400[] PROGMEM = "Bad Request"; -static const char T_HTTP_CODE_401[] PROGMEM = "Unauthorized"; -static const char T_HTTP_CODE_402[] PROGMEM = "Payment Required"; -static const char T_HTTP_CODE_403[] PROGMEM = "Forbidden"; -static const char T_HTTP_CODE_404[] PROGMEM = "Not Found"; -static const char T_HTTP_CODE_405[] PROGMEM = "Method Not Allowed"; -static const char T_HTTP_CODE_406[] PROGMEM = "Not Acceptable"; -static const char T_HTTP_CODE_407[] PROGMEM = "Proxy Authentication Required"; -static const char T_HTTP_CODE_408[] PROGMEM = "Request Time-out"; -static const char T_HTTP_CODE_409[] PROGMEM = "Conflict"; -static const char T_HTTP_CODE_410[] PROGMEM = "Gone"; -static const char T_HTTP_CODE_411[] PROGMEM = "Length Required"; -static const char T_HTTP_CODE_412[] PROGMEM = "Precondition Failed"; -static const char T_HTTP_CODE_413[] PROGMEM = "Request Entity Too Large"; -static const char T_HTTP_CODE_414[] PROGMEM = "Request-URI Too Large"; -static const char T_HTTP_CODE_415[] PROGMEM = "Unsupported Media Type"; -static const char T_HTTP_CODE_416[] PROGMEM = "Requested range not satisfiable"; -static const char T_HTTP_CODE_417[] PROGMEM = "Expectation Failed"; -static const char T_HTTP_CODE_500[] PROGMEM = "Internal Server Error"; -static const char T_HTTP_CODE_501[] PROGMEM = "Not Implemented"; -static const char T_HTTP_CODE_502[] PROGMEM = "Bad Gateway"; -static const char T_HTTP_CODE_503[] PROGMEM = "Service Unavailable"; -static const char T_HTTP_CODE_504[] PROGMEM = "Gateway Time-out"; -static const char T_HTTP_CODE_505[] PROGMEM = "HTTP Version not supported"; -static const char T_HTTP_CODE_ANY[] PROGMEM = "Unknown code"; - -// other -static const char T__opaque[] PROGMEM = "\", opaque=\""; -static const char T_13[] PROGMEM = "13"; -static const char T_asyncesp[] PROGMEM = "asyncesp"; -static const char T_auth_nonce[] PROGMEM = "\", qop=\"auth\", nonce=\""; -static const char T_cnonce[] PROGMEM = "cnonce"; -static const char T_data_[] PROGMEM = "data: "; -static const char T_event_[] PROGMEM = "event: "; -static const char T_filename[] PROGMEM = "filename"; -static const char T_gzip[] PROGMEM = "gzip"; -static const char T_Host[] PROGMEM = "Host"; -static const char T_id__[] PROGMEM = "id: "; -static const char T_name[] PROGMEM = "name"; -static const char T_nc[] PROGMEM = "nc"; -static const char T_nonce[] PROGMEM = "nonce"; -static const char T_opaque[] PROGMEM = "opaque"; -static const char T_qop[] PROGMEM = "qop"; -static const char T_realm[] PROGMEM = "realm"; -static const char T_realm__[] PROGMEM = "realm=\""; -static const char T_response[] PROGMEM = "response"; -static const char T_retry_[] PROGMEM = "retry: "; -static const char T_rn[] PROGMEM = "\r\n"; -static const char T_rnrn[] PROGMEM = "\r\n\r\n"; -static const char T_uri[] PROGMEM = "uri"; -static const char T_username[] PROGMEM = "username"; - -#endif // ESP8622 - -} // namespace asyncsrv {} diff --git a/lib/ESPAsyncWebServer/src/port/SHA1Builder.cpp b/lib/ESPAsyncWebServer/src/port/SHA1Builder.cpp deleted file mode 100644 index 901fb80..0000000 --- a/lib/ESPAsyncWebServer/src/port/SHA1Builder.cpp +++ /dev/null @@ -1,284 +0,0 @@ -/* - * FIPS-180-1 compliant SHA-1 implementation - * - * Copyright (C) 2006-2015, ARM Limited, All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * This file is part of mbed TLS (https://tls.mbed.org) - * Modified for esp32 by Lucas Saavedra Vaz on 11 Jan 2024 - */ - -#include -#if ESP_IDF_VERSION_MAJOR < 5 - -#include "SHA1Builder.h" - -// 32-bit integer manipulation macros (big endian) - -#ifndef GET_UINT32_BE -#define GET_UINT32_BE(n, b, i) \ - { (n) = ((uint32_t)(b)[(i)] << 24) | ((uint32_t)(b)[(i) + 1] << 16) | ((uint32_t)(b)[(i) + 2] << 8) | ((uint32_t)(b)[(i) + 3]); } -#endif - -#ifndef PUT_UINT32_BE -#define PUT_UINT32_BE(n, b, i) \ - { \ - (b)[(i)] = (uint8_t)((n) >> 24); \ - (b)[(i) + 1] = (uint8_t)((n) >> 16); \ - (b)[(i) + 2] = (uint8_t)((n) >> 8); \ - (b)[(i) + 3] = (uint8_t)((n)); \ - } -#endif - -// Constants - -static const uint8_t sha1_padding[64] = {0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - -// Private methods - -void SHA1Builder::process(const uint8_t *data) { - uint32_t temp, W[16], A, B, C, D, E; - - GET_UINT32_BE(W[0], data, 0); - GET_UINT32_BE(W[1], data, 4); - GET_UINT32_BE(W[2], data, 8); - GET_UINT32_BE(W[3], data, 12); - GET_UINT32_BE(W[4], data, 16); - GET_UINT32_BE(W[5], data, 20); - GET_UINT32_BE(W[6], data, 24); - GET_UINT32_BE(W[7], data, 28); - GET_UINT32_BE(W[8], data, 32); - GET_UINT32_BE(W[9], data, 36); - GET_UINT32_BE(W[10], data, 40); - GET_UINT32_BE(W[11], data, 44); - GET_UINT32_BE(W[12], data, 48); - GET_UINT32_BE(W[13], data, 52); - GET_UINT32_BE(W[14], data, 56); - GET_UINT32_BE(W[15], data, 60); - -#define sha1_S(x, n) ((x << n) | ((x & 0xFFFFFFFF) >> (32 - n))) - -#define sha1_R(t) (temp = W[(t - 3) & 0x0F] ^ W[(t - 8) & 0x0F] ^ W[(t - 14) & 0x0F] ^ W[t & 0x0F], (W[t & 0x0F] = sha1_S(temp, 1))) - -#define sha1_P(a, b, c, d, e, x) \ - { \ - e += sha1_S(a, 5) + sha1_F(b, c, d) + sha1_K + x; \ - b = sha1_S(b, 30); \ - } - - A = state[0]; - B = state[1]; - C = state[2]; - D = state[3]; - E = state[4]; - -#define sha1_F(x, y, z) (z ^ (x & (y ^ z))) -#define sha1_K 0x5A827999 - - sha1_P(A, B, C, D, E, W[0]); - sha1_P(E, A, B, C, D, W[1]); - sha1_P(D, E, A, B, C, W[2]); - sha1_P(C, D, E, A, B, W[3]); - sha1_P(B, C, D, E, A, W[4]); - sha1_P(A, B, C, D, E, W[5]); - sha1_P(E, A, B, C, D, W[6]); - sha1_P(D, E, A, B, C, W[7]); - sha1_P(C, D, E, A, B, W[8]); - sha1_P(B, C, D, E, A, W[9]); - sha1_P(A, B, C, D, E, W[10]); - sha1_P(E, A, B, C, D, W[11]); - sha1_P(D, E, A, B, C, W[12]); - sha1_P(C, D, E, A, B, W[13]); - sha1_P(B, C, D, E, A, W[14]); - sha1_P(A, B, C, D, E, W[15]); - sha1_P(E, A, B, C, D, sha1_R(16)); - sha1_P(D, E, A, B, C, sha1_R(17)); - sha1_P(C, D, E, A, B, sha1_R(18)); - sha1_P(B, C, D, E, A, sha1_R(19)); - -#undef sha1_K -#undef sha1_F - -#define sha1_F(x, y, z) (x ^ y ^ z) -#define sha1_K 0x6ED9EBA1 - - sha1_P(A, B, C, D, E, sha1_R(20)); - sha1_P(E, A, B, C, D, sha1_R(21)); - sha1_P(D, E, A, B, C, sha1_R(22)); - sha1_P(C, D, E, A, B, sha1_R(23)); - sha1_P(B, C, D, E, A, sha1_R(24)); - sha1_P(A, B, C, D, E, sha1_R(25)); - sha1_P(E, A, B, C, D, sha1_R(26)); - sha1_P(D, E, A, B, C, sha1_R(27)); - sha1_P(C, D, E, A, B, sha1_R(28)); - sha1_P(B, C, D, E, A, sha1_R(29)); - sha1_P(A, B, C, D, E, sha1_R(30)); - sha1_P(E, A, B, C, D, sha1_R(31)); - sha1_P(D, E, A, B, C, sha1_R(32)); - sha1_P(C, D, E, A, B, sha1_R(33)); - sha1_P(B, C, D, E, A, sha1_R(34)); - sha1_P(A, B, C, D, E, sha1_R(35)); - sha1_P(E, A, B, C, D, sha1_R(36)); - sha1_P(D, E, A, B, C, sha1_R(37)); - sha1_P(C, D, E, A, B, sha1_R(38)); - sha1_P(B, C, D, E, A, sha1_R(39)); - -#undef sha1_K -#undef sha1_F - -#define sha1_F(x, y, z) ((x & y) | (z & (x | y))) -#define sha1_K 0x8F1BBCDC - - sha1_P(A, B, C, D, E, sha1_R(40)); - sha1_P(E, A, B, C, D, sha1_R(41)); - sha1_P(D, E, A, B, C, sha1_R(42)); - sha1_P(C, D, E, A, B, sha1_R(43)); - sha1_P(B, C, D, E, A, sha1_R(44)); - sha1_P(A, B, C, D, E, sha1_R(45)); - sha1_P(E, A, B, C, D, sha1_R(46)); - sha1_P(D, E, A, B, C, sha1_R(47)); - sha1_P(C, D, E, A, B, sha1_R(48)); - sha1_P(B, C, D, E, A, sha1_R(49)); - sha1_P(A, B, C, D, E, sha1_R(50)); - sha1_P(E, A, B, C, D, sha1_R(51)); - sha1_P(D, E, A, B, C, sha1_R(52)); - sha1_P(C, D, E, A, B, sha1_R(53)); - sha1_P(B, C, D, E, A, sha1_R(54)); - sha1_P(A, B, C, D, E, sha1_R(55)); - sha1_P(E, A, B, C, D, sha1_R(56)); - sha1_P(D, E, A, B, C, sha1_R(57)); - sha1_P(C, D, E, A, B, sha1_R(58)); - sha1_P(B, C, D, E, A, sha1_R(59)); - -#undef sha1_K -#undef sha1_F - -#define sha1_F(x, y, z) (x ^ y ^ z) -#define sha1_K 0xCA62C1D6 - - sha1_P(A, B, C, D, E, sha1_R(60)); - sha1_P(E, A, B, C, D, sha1_R(61)); - sha1_P(D, E, A, B, C, sha1_R(62)); - sha1_P(C, D, E, A, B, sha1_R(63)); - sha1_P(B, C, D, E, A, sha1_R(64)); - sha1_P(A, B, C, D, E, sha1_R(65)); - sha1_P(E, A, B, C, D, sha1_R(66)); - sha1_P(D, E, A, B, C, sha1_R(67)); - sha1_P(C, D, E, A, B, sha1_R(68)); - sha1_P(B, C, D, E, A, sha1_R(69)); - sha1_P(A, B, C, D, E, sha1_R(70)); - sha1_P(E, A, B, C, D, sha1_R(71)); - sha1_P(D, E, A, B, C, sha1_R(72)); - sha1_P(C, D, E, A, B, sha1_R(73)); - sha1_P(B, C, D, E, A, sha1_R(74)); - sha1_P(A, B, C, D, E, sha1_R(75)); - sha1_P(E, A, B, C, D, sha1_R(76)); - sha1_P(D, E, A, B, C, sha1_R(77)); - sha1_P(C, D, E, A, B, sha1_R(78)); - sha1_P(B, C, D, E, A, sha1_R(79)); - -#undef sha1_K -#undef sha1_F - - state[0] += A; - state[1] += B; - state[2] += C; - state[3] += D; - state[4] += E; -} - -// Public methods - -void SHA1Builder::begin(void) { - total[0] = 0; - total[1] = 0; - - state[0] = 0x67452301; - state[1] = 0xEFCDAB89; - state[2] = 0x98BADCFE; - state[3] = 0x10325476; - state[4] = 0xC3D2E1F0; - - memset(buffer, 0x00, sizeof(buffer)); - memset(hash, 0x00, sizeof(hash)); -} - -void SHA1Builder::add(const uint8_t *data, size_t len) { - size_t fill; - uint32_t left; - - if (len == 0) { - return; - } - - left = total[0] & 0x3F; - fill = 64 - left; - - total[0] += (uint32_t)len; - total[0] &= 0xFFFFFFFF; - - if (total[0] < (uint32_t)len) { - total[1]++; - } - - if (left && len >= fill) { - memcpy((void *)(buffer + left), data, fill); - process(buffer); - data += fill; - len -= fill; - left = 0; - } - - while (len >= 64) { - process(data); - data += 64; - len -= 64; - } - - if (len > 0) { - memcpy((void *)(buffer + left), data, len); - } -} - -void SHA1Builder::calculate(void) { - uint32_t last, padn; - uint32_t high, low; - uint8_t msglen[8]; - - high = (total[0] >> 29) | (total[1] << 3); - low = (total[0] << 3); - - PUT_UINT32_BE(high, msglen, 0); - PUT_UINT32_BE(low, msglen, 4); - - last = total[0] & 0x3F; - padn = (last < 56) ? (56 - last) : (120 - last); - - add((uint8_t *)sha1_padding, padn); - add(msglen, 8); - - PUT_UINT32_BE(state[0], hash, 0); - PUT_UINT32_BE(state[1], hash, 4); - PUT_UINT32_BE(state[2], hash, 8); - PUT_UINT32_BE(state[3], hash, 12); - PUT_UINT32_BE(state[4], hash, 16); -} - -void SHA1Builder::getBytes(uint8_t *output) { - memcpy(output, hash, SHA1_HASH_SIZE); -} - -#endif // ESP_IDF_VERSION_MAJOR < 5 diff --git a/lib/ESPAsyncWebServer/src/port/SHA1Builder.h b/lib/ESPAsyncWebServer/src/port/SHA1Builder.h deleted file mode 100644 index da9a77a..0000000 --- a/lib/ESPAsyncWebServer/src/port/SHA1Builder.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef SHA1Builder_h -#define SHA1Builder_h - -#include -#include - -#define SHA1_HASH_SIZE 20 - -class SHA1Builder { - private: - uint32_t total[2]; /* number of bytes processed */ - uint32_t state[5]; /* intermediate digest state */ - unsigned char buffer[64]; /* data block being processed */ - uint8_t hash[SHA1_HASH_SIZE]; /* SHA-1 result */ - - void process(const uint8_t* data); - - public: - void begin(); - void add(const uint8_t* data, size_t len); - void calculate(); - void getBytes(uint8_t* output); -}; - -#endif // SHA1Builder_h diff --git a/lib/PsychicHttp/CHANGELOG.md b/lib/PsychicHttp/CHANGELOG.md new file mode 100644 index 0000000..ca99a11 --- /dev/null +++ b/lib/PsychicHttp/CHANGELOG.md @@ -0,0 +1,34 @@ +# v1.2.1 + +* Fix bug with missing include preventing the HTTPS server from compiling. + +# v1.2 + +* Added TemplatePrinter from https://github.com/Chris--A/PsychicHttp/tree/templatePrint +* Support using as ESP IDF component +* Optional using https server in ESP IDF +* Fixed bug with headers +* Add ESP IDF example + CI script +* Added Arduino Captive Portal example and OTAUpdate from @06GitHub +* HTTPS fix for ESP-IDF v5.0.2+ from @06GitHub +* lots of bugfixes from @mathieucarbou + +Thanks to @Chris--A, @06GitHub, and @dzungpv for your contributions. + +# v1.1 + +* Changed the internal structure to support request handlers on endpoints and generic requests that do not match an endpoint + * websockets, uploads, etc should now create an appropriate handler and attach to an endpoint with the server.on() syntax +* Added PsychicClient to abstract away some of the internals of ESP-IDF sockets + add convenience + * onOpen and onClose callbacks have changed as a result +* Added support for EventSource / SSE +* Added support for multipart file uploads +* changed getParam() to return a PsychicWebParameter in line with ESPAsyncWebserver +* Renamed various classes / files: + * PsychicHttpFileResponse -> PsychicFileResponse + * PsychicHttpServerEndpoint -> PsychicEndpoint + * PsychicHttpServerRequest -> PsychicRequest + * PsychicHttpServerResponse -> PsychicResponse + * PsychicHttpWebsocket.h -> PsychicWebSocket.h + * Websocket => WebSocket +* Quite a few bugfixes from the community. Thank you @glennsky, @gb88, @KastanEr, @kstam, and @zekageri \ No newline at end of file diff --git a/lib/ESPAsyncWebServer/CMakeLists.txt b/lib/PsychicHttp/CMakeLists.txt similarity index 82% rename from lib/ESPAsyncWebServer/CMakeLists.txt rename to lib/PsychicHttp/CMakeLists.txt index 64292ec..4f76477 100644 --- a/lib/ESPAsyncWebServer/CMakeLists.txt +++ b/lib/PsychicHttp/CMakeLists.txt @@ -8,7 +8,9 @@ set(COMPONENT_ADD_INCLUDEDIRS set(COMPONENT_REQUIRES "arduino-esp32" - "AsyncTCP" + "esp_https_server" + "ArduinoJson" + "UrlEncode" ) register_component() diff --git a/lib/PsychicHttp/LICENSE b/lib/PsychicHttp/LICENSE new file mode 100644 index 0000000..8e797d9 --- /dev/null +++ b/lib/PsychicHttp/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2024 Jeremy Poulter, Zachary Smith, and Mathieu Carbou + +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. \ No newline at end of file diff --git a/lib/PsychicHttp/README.md b/lib/PsychicHttp/README.md new file mode 100644 index 0000000..5c4289e --- /dev/null +++ b/lib/PsychicHttp/README.md @@ -0,0 +1,826 @@ +# PsychicHttp - HTTP on your ESP 🧙🔮 + +PsychicHttp is a webserver library for ESP32 + Arduino framework which uses the [ESP-IDF HTTP Server](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/protocols/esp_http_server.html) library under the hood. It is written in a similar style to the [Arduino WebServer](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer), [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer), and [ArduinoMongoose](https://github.com/jeremypoulter/ArduinoMongoose) libraries to make writing code simple and porting from those other libraries straightforward. + +# Features + +* Asynchronous approach (server runs in its own FreeRTOS thread) +* Handles all HTTP methods with lots of convenience functions: + * GET/POST parameters + * get/set headers + * get/set cookies + * basic key/value session data storage + * authentication (basic and digest mode) +* HTTPS / SSL support +* Static fileserving (SPIFFS, LittleFS, etc.) +* Chunked response serving for large files +* File uploads (Basic + Multipart) +* Websocket support with onOpen, onFrame, and onClose callbacks +* EventSource / SSE support with onOpen, and onClose callbacks +* Request filters, including Client vs AP mode (ON_STA_FILTER / ON_AP_FILTER) +* TemplatePrinter class for dynamic variables at runtime + +## Differences from ESPAsyncWebserver + +* No templating system (anyone actually use this?) +* No url rewriting (but you can use request->redirect) + +# Usage + +## Installation + +### Platformio + +[PlatformIO](http://platformio.org) is an open source ecosystem for IoT development. + + Add "PsychicHttp" to project using [Project Configuration File `platformio.ini`](http://docs.platformio.org/page/projectconf.html) and [lib_deps](http://docs.platformio.org/page/projectconf/section_env_library.html#lib-deps) option: + +```ini +[env:myboard] +platform = espressif... +board = ... +framework = arduino + +# using the latest stable version +lib_deps = hoeken/PsychicHttp + +# or using GIT Url (the latest development version) +lib_deps = https://github.com/hoeken/PsychicHttp +``` + +### Installation - Arduino + +Open *Tools -> Manage Libraries...* and search for PsychicHttp. + +# Principles of Operation + +## Things to Note + +* PsychicHttp is a fully asynchronous server and as such does not run on the loop thread. +* You should not use yield or delay or any function that uses them inside the callbacks. +* The server is smart enough to know when to close the connection and free resources. +* You can not send more than one response to a single request. + +## PsychicHttp + +* Listens for connections. +* Wraps the incoming request into PsychicRequest. +* Keeps track of clients + calls optional callbacks on client open and close. +* Find the appropriate handler (if any) for a request and pass it on. + +## Request Life Cycle + +* TCP connection is received by the server. +* HTTP request is wrapped inside ```PsychicRequest``` object + TCP Connection wrapped inside PsychicConnection object. +* When the request head is received, the server goes through all ```PsychicEndpoints``` and finds one that matches the url + method. + * ```handler->filter()``` and ```handler->canHandle()``` are called on the handler to verify the handler should process the request. + * ```handler->needsAuthentication()``` is called and sends an authorization response if required. + * ```handler->handleRequest()``` is called to actually process the HTTP request. +* If the handler cannot process the request, the server will loop through any global handlers and call that handler if it passes filter(), canHandle(), and needsAuthentication(). +* If no global handlers are called, the server.defaultEndpoint handler will be called. +* Each handler is responsible for processing the request and sending a response. +* When the response is sent, the client is closed and freed from the memory. + * Unless its a special handler like websockets or eventsource. + +![Flowchart of Request Lifecycle](/assets/request-flow.svg) + +### Handlers + +* ```PsychicHandler``` is used for processing and responding to specific HTTP requests. +* ```PsychicHandler``` instances can be attached to any endpoint or as global handlers. +* Setting a ```Filter``` to the ```PsychicHandler``` controls when to apply the handler, decision can be based on + request method, url, request host/port/target host, the request client's localIP or remoteIP. +* Two filter callbacks are provided: ```ON_AP_FILTER``` to execute the rewrite when request is made to the AP interface, + ```ON_STA_FILTER``` to execute the rewrite when request is made to the STA interface. +* The ```canHandle``` method is used for handler specific control on whether the requests can be handled. Decision can be based on request method, request url, request host/port/target host. +* Depending on how the handler is implemented, it may provide callbacks for adding your own custom processing code to the handler. +* Global ```Handlers``` are evaluated in the order they are attached to the server. The ```canHandle``` is called only + if the ```Filter``` that was set to the ```Handler``` return true. +* The first global ```Handler``` that can handle the request is selected, no further processing of handlers is called. + +![Flowchart of Request Lifecycle](/assets/handler-callbacks.svg) + +### Responses and how do they work + +* The ```PsychicResponse``` objects are used to send the response data back to the client. +* Typically the response should be fully generated and sent from the callback. +* It may be possible to generate the response outside the callback, but it will be difficult. + * The exceptions are websockets + eventsource where the response is sent, but the connection is maintained and new data can be sent/received outside the handler. + +# Porting From ESPAsyncWebserver + +If you have existing code using ESPAsyncWebserver, you will feel right at home with PsychicHttp. Even if internally it is much different, the external interface is very similar. Some things are mostly cosmetic, like different class names and callback definitions. A few things might require a bit more in-depth approach. If you're porting your code and run into issues that aren't covered here, please post and issue. + +## Globals Stuff + +* Change your #include to ```#include ``` +* Change your server instance: ```PsychicHttpServer server;``` +* Define websocket handler if you have one: ```PsychicWebSocketHandler websocketHandler;``` +* Define eventsource if you have one: ```PsychicEventSource eventSource;``` + +## setup() Stuff + +* no more server.begin(), call server.listen(80), before you add your handlers +* server has a configurable limit on .on() endpoints. change it with ```server.config.max_uri_handlers = 20;``` as needed. +* check your callback function definitions: + * AsyncWebServerRequest -> PsychicRequest + * no more onBody() event + * for small bodies (server.maxRequestBodySize, default 16k) it will be automatically loaded and accessed by request->body() + * for large bodies, use an upload handler and onUpload() + * websocket callbacks are much different (and simpler!) + * websocket / eventsource handlers get attached to url in server.on("/url", &handler) instead of passing url to handler constructor. + * eventsource callbacks are onOpen and onClose now. +* HTTP_ANY is not supported by ESP-IDF, so we can't use it either. +* NO server.onFileUpload(onUpload); (you could attach an UploadHandler to the default endpoint i guess?) +* NO server.onRequestBody(onBody); (same) + +## Requests / Responses + +* request->send is now request->reply() +* if you create a response, call response->send() directly, not request->send(reply) +* request->headers() is not supported by ESP-IDF, you have to just check for the header you need. +* No AsyncCallbackJsonWebHandler (for now... can add if needed) +* No request->beginResponse(). Instanciate a PsychicResponse instead: ```PsychicResponse response(request);``` +* No PROGMEM suppport (its not relevant to ESP32: https://esp32.com/viewtopic.php?t=20595) +* No Stream response support just yet + +# Usage + +## Create the Server + +Here is an example of the typical server setup: + +```cpp +#include +PsychicHttpServer server; + +void setup() +{ + //optional low level setup server config stuff here. + //server.config is an ESP-IDF httpd_config struct + //see: https://docs.espressif.com/projects/esp-idf/en/v4.4.6/esp32/api-reference/protocols/esp_http_server.html#_CPPv412httpd_config + //increase maximum number of uri endpoint handlers (.on() calls) + server.config.max_uri_handlers = 20; + + //connect to wifi + + //start the server listening on port 80 (standard HTTP port) + server.listen(80); + + //call server methods to attach endpoints and handlers + server.on(...); + server.serveStatic(...); + server.attachHandler(...); +} +``` + +## Add Handlers + +One major difference from ESPAsyncWebserver is that handlers can be attached to a specific url (endpoint) or as a global handler. The reason for this, is that attaching to a specific URL is more efficient and makes for cleaner code. + +### Endpoint Handlers + +An endpoint is basically just the URL path (eg. /path/to/file) without any query string. The ```server.on(...)``` function is a convenience function for creating endpoints and attaching a handler to them. There are two main styles: attaching a basic ```WebRequest``` handler and attaching an external handler. + +```cpp +//creates a basic PsychicWebHandler that calls the request_callback callback +server.on("/url", HTTP_GET, request_callback); + +//same as above, but defaults to HTTP_GET +server.on("/url", request_callback); + +//attaches a websocket handler to /ws +PsychicWebSocketHandler websocketHandler; +server.on("/ws", &websocketHandler); +``` + +The ```server.on(...)``` returns a pointer to the endpoint, which can be used to call various functions like ```setHandler()```, ```setFilter()```, and ```setAuthentication()```. + +```cpp +//respond to /url only from requests to the AP +server.on("/url", HTTP_GET, request_callback)->setFilter(ON_AP_FILTER); + +//require authentication on /url +server.on("/url", HTTP_GET, request_callback)->setAuthentication("user", "pass"); + +//attach websocket handler to /ws +PsychicWebSocketHandler websocketHandler; +server.on("/ws")->attachHandler(&websocketHandler); +``` + +### Basic Requests + +The ```PsychicWebHandler``` class is for handling standard web requests. It provides a single callback: ```onRequest()```. This callback is called when the handler receives a valid HTTP request. + +One major difference from ESPAsyncWebserver is that this callback needs to return an esp_err_t variable to let the server know the result of processing the request. The ```response->reply()``` and ```request->send()``` functions will return this. It is a good habit to return the result of these functions as sending the response will close the connection. + +The function definition for the onRequest callback is: + +```cpp +esp_err_t function_name(PsychicRequest *request); +``` + +Here is a simple example that sends back the client's IP on the URL /ip + +```cpp +server.on("/ip", [](PsychicRequest *request) +{ + String output = "Your IP is: " + request->client()->remoteIP().toString(); + return request->reply(output.c_str()); +}); +``` + +### Uploads + +The ```PsychicUploadHandler``` class is for handling uploads, both large POST bodies and multipart encoded forms. It provides two callbacks: ```onUpload()``` and ```onRequest()```. + +```onUpload(...)``` is called when there is new data. This function may be called multiple times so that you can process the data in chunks. The function definition for the onUpload callback is: + +```cpp +esp_err_t function_name(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final); +``` + +* request is a pointer to the Request object +* filename is the name of the uploaded file +* index is the overall byte position of the current data +* data is a pointer to the data buffer +* len is the length of the data buffer +* final is a flag to tell if its the last chunk of data + +```onRequest(...)``` is called after the successful handling of the upload. Its definition and usage is the same as the basic request example as above. + +#### Basic Upload (file is the entire POST body) + +It's worth noting that there is no standard way of passing in a filename for this method, so the handler attempts to guess the filename with the following methods: + +* Checking the Content-Disposition header +* Checking the _filename query parameter (eg. /upload?filename=filename.txt becomes filename.txt) +* Checking the url and taking the last part as filename (eg. /upload/filename.txt becomes filename.txt). You must set a wildcard url for this to work as in the example below. + +```cpp +//handle a very basic upload as post body + PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); + uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + Serial.printf("Writing %d/%d bytes to: %s\n", (int)index+(int)len, request->contentLength(), path.c_str()); + + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + uploadHandler->onRequest([](PsychicRequest *request) + { + String url = "/" + request->getFilename(); + String output = "" + url + ""; + + return request->reply(output.c_str()); + }); + + //wildcard basic file upload - POST to /upload/filename.ext + server.on("/upload/*", HTTP_POST, uploadHandler); +``` + +#### Multipart Upload + +Very similar to the basic upload, with 2 key differences: + +* multipart requests don't know the total size of the file until after it has been fully processed. You can get a rough idea with request->contentLength(), but that is the length of the entire multipart encoded request. +* you can access form variables, including multipart file infor (name + size) in the onRequest handler using request->getParam() + +```cpp + //a little bit more complicated multipart form + PsychicUploadHandler *multipartHandler = new PsychicUploadHandler(); + multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + //some progress over serial. + Serial.printf("Writing %d bytes to: %s\n", (int)len, path.c_str()); + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + multipartHandler->onRequest([](PsychicRequest *request) + { + PsychicWebParameter *file = request->getParam("file_upload"); + + String url = "/" + file->value(); + String output; + + output += "" + url + "
\n"; + output += "Bytes: " + String(file->size()) + "
\n"; + output += "Param 1: " + request->getParam("param1")->value() + "
\n"; + output += "Param 2: " + request->getParam("param2")->value() + "
\n"; + + return request->reply(output.c_str()); + }); + + //upload to /multipart url + server.on("/multipart", HTTP_POST, multipartHandler); +``` + +### Static File Serving + +The ```PsychicStaticFileHandler``` is a special handler that does not provide any callbacks. It is used to serve a file or files from a specific directory in a filesystem to a directory on the webserver. The syntax is exactly the same as ESPAsyncWebserver. Anything that is derived from the ```FS``` class should work (eg. SPIFFS, LittleFS, SD, etc) + +A couple important notes: + +* If it finds a file with an extra .gz extension, it will serve it as gzip encoded (eg: /targetfile.ext -> {targetfile.ext}.gz) +* If the file is larger than FILE_CHUNK_SIZE (default 8kb) then it will send it as a chunked response. +* It will detect most basic filetypes and automatically set the appropriate Content-Type + +The ```server.serveStatic()``` function handles creating the handler and assigning it to the server: + +```cpp +//serve static files from LittleFS/www on / only to clients on same wifi network +//this is where our /index.html file lives +server.serveStatic("/", LittleFS, "/www/")->setFilter(ON_STA_FILTER); + +//serve static files from LittleFS/www-ap on / only to clients on SoftAP +//this is where our /index.html file lives +server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); + +//serve static files from LittleFS/img on /img +//it's more efficient to serve everything from a single www directory, but this is also possible. +server.serveStatic("/img", LittleFS, "/img/"); + +//you can also serve single files +server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); +``` + +You could also theoretically use the file response directly: + +```cpp +server.on("/ip", [](PsychicRequest *request) +{ + String filename = "/path/to/file"; + PsychicFileResponse response(request, LittleFS, filename); + + return response.send(); +}); +PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path) +``` + +### Websockets + +The ```PsychicWebSocketHandler``` class is for handling WebSocket connections. It provides 3 callbacks: + +```onOpen(...)``` is called when a new WebSocket client connects. +```onFrame(...)``` is called when a new WebSocket frame has arrived. +```onClose(...)``` is called when a new WebSocket client disconnects. + +Here are the callback definitions: + +```cpp +void open_function(PsychicWebSocketClient *client); +esp_err_t frame_function(PsychicWebSocketRequest *request, httpd_ws_frame *frame); +void close_function(PsychicWebSocketClient *client); +``` + +WebSockets were the main reason for starting PsychicHttp, so they are well tested. They are also much simplified from the ESPAsyncWebserver style. You do not need to worry about error handling, partial frame assembly, PONG messages, etc. The onFrame() function is called when a complete frame has been received, and can handle frames up to the entire available heap size. + +Here is a basic example of using WebSockets: + +```cpp + //create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed. + PsychicWebSocketHandler websocketHandler(); + + websocketHandler.onOpen([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); + client->sendMessage("Hello!"); + }); + + websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { + Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); + return request->reply(frame); + }); + + websocketHandler.onClose([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); + }); + + //attach the handler to /ws. You can then connect to ws://ip.address/ws + server.on("/ws", &websocketHandler); +``` + +The onFrame() callback has 2 parameters: + +* ```PsychicWebSocketRequest *request``` a special request with helper functions for replying in websocket format. +* ```httpd_ws_frame *frame``` ESP-IDF websocket struct. The important struct members we care about are: + * ```uint8_t *payload; /*!< Pre-allocated data buffer */``` + * ```size_t len; /*!< Length of the WebSocket data */``` + +For sending data on the websocket connection, there are 3 methods: + +* ```request->reply()``` - only available in the onFrame() callback context. +* ```webSocketHandler.sendAll()``` - can be used anywhere to send websocket messages to all connected clients. +* ```client->send()``` - can be used anywhere* to send a websocket message to a specific client + +All of the above functions either accept simple ```char *``` string of you can construct your own httpd_ws_frame. + +*Special Note:* Do not hold on to the ```PsychicWebSocketClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects. Instead, store the ```int client->socket()```. Then when you want to send a message, use this code: + +```cpp +//make sure our client is still connected. +PsychicWebSocketClient *client = websocketHandler.getClient(socket); +if (client != NULL) + client->send("Your Message") +``` + +### EventSource / SSE + +The ```PsychicEventSource``` class is for handling EventSource / SSE connections. It provides 2 callbacks: + +```onOpen(...)``` is called when a new EventSource client connects. +```onClose(...)``` is called when a new EventSource client disconnects. + +Here are the callback definitions: + +```cpp +void open_function(PsychicEventSourceClient *client); +void close_function(PsychicEventSourceClient *client); +``` + +Here is a basic example of using PsychicEventSource: + +```cpp + //create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed. + PsychicEventSource eventSource; + + eventSource.onOpen([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); + client->send("Hello user!", NULL, millis(), 1000); + }); + + eventSource.onClose([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); + }); + + //attach the handler to /events + server.on("/events", &eventSource); +``` + +For sending data on the EventSource connection, there are 2 methods: + +* ```eventSource.send()``` - can be used anywhere to send events to all connected clients. +* ```client->send()``` - can be used anywhere* to send events to a specific client + +All of the above functions accept a simple ```char *``` message, and optionally: ```char *``` event name, id, and reconnect time. + +*Special Note:* Do not hold on to the ```PsychicEventSourceClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects. Instead, store the ```int client->socket()```. Then when you want to send a message, use this code: + +```cpp +//make sure our client is still connected. +PsychicEventSourceClient *client = eventSource.getClient(socket); +if (client != NULL) + client->send("Your Event") +``` + +### HTTPS / SSL + +PsychicHttp supports HTTPS / SSL out of the box, however there are some limitations (see performance below). Enabling it also increases the code size by about 100kb. To use HTTPS, you need to modify your setup like so: + +```cpp +#include +#include +PsychicHttpsServer server; +server.listen(443, server_cert, server_key); +``` + +```server_cert``` and ```server_key``` are both ```const char *``` parameters which contain the server certificate and private key, respectively. + +To generate your own key and self signed certificate, you can use the command below: + +``` +openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -sha256 -days 365 +``` + +Including the ```PsychicHttpsServer.h``` also defines ```PSY_ENABLE_SSL``` which you can use in your code to allow enabling / disabling calls in your code based on if the HTTPS server is available: + +```cpp +//our main server object +#ifdef PSY_ENABLE_SSL + PsychicHttpsServer server; +#else + PsychicHttpServer server; +#endif +``` + +Last, but not least, you can create a separate HTTP server on port 80 that redirects all requests to the HTTPS server: + +```cpp +//this creates a 2nd server listening on port 80 and redirects all requests HTTPS +PsychicHttpServer *redirectServer = new PsychicHttpServer(); +redirectServer->config.ctrl_port = 20420; // just a random port different from the default one +redirectServer->listen(80); +redirectServer->onNotFound([](PsychicRequest *request) { + String url = "https://" + request->host() + request->url(); + return request->redirect(url.c_str()); +}); +``` + +# TemplatePrinter + +**This is not specific to PsychicHttp, and it works with any `Print` object. You could for example, template data out to `File`, `Serial`, etc...**. + +The template engine is a `Print` interface and can be printed to directly, however, if you are just templating a few short strings, I'd probably just use `response.printf()` instead. **Its benefit will be seen when templating large inputs such as files.** + +One benefit may be **templating a **JSON** file avoiding the need to use ArduinoJson.** + +Before closing the underlying `Print`/`Stream` that this writes to, it must be flushed as small amounts of data can be buffered. A convenience method to take care of this is shows in `example 3`. + +The header file is not currently added to `PsychicHttp.h` and users will have to add it manually: + +```C++ +#include +``` + +## Template parameter definition: + +- Must start and end with a preset delimiter, the default is `%` +- Can only contain `a-z`, `A-Z`, `0-9`, and `_` +- Maximum length of 63 characters (buffer is 64 including `null`). +- A parameter must not be zero length (not including delimiters). +- Spaces or any other character do not match as a parameter, and will be output as is. +- Valid examples + - `%MY_PARAM%` + - `%SOME1%` +- **Invalid** examples + - `%MY PARAM%` + - `%SOME1 %` + - `%UNFINISHED` + - `%%` + +## Template processing +A function or lambda is used to receive the parameter replacement. + +```C++ +bool templateHandler(Print &output, const char *param){ + //... +} + +[](Print &output, const char *param){ + //... +} +``` + +Parameters: +- `Print &output` - the underlying `Print`, print the results of templating to this. +- `const char *param` - a string containing the current parameter. + +The handler must return a `bool`. +- `true`: the parameter was handled, continue as normal. +- `false`: the input detected as a parameter is not, print literal. + +See output in **example 1** regarding the effects of returning `true` or `false`. + +## Template input handler +This is not needed unless using the static convenience function `TemplatePrinter::start()`. See **example 3**. + +```C++ +bool inputHandler(TemplatePrinter &printer){ + //... +} + +[](TemplatePrinter &printer){ + //... +} +``` + +Parameters: +- `TemplatePrinter &printer` - The template engine, print your template text to this for processing. + + +## Example 1 - Simple use with `PsychicStreamResponse`: +This example highlights its most basic usage. + +```C++ + +// Function to handle parameter requests. + +bool templateHandler(Print &output, const char *param){ + + if(strcmp(param, "FREE_HEAP") == 0){ + output.print((double)ESP.getFreeHeap() / 1024.0, 2); + + }else if(strcmp(param, "MIN_FREE_HEAP") == 0){ + output.print((double)ESP.getMinFreeHeap() / 1024.0, 2); + + }else if(strcmp(param, "MAX_ALLOC_HEAP") == 0){ + output.print((double)ESP.getMaxAllocHeap() / 1024.0, 2); + + }else if(strcmp(param, "HEAP_SIZE") == 0){ + output.print((double)ESP.getHeapSize() / 1024.0, 2); + }else{ + return false; + } + output.print("Kb"); + return true; +} + +// Example serving a request +server.on("/template", [](PsychicRequest *request) { + PsychicStreamResponse response(request, "text/plain"); + + response.beginSend(); + + TemplatePrinter printer(response, templateHandler); + + printer.println("My ESP has %FREE_HEAP% left. Its lifetime minimum heap is %MIN_FREE_HEAP%."); + printer.println("The maximum allocation size is %MAX_ALLOC_HEAP%, and its total size is %HEAP_SIZE%."); + printer.println("This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%."); + printer.println("This line finished with %UNFIN"); + printer.flush(); + + return response.endSend(); +}); +``` + +The output for example looks like: +``` +My ESP has 170.92Kb left. Its lifetime minimum heap is 169.83Kb. +The maximum allocation size is 107.99Kb, and its total size is 284.19Kb. +This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%. +This line finished with %UNFIN +``` + +## Example 2 - Templating a file + +```C++ +server.on("/home", [](PsychicRequest *request) { + PsychicStreamResponse response(request, "text/html"); + File file = SD.open("/www/index.html"); + + response.beginSend(); + + TemplatePrinter printer(response, templateHandler); + + printer.copyFrom(file); + printer.flush(); + file.close(); + + return response.endSend(); +}); +``` + +## Example 3 - Using the `TemplatePrinter::start` method. +This static method allows an RAII approach, allowing you to template a stream, etc... without needing a `flush()`. The function call is laid out as: + +```C++ +TemplatePrinter::start(host_stream, template_handler, input_handler); +``` + +\*these examples use the `templateHandler` function defined in example 1. + +### Serve a file like example 2 +```C++ +server.on("/home", [](PsychicRequest *request) { + PsychicStreamResponse response(request, "text/html"); + File file = SD.open("/www/index.html"); + + response.beginSend(); + TemplatePrinter::start(response, templateHandler, [&file](TemplatePrinter &printer){ + printer.copyFrom(file); + }); + file.close(); + + return response.endSend(); +}); +``` + +### Template a string like example 1 +```C++ +server.on("/template2", [](PsychicRequest *request) { + + PsychicStreamResponse response(request, "text/plain"); + + response.beginSend(); + + TemplatePrinter::start(response, templateHandler, [](TemplatePrinter &printer){ + printer.println("My ESP has %FREE_HEAP% left. Its lifetime minimum heap is %MIN_FREE_HEAP%."); + printer.println("The maximum allocation size is %MAX_ALLOC_HEAP%, and its total size is %HEAP_SIZE%."); + printer.println("This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%."); + }); + + return response.endSend(); +}); +``` + +# Performance + +In order to really see the differences between libraries, I created some basic benchmark firmwares for PsychicHttp, ESPAsyncWebserver, and ArduinoMongoose. I then ran the loadtest-http.sh and loadtest-websocket.sh scripts against each firmware to get some real numbers on the performance of each server library. All of the code and results are available in the /benchmark folder. If you want to see the collated data and graphs, there is a [LibreOffice spreadsheet](/benchmark/comparison.ods). + +![Performance graph](/benchmark/performance.png) +![Latency graph](/benchmark/latency.png) + +## HTTPS / SSL + +Yes, PsychicHttp supports SSL out of the box, but there are a few caveats: + +* Due to memory limitations, it can only handle 2 connections at a time. Each SSL connection takes about 45k ram, and a blank PsychicHttp sketch has about 150k ram free. +* Speed and latency are still pretty good (see graph above) but the SSH handshake seems to take 1500ms. With websockets or browser its not an issue since the connection is kept alive, but if you are loading requests in another way it will be a bit slow +* Unless you want to expose your ESP to the internet, you are limited to self signed keys and the annoying browser security warnings that come with them. + +## Analysis + +The results clearly show some of the reasons for writing PsychicHttp: ESPAsyncWebserver crashes under heavy load on each test, across the board in a 60s test. That means in normal usage, you're just rolling the dice with how long it will go until it crashes. Every other number is moot, IMHO. + +ArduinoMongoose doesn't crash under heavy load, but it does bog down with extremely high latency (15s) for web requests and appears to not even respond at the highest loadings as the loadtest script crashes instead. The code itself doesnt crash, so bonus points there. After the high load, it does go back to serving normally. One area ArduinoMongoose does shine, is in websockets where its performance is almost 2x the performance of PsychicHttp. Both in requests per second and latency. Clearly an area of improvement for PsychicHttp. + +PsychicHttp has good performance across the board. No crashes and continously responds during each test. It is a clear winner in requests per second when serving files from memory, dynamic JSON, and has consistent performance when serving files from LittleFS. The only real downside is the lower performance of the websockets with a single connection handling 38rps, and maxing out at 120rps across multiple connections. + +## Takeaways + +With all due respect to @me-no-dev who has done some amazing work in the open source community, I cannot recommend anyone use the ESPAsyncWebserver for anything other than simple projects that don't need to be reliable. Even then, PsychicHttp has taken the arcane api of the ESP-IDF web server library and made it nice and friendly to use with a very similar API to ESPAsyncWebserver. Also, ESPAsyncWebserver is more or less abandoned, with 150 open issues, 77 pending pull requests, and the last commit in over 2 years. + +ArduinoMongoose is a good alternative, although the latency issues when it gets fully loaded can be very annoying. I believe it is also cross platform to other microcontrollers as well, but I haven't tested that. The other issue here is that it is based on an old version of a modified Mongoose library that will be difficult to update as it is a major revision behind and several security updates behind as well. Big thanks to @jeremypoulter though as PsychicHttp is a fork of ArduinoMongoose so it's built on strong bones. + +# Roadmap + +## v1.2: ESPAsyncWebserver Parity + + +Change: +Modify the request handling to bring initail url matching and filtering into PsychicHttpServer itself. + +Benefits: +* Fix a bug with filter() where endpoint is matched, but filter fails and it doesn't continue matching further endpoints (checks are in different codebases) +* HTTP_ANY support +* unlimited endpoints + * we would use a List to store endpoints + * dont have to pre-declare config.max_uri_handlers; +* much more flexibility for future + +Issues +* it would log a warning on every request as if its a 404. (httpd_uri.c:298) +* req->user_ctx is not passed in. (httpd_uri.c:309) + * but... user_ctx is something we could store in the psychicendpoint data + * Websocket support assumes an endpoint with matching url / method (httpd_uri.c:312) + * we could copy and bring this code into our own internal request processor + * would need to manually maintain more code (~100 lines?) and be more prone to esp-idf http_server updates causing problems. + +How to implement +* set config.max_uri_handlers = 1; +* possibly do not register any uri_handlers (looks like it would be fastest way to exit httpd_find_uri_handler (httpd_uri.c:94)) + * looks like 404 is set by default, so should work. +* modify PsychicEndpoint to store the stuff we would pass to http_server +* create a new function handleRequest() before PsychicHttpServer::defaultNotFoundHandler to process incoming requests. + * bring in code from PsychicHttpServer::notFoundHandler + * add new code to loop over endpoints to call match and filter +* bring code from esp-idf library + +* templating system +* regex url matching +* rewrite urls? +* What else are we missing? + + +## Longterm Wants + +* investigate websocket performance gap +* support for esp-idf framework +* support for arduino 3.0 framework +* Enable worker based multithreading with esp-idf v5.x +* 100-continue support? + +If anyone wants to take a crack at implementing any of the above features I am more than happy to accept pull requests. diff --git a/lib/PsychicHttp/RELEASE.md b/lib/PsychicHttp/RELEASE.md new file mode 100644 index 0000000..98db528 --- /dev/null +++ b/lib/PsychicHttp/RELEASE.md @@ -0,0 +1,6 @@ +* Update CHANGELOG +* Bump version in library.json +* Bump version in library.properties +* Make new release + tag + * this will get pulled in automatically by Arduino Library Indexer +* run ```pio pkg publish``` to publish to Platform.io \ No newline at end of file diff --git a/lib/PsychicHttp/assets/handler-callbacks.svg b/lib/PsychicHttp/assets/handler-callbacks.svg new file mode 100644 index 0000000..62fcbdf --- /dev/null +++ b/lib/PsychicHttp/assets/handler-callbacks.svg @@ -0,0 +1,4 @@ + + + +
WebHandler
WebHandler
Handlers with Callbacks
Handlers with Callbacks
onRequest()
onRequest()
WebSocketHandler
WebSocketHandler
onOpen()
onOpen()
onFrame()
onFrame()
onClose()
onClose()
UploadHandler
UploadHandler
onRequest()
onRequest()
onUpload()
onUpload()
EventSource
EventSource
onOpen()
onOpen()
onClose()
onClose()
Text is not SVG - cannot display
\ No newline at end of file diff --git a/lib/PsychicHttp/assets/request-flow.svg b/lib/PsychicHttp/assets/request-flow.svg new file mode 100644 index 0000000..999fb00 --- /dev/null +++ b/lib/PsychicHttp/assets/request-flow.svg @@ -0,0 +1,4 @@ + + + +
HTTP Request
HTTP Request
Yes
Yes
No
No
Endpoint Matched?
Endpoint Matche...
PsychicHandler
PsychicHandler
Yes
Yes
Matching
 Handler?
Matching...
Default Endpoint
Default Endpoint
No
No
Yes
Yes
New Connection?
New Connection?
server.onOpen Callback
server.onOpen Callba...
Connection Closed?
Connection Closed?
handler.onClose
server.onClose
Callbacks
handler.onClose...
Finish
Finish
handler.onOpen + specific callbacks
handler.onOpen + spe...
filter() ?
filter() ?
canHandle() ?
canHandle() ?
authenticate() ?
authenticate() ?
requestAuthentication()
requestAuthentication()
handleRequest()
handleRequest()
response.send()
response.send()
PsychicHttp Request Flow
PsychicHttp Request Flow
Text is not SVG - cannot display
\ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/arduinomongoose/.gitignore b/lib/PsychicHttp/benchmark/arduinomongoose/.gitignore new file mode 100644 index 0000000..9e5f911 --- /dev/null +++ b/lib/PsychicHttp/benchmark/arduinomongoose/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/lib/PsychicHttp/benchmark/arduinomongoose/data/www/alien.png b/lib/PsychicHttp/benchmark/arduinomongoose/data/www/alien.png new file mode 100644 index 0000000000000000000000000000000000000000..a030da0761a60fbfc813fbef0716f801b7aa5ca6 GIT binary patch literal 28598 zcmYJaWmuG5*Dws@07I9c(v3)ofOHIvbW4}Clyr9^NOyN5-5@DFv~)>#H+&~v_w#_92^|Fq=bkf931@l^B)8e_|M?S$Wr;jgr@UiH3RbUqIq6nxmxyTg3pllF+C|;?J8R22|OHS|$5C&}~C$Cp7CMXc++U4Q4NgN|sM6}41E?6Tp^Y-YI z6#fhiLo&L$mU?MXaTZi`JA7SWs9of%+UM(=S3bi+!8kp6V9v*Az4tWod4Y=G#KQ2K zDcB2cxINq5-$Y_hu6%la!EBxtD%w8(hSb9e7Pwy4*Eb&|887A+KvZ>oyX1?J- zM_ibe8Z;Ta%NmvTHtCP(e8mmObB2XPqVT@qFQ_-LKpD z4T?X3%T@C>*_O^`P3Wj<7WhmK$V3NUz+mPR`cM4KCr_Q)UaR$Yx(0YN%qF?DZ&y`N zAGIOqU_mk>5Z1YkX;uX#^55%EvUo4Qj6EtfAk?lN4O`*o5JZ5poyLV8JS&{(Mbn>< zPX+E(pQQT2yBS@Z13j>QB*B5TH)n+hSOA@+1Vb@U^q_?yjMU z2~%{?Q*7L)c~U=7T$BaddgOG67^VOH%SVnbMvG+wfrm5+(-4@EWOT?a39i3J(fMud z%doaamh8Zr9~Utk%RLx+ZlaL!XTy>H=3cr z^tlDkmg$ILOCXLaEFjp1f~;drXNd6l^U4k(HtE%O+S*0|5bJ9YXdDoR6psUhR~e*p zR7Vh`eK)eTaTZ4Rx+QqIV=*g2R3HQyvbx^*=o>lZq0au<&P=SA?v8o{G0U$*5?K%? z7YZT%LRl#i{D%PHQC9gENY)

&213^-9%ncHPP24Z3H*pr~$xg!|8Chf_Iiynyz> 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 c1aJ>H;&fJ5 THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/benchmark/arduinomongoose/platformio.ini b/lib/PsychicHttp/benchmark/arduinomongoose/platformio.ini new file mode 100644 index 0000000..b5604fb --- /dev/null +++ b/lib/PsychicHttp/benchmark/arduinomongoose/platformio.ini @@ -0,0 +1,22 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env] +platform = espressif32 +framework = arduino +board = esp32dev +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +lib_deps = + jeremypoulter/ArduinoMongoose + bblanchon/ArduinoJson +board_build.filesystem = littlefs + +[env:default] \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/arduinomongoose/src/main.cpp b/lib/PsychicHttp/benchmark/arduinomongoose/src/main.cpp new file mode 100644 index 0000000..f7e0a9d --- /dev/null +++ b/lib/PsychicHttp/benchmark/arduinomongoose/src/main.cpp @@ -0,0 +1,234 @@ +/* Wi-Fi STA Connect and Disconnect Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + +*/ +#include +#include +#include +#include +#include +#include + +const char *ssid = ""; +const char *password = ""; + +MongooseHttpServer server; + +const char *htmlContent = R"( + + + + Sample HTML + + +

Hello, World!

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+ + +)"; + +bool connectToWifi() +{ + Serial.println(); + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + WiFi.setSleep(false); + WiFi.useStaticBuffers(true); + + WiFi.begin(ssid, password); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void setup() +{ + Serial.begin(115200); + delay(10); + Serial.println("ArduinoMongoose Benchmark"); + + // We start by connecting to a WiFi network + // To debug, please enable Core Debug Level to Verbose + if (connectToWifi()) + { + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + //start our server + Mongoose.begin(); + server.begin(80); + + //index file + server.on("/", HTTP_GET, [](MongooseHttpServerRequest *request) + { + request->send(200, "text/html", htmlContent); + }); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/api", HTTP_GET, [](MongooseHttpServerRequest *request) + { + //create a response object + StaticJsonDocument<128> output; + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (request->hasParam("foo")) + { + String foo = request->getParam("foo"); + output["foo"] = foo; + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + request->send(200, "application/json", jsonBuffer.c_str()); + }); + + //websocket + server.on("/ws$")-> + onFrame([](MongooseHttpWebSocketConnection *connection, int flags, uint8_t *data, size_t len) { + connection->send(WEBSOCKET_OP_TEXT, data, len); + //server.sendAll(connection, (char *)data); + }); + + //hack - no servestatic + server.on("/alien.png", HTTP_GET, [](MongooseHttpServerRequest *request) + { + //open our file + File fp = LittleFS.open("/www/alien.png"); + size_t length = fp.size(); + + //read our data + uint8_t * data = (uint8_t *)malloc(length); + if (data != NULL) + { + fp.readBytes((char *)data, length); + + //send it off + MongooseHttpServerResponseBasic *response = request->beginResponse(); + response->setContent(data, length); + response->setContentType("image/png"); + response->setCode(200); + request->send(response); + + //free the memory + free(data); + } + else + request->send(503); + }); + } +} + +void loop() +{ + Mongoose.poll(1000); +} \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/arduinomongoose/test/README b/lib/PsychicHttp/benchmark/arduinomongoose/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/lib/PsychicHttp/benchmark/arduinomongoose/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/lib/PsychicHttp/benchmark/comparison.ods b/lib/PsychicHttp/benchmark/comparison.ods new file mode 100644 index 0000000000000000000000000000000000000000..47e6587342544ef945bd9e94c41b83f41deec708 GIT binary patch literal 82456 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 tyUIDC literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/benchmark/espasyncwebserver/.gitignore b/lib/PsychicHttp/benchmark/espasyncwebserver/.gitignore new file mode 100644 index 0000000..9e5f911 --- /dev/null +++ b/lib/PsychicHttp/benchmark/espasyncwebserver/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/lib/PsychicHttp/benchmark/espasyncwebserver/data/www/alien.png b/lib/PsychicHttp/benchmark/espasyncwebserver/data/www/alien.png new file mode 100644 index 0000000000000000000000000000000000000000..a030da0761a60fbfc813fbef0716f801b7aa5ca6 GIT binary patch literal 28598 zcmYJaWmuG5*Dws@07I9c(v3)ofOHIvbW4}Clyr9^NOyN5-5@DFv~)>#H+&~v_w#_92^|Fq=bkf931@l^B)8e_|M?S$Wr;jgr@UiH3RbUqIq6nxmxyTg3pllF+C|;?J8R22|OHS|$5C&}~C$Cp7CMXc++U4Q4NgN|sM6}41E?6Tp^Y-YI z6#fhiLo&L$mU?MXaTZi`JA7SWs9of%+UM(=S3bi+!8kp6V9v*Az4tWod4Y=G#KQ2K zDcB2cxINq5-$Y_hu6%la!EBxtD%w8(hSb9e7Pwy4*Eb&|887A+KvZ>oyX1?J- zM_ibe8Z;Ta%NmvTHtCP(e8mmObB2XPqVT@qFQ_-LKpD z4T?X3%T@C>*_O^`P3Wj<7WhmK$V3NUz+mPR`cM4KCr_Q)UaR$Yx(0YN%qF?DZ&y`N zAGIOqU_mk>5Z1YkX;uX#^55%EvUo4Qj6EtfAk?lN4O`*o5JZ5poyLV8JS&{(Mbn>< zPX+E(pQQT2yBS@Z13j>QB*B5TH)n+hSOA@+1Vb@U^q_?yjMU z2~%{?Q*7L)c~U=7T$BaddgOG67^VOH%SVnbMvG+wfrm5+(-4@EWOT?a39i3J(fMud z%doaamh8Zr9~Utk%RLx+ZlaL!XTy>H=3cr z^tlDkmg$ILOCXLaEFjp1f~;drXNd6l^U4k(HtE%O+S*0|5bJ9YXdDoR6psUhR~e*p zR7Vh`eK)eTaTZ4Rx+QqIV=*g2R3HQyvbx^*=o>lZq0au<&P=SA?v8o{G0U$*5?K%? z7YZT%LRl#i{D%PHQC9gENY)

&213^-9%ncHPP24Z3H*pr~$xg!|8Chf_Iiynyz> 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 c1aJ>H;&fJ5 THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/benchmark/espasyncwebserver/platformio.ini b/lib/PsychicHttp/benchmark/espasyncwebserver/platformio.ini new file mode 100644 index 0000000..0f883e3 --- /dev/null +++ b/lib/PsychicHttp/benchmark/espasyncwebserver/platformio.ini @@ -0,0 +1,22 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env] +platform = espressif32 +framework = arduino +board = esp32dev +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +lib_deps = + https://github.com/me-no-dev/ESPAsyncWebServer + bblanchon/ArduinoJson +board_build.filesystem = littlefs + +[env:default] \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/espasyncwebserver/src/main.cpp b/lib/PsychicHttp/benchmark/espasyncwebserver/src/main.cpp new file mode 100644 index 0000000..c76c1cd --- /dev/null +++ b/lib/PsychicHttp/benchmark/espasyncwebserver/src/main.cpp @@ -0,0 +1,276 @@ +/* Wi-Fi STA Connect and Disconnect Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + +*/ +#include +#include +#include +#include +#include + +const char *ssid = ""; +const char *password = ""; + +AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); + +const char *htmlContent = R"( + + + + Sample HTML + + +

Hello, World!

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+ + +)"; + +bool connectToWifi() +{ + Serial.println(); + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + WiFi.setSleep(false); + WiFi.useStaticBuffers(true); + + WiFi.begin(ssid, password); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + if(type == WS_EVT_CONNECT){ + //client connected + // Serial.printf("ws[%s][%u] connect\n", server->url(), client->id()); + // client->printf("Hello Client %u :)", client->id()); + // client->ping(); + } else if(type == WS_EVT_DISCONNECT){ + //client disconnected + // Serial.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id()); + } else if(type == WS_EVT_ERROR){ + //error was received from the other end + // Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); + } else if(type == WS_EVT_PONG){ + //pong message was received (in response to a ping request maybe) + // Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); + } else if(type == WS_EVT_DATA){ + //data packet + AwsFrameInfo * info = (AwsFrameInfo*)arg; + if(info->final && info->index == 0 && info->len == len){ + //the whole message is in a single frame and we got all of it's data + // Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); + if(info->opcode == WS_TEXT){ + data[len] = 0; + // Serial.printf("%s\n", (char*)data); + } else { + // for(size_t i=0; i < info->len; i++){ + // Serial.printf("%02x ", data[i]); + // } + // Serial.printf("\n"); + } + if(info->opcode == WS_TEXT) + { + client->text((char *)data, len); + } + // else + // client->binary("I got your binary message"); + } else { + //message is comprised of multiple frames or the frame is split into multiple packets + if(info->index == 0){ + // if(info->num == 0) + // Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + // Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); + } + + Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len); + if(info->message_opcode == WS_TEXT){ + data[len] = 0; + // Serial.printf("%s\n", (char*)data); + } else { + // for(size_t i=0; i < len; i++){ + // Serial.printf("%02x ", data[i]); + // } + // Serial.printf("\n"); + } + + if((info->index + len) == info->len){ + // Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); + if(info->final){ + // Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + if(info->message_opcode == WS_TEXT) + { + client->text((char *)data, info->len); + } + // else + // client->binary("I got your binary message"); + } + } + } + } +} + +void setup() +{ + Serial.begin(115200); + delay(10); + Serial.println("ESPAsyncWebserver Benchmark"); + + // We start by connecting to a WiFi network + // To debug, please enable Core Debug Level to Verbose + if (connectToWifi()) + { + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { + request->send(200, "text/html", htmlContent); + }); + + //serve static files from LittleFS/www on / + server.serveStatic("/", LittleFS, "/www/"); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/api", HTTP_GET, [](AsyncWebServerRequest *request) + { + //create a response object + StaticJsonDocument<128> output; + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (request->hasParam("foo")) + { + AsyncWebParameter* foo = request->getParam("foo"); + output["foo"] = foo->value(); + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + request->send(200, "application/json", jsonBuffer.c_str()); + }); + + ws.onEvent(onEvent); + server.addHandler(&ws); + + server.begin(); + } +} + +void loop() +{ + ws.cleanupClients(); + delay(1000); +} \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/espasyncwebserver/test/README b/lib/PsychicHttp/benchmark/espasyncwebserver/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/lib/PsychicHttp/benchmark/espasyncwebserver/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/lib/PsychicHttp/benchmark/eventsource-client-test.js b/lib/PsychicHttp/benchmark/eventsource-client-test.js new file mode 100644 index 0000000..8253789 --- /dev/null +++ b/lib/PsychicHttp/benchmark/eventsource-client-test.js @@ -0,0 +1,39 @@ +#!/usr/bin/env node + +const EventSource = require('eventsource'); +const url = 'http://192.168.2.131/events'; + +async function eventSourceClient() { + console.log(`Starting test`); + for (let i = 0; i < 1000000; i++) + { + if (i % 100 == 0) + console.log(`Count: ${i}`); + + let eventSource = new EventSource(url); + + eventSource.onopen = () => { + //console.log('EventSource connection opened.'); + }; + + eventSource.onerror = (error) => { + console.error('EventSource error:', error); + + // Close the connection on error + eventSource.close(); + }; + + await new Promise((resolve) => { + eventSource.onmessage = (event) => { + //console.log('Received message:', event.data); + + // Close the connection after receiving the first message + eventSource.close(); + + resolve(); + } + }); + } +} + +eventSourceClient(); \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/http-client-test.js b/lib/PsychicHttp/benchmark/http-client-test.js new file mode 100644 index 0000000..29588b9 --- /dev/null +++ b/lib/PsychicHttp/benchmark/http-client-test.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +const axios = require('axios'); + +const url = 'http://192.168.2.131/api'; +const queryParams = { + foo: 'bar', + foo1: 'bar', + foo2: 'bar', + foo3: 'bar', + foo4: 'bar', + foo5: 'bar', + foo6: 'bar', +}; + +const totalRequests = 1000000; +const requestsPerCount = 100; + +let requestCount = 0; + +function fetchData() { + axios.get(url, { params: queryParams }) + .then(response => { + requestCount++; + + if (requestCount % requestsPerCount === 0) { + console.log(`Requests completed: ${requestCount}`); + } + + if (requestCount < totalRequests) { + fetchData(); + } else { + console.log('All requests completed.'); + } + }) + .catch(error => { + console.error('Error making request:', error.message); + }); +} + +// Start making requests +console.log(`Starting test`); +fetchData(); \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/latency.png b/lib/PsychicHttp/benchmark/latency.png new file mode 100644 index 0000000000000000000000000000000000000000..1cedc9372a018705ec8ea78b4cf5bfc947c6e318 GIT binary patch literal 50143 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 z%0DN-?qQwH!i^+R&pc$lq7IueZ+OxJOrecB2(!=$q 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 E2Mk-18UO$Q literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/benchmark/loadtest-http.sh b/lib/PsychicHttp/benchmark/loadtest-http.sh new file mode 100644 index 0000000..4e75b84 --- /dev/null +++ b/lib/PsychicHttp/benchmark/loadtest-http.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +#Command to install the testers: +# npm install -g autocannon + +TEST_IP="192.168.2.131" +TEST_TIME=60 +LOG_FILE=psychic-http-loadtest.log +TIMEOUT=10000 +PROTOCOL=http +#PROTOCOL=https + +if test -f "$LOG_FILE"; then + rm $LOG_FILE +fi + +for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20 +#for CONCURRENCY in 20 +do + printf "\n\nCLIENTS: *** $CONCURRENCY ***\n\n" >> $LOG_FILE + echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/" + #loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --timeout $TIMEOUT "$PROTOCOL://$TEST_IP/" --quiet >> $LOG_FILE + autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/" >> $LOG_FILE 2>&1 + printf "\n\n----------------\n\n" >> $LOG_FILE + sleep 1 + + echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/api" + #loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --timeout $TIMEOUT "$PROTOCOL://$TEST_IP/api?foo=bar" --quiet >> $LOG_FILE + autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/api?foo=bar" >> $LOG_FILE 2>&1 + printf "\n\n----------------\n\n" >> $LOG_FILE + sleep 1 + + echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/alien.png" + #loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --timeout $TIMEOUT "$PROTOCOL://$TEST_IP/alien.png" --quiet >> $LOG_FILE + autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/alien.png" >> $LOG_FILE 2>&1 + printf "\n\n----------------\n\n" >> $LOG_FILE + sleep 1 +done \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/loadtest-websocket.sh b/lib/PsychicHttp/benchmark/loadtest-websocket.sh new file mode 100644 index 0000000..a9b5a41 --- /dev/null +++ b/lib/PsychicHttp/benchmark/loadtest-websocket.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +#Command to install the testers: +# npm install -g loadtest + +TEST_IP="192.168.2.131" +TEST_TIME=60 +LOG_FILE=psychic-websocket-loadtest.log +PROTOCOL=ws +#PROTOCOL=wss + +if test -f "$LOG_FILE"; then + rm $LOG_FILE +fi + +for CONCURRENCY in 1 2 3 4 5 6 7 +do + printf "\n\nCLIENTS: *** $CONCURRENCY ***\n\n" >> $LOG_FILE + echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/ws" + loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE + sleep 1 +done + +for CONNECTIONS in 8 10 16 20 +#for CONNECTIONS in 20 +do + CONCURRENCY=$((CONNECTIONS / 2)) + printf "\n\nCLIENTS: *** $CONNECTIONS ***\n\n" >> $LOG_FILE + echo "Testing $CONNECTIONS clients on $PROTOCOL://$TEST_IP/ws" + loadtest -c $CONCURRENCY --cores 2 -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE + sleep 1 +done \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/package.json b/lib/PsychicHttp/benchmark/package.json new file mode 100644 index 0000000..3237d56 --- /dev/null +++ b/lib/PsychicHttp/benchmark/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "axios": "^1.6.2", + "eventsource": "^2.0.2", + "ws": "^8.14.2" + } +} diff --git a/lib/PsychicHttp/benchmark/performance.png b/lib/PsychicHttp/benchmark/performance.png new file mode 100644 index 0000000000000000000000000000000000000000..81d2a3f4841593c573dd828ff0d7845c3044c725 GIT binary patch literal 49201 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"N)0%H7lIx**@ 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

&213^-9%ncHPP24Z3H*pr~$xg!|8Chf_Iiynyz> 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 c1aJ>H;&fJ5 THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/benchmark/psychichttp/platformio.ini b/lib/PsychicHttp/benchmark/psychichttp/platformio.ini new file mode 100644 index 0000000..868f0ca --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttp/platformio.ini @@ -0,0 +1,22 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env] +platform = espressif32 +framework = arduino +board = esp32dev +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +lib_deps = + https://github.com/hoeken/PsychicHttp + bblanchon/ArduinoJson +board_build.filesystem = littlefs + +[env:default] diff --git a/lib/PsychicHttp/benchmark/psychichttp/src/main.cpp b/lib/PsychicHttp/benchmark/psychichttp/src/main.cpp new file mode 100644 index 0000000..c42d644 --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttp/src/main.cpp @@ -0,0 +1,228 @@ +/* Wi-Fi STA Connect and Disconnect Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + +*/ +#include +#include +#include +#include +#include +#include "_secret.h" + +#ifndef WIFI_SSID + #error "You need to enter your wifi credentials. Copy secret.h to _secret.h and enter your credentials there." +#endif + +//Enter your WIFI credentials in secret.h +const char *ssid = WIFI_SSID; +const char *password = WIFI_PASS; + +PsychicHttpServer server; +PsychicWebSocketHandler websocketHandler; +PsychicEventSource eventSource; + +const char *htmlContent = R"( + + + + Sample HTML + + +

Hello, World!

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+ + +)"; + +bool connectToWifi() +{ + Serial.println(); + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + WiFi.setSleep(false); + WiFi.useStaticBuffers(true); + + WiFi.begin(ssid, password); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void setup() +{ + Serial.begin(115200); + delay(10); + Serial.println("PsychicHTTP Benchmark"); + + if (connectToWifi()) + { + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + //start our server + server.listen(80); + + //our index + server.on("/", HTTP_GET, [](PsychicRequest *request) + { + return request->reply(200, "text/html", htmlContent); + }); + + //serve static files from LittleFS/www on / + server.serveStatic("/", LittleFS, "/www/"); + + //a websocket echo server + websocketHandler.onOpen([](PsychicWebSocketClient *client) { + client->sendMessage("Hello!"); + }); + websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { + request->reply(frame); + return ESP_OK; + }); + server.on("/ws", &websocketHandler); + + //EventSource server + eventSource.onOpen([](PsychicEventSourceClient *client) { + client->send("Hello", NULL, millis(), 1000); + }); + server.on("/events", &eventSource); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/api", HTTP_GET, [](PsychicRequest *request) + { + //create a response object + StaticJsonDocument<128> output; + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (request->hasParam("foo")) + { + String foo = request->getParam("foo")->value(); + output["foo"] = foo; + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + return request->reply(200, "application/json", jsonBuffer.c_str()); + }); + } +} + +unsigned long last; +void loop() +{ + if (millis() - last > 1000) + { + Serial.printf("Free Heap: %d\n", esp_get_free_heap_size()); + last = millis(); + } +} \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/psychichttp/src/secret.h b/lib/PsychicHttp/benchmark/psychichttp/src/secret.h new file mode 100644 index 0000000..6d4bb15 --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttp/src/secret.h @@ -0,0 +1,2 @@ +#define WIFI_SSID "Your_SSID" +#define WIFI_PASS "Your_PASS" \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/psychichttp/test/README b/lib/PsychicHttp/benchmark/psychichttp/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttp/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/lib/PsychicHttp/benchmark/psychichttps/.gitignore b/lib/PsychicHttp/benchmark/psychichttps/.gitignore new file mode 100644 index 0000000..9e5f911 --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttps/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/lib/PsychicHttp/benchmark/psychichttps/data/server.crt b/lib/PsychicHttp/benchmark/psychichttps/data/server.crt new file mode 100644 index 0000000..34a1e01 --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttps/data/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDKzCCAhOgAwIBAgIUBxM3WJf2bP12kAfqhmhhjZWv0ukwDQYJKoZIhvcNAQEL +BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMTgx +MDE3MTEzMjU3WhcNMjgxMDE0MTEzMjU3WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ +UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T +sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k +qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd +GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4 +sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb +jAn4PCuR2akdF4G8WLUeDWECAwEAAaNTMFEwHQYDVR0OBBYEFMnmdJKOEepXrHI/ +ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud +EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADiXIGEkSsN0SLSfCF1VNWO3 +emBurfOcDq4EGEaxRKAU0814VEmU87btIDx80+z5Dbf+GGHCPrY7odIkxGNn0DJY +W1WcF+DOcbiWoUN6DTkAML0SMnp8aGj9ffx3x+qoggT+vGdWVVA4pgwqZT7Ybntx +bkzcNFW0sqmCv4IN1t4w6L0A87ZwsNwVpre/j6uyBw7s8YoJHDLRFT6g7qgn0tcN +ZufhNISvgWCVJQy/SZjNBHSpnIdCUSJAeTY2mkM4sGxY0Widk8LnjydxZUSxC3Nl +hb6pnMh3jRq4h0+5CZielA4/a+TdrNPv/qok67ot/XJdY3qHCCd8O2b14OVq9jo= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/psychichttps/data/server.key b/lib/PsychicHttp/benchmark/psychichttps/data/server.key new file mode 100644 index 0000000..a591325 --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttps/data/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwYp7epz++0QkH +JioMD7U7BitLgpcYPi8Cid1l7snt6Kp546iQsDBJ3l8xnRtPU7ANEsjT8KxIHmyw +h/NGp94FlOKRw3ahh3yUGtowS9vdHv+S+TAfuj07NjSnKIyv5KnGZJ+fDFl4Q1tT +aQJybY1Z4itirL6/2CGEm8g/iYhLNDBsRMfpDpfXe4URyWiM3Rhf7ztqZdveb9al +3pAJZIDTLWCFQI1MvQjKamkAQkES/gZj0iUZFwbGJPBj54nkuLFLKedw7DbwgrVg +0+n3fQ9b/gQepw5PxQjyobY2DsDgGZV+MFjUmaUTa+XX68SrG4wJ+DwrkdmpHReB +vFi1Hg1hAgMBAAECggEAaTCnZkl/7qBjLexIryC/CBBJyaJ70W1kQ7NMYfniWwui +f0aRxJgOdD81rjTvkINsPp+xPRQO6oOadjzdjImYEuQTqrJTEUnntbu924eh+2D9 +Mf2CAanj0mglRnscS9mmljZ0KzoGMX6Z/EhnuS40WiJTlWlH6MlQU/FDnwC6U34y +JKy6/jGryfsx+kGU/NRvKSru6JYJWt5v7sOrymHWD62IT59h3blOiP8GMtYKeQlX +49om9Mo1VTIFASY3lrxmexbY+6FG8YO+tfIe0tTAiGrkb9Pz6tYbaj9FjEWOv4Vc ++3VMBUVdGJjgqvE8fx+/+mHo4Rg69BUPfPSrpEg7sQKBgQDlL85G04VZgrNZgOx6 +pTlCCl/NkfNb1OYa0BELqWINoWaWQHnm6lX8YjrUjwRpBF5s7mFhguFjUjp/NW6D +0EEg5BmO0ePJ3dLKSeOA7gMo7y7kAcD/YGToqAaGljkBI+IAWK5Su5yldrECTQKG +YnMKyQ1MWUfCYEwHtPvFvE5aPwKBgQDFBWXekpxHIvt/B41Cl/TftAzE7/f58JjV +MFo/JCh9TDcH6N5TMTRS1/iQrv5M6kJSSrHnq8pqDXOwfHLwxetpk9tr937VRzoL +CuG1Ar7c1AO6ujNnAEmUVC2DppL/ck5mRPWK/kgLwZSaNcZf8sydRgphsW1ogJin +7g0nGbFwXwKBgQCPoZY07Pr1TeP4g8OwWTu5F6dSvdU2CAbtZthH5q98u1n/cAj1 +noak1Srpa3foGMTUn9CHu+5kwHPIpUPNeAZZBpq91uxa5pnkDMp3UrLIRJ2uZyr8 +4PxcknEEh8DR5hsM/IbDcrCJQglM19ZtQeW3LKkY4BsIxjDf45ymH407IQKBgE/g +Ul6cPfOxQRlNLH4VMVgInSyyxWx1mODFy7DRrgCuh5kTVh+QUVBM8x9lcwAn8V9/ +nQT55wR8E603pznqY/jX0xvAqZE6YVPcw4kpZcwNwL1RhEl8GliikBlRzUL3SsW3 +q30AfqEViHPE3XpE66PPo6Hb1ymJCVr77iUuC3wtAoGBAIBrOGunv1qZMfqmwAY2 +lxlzRgxgSiaev0lTNxDzZkmU/u3dgdTwJ5DDANqPwJc6b8SGYTp9rQ0mbgVHnhIB +jcJQBQkTfq6Z0H6OoTVi7dPs3ibQJFrtkoyvYAbyk36quBmNRjVh6rc8468bhXYr +v/t+MeGJP/0Zw8v/X2CFll96 +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/psychichttps/data/www/alien.png b/lib/PsychicHttp/benchmark/psychichttps/data/www/alien.png new file mode 100644 index 0000000000000000000000000000000000000000..a030da0761a60fbfc813fbef0716f801b7aa5ca6 GIT binary patch literal 28598 zcmYJaWmuG5*Dws@07I9c(v3)ofOHIvbW4}Clyr9^NOyN5-5@DFv~)>#H+&~v_w#_92^|Fq=bkf931@l^B)8e_|M?S$Wr;jgr@UiH3RbUqIq6nxmxyTg3pllF+C|;?J8R22|OHS|$5C&}~C$Cp7CMXc++U4Q4NgN|sM6}41E?6Tp^Y-YI z6#fhiLo&L$mU?MXaTZi`JA7SWs9of%+UM(=S3bi+!8kp6V9v*Az4tWod4Y=G#KQ2K zDcB2cxINq5-$Y_hu6%la!EBxtD%w8(hSb9e7Pwy4*Eb&|887A+KvZ>oyX1?J- zM_ibe8Z;Ta%NmvTHtCP(e8mmObB2XPqVT@qFQ_-LKpD z4T?X3%T@C>*_O^`P3Wj<7WhmK$V3NUz+mPR`cM4KCr_Q)UaR$Yx(0YN%qF?DZ&y`N zAGIOqU_mk>5Z1YkX;uX#^55%EvUo4Qj6EtfAk?lN4O`*o5JZ5poyLV8JS&{(Mbn>< zPX+E(pQQT2yBS@Z13j>QB*B5TH)n+hSOA@+1Vb@U^q_?yjMU z2~%{?Q*7L)c~U=7T$BaddgOG67^VOH%SVnbMvG+wfrm5+(-4@EWOT?a39i3J(fMud z%doaamh8Zr9~Utk%RLx+ZlaL!XTy>H=3cr z^tlDkmg$ILOCXLaEFjp1f~;drXNd6l^U4k(HtE%O+S*0|5bJ9YXdDoR6psUhR~e*p zR7Vh`eK)eTaTZ4Rx+QqIV=*g2R3HQyvbx^*=o>lZq0au<&P=SA?v8o{G0U$*5?K%? z7YZT%LRl#i{D%PHQC9gENY)

&213^-9%ncHPP24Z3H*pr~$xg!|8Chf_Iiynyz> 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 c1aJ>H;&fJ5 THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/benchmark/psychichttps/platformio.ini b/lib/PsychicHttp/benchmark/psychichttps/platformio.ini new file mode 100644 index 0000000..868f0ca --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttps/platformio.ini @@ -0,0 +1,22 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env] +platform = espressif32 +framework = arduino +board = esp32dev +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +lib_deps = + https://github.com/hoeken/PsychicHttp + bblanchon/ArduinoJson +board_build.filesystem = littlefs + +[env:default] diff --git a/lib/PsychicHttp/benchmark/psychichttps/src/main.cpp b/lib/PsychicHttp/benchmark/psychichttps/src/main.cpp new file mode 100644 index 0000000..2639758 --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttps/src/main.cpp @@ -0,0 +1,240 @@ +/* Wi-Fi STA Connect and Disconnect Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + +*/ +#include +#include +#include +#include +#include +#include "_secret.h" +#include +#include + +#ifndef WIFI_SSID + #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." +#endif + +//Enter your WIFI credentials in secret.h +const char *ssid = WIFI_SSID; +const char *password = WIFI_PASS; + +PsychicHttpsServer server; +PsychicWebSocketHandler websocketHandler; + +String server_cert; +String server_key; + +const char *htmlContent = R"( + + + + Sample HTML + + +

Hello, World!

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod + rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper + arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit + accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. + Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo + dapibus elit, id varius sem dui id lacus.

+ + +)"; + +bool connectToWifi() +{ + Serial.println(); + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + WiFi.setSleep(false); + + WiFi.begin(ssid, password); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void setup() +{ + Serial.begin(115200); + delay(10); + Serial.println("PsychicHTTP Benchmark"); + + if (connectToWifi()) + { + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + File fp = LittleFS.open("/server.crt"); + if (fp) { + server_cert = fp.readString(); + } else { + Serial.println("server.pem not found, SSL not available"); + return; + } + fp.close(); + + File fp2 = LittleFS.open("/server.key"); + if (fp2) { + server_key = fp2.readString(); + } else { + Serial.println("server.key not found, SSL not available"); + return; + } + fp2.close(); + + //start our server + server.listen(443, server_cert.c_str(), server_key.c_str()); + + //our index + server.on("/", HTTP_GET, [](PsychicRequest *request) + { + return request->reply(200, "text/html", htmlContent); + }); + + //serve static files from LittleFS/www on / + server.serveStatic("/", LittleFS, "/www/"); + + //a websocket echo server + websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { + request->reply(frame); + return ESP_OK; + }); + server.on("/ws", &websocketHandler); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/api", HTTP_GET, [](PsychicRequest *request) + { + //create a response object + StaticJsonDocument<128> output; + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (request->hasParam("foo")) + { + String foo = request->getParam("foo")->value(); + output["foo"] = foo; + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + return request->reply(200, "application/json", jsonBuffer.c_str()); + }); + } +} + +unsigned long last; +void loop() +{ + if (millis() - last > 1000) + { + Serial.printf("Free Heap: %d\n", esp_get_free_heap_size()); + last = millis(); + } +} \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/psychichttps/src/secret.h b/lib/PsychicHttp/benchmark/psychichttps/src/secret.h new file mode 100644 index 0000000..6d4bb15 --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttps/src/secret.h @@ -0,0 +1,2 @@ +#define WIFI_SSID "Your_SSID" +#define WIFI_PASS "Your_PASS" \ No newline at end of file diff --git a/lib/PsychicHttp/benchmark/psychichttps/test/README b/lib/PsychicHttp/benchmark/psychichttps/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/lib/PsychicHttp/benchmark/psychichttps/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/lib/PsychicHttp/benchmark/results/arduinomongoose-http-loadtest.log b/lib/PsychicHttp/benchmark/results/arduinomongoose-http-loadtest.log new file mode 100644 index 0000000..24a6e34 --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/arduinomongoose-http-loadtest.log @@ -0,0 +1,1172 @@ + + +CLIENTS: *** 1 *** + +Running 60s test @ http://192.168.2.131/ +1 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 720 ms │ 14880 ms │ 29087 ms │ 29519 ms │ 15024.03 ms │ 8572.14 ms │ 29737 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 11 │ 11 │ 12 │ 14 │ 12.47 │ 0.87 │ 11 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 48.2 kB │ 48.2 kB │ 52.6 kB │ 61.3 kB │ 54.6 kB │ 3.79 kB │ 48.2 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 748 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +1k requests in 60.07s, 3.28 MB read +748 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +1 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 781 ms │ 15061 ms │ 29571 ms │ 30046 ms │ 15137.29 ms │ 8833.98 ms │ 30393 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 17 │ 18 │ 20 │ 23 │ 20.34 │ 1.27 │ 17 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 2.99 kB │ 3.15 kB │ 3.52 kB │ 4.03 kB │ 3.57 kB │ 220 B │ 2.99 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1220 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.06s, 214 kB read +1k errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +1 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 969 ms │ 15300 ms │ 29831 ms │ 30132 ms │ 15143.83 ms │ 8807.54 ms │ 30385 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 3 │ 3 │ 3 │ 4 │ 3.15 │ 0.36 │ 3 │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 86.1 kB │ 86.1 kB │ 86.1 kB │ 115 kB │ 90.4 kB │ 10.3 kB │ 86.1 kB │ +└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 189 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +378 requests in 60.06s, 5.43 MB read +188 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 2 *** + +Running 60s test @ http://192.168.2.131/ +2 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 803 ms │ 14773 ms │ 29269 ms │ 29642 ms │ 14925.25 ms │ 8556.82 ms │ 29974 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬───────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 17 │ 17 │ 19 │ 21 │ 18.9 │ 0.98 │ 17 │ +├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 74.5 kB │ 74.5 kB │ 83.3 kB │ 92 kB │ 82.8 kB │ 4.29 kB │ 74.5 kB │ +└───────────┴─────────┴─────────┴─────────┴───────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1134 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.06s, 4.97 MB read +29 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +2 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 772 ms │ 15252 ms │ 29307 ms │ 29709 ms │ 15094.92 ms │ 8732.51 ms │ 30070 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 26 │ 27 │ 29 │ 32 │ 29.72 │ 1.43 │ 26 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 4.58 kB │ 4.75 kB │ 5.11 kB │ 5.63 kB │ 5.23 kB │ 251 B │ 4.58 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1783 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.06s, 314 kB read +1k errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +2 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 856 ms │ 15635 ms │ 28880 ms │ 29310 ms │ 15261.99 ms │ 8577.65 ms │ 29700 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 4 │ 4 │ 5 │ 6 │ 4.64 │ 0.64 │ 4 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 115 kB │ 115 kB │ 144 kB │ 172 kB │ 133 kB │ 18.1 kB │ 115 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 278 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +558 requests in 60.06s, 7.98 MB read +17 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 3 *** + +Running 60s test @ http://192.168.2.131/ +3 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 794 ms │ 15166 ms │ 29323 ms │ 29697 ms │ 15066.78 ms │ 8676.25 ms │ 30114 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 22 │ 23 │ 26 │ 29 │ 25.99 │ 1.42 │ 22 │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 96.4 kB │ 101 kB │ 114 kB │ 127 kB │ 114 kB │ 6.22 kB │ 96.4 kB │ +└───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1559 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.06s, 6.83 MB read +14 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +3 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 826 ms │ 14949 ms │ 29377 ms │ 29790 ms │ 15049.95 ms │ 8720.15 ms │ 30168 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 32 │ 32 │ 36 │ 39 │ 36.1 │ 1.82 │ 32 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 5.63 kB │ 5.63 kB │ 6.34 kB │ 6.87 kB │ 6.36 kB │ 319 B │ 5.63 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2166 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.1s, 381 kB read +1k errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +3 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬─────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼─────────┼──────────┤ +│ Latency │ 995 ms │ 15127 ms │ 29464 ms │ 29993 ms │ 15219.94 ms │ 8704 ms │ 30258 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴─────────┴──────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 3 │ 3 │ 6 │ 6 │ 5.62 │ 0.99 │ 3 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 86.1 kB │ 86.1 kB │ 172 kB │ 172 kB │ 161 kB │ 28.3 kB │ 86.1 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 337 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +677 requests in 60.07s, 9.67 MB read +54 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 4 *** + +Running 60s test @ http://192.168.2.131/ +4 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 702 ms │ 15255 ms │ 29811 ms │ 30226 ms │ 15144.93 ms │ 8830.16 ms │ 30616 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ +│ Req/Sec │ 26 │ 27 │ 29 │ 32 │ 29.05 │ 1.58 │ 26 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ +│ Bytes/Sec │ 114 kB │ 118 kB │ 127 kB │ 140 kB │ 127 kB │ 6.9 kB │ 114 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1743 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.06s, 7.63 MB read +20 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +4 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬───────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼───────────┼──────────┤ +│ Latency │ 762 ms │ 15231 ms │ 29096 ms │ 29534 ms │ 15112.66 ms │ 8660.6 ms │ 29997 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴───────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ +│ Req/Sec │ 35 │ 35 │ 41 │ 44 │ 40.89 │ 1.9 │ 35 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ +│ Bytes/Sec │ 6.16 kB │ 6.16 kB │ 7.22 kB │ 7.75 kB │ 7.2 kB │ 334 B │ 6.16 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2453 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.06s, 432 kB read +977 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +4 connections +1 workers + + +┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 1175 ms │ 15413 ms │ 29485 ms │ 30066 ms │ 15217.07 ms │ 8604.35 ms │ 30126 ms │ +└─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 4 │ 4 │ 8 │ 8 │ 6.47 │ 1.87 │ 4 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 115 kB │ 115 kB │ 230 kB │ 230 kB │ 186 kB │ 53.6 kB │ 115 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 388 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +780 requests in 60.05s, 11.1 MB read +39 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 5 *** + +Running 60s test @ http://192.168.2.131/ +5 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 802 ms │ 14978 ms │ 29391 ms │ 29863 ms │ 15042.57 ms │ 8642.88 ms │ 30280 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ +│ Req/Sec │ 26 │ 29 │ 33 │ 37 │ 33.19 │ 2.13 │ 26 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ +│ Bytes/Sec │ 114 kB │ 127 kB │ 145 kB │ 162 kB │ 145 kB │ 9.3 kB │ 114 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1991 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.07s, 8.72 MB read +10 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +5 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 724 ms │ 15356 ms │ 29304 ms │ 29791 ms │ 15198.71 ms │ 8761.23 ms │ 30174 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 40 │ 40 │ 45 │ 50 │ 44.84 │ 2.78 │ 40 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 7.04 kB │ 7.04 kB │ 7.92 kB │ 8.81 kB │ 7.89 kB │ 489 B │ 7.04 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2690 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.07s, 473 kB read +603 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +5 connections +1 workers + + +┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 1250 ms │ 15289 ms │ 29607 ms │ 30193 ms │ 15345.82 ms │ 8719.55 ms │ 30354 ms │ +└─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 10 │ 7.25 │ 2.25 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 172 kB │ 287 kB │ 208 kB │ 64.5 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 435 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +875 requests in 60.06s, 12.5 MB read +36 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 6 *** + +Running 60s test @ http://192.168.2.131/ +6 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 847 ms │ 14920 ms │ 28899 ms │ 29392 ms │ 15008.54 ms │ 8456.33 ms │ 29870 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 33 │ 33 │ 36 │ 39 │ 35.75 │ 1.75 │ 33 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 145 kB │ 145 kB │ 158 kB │ 171 kB │ 157 kB │ 7.65 kB │ 145 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2145 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.06s, 9.4 MB read +3 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +6 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 890 ms │ 14807 ms │ 29142 ms │ 29732 ms │ 14934.33 ms │ 8513.39 ms │ 30168 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 44 │ 44 │ 48 │ 53 │ 48.3 │ 2.22 │ 44 │ +├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 7.75 kB │ 7.75 kB │ 8.5 kB │ 9.38 kB │ 8.54 kB │ 395 B │ 7.74 kB │ +└───────────┴─────────┴─────────┴────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2898 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +6k requests in 60.07s, 512 kB read +386 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +6 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 853 ms │ 15325 ms │ 29742 ms │ 30578 ms │ 15287.1 ms │ 8714.92 ms │ 30650 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 6 │ 6 │ 6 │ 12 │ 7.7 │ 2.44 │ 6 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 172 kB │ 172 kB │ 172 kB │ 345 kB │ 221 kB │ 70 kB │ 172 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 462 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +930 requests in 60.06s, 13.3 MB read +24 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 7 *** + +Running 60s test @ http://192.168.2.131/ +7 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 785 ms │ 15141 ms │ 29506 ms │ 29981 ms │ 15124.91 ms │ 8769.84 ms │ 30379 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ +│ Req/Sec │ 32 │ 33 │ 37 │ 40 │ 36.64 │ 2.04 │ 32 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ +│ Bytes/Sec │ 140 kB │ 145 kB │ 162 kB │ 175 kB │ 160 kB │ 8.9 kB │ 140 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2198 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.06s, 9.63 MB read +1 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +7 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 773 ms │ 15322 ms │ 29472 ms │ 29911 ms │ 15145.75 ms │ 8792.14 ms │ 30238 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 45 │ 45 │ 51 │ 56 │ 50.62 │ 3.07 │ 45 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 7.97 kB │ 7.97 kB │ 9.03 kB │ 9.92 kB │ 8.96 kB │ 541 B │ 7.96 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3037 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +6k requests in 60.06s, 538 kB read +252 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +7 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 903 ms │ 15426 ms │ 29337 ms │ 30187 ms │ 15395.14 ms │ 8592.24 ms │ 30239 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 7 │ 7 │ 7 │ 14 │ 8.06 │ 2.03 │ 7 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 201 kB │ 201 kB │ 201 kB │ 402 kB │ 231 kB │ 58 kB │ 201 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 483 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +973 requests in 60.06s, 13.9 MB read +27 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 8 *** + +Running 60s test @ http://192.168.2.131/ +8 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 947 ms │ 15230 ms │ 29194 ms │ 29710 ms │ 15139.81 ms │ 8698.37 ms │ 30418 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 31 │ 33 │ 38 │ 43 │ 37.92 │ 2.7 │ 31 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 136 kB │ 145 kB │ 167 kB │ 188 kB │ 166 kB │ 11.8 kB │ 136 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2275 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.06s, 9.96 MB read +1 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +8 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 818 ms │ 15102 ms │ 29043 ms │ 29502 ms │ 14999.69 ms │ 8454.75 ms │ 30295 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 44 │ 45 │ 51 │ 60 │ 51.37 │ 3.35 │ 44 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 7.79 kB │ 7.97 kB │ 9.03 kB │ 10.6 kB │ 9.09 kB │ 591 B │ 7.79 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3082 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +6k requests in 60s, 546 kB read +222 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +8 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 951 ms │ 15565 ms │ 30182 ms │ 30252 ms │ 15396.59 ms │ 8860.08 ms │ 31138 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 7 │ 8 │ 8 │ 14 │ 8.56 │ 1.59 │ 7 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 201 kB │ 230 kB │ 230 kB │ 402 kB │ 245 kB │ 45.5 kB │ 201 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 513 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +1k requests in 60.06s, 14.7 MB read +31 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 9 *** + +Running 60s test @ http://192.168.2.131/ +9 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬───────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼───────────┼──────────┤ +│ Latency │ 951 ms │ 15246 ms │ 28914 ms │ 29402 ms │ 15108.58 ms │ 8543.6 ms │ 30524 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴───────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 31 │ 32 │ 37 │ 43 │ 37.42 │ 2.9 │ 31 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 136 kB │ 140 kB │ 162 kB │ 188 kB │ 164 kB │ 12.7 kB │ 136 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2245 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.05s, 9.83 MB read +1 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +9 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 885 ms │ 15043 ms │ 29217 ms │ 29730 ms │ 14953.4 ms │ 8530.16 ms │ 30399 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 44 │ 46 │ 52 │ 58 │ 52.1 │ 3.22 │ 44 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 7.79 kB │ 8.14 kB │ 9.21 kB │ 10.3 kB │ 9.22 kB │ 570 B │ 7.79 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3126 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +6k requests in 60.07s, 553 kB read +189 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +9 connections +1 workers + + +┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 1079 ms │ 15444 ms │ 28978 ms │ 29935 ms │ 15389.63 ms │ 8469.15 ms │ 30003 ms │ +└─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 9 │ 10 │ 8.56 │ 2.21 │ 1 │ +├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 258 kB │ 287 kB │ 245 kB │ 63.4 kB │ 28.7 kB │ +└───────────┴─────┴──────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 513 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +1k requests in 60.06s, 14.7 MB read +20 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 10 *** + +Running 60s test @ http://192.168.2.131/ +10 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 996 ms │ 15183 ms │ 29343 ms │ 29946 ms │ 15135.07 ms │ 8665.13 ms │ 30794 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 32 │ 32 │ 37 │ 43 │ 37.59 │ 2.43 │ 32 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 140 kB │ 140 kB │ 162 kB │ 188 kB │ 165 kB │ 10.6 kB │ 140 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2255 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.06s, 9.88 MB read +5 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +10 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 876 ms │ 15390 ms │ 29322 ms │ 29837 ms │ 15193.95 ms │ 8726.45 ms │ 30887 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 46 │ 47 │ 52 │ 60 │ 52.42 │ 3.63 │ 46 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 8.14 kB │ 8.32 kB │ 9.21 kB │ 10.6 kB │ 9.28 kB │ 642 B │ 8.14 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3145 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +6k requests in 60.07s, 557 kB read +193 errors (0 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +10 connections +1 workers + + +┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 1126 ms │ 15812 ms │ 30288 ms │ 31239 ms │ 15913.56 ms │ 8747.22 ms │ 32439 ms │ +└─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────┬─────────┬────────┬────────┬────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼─────────┼────────┼────────┼────────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 2 │ 10 │ 10 │ 8.84 │ 2.37 │ 2 │ +├───────────┼─────┼─────────┼────────┼────────┼────────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 57.4 kB │ 287 kB │ 287 kB │ 254 kB │ 68 kB │ 57.4 kB │ +└───────────┴─────┴─────────┴────────┴────────┴────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 530 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +1k requests in 60.06s, 15.2 MB read +42 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 15 *** + +Running 60s test @ http://192.168.2.131/ +15 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +15 connections +1 workers + + +┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 690 ms │ 12131 ms │ 28685 ms │ 29798 ms │ 13051.88 ms │ 8385.32 ms │ 32739 ms │ +└─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 46 │ 50 │ 56 │ 63 │ 55.99 │ 3.53 │ 46 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 8.14 kB │ 8.86 kB │ 9.92 kB │ 11.2 kB │ 9.91 kB │ 624 B │ 8.14 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3359 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +7k requests in 60.06s, 595 kB read +269 errors (5 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +15 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + + + +CLIENTS: *** 20 *** + +Running 60s test @ http://192.168.2.131/ +20 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +20 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 377 ms │ 5749 ms │ 19598 ms │ 21657 ms │ 6953.55 ms │ 5299.16 ms │ 23969 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 45 │ 45 │ 55 │ 60 │ 54.27 │ 3.9 │ 45 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 7.88 kB │ 7.88 kB │ 9.63 kB │ 10.6 kB │ 9.53 kB │ 687 B │ 7.88 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3256 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +7k requests in 60.06s, 571 kB read +312 errors (30 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +20 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + diff --git a/lib/PsychicHttp/benchmark/results/arduinomongoose-websocket-loadtest.log b/lib/PsychicHttp/benchmark/results/arduinomongoose-websocket-loadtest.log new file mode 100644 index 0000000..8c18045 --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/arduinomongoose-websocket-loadtest.log @@ -0,0 +1,246 @@ + + +CLIENTS: *** 1 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 1 +Agent: none + +Completed requests: 3750 +Total errors: 0 +Total time: 60.001 s +Mean latency: 15.5 ms +Effective rps: 62 + +Percentage of requests served within a certain time + 50% 12 ms + 90% 18 ms + 95% 36 ms + 99% 80 ms + 100% 223 ms (longest request) + + +CLIENTS: *** 2 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 2 +Agent: none + +Completed requests: 5795 +Total errors: 0 +Total time: 60.004 s +Mean latency: 20.2 ms +Effective rps: 97 + +Percentage of requests served within a certain time + 50% 16 ms + 90% 27 ms + 95% 64 ms + 99% 86 ms + 100% 108 ms (longest request) + + +CLIENTS: *** 3 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 3 +Agent: none + +Completed requests: 7445 +Total errors: 0 +Total time: 60.003 s +Mean latency: 23.6 ms +Effective rps: 124 + +Percentage of requests served within a certain time + 50% 19 ms + 90% 32 ms + 95% 70 ms + 99% 92 ms + 100% 121 ms (longest request) + + +CLIENTS: *** 4 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 4 +Agent: none + +Completed requests: 8751 +Total errors: 0 +Total time: 60.005 s +Mean latency: 26.9 ms +Effective rps: 146 + +Percentage of requests served within a certain time + 50% 22 ms + 90% 38 ms + 95% 73 ms + 99% 95 ms + 100% 115 ms (longest request) + + +CLIENTS: *** 5 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 5 +Agent: none + +Completed requests: 9953 +Total errors: 0 +Total time: 60.004 s +Mean latency: 29.6 ms +Effective rps: 166 + +Percentage of requests served within a certain time + 50% 25 ms + 90% 42 ms + 95% 74 ms + 99% 93 ms + 100% 116 ms (longest request) + + +CLIENTS: *** 6 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 6 +Agent: none + +Completed requests: 10871 +Total errors: 0 +Total time: 60.005 s +Mean latency: 32.6 ms +Effective rps: 181 + +Percentage of requests served within a certain time + 50% 27 ms + 90% 50 ms + 95% 82 ms + 99% 100 ms + 100% 116 ms (longest request) + + +CLIENTS: *** 7 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 7 +Agent: none + +Completed requests: 11777 +Total errors: 0 +Total time: 60.003 s +Mean latency: 35.1 ms +Effective rps: 196 + +Percentage of requests served within a certain time + 50% 30 ms + 90% 66 ms + 95% 83 ms + 99% 101 ms + 100% 137 ms (longest request) + + +CLIENTS: *** 8 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 8 +Running on cores: 2 +Agent: none + +Completed requests: 11639 +Total errors: 0 +Total time: 60.004 s +Mean latency: 35.4 ms +Effective rps: 194 + +Percentage of requests served within a certain time + 50% 30 ms + 90% 67 ms + 95% 86 ms + 99% 106 ms + 100% 135 ms (longest request) + + +CLIENTS: *** 10 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 10 +Running on cores: 2 +Agent: none + +Completed requests: 11619 +Total errors: 0 +Total time: 60.004 s +Mean latency: 35.6 ms +Effective rps: 194 + +Percentage of requests served within a certain time + 50% 30 ms + 90% 71 ms + 95% 87 ms + 99% 105 ms + 100% 125 ms (longest request) + + +CLIENTS: *** 16 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 16 +Running on cores: 2 +Agent: none + +Completed requests: 15314 +Total errors: 0 +Total time: 60.005 s +Mean latency: 54.2 ms +Effective rps: 255 + +Percentage of requests served within a certain time + 50% 46 ms + 90% 91 ms + 95% 105 ms + 99% 127 ms + 100% 826 ms (longest request) + + +CLIENTS: *** 20 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 20 +Running on cores: 2 +Agent: none + +Completed requests: 15370 +Total errors: 0 +Total time: 60.005 s +Mean latency: 57.7 ms +Effective rps: 256 + +Percentage of requests served within a certain time + 50% 48 ms + 90% 96 ms + 95% 110 ms + 99% 132 ms + 100% 851 ms (longest request) diff --git a/lib/PsychicHttp/benchmark/results/espasync-http-loadtest.log b/lib/PsychicHttp/benchmark/results/espasync-http-loadtest.log new file mode 100644 index 0000000..cafa123 --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/espasync-http-loadtest.log @@ -0,0 +1,1165 @@ + + +CLIENTS: *** 1 *** + +Running 60s test @ http://192.168.2.131/ +1 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 32 ms │ 40 ms │ 118 ms │ 118 ms │ 54.67 ms │ 29.29 ms │ 118 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.1 │ 0.31 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 4.38 kB │ 438 B │ 1.31 kB │ 4.38 kB │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 6 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +12 requests in 60.08s, 26.3 kB read +5 errors (5 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +1 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 61 ms │ 64 ms │ 131 ms │ 131 ms │ 93.84 ms │ 31.05 ms │ 131 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬───────┬────────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼───────┼────────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.1 │ 0.31 │ 1 │ +├───────────┼─────┼──────┼─────┼───────┼────────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 174 B │ 17.4 B │ 52.2 B │ 174 B │ +└───────────┴─────┴──────┴─────┴───────┴────────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 6 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +12 requests in 60.08s, 1.04 kB read +5 errors (5 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +1 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 135 ms │ 142 ms │ 162 ms │ 162 ms │ 145.84 ms │ 10.32 ms │ 162 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.1 │ 0.31 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 2.88 kB │ 8.63 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 6 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +12 requests in 60.07s, 173 kB read +5 errors (5 timeouts) + + +---------------- + + + +CLIENTS: *** 2 *** + +Running 60s test @ http://192.168.2.131/ +2 connections +1 workers + + +┌─────────┬───────┬───────┬───────┬───────┬──────────┬──────────┬───────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼───────┼───────┼──────────┼──────────┼───────┤ +│ Latency │ 29 ms │ 52 ms │ 93 ms │ 93 ms │ 56.09 ms │ 17.92 ms │ 93 ms │ +└─────────┴───────┴───────┴───────┴───────┴──────────┴──────────┴───────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.2 │ 0.61 │ 2 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 8.76 kB │ 876 B │ 2.63 kB │ 8.76 kB │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 12 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +24 requests in 60.08s, 52.5 kB read +10 errors (10 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +2 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 60 ms │ 106 ms │ 126 ms │ 126 ms │ 102.5 ms │ 21.86 ms │ 126 ms │ +└─────────┴───────┴────────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬───────┬────────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.2 │ 0.61 │ 2 │ +├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 348 B │ 34.8 B │ 104 B │ 348 B │ +└───────────┴─────┴──────┴─────┴───────┴────────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 12 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +24 requests in 60.08s, 2.09 kB read +10 errors (10 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +2 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 228 ms │ 245 ms │ 268 ms │ 268 ms │ 245.34 ms │ 13.38 ms │ 268 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.2 │ 0.61 │ 2 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 57.5 kB │ 5.75 kB │ 17.3 kB │ 57.5 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 12 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +24 requests in 60.07s, 345 kB read +10 errors (10 timeouts) + + +---------------- + + + +CLIENTS: *** 3 *** + +Running 60s test @ http://192.168.2.131/ +3 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 40 ms │ 97 ms │ 133 ms │ 133 ms │ 95.45 ms │ 30.82 ms │ 133 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.3 │ 0.91 │ 3 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 13.1 kB │ 1.31 kB │ 3.94 kB │ 13.1 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 18 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +36 requests in 60.08s, 78.8 kB read +15 errors (15 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +3 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 55 ms │ 66 ms │ 211 ms │ 211 ms │ 82.78 ms │ 37.03 ms │ 211 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬───────┬────────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.3 │ 0.91 │ 3 │ +├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 522 B │ 52.2 B │ 157 B │ 522 B │ +└───────────┴─────┴──────┴─────┴───────┴────────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 18 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +36 requests in 60.1s, 3.13 kB read +15 errors (15 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +3 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 215 ms │ 278 ms │ 348 ms │ 348 ms │ 280.56 ms │ 30.51 ms │ 348 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.3 │ 0.91 │ 3 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.3 kB │ 8.62 kB │ 25.9 kB │ 86.3 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 18 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +36 requests in 60.15s, 518 kB read +15 errors (15 timeouts) + + +---------------- + + + +CLIENTS: *** 4 *** + +Running 60s test @ http://192.168.2.131/ +4 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 29 ms │ 76 ms │ 137 ms │ 137 ms │ 86.55 ms │ 35.16 ms │ 137 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 4 │ 0.4 │ 1.21 │ 4 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 17.5 kB │ 1.75 kB │ 5.25 kB │ 17.5 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 24 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +48 requests in 60.13s, 105 kB read +20 errors (20 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +4 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬───────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼───────────┼────────┤ +│ Latency │ 54 ms │ 108 ms │ 523 ms │ 523 ms │ 164.71 ms │ 130.96 ms │ 523 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴───────────┴────────┘ +┌───────────┬─────┬──────┬─────┬───────┬────────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 4 │ 0.4 │ 1.21 │ 4 │ +├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 696 B │ 69.6 B │ 209 B │ 696 B │ +└───────────┴─────┴──────┴─────┴───────┴────────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 24 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +48 requests in 60.09s, 4.18 kB read +20 errors (20 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +4 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬─────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼─────────┼────────┤ +│ Latency │ 322 ms │ 379 ms │ 405 ms │ 405 ms │ 372.55 ms │ 20.5 ms │ 405 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴─────────┴────────┘ +┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 4 │ 0.4 │ 1.21 │ 4 │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 115 kB │ 11.5 kB │ 34.5 kB │ 115 kB │ +└───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 24 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +48 requests in 60.08s, 690 kB read +20 errors (20 timeouts) + + +---------------- + + + +CLIENTS: *** 5 *** + +Running 60s test @ http://192.168.2.131/ +5 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬─────────┬─────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼─────────┼─────────┼────────┤ +│ Latency │ 36 ms │ 104 ms │ 148 ms │ 148 ms │ 94.7 ms │ 35.9 ms │ 148 ms │ +└─────────┴───────┴────────┴────────┴────────┴─────────┴─────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 5 │ 0.5 │ 1.5 │ 5 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 21.9 kB │ 2.19 kB │ 6.57 kB │ 21.9 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 30 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +60 requests in 60.05s, 131 kB read +25 errors (25 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +5 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 66 ms │ 123 ms │ 263 ms │ 263 ms │ 127.67 ms │ 37.33 ms │ 263 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬───────┬──────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼───────┼──────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 5 │ 0.5 │ 1.5 │ 5 │ +├───────────┼─────┼──────┼─────┼───────┼──────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 870 B │ 87 B │ 261 B │ 870 B │ +└───────────┴─────┴──────┴─────┴───────┴──────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 30 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +60 requests in 60.07s, 5.22 kB read +25 errors (25 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +5 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 348 ms │ 398 ms │ 465 ms │ 465 ms │ 399.27 ms │ 31.99 ms │ 465 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 5 │ 0.5 │ 1.5 │ 5 │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 144 kB │ 14.4 kB │ 43.1 kB │ 144 kB │ +└───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 30 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +60 requests in 60.08s, 863 kB read +25 errors (25 timeouts) + + +---------------- + + + +CLIENTS: *** 6 *** + +Running 60s test @ http://192.168.2.131/ +6 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 34 ms │ 111 ms │ 188 ms │ 188 ms │ 113.25 ms │ 41.29 ms │ 188 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 6 │ 0.6 │ 1.81 │ 6 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 26.3 kB │ 2.63 kB │ 7.88 kB │ 26.3 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +72 requests in 60.07s, 158 kB read +30 errors (30 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +6 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 61 ms │ 121 ms │ 296 ms │ 296 ms │ 134.95 ms │ 46.95 ms │ 296 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 6 │ 0.6 │ 1.81 │ 6 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.05 kB │ 105 B │ 315 B │ 1.05 kB │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +72 requests in 60.09s, 6.3 kB read +30 errors (30 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +6 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 322 ms │ 422 ms │ 522 ms │ 522 ms │ 422.98 ms │ 51.58 ms │ 522 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 6 │ 0.6 │ 1.81 │ 6 │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 173 kB │ 17.2 kB │ 51.7 kB │ 173 kB │ +└───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +72 requests in 60.08s, 1.04 MB read +30 errors (30 timeouts) + + +---------------- + + + +CLIENTS: *** 7 *** + +Running 60s test @ http://192.168.2.131/ +7 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 60 ms │ 129 ms │ 220 ms │ 280 ms │ 127.81 ms │ 51.08 ms │ 280 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 7 │ 0.7 │ 2.1 │ 7 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 30.7 kB │ 3.06 kB │ 9.19 kB │ 30.6 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 42 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +84 requests in 60.08s, 184 kB read +35 errors (35 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +7 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 63 ms │ 125 ms │ 252 ms │ 289 ms │ 126.22 ms │ 46.41 ms │ 289 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 7 │ 0.7 │ 2.1 │ 7 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.23 kB │ 123 B │ 368 B │ 1.23 kB │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 42 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +84 requests in 60.07s, 7.35 kB read +35 errors (35 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +7 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬──────────┬───────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼──────────┼───────────┼────────┤ +│ Latency │ 144 ms │ 439 ms │ 877 ms │ 921 ms │ 439.3 ms │ 138.35 ms │ 921 ms │ +└─────────┴────────┴────────┴────────┴────────┴──────────┴───────────┴────────┘ +┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 7 │ 0.69 │ 1.87 │ 1 │ +├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 201 kB │ 19.6 kB │ 53.7 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 41 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +83 requests in 60.07s, 1.18 MB read +35 errors (35 timeouts) + + +---------------- + + + +CLIENTS: *** 8 *** + +Running 60s test @ http://192.168.2.131/ +8 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼────────┼──────────┼────────┤ +│ Latency │ 74 ms │ 141 ms │ 193 ms │ 198 ms │ 143 ms │ 31.11 ms │ 198 ms │ +└─────────┴───────┴────────┴────────┴────────┴────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬───────┬────────┬─────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼───────┼────────┼─────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 8 │ 0.8 │ 2.41 │ 8 │ +├───────────┼─────┼──────┼─────┼───────┼────────┼─────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 35 kB │ 3.5 kB │ 10.5 kB │ 35 kB │ +└───────────┴─────┴──────┴─────┴───────┴────────┴─────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 48 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +96 requests in 60.08s, 210 kB read +40 errors (40 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +8 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 59 ms │ 138 ms │ 284 ms │ 319 ms │ 139.8 ms │ 53.96 ms │ 319 ms │ +└─────────┴───────┴────────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬────────┬───────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼────────┼───────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 8 │ 0.8 │ 2.36 │ 1 │ +├───────────┼─────┼──────┼─────┼────────┼───────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.4 kB │ 140 B │ 411 B │ 175 B │ +└───────────┴─────┴──────┴─────┴────────┴───────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 48 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +96 requests in 60.07s, 8.4 kB read +40 errors (40 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +8 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + + + +CLIENTS: *** 9 *** + +Running 60s test @ http://192.168.2.131/ +9 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 31 ms │ 116 ms │ 206 ms │ 210 ms │ 109.58 ms │ 45.11 ms │ 210 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 9 │ 0.9 │ 2.7 │ 9 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 39.4 kB │ 3.94 kB │ 11.8 kB │ 39.4 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 54 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +108 requests in 60.08s, 236 kB read +45 errors (45 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +9 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 66 ms │ 131 ms │ 313 ms │ 349 ms │ 139.95 ms │ 60.66 ms │ 349 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 9 │ 0.9 │ 2.7 │ 9 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.57 kB │ 158 B │ 473 B │ 1.57 kB │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 54 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +108 requests in 60.08s, 9.45 kB read +45 errors (45 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +9 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + + + +CLIENTS: *** 10 *** + +Running 60s test @ http://192.168.2.131/ +10 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 35 ms │ 96 ms │ 264 ms │ 267 ms │ 103.97 ms │ 54.56 ms │ 267 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 10 │ 1 │ 3 │ 10 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 43.8 kB │ 4.38 kB │ 13.1 kB │ 43.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 60 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +120 requests in 60.08s, 263 kB read +50 errors (50 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +10 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 64 ms │ 144 ms │ 411 ms │ 437 ms │ 156.64 ms │ 80.67 ms │ 437 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 10 │ 1 │ 2.83 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.75 kB │ 175 B │ 495 B │ 175 B │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 60 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +120 requests in 60.08s, 10.5 kB read +50 errors (50 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +10 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + + + +CLIENTS: *** 15 *** + +Running 60s test @ http://192.168.2.131/ +15 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬──────────┬─────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼──────────┼─────────┼────────┤ +│ Latency │ 29 ms │ 104 ms │ 359 ms │ 362 ms │ 121.5 ms │ 77.5 ms │ 362 ms │ +└─────────┴───────┴────────┴────────┴────────┴──────────┴─────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 14 │ 1.34 │ 4.02 │ 12 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 52.6 kB │ 5.12 kB │ 15.4 kB │ 48.3 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 80 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +170 requests in 60.09s, 307 kB read +75 errors (75 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +15 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬───────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼───────────┼────────┤ +│ Latency │ 91 ms │ 148 ms │ 505 ms │ 574 ms │ 174.38 ms │ 100.31 ms │ 574 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴───────────┴────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 15 │ 1.5 │ 4.06 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 2.63 kB │ 263 B │ 710 B │ 175 B │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 90 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +180 requests in 60.08s, 15.8 kB read +75 errors (75 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +15 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + + + +CLIENTS: *** 20 *** + +Running 60s test @ http://192.168.2.131/ +20 connections +1 workers + + +┌─────────┬───────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ +│ Latency │ 62 ms │ 1134 ms │ 6942 ms │ 6976 ms │ 1884.35 ms │ 1805.12 ms │ 6976 ms │ +└─────────┴───────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 15 │ 1.44 │ 4.15 │ 4 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 57.2 kB │ 4.42 kB │ 14.2 kB │ 5.82 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 86 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +211 requests in 60.08s, 265 kB read +105 errors (100 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +20 connections +1 workers + + +┌─────────┬───────┬────────┬─────────┬─────────┬───────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼─────────┼─────────┼───────────┼────────────┼─────────┤ +│ Latency │ 62 ms │ 232 ms │ 4030 ms │ 6972 ms │ 586.75 ms │ 1218.03 ms │ 6972 ms │ +└─────────┴───────┴────────┴─────────┴─────────┴───────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 11 │ 1.39 │ 3.08 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.89 kB │ 238 B │ 529 B │ 172 B │ +└───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 83 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +203 requests in 60.09s, 14.3 kB read +100 errors (100 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +20 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + diff --git a/lib/PsychicHttp/benchmark/results/espasync-websocket-loadtest.log b/lib/PsychicHttp/benchmark/results/espasync-websocket-loadtest.log new file mode 100644 index 0000000..acc21d1 --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/espasync-websocket-loadtest.log @@ -0,0 +1,252 @@ + + +CLIENTS: *** 1 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 1 +Agent: none + +Completed requests: 4231 +Total errors: 0 +Total time: 60.002 s +Mean latency: 13.6 ms +Effective rps: 71 + +Percentage of requests served within a certain time + 50% 10 ms + 90% 16 ms + 95% 24 ms + 99% 81 ms + 100% 280 ms (longest request) + + +CLIENTS: *** 2 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 2 +Agent: none + +Completed requests: 5914 +Total errors: 0 +Total time: 60.001 s +Mean latency: 19.7 ms +Effective rps: 99 + +Percentage of requests served within a certain time + 50% 15 ms + 90% 26 ms + 95% 67 ms + 99% 86 ms + 100% 109 ms (longest request) + + +CLIENTS: *** 3 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 3 +Agent: none + +Completed requests: 8204 +Total errors: 0 +Total time: 60.003 s +Mean latency: 21.4 ms +Effective rps: 137 + +Percentage of requests served within a certain time + 50% 17 ms + 90% 29 ms + 95% 68 ms + 99% 87 ms + 100% 104 ms (longest request) + + +CLIENTS: *** 4 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 4 +Agent: none + +Completed requests: 9634 +Total errors: 0 +Total time: 60.004 s +Mean latency: 24.4 ms +Effective rps: 161 + +Percentage of requests served within a certain time + 50% 19 ms + 90% 33 ms + 95% 73 ms + 99% 91 ms + 100% 145 ms (longest request) + + +CLIENTS: *** 5 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 5 +Agent: none + +Completed requests: 10759 +Total errors: 0 +Total time: 60.003 s +Mean latency: 27.3 ms +Effective rps: 179 + +Percentage of requests served within a certain time + 50% 22 ms + 90% 39 ms + 95% 76 ms + 99% 95 ms + 100% 117 ms (longest request) + + +CLIENTS: *** 6 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 6 +Agent: none + +Completed requests: 11302 +Total errors: 0 +Total time: 60.004 s +Mean latency: 31.3 ms +Effective rps: 188 + +Percentage of requests served within a certain time + 50% 26 ms + 90% 58 ms + 95% 81 ms + 99% 100 ms + 100% 122 ms (longest request) + + +CLIENTS: *** 7 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 7 +Agent: none + +Completed requests: 12713 +Total errors: 0 +Total time: 60.003 s +Mean latency: 32.5 ms +Effective rps: 212 + +Percentage of requests served within a certain time + 50% 27 ms + 90% 52 ms + 95% 81 ms + 99% 99 ms + 100% 125 ms (longest request) + + +CLIENTS: *** 8 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 8 +Running on cores: 2 +Agent: none + +Completed requests: 13157 +Total errors: 0 +Total time: 60.003 s +Mean latency: 35.9 ms +Effective rps: 219 + +Percentage of requests served within a certain time + 50% 30 ms + 90% 71 ms + 95% 88 ms + 99% 107 ms + 100% 132 ms (longest request) + + +CLIENTS: *** 10 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 10 +Running on cores: 2 +Agent: none + +Completed requests: 13417 +Total errors: 2 +Total time: 60.001 s +Mean latency: 34.4 ms +Effective rps: 224 + +Percentage of requests served within a certain time + 50% 30 ms + 90% 53 ms + 95% 81 ms + 99% 101 ms + 100% 124 ms (longest request) + + -1: 2 errors + + +CLIENTS: *** 16 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 16 +Running on cores: 2 +Agent: none + +Completed requests: 12804 +Total errors: 7 +Total time: 60.001 s +Mean latency: 36.4 ms +Effective rps: 213 + +Percentage of requests served within a certain time + 50% 30 ms + 90% 70 ms + 95% 86 ms + 99% 106 ms + 100% 135 ms (longest request) + + -1: 7 errors + + +CLIENTS: *** 20 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 20 +Running on cores: 2 +Agent: none + +Completed requests: 8421 +Total errors: 13 +Total time: 60.003 s +Mean latency: 37.2 ms +Effective rps: 140 + +Percentage of requests served within a certain time + 50% 20 ms + 90% 50 ms + 95% 77 ms + 99% 105 ms + 100% 9227 ms (longest request) + + -1: 13 errors diff --git a/lib/PsychicHttp/benchmark/results/psychic-http-loadtest.log b/lib/PsychicHttp/benchmark/results/psychic-http-loadtest.log new file mode 100644 index 0000000..148d378 --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/psychic-http-loadtest.log @@ -0,0 +1,1172 @@ + + +CLIENTS: *** 1 *** + +Running 60s test @ http://192.168.2.131/ +1 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 24 ms │ 31 ms │ 100 ms │ 134 ms │ 39.16 ms │ 22.82 ms │ 270 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 18 │ 21 │ 25 │ 30 │ 25.19 │ 2.28 │ 18 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 78.1 kB │ 91.1 kB │ 108 kB │ 130 kB │ 109 kB │ 9.88 kB │ 78.1 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1511 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.05s, 6.55 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +1 connections +1 workers + + +┌─────────┬───────┬───────┬───────┬───────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼───────┼───────┼──────────┼──────────┼────────┤ +│ Latency │ 19 ms │ 25 ms │ 91 ms │ 99 ms │ 31.42 ms │ 18.78 ms │ 116 ms │ +└─────────┴───────┴───────┴───────┴───────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 26 │ 28 │ 31 │ 34 │ 31.29 │ 1.79 │ 26 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 3.46 kB │ 3.73 kB │ 4.16 kB │ 4.56 kB │ 4.19 kB │ 241 B │ 3.46 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1877 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.06s, 251 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +1 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 141 ms │ 201 ms │ 317 ms │ 331 ms │ 206.85 ms │ 51.73 ms │ 366 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 4 │ 4 │ 5 │ 6 │ 4.82 │ 0.62 │ 4 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 115 kB │ 115 kB │ 144 kB │ 173 kB │ 139 kB │ 17.8 kB │ 115 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 289 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +290 requests in 60.07s, 8.32 MB read + + +---------------- + + + +CLIENTS: *** 2 *** + +Running 60s test @ http://192.168.2.131/ +2 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 26 ms │ 41 ms │ 167 ms │ 203 ms │ 55.52 ms │ 37.23 ms │ 373 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 29 │ 31 │ 35 │ 40 │ 35.64 │ 2.36 │ 29 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 126 kB │ 135 kB │ 152 kB │ 174 kB │ 155 kB │ 10.2 kB │ 126 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2138 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.06s, 9.27 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +2 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 25 ms │ 35 ms │ 103 ms │ 112 ms │ 43.19 ms │ 21.44 ms │ 142 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 40 │ 40 │ 46 │ 50 │ 45.7 │ 2.76 │ 40 │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 5.36 kB │ 5.36 kB │ 6.17 kB │ 6.7 kB │ 6.12 kB │ 370 B │ 5.36 kB │ +└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2742 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.06s, 367 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +2 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 188 ms │ 322 ms │ 475 ms │ 507 ms │ 324.51 ms │ 70.51 ms │ 535 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.15 │ 0.63 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 177 kB │ 18 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 369 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +371 requests in 60.06s, 10.6 MB read + + +---------------- + + + +CLIENTS: *** 3 *** + +Running 60s test @ http://192.168.2.131/ +3 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 31 ms │ 54 ms │ 185 ms │ 212 ms │ 68.63 ms │ 39.36 ms │ 386 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 38 │ 39 │ 43 │ 47 │ 43.32 │ 2.31 │ 38 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 165 kB │ 169 kB │ 187 kB │ 204 kB │ 188 kB │ 10 kB │ 165 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2599 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.06s, 11.3 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +3 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 29 ms │ 42 ms │ 116 ms │ 126 ms │ 51.59 ms │ 23.91 ms │ 175 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 40 │ 51 │ 58 │ 64 │ 57.59 │ 4.25 │ 40 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 5.36 kB │ 6.83 kB │ 7.78 kB │ 8.58 kB │ 7.72 kB │ 569 B │ 5.36 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3455 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.06s, 463 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +3 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬───────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼───────────┼────────┤ +│ Latency │ 203 ms │ 458 ms │ 761 ms │ 814 ms │ 474.54 ms │ 168.16 ms │ 902 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴───────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 4 │ 5 │ 6 │ 7 │ 6.29 │ 0.67 │ 4 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 115 kB │ 144 kB │ 173 kB │ 201 kB │ 181 kB │ 19 kB │ 115 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 377 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +380 requests in 60.06s, 10.9 MB read + + +---------------- + + + +CLIENTS: *** 4 *** + +Running 60s test @ http://192.168.2.131/ +4 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 37 ms │ 63 ms │ 206 ms │ 239 ms │ 81.32 ms │ 45.45 ms │ 396 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 42 │ 44 │ 49 │ 56 │ 48.87 │ 3.01 │ 42 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 182 kB │ 191 kB │ 213 kB │ 243 kB │ 212 kB │ 13 kB │ 182 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2932 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.05s, 12.7 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +4 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 33 ms │ 50 ms │ 123 ms │ 131 ms │ 58.29 ms │ 23.99 ms │ 159 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 59 │ 59 │ 68 │ 78 │ 68 │ 4.3 │ 59 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 7.91 kB │ 7.91 kB │ 9.12 kB │ 10.5 kB │ 9.11 kB │ 576 B │ 7.91 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4080 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.05s, 547 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +4 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ +│ Latency │ 326 ms │ 534 ms │ 1064 ms │ 1141 ms │ 635.61 ms │ 238.71 ms │ 1210 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.27 │ 0.63 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 180 kB │ 18.1 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 376 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +380 requests in 60.05s, 10.8 MB read + + +---------------- + + + +CLIENTS: *** 5 *** + +Running 60s test @ http://192.168.2.131/ +5 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 44 ms │ 74 ms │ 222 ms │ 261 ms │ 91.54 ms │ 47.88 ms │ 417 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 47 │ 47 │ 54 │ 60 │ 54.29 │ 3.05 │ 47 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 204 kB │ 204 kB │ 234 kB │ 260 kB │ 235 kB │ 13.2 kB │ 204 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3257 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.06s, 14.1 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +5 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 36 ms │ 57 ms │ 126 ms │ 138 ms │ 64.99 ms │ 24.42 ms │ 195 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 64 │ 69 │ 76 │ 84 │ 76.32 │ 3.97 │ 64 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 8.58 kB │ 9.25 kB │ 10.2 kB │ 11.3 kB │ 10.2 kB │ 532 B │ 8.58 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4579 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.06s, 614 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +5 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ +│ Latency │ 439 ms │ 652 ms │ 1393 ms │ 1458 ms │ 780.62 ms │ 290.11 ms │ 2018 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 4 │ 5 │ 6 │ 8 │ 6.35 │ 0.78 │ 4 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 115 kB │ 144 kB │ 173 kB │ 230 kB │ 183 kB │ 22.1 kB │ 115 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 381 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +386 requests in 60.06s, 11 MB read + + +---------------- + + + +CLIENTS: *** 6 *** + +Running 60s test @ http://192.168.2.131/ +6 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 50 ms │ 85 ms │ 238 ms │ 268 ms │ 102.35 ms │ 50.03 ms │ 517 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 49 │ 53 │ 58 │ 65 │ 58.3 │ 3.13 │ 49 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 213 kB │ 230 kB │ 252 kB │ 282 kB │ 253 kB │ 13.6 kB │ 213 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3498 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.05s, 15.2 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +6 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 41 ms │ 64 ms │ 138 ms │ 151 ms │ 74.53 ms │ 27.06 ms │ 286 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 71 │ 71 │ 80 │ 90 │ 79.92 │ 4.07 │ 71 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 9.59 kB │ 9.59 kB │ 10.8 kB │ 12.2 kB │ 10.8 kB │ 548 B │ 9.59 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4795 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.06s, 647 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +6 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬──────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼───────────┼──────────┼─────────┤ +│ Latency │ 579 ms │ 840 ms │ 1766 ms │ 1816 ms │ 973.38 ms │ 355.3 ms │ 2392 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴───────────┴──────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 4 │ 4 │ 6 │ 8 │ 6.1 │ 0.87 │ 4 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 115 kB │ 115 kB │ 173 kB │ 230 kB │ 176 kB │ 25 kB │ 115 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 366 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +372 requests in 60.06s, 10.5 MB read + + +---------------- + + + +CLIENTS: *** 7 *** + +Running 60s test @ http://192.168.2.131/ +7 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 57 ms │ 96 ms │ 249 ms │ 293 ms │ 113.4 ms │ 53.05 ms │ 640 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 53 │ 54 │ 62 │ 68 │ 61.44 │ 3.82 │ 53 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 230 kB │ 234 kB │ 269 kB │ 295 kB │ 266 kB │ 16.6 kB │ 230 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3686 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.05s, 16 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +7 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 45 ms │ 69 ms │ 145 ms │ 154 ms │ 79.11 ms │ 27.06 ms │ 274 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬───────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼───────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 78 │ 80 │ 89 │ 99 │ 87.9 │ 4.78 │ 78 │ +├───────────┼─────────┼─────────┼───────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 10.5 kB │ 10.8 kB │ 12 kB │ 13.4 kB │ 11.9 kB │ 644 B │ 10.5 kB │ +└───────────┴─────────┴─────────┴───────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5274 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.07s, 712 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +7 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 671 ms │ 952 ms │ 1965 ms │ 2059 ms │ 1094.09 ms │ 402.31 ms │ 3654 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 8 │ 6.34 │ 0.79 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 230 kB │ 182 kB │ 22.7 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 380 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +387 requests in 60.07s, 10.9 MB read + + +---------------- + + + +CLIENTS: *** 8 *** + +Running 60s test @ http://192.168.2.131/ +8 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼─────────┤ +│ Latency │ 56 ms │ 95 ms │ 243 ms │ 275 ms │ 110.53 ms │ 52.27 ms │ 1027 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 52 │ 54 │ 62 │ 73 │ 62.99 │ 4.89 │ 52 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 226 kB │ 234 kB │ 269 kB │ 317 kB │ 273 kB │ 21.2 kB │ 226 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3779 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.06s, 16.4 MB read +6 errors (6 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +8 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬─────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼─────────┼──────────┼────────┤ +│ Latency │ 45 ms │ 70 ms │ 146 ms │ 156 ms │ 80.9 ms │ 28.68 ms │ 308 ms │ +└─────────┴───────┴───────┴────────┴────────┴─────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 77 │ 77 │ 86 │ 95 │ 85.92 │ 4.89 │ 77 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 10.4 kB │ 10.4 kB │ 11.6 kB │ 12.8 kB │ 11.6 kB │ 659 B │ 10.4 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5155 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.07s, 696 kB read +6 errors (6 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +8 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 699 ms │ 979 ms │ 2038 ms │ 2128 ms │ 1111.14 ms │ 406.26 ms │ 3506 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.25 │ 0.6 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 180 kB │ 17.1 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 375 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +389 requests in 60.06s, 10.8 MB read +6 errors (6 timeouts) + + +---------------- + + + +CLIENTS: *** 9 *** + +Running 60s test @ http://192.168.2.131/ +9 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 57 ms │ 96 ms │ 249 ms │ 277 ms │ 112.81 ms │ 51.23 ms │ 626 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 52 │ 57 │ 61 │ 68 │ 61.7 │ 3.18 │ 52 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 226 kB │ 247 kB │ 265 kB │ 295 kB │ 268 kB │ 13.8 kB │ 226 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3702 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.06s, 16.1 MB read +12 errors (12 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +9 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 45 ms │ 70 ms │ 144 ms │ 154 ms │ 79.64 ms │ 27.25 ms │ 239 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬───────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 76 │ 79 │ 87 │ 96 │ 87.34 │ 4.79 │ 76 │ +├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 10.3 kB │ 10.7 kB │ 11.8 kB │ 13 kB │ 11.8 kB │ 647 B │ 10.3 kB │ +└───────────┴─────────┴─────────┴─────────┴───────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5240 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.1s, 707 kB read +12 errors (12 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +9 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 664 ms │ 952 ms │ 2001 ms │ 2055 ms │ 1092.56 ms │ 399.94 ms │ 3588 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.35 │ 0.63 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 183 kB │ 18 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 381 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +402 requests in 60.06s, 11 MB read +12 errors (12 timeouts) + + +---------------- + + + +CLIENTS: *** 10 *** + +Running 60s test @ http://192.168.2.131/ +10 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 56 ms │ 97 ms │ 244 ms │ 277 ms │ 113.43 ms │ 51.43 ms │ 616 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 48 │ 51 │ 61 │ 69 │ 61.42 │ 3.92 │ 48 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 208 kB │ 221 kB │ 265 kB │ 300 kB │ 266 kB │ 17 kB │ 208 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3685 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.06s, 16 MB read +18 errors (18 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +10 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 45 ms │ 71 ms │ 147 ms │ 154 ms │ 81.57 ms │ 28.91 ms │ 335 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 75 │ 76 │ 86 │ 92 │ 85.32 │ 4.46 │ 75 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 10.1 kB │ 10.3 kB │ 11.6 kB │ 12.4 kB │ 11.5 kB │ 601 B │ 10.1 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5119 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.06s, 691 kB read +18 errors (18 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +10 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 705 ms │ 940 ms │ 1962 ms │ 2052 ms │ 1075.72 ms │ 385.45 ms │ 3313 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.45 │ 0.65 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 186 kB │ 18.5 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 387 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +415 requests in 60.05s, 11.1 MB read +18 errors (18 timeouts) + + +---------------- + + + +CLIENTS: *** 15 *** + +Running 60s test @ http://192.168.2.131/ +15 connections +1 workers + +node:internal/event_target:1084 + process.nextTick(() => { throw err; }); + ^ + +TypeError: colorize is not a function + at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 + at Array.forEach () + at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) + at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) + at EventEmitter.emit (node:events:527:35) + at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) + at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) + at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) + at Worker.emit (node:events:515:28) + at MessagePort. (node:internal/worker:263:53) + +Node.js v21.1.0 + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +15 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼─────────┤ +│ Latency │ 44 ms │ 69 ms │ 158 ms │ 318 ms │ 82.99 ms │ 44.17 ms │ 1074 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴─────────┘ +┌───────────┬─────────┬────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 22 │ 23 │ 88 │ 98 │ 83.74 │ 16.45 │ 22 │ +├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 2.97 kB │ 3.1 kB │ 11.9 kB │ 13.2 kB │ 11.3 kB │ 2.22 kB │ 2.97 kB │ +└───────────┴─────────┴────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5024 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.06s, 678 kB read +48 errors (48 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +15 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 665 ms │ 954 ms │ 1989 ms │ 2105 ms │ 1092.91 ms │ 396.34 ms │ 3476 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.35 │ 0.66 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 183 kB │ 18.8 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 381 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +444 requests in 60.06s, 11 MB read +48 errors (48 timeouts) + + +---------------- + + + +CLIENTS: *** 20 *** + +Running 60s test @ http://192.168.2.131/ +20 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 57 ms │ 97 ms │ 255 ms │ 322 ms │ 114.69 ms │ 55.79 ms │ 683 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 26 │ 48 │ 61 │ 67 │ 60.75 │ 5.8 │ 26 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 113 kB │ 208 kB │ 265 kB │ 291 kB │ 264 kB │ 25.2 kB │ 113 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3645 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.05s, 15.8 MB read +78 errors (78 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +20 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 44 ms │ 69 ms │ 142 ms │ 153 ms │ 78.14 ms │ 26.88 ms │ 256 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬───────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ +│ Req/Sec │ 76 │ 78 │ 88 │ 100 │ 88.92 │ 5.59 │ 76 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ +│ Bytes/Sec │ 10.3 kB │ 10.5 kB │ 11.9 kB │ 13.5 kB │ 12 kB │ 754 B │ 10.3 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴───────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5335 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.07s, 720 kB read +78 errors (78 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +20 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 705 ms │ 940 ms │ 1991 ms │ 2070 ms │ 1085.97 ms │ 389.59 ms │ 3357 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.39 │ 0.64 │ 5 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 184 kB │ 18.2 kB │ 144 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 383 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +481 requests in 60.06s, 11 MB read +78 errors (78 timeouts) + + +---------------- + diff --git a/lib/PsychicHttp/benchmark/results/psychic-ssl-http-loadtest.log b/lib/PsychicHttp/benchmark/results/psychic-ssl-http-loadtest.log new file mode 100644 index 0000000..17e3414 --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/psychic-ssl-http-loadtest.log @@ -0,0 +1,1194 @@ + + +CLIENTS: *** 1 *** + +Running 60s test @ https://192.168.2.131/ +1 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬─────────┬─────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼─────────┼─────────┼─────────┤ +│ Latency │ 33 ms │ 40 ms │ 139 ms │ 157 ms │ 51.6 ms │ 58.1 ms │ 1757 ms │ +└─────────┴───────┴───────┴────────┴────────┴─────────┴─────────┴─────────┘ +┌───────────┬─────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 5 │ 20 │ 24 │ 19.19 │ 4.05 │ 5 │ +├───────────┼─────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 21.7 kB │ 86.8 kB │ 104 kB │ 83.2 kB │ 17.5 kB │ 21.7 kB │ +└───────────┴─────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1151 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +1k requests in 60.07s, 4.99 MB read + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +1 connections +1 workers + + +┌─────────┬───────┬───────┬───────┬────────┬──────────┬──────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼───────┼────────┼──────────┼──────────┼─────────┤ +│ Latency │ 21 ms │ 27 ms │ 89 ms │ 100 ms │ 32.17 ms │ 43.71 ms │ 1745 ms │ +└─────────┴───────┴───────┴───────┴────────┴──────────┴──────────┴─────────┘ +┌───────────┬─────┬─────────┬─────────┬─────────┬────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 8 │ 32 │ 38 │ 30.57 │ 5.91 │ 8 │ +├───────────┼─────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 1.07 kB │ 4.29 kB │ 5.09 kB │ 4.1 kB │ 791 B │ 1.07 kB │ +└───────────┴─────┴─────────┴─────────┴─────────┴────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1834 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.07s, 246 kB read + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +1 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼───────────┼─────────┤ +│ Latency │ 216 ms │ 252 ms │ 400 ms │ 408 ms │ 281.55 ms │ 122.23 ms │ 1871 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴───────────┴─────────┘ +┌───────────┬─────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 1 │ 4 │ 5 │ 3.54 │ 0.83 │ 1 │ +├───────────┼─────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 28.8 kB │ 115 kB │ 144 kB │ 102 kB │ 23.8 kB │ 28.8 kB │ +└───────────┴─────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 212 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +213 requests in 60.07s, 6.1 MB read + + +---------------- + + + +CLIENTS: *** 2 *** + +Running 60s test @ https://192.168.2.131/ +2 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼───────────┼─────────┤ +│ Latency │ 37 ms │ 54 ms │ 149 ms │ 164 ms │ 64.14 ms │ 106.26 ms │ 3230 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 0 │ 0 │ 33 │ 36 │ 30.92 │ 7.43 │ 27 │ +├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 143 kB │ 156 kB │ 134 kB │ 32.2 kB │ 117 kB │ +└───────────┴─────┴──────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1855 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.06s, 8.05 MB read + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +2 connections +1 workers + + +┌─────────┬───────┬───────┬───────┬────────┬──────────┬─────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼───────┼────────┼──────────┼─────────┼─────────┤ +│ Latency │ 27 ms │ 38 ms │ 95 ms │ 106 ms │ 44.65 ms │ 87.8 ms │ 3211 ms │ +└─────────┴───────┴───────┴───────┴────────┴──────────┴─────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 47 │ 52 │ 44.29 │ 10.57 │ 35 │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 6.35 kB │ 7.02 kB │ 5.98 kB │ 1.43 kB │ 4.72 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2657 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.07s, 359 kB read + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +2 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼───────────┼─────────┤ +│ Latency │ 320 ms │ 456 ms │ 633 ms │ 701 ms │ 488.51 ms │ 296.92 ms │ 3820 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 4 │ 5 │ 4.09 │ 1.09 │ 3 │ +├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 115 kB │ 144 kB │ 118 kB │ 31.2 kB │ 86.3 kB │ +└───────────┴─────┴──────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 245 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +247 requests in 60.07s, 7.05 MB read + + +---------------- + + + +CLIENTS: *** 3 *** + +Running 60s test @ https://192.168.2.131/ +3 connections +1 workers + + +┌─────────┬─────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 1535 ms │ 6477 ms │ 14517 ms │ 14517 ms │ 6462.25 ms │ 2849.18 ms │ 14517 ms │ +└─────────┴─────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.27 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +75 requests in 60.1s, 156 kB read +5 errors (0 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +3 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 1517 ms │ 1638 ms │ 3675 ms │ 3675 ms │ 1697.95 ms │ 343.44 ms │ 3675 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 35 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +73 requests in 60.11s, 4.72 kB read +1 errors (0 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +3 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ +│ Latency │ 386 ms │ 9117 ms │ 50313 ms │ 50313 ms │ 12942.77 ms │ 11833.9 ms │ 50313 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.57 │ 0.7 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.4 kB │ 16.3 kB │ 19.9 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 34 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +68 requests in 60.11s, 979 kB read +28 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 4 *** + +Running 60s test @ https://192.168.2.131/ +4 connections +1 workers + + +┌─────────┬─────────┬─────────┬──────────┬──────────┬─────────────┬─────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼──────────┼──────────┼─────────────┼─────────────┼──────────┤ +│ Latency │ 1530 ms │ 1686 ms │ 40733 ms │ 40733 ms │ 10336.06 ms │ 12608.67 ms │ 40733 ms │ +└─────────┴─────────┴─────────┴──────────┴──────────┴─────────────┴─────────────┴──────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.27 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +76 requests in 60.15s, 156 kB read +11 errors (0 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +4 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬──────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼──────────┼─────────┤ +│ Latency │ 1495 ms │ 1586 ms │ 4490 ms │ 4490 ms │ 1673.35 ms │ 485.3 ms │ 4490 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴──────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 35 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +74 requests in 60.15s, 4.72 kB read +3 errors (0 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +4 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬─────────────┬─────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼─────────────┼─────────────┼──────────┤ +│ Latency │ 340 ms │ 3642 ms │ 49950 ms │ 49950 ms │ 13180.63 ms │ 14189.21 ms │ 49950 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴─────────────┴─────────────┴──────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 2 │ 0.59 │ 0.65 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 28.8 kB │ 57.6 kB │ 16.8 kB │ 18.4 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 35 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +72 requests in 60.15s, 1.01 MB read +30 errors (0 timeouts) + + +---------------- + + + +CLIENTS: *** 5 *** + +Running 60s test @ https://192.168.2.131/ +5 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 1449 ms │ 1597 ms │ 4810 ms │ 4810 ms │ 1691.72 ms │ 537.09 ms │ 4810 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.53 kB │ 2.14 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 35 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +75 requests in 60.13s, 152 kB read +11 errors (0 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +5 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 1510 ms │ 1584 ms │ 4971 ms │ 4971 ms │ 1685.03 ms │ 564.99 ms │ 4971 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 35 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +75 requests in 60.14s, 4.72 kB read +1 errors (0 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +5 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 409 ms │ 5565 ms │ 38132 ms │ 38132 ms │ 9430.76 ms │ 9263.66 ms │ 38132 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.56 │ 0.62 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 57.6 kB │ 15.8 kB │ 17.8 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 33 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +69 requests in 60.14s, 950 kB read +29 errors (1 timeouts) + + +---------------- + + + +CLIENTS: *** 6 *** + +Running 60s test @ https://192.168.2.131/ +6 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 1007 ms │ 1603 ms │ 5373 ms │ 5373 ms │ 1696.69 ms │ 639.15 ms │ 5373 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.53 kB │ 2.14 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 35 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +76 requests in 60.14s, 152 kB read +15 errors (2 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +6 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 976 ms │ 1586 ms │ 6118 ms │ 6118 ms │ 1683.15 ms │ 773.87 ms │ 6118 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 35 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +76 requests in 60.14s, 4.72 kB read +4 errors (2 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +6 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 437 ms │ 3543 ms │ 18174 ms │ 18174 ms │ 4264.16 ms │ 3629.94 ms │ 18174 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.54 │ 0.6 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 15.3 kB │ 17 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 32 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +69 requests in 60.15s, 921 kB read +30 errors (5 timeouts) + + +---------------- + + + +CLIENTS: *** 7 *** + +Running 60s test @ https://192.168.2.131/ +7 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬───────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼───────────┼──────────┤ +│ Latency │ 979 ms │ 1600 ms │ 37778 ms │ 37778 ms │ 2654.48 ms │ 5987.7 ms │ 37778 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴────────────┴───────────┴──────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.27 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +80 requests in 60.1s, 156 kB read +17 errors (7 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +7 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 928 ms │ 1591 ms │ 7128 ms │ 7128 ms │ 1648.53 ms │ 955.92 ms │ 7128 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +79 requests in 60.09s, 4.86 kB read +13 errors (8 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +7 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬───────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼───────────┼────────────┼──────────┤ +│ Latency │ 487 ms │ 1740 ms │ 24134 ms │ 24134 ms │ 3069.3 ms │ 4835.41 ms │ 24134 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴───────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.4 │ 0.53 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 11.5 kB │ 15 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 24 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +69 requests in 60.08s, 691 kB read +37 errors (21 timeouts) + + +---------------- + + + +CLIENTS: *** 8 *** + +Running 60s test @ https://192.168.2.131/ +8 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 892 ms │ 1585 ms │ 6973 ms │ 6973 ms │ 1613.52 ms │ 927.38 ms │ 6973 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.62 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.68 kB │ 2.11 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 37 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +89 requests in 60.09s, 161 kB read +25 errors (16 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +8 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 964 ms │ 1593 ms │ 7380 ms │ 7380 ms │ 1645.62 ms │ 998.66 ms │ 7380 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 70.6 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +86 requests in 60.1s, 4.86 kB read +14 errors (14 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +8 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 341 ms │ 4820 ms │ 26999 ms │ 26999 ms │ 7220.49 ms │ 6874.47 ms │ 26999 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.56 │ 0.67 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.4 kB │ 15.8 kB │ 19.3 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 33 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +84 requests in 60.08s, 950 kB read +41 errors (27 timeouts) + + +---------------- + + + +CLIENTS: *** 9 *** + +Running 60s test @ https://192.168.2.131/ +9 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ +│ Latency │ 960 ms │ 1598 ms │ 7751 ms │ 7751 ms │ 1665.89 ms │ 1056.66 ms │ 7751 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.13 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +93 requests in 60.1s, 156 kB read +25 errors (21 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +9 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ +│ Latency │ 933 ms │ 1589 ms │ 7703 ms │ 7703 ms │ 1649.14 ms │ 1051.77 ms │ 7703 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +93 requests in 60.09s, 4.86 kB read +21 errors (20 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +9 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼─────────┤ +│ Latency │ 1049 ms │ 1783 ms │ 7382 ms │ 7382 ms │ 1758 ms │ 1027.58 ms │ 7382 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.57 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 28.8 kB │ 28.8 kB │ 16.3 kB │ 14.3 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 34 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +91 requests in 60.1s, 979 kB read +48 errors (29 timeouts) + + +---------------- + + + +CLIENTS: *** 10 *** + +Running 60s test @ https://192.168.2.131/ +10 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ +│ Latency │ 979 ms │ 1597 ms │ 7548 ms │ 7548 ms │ 1660.23 ms │ 1024.01 ms │ 7548 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.13 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +100 requests in 60.09s, 156 kB read +29 errors (26 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +10 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 976 ms │ 1590 ms │ 7480 ms │ 7480 ms │ 1628.95 ms │ 1021.2 ms │ 7480 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +101 requests in 60.12s, 4.86 kB read +28 errors (28 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +10 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 585 ms │ 5882 ms │ 27503 ms │ 27503 ms │ 6338.82 ms │ 6773.89 ms │ 27503 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.54 │ 0.6 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 15.3 kB │ 17 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 32 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +97 requests in 60.14s, 921 kB read +54 errors (38 timeouts) + + +---------------- + + + +CLIENTS: *** 15 *** + +Running 60s test @ https://192.168.2.131/ +15 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ +│ Latency │ 957 ms │ 1601 ms │ 7609 ms │ 7609 ms │ 1654.73 ms │ 1034.04 ms │ 7609 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.13 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +135 requests in 60.14s, 156 kB read +64 errors (56 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +15 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 966 ms │ 1576 ms │ 7369 ms │ 7369 ms │ 1612.68 ms │ 991.59 ms │ 7369 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.62 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 83.3 B │ 65.6 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 37 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +136 requests in 60.14s, 5 kB read +61 errors (60 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +15 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬──────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼──────────┼────────────┼──────────┤ +│ Latency │ 300 ms │ 7281 ms │ 34953 ms │ 34953 ms │ 10153 ms │ 9647.72 ms │ 34953 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴──────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.52 │ 0.68 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.4 kB │ 14.9 kB │ 19.3 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 31 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +132 requests in 60.11s, 892 kB read +84 errors (70 timeouts) + + +---------------- + + + +CLIENTS: *** 20 *** + +Running 60s test @ https://192.168.2.131/ +20 connections +1 workers + + +┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ +│ Latency │ 944 ms │ 1600 ms │ 36783 ms │ 36783 ms │ 2881.21 ms │ 6487.79 ms │ 36783 ms │ +└─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ +┌───────────┬─────┬──────┬─────┬─────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────┼─────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.49 │ 0.54 │ 1 │ +├───────────┼─────┼──────┼─────┼─────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 4.34 kB │ 2.1 kB │ 2.31 kB │ 4.34 kB │ +└───────────┴─────┴──────┴─────┴─────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 29 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +165 requests in 60.1s, 126 kB read +96 errors (92 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/api?foo=bar +20 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 931 ms │ 1568 ms │ 7097 ms │ 7097 ms │ 1637.53 ms │ 950.49 ms │ 7097 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ +├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ +└───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 36 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +170 requests in 60.12s, 4.86 kB read +87 errors (85 timeouts) + + +---------------- + +Running 60s test @ https://192.168.2.131/alien.png +20 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ +│ Latency │ 1116 ms │ 1707 ms │ 7481 ms │ 7481 ms │ 1754.83 ms │ 1032.66 ms │ 7481 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ +┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.57 │ 0.5 │ 1 │ +├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 0 B │ 0 B │ 28.8 kB │ 28.8 kB │ 16.3 kB │ 14.3 kB │ 28.8 kB │ +└───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 34 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +168 requests in 60.09s, 979 kB read +114 errors (95 timeouts) + + +---------------- + diff --git a/lib/PsychicHttp/benchmark/results/psychic-ssl-websocket-loadtest.log b/lib/PsychicHttp/benchmark/results/psychic-ssl-websocket-loadtest.log new file mode 100644 index 0000000..4e9aa5c --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/psychic-ssl-websocket-loadtest.log @@ -0,0 +1,246 @@ + + +CLIENTS: *** 1 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 1 +Agent: none + +Completed requests: 2039 +Total errors: 0 +Total time: 60.002 s +Mean latency: 27.8 ms +Effective rps: 34 + +Percentage of requests served within a certain time + 50% 24 ms + 90% 34 ms + 95% 62 ms + 99% 97 ms + 100% 109 ms (longest request) + + +CLIENTS: *** 2 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 2 +Agent: none + +Completed requests: 2969 +Total errors: 0 +Total time: 60.003 s +Mean latency: 37.7 ms +Effective rps: 49 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 52 ms + 95% 92 ms + 99% 110 ms + 100% 126 ms (longest request) + + +CLIENTS: *** 3 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 3 +Agent: none + +Completed requests: 2883 +Total errors: 0 +Total time: 60.003 s +Mean latency: 38.2 ms +Effective rps: 48 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 52 ms + 95% 86 ms + 99% 109 ms + 100% 1711 ms (longest request) + + +CLIENTS: *** 4 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 4 +Agent: none + +Completed requests: 2858 +Total errors: 0 +Total time: 60.003 s +Mean latency: 37.9 ms +Effective rps: 48 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 49 ms + 95% 76 ms + 99% 104 ms + 100% 1740 ms (longest request) + + +CLIENTS: *** 5 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 5 +Agent: none + +Completed requests: 2772 +Total errors: 0 +Total time: 60.003 s +Mean latency: 38.6 ms +Effective rps: 46 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 49 ms + 95% 79 ms + 99% 106 ms + 100% 1634 ms (longest request) + + +CLIENTS: *** 6 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 6 +Agent: none + +Completed requests: 2722 +Total errors: 0 +Total time: 60.003 s +Mean latency: 38.7 ms +Effective rps: 45 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 49 ms + 95% 72 ms + 99% 102 ms + 100% 1694 ms (longest request) + + +CLIENTS: *** 7 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 7 +Agent: none + +Completed requests: 2552 +Total errors: 0 +Total time: 60.003 s +Mean latency: 40.7 ms +Effective rps: 43 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 52 ms + 95% 86 ms + 99% 112 ms + 100% 1816 ms (longest request) + + +CLIENTS: *** 8 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 8 +Running on cores: 2 +Agent: none + +Completed requests: 2507 +Total errors: 0 +Total time: 60.005 s +Mean latency: 40.8 ms +Effective rps: 42 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 50 ms + 95% 80 ms + 99% 112 ms + 100% 1646 ms (longest request) + + +CLIENTS: *** 10 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 10 +Running on cores: 2 +Agent: none + +Completed requests: 2265 +Total errors: 0 +Total time: 60.008 s +Mean latency: 43.7 ms +Effective rps: 38 + +Percentage of requests served within a certain time + 50% 33 ms + 90% 52 ms + 95% 79 ms + 99% 114 ms + 100% 1675 ms (longest request) + + +CLIENTS: *** 16 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 16 +Running on cores: 2 +Agent: none + +Completed requests: 1795 +Total errors: 0 +Total time: 60.003 s +Mean latency: 49.7 ms +Effective rps: 30 + +Percentage of requests served within a certain time + 50% 33 ms + 90% 51 ms + 95% 77 ms + 99% 112 ms + 100% 1741 ms (longest request) + + +CLIENTS: *** 20 *** + + +Target URL: wss://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 20 +Running on cores: 2 +Agent: none + +Completed requests: 1603 +Total errors: 0 +Total time: 60.004 s +Mean latency: 54.1 ms +Effective rps: 27 + +Percentage of requests served within a certain time + 50% 33 ms + 90% 60 ms + 95% 94 ms + 99% 133 ms + 100% 1729 ms (longest request) diff --git a/lib/PsychicHttp/benchmark/results/psychic-v1.1-http-loadtest.log b/lib/PsychicHttp/benchmark/results/psychic-v1.1-http-loadtest.log new file mode 100644 index 0000000..9fc5dae --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/psychic-v1.1-http-loadtest.log @@ -0,0 +1,1179 @@ + + +CLIENTS: *** 1 *** + +Running 60s test @ http://192.168.2.131/ +1 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 25 ms │ 34 ms │ 137 ms │ 175 ms │ 44.75 ms │ 33.04 ms │ 363 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 13 │ 14 │ 23 │ 27 │ 22.05 │ 3.62 │ 13 │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 56.4 kB │ 60.7 kB │ 99.8 kB │ 117 kB │ 95.6 kB │ 15.7 kB │ 56.4 kB │ +└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1323 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +1k requests in 60.06s, 5.74 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +1 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 21 ms │ 29 ms │ 122 ms │ 173 ms │ 40.61 ms │ 38.19 ms │ 646 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬───────┬───────┬─────────┬─────────┬─────────┬─────────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼───────┼───────┼─────────┼─────────┼─────────┼─────────┼───────┤ +│ Req/Sec │ 2 │ 6 │ 27 │ 34 │ 24.29 │ 7.62 │ 2 │ +├───────────┼───────┼───────┼─────────┼─────────┼─────────┼─────────┼───────┤ +│ Bytes/Sec │ 268 B │ 804 B │ 3.62 kB │ 4.56 kB │ 3.25 kB │ 1.02 kB │ 268 B │ +└───────────┴───────┴───────┴─────────┴─────────┴─────────┴─────────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1457 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +1k requests in 60.07s, 195 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +1 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 254 ms │ 291 ms │ 441 ms │ 487 ms │ 311.18 ms │ 52.89 ms │ 503 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 2 │ 2 │ 3 │ 4 │ 3.2 │ 0.58 │ 2 │ +├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 57.6 kB │ 57.6 kB │ 86.4 kB │ 115 kB │ 92.1 kB │ 16.4 kB │ 57.6 kB │ +└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 192 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +193 requests in 60.07s, 5.53 MB read + + +---------------- + + + +CLIENTS: *** 2 *** + +Running 60s test @ http://192.168.2.131/ +2 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 29 ms │ 46 ms │ 184 ms │ 213 ms │ 63.03 ms │ 43.98 ms │ 437 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 13 │ 13 │ 34 │ 43 │ 31.44 │ 7.44 │ 13 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 56.4 kB │ 56.4 kB │ 148 kB │ 187 kB │ 136 kB │ 32.2 kB │ 56.4 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 1886 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +2k requests in 60.07s, 8.18 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +2 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 27 ms │ 37 ms │ 107 ms │ 120 ms │ 44.09 ms │ 20.34 ms │ 188 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬───────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ +│ Req/Sec │ 24 │ 25 │ 46 │ 52 │ 44.79 │ 6.1 │ 24 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ +│ Bytes/Sec │ 3.22 kB │ 3.35 kB │ 6.17 kB │ 6.97 kB │ 6 kB │ 817 B │ 3.22 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴───────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2687 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.06s, 360 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +2 connections +1 workers + + +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 375 ms │ 552 ms │ 759 ms │ 848 ms │ 556.76 ms │ 95.75 ms │ 875 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 3 │ 3 │ 4 │ 4 │ 3.59 │ 0.53 │ 3 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 115 kB │ 103 kB │ 15.1 kB │ 86.3 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 215 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +217 requests in 60.09s, 6.19 MB read + + +---------------- + + + +CLIENTS: *** 3 *** + +Running 60s test @ http://192.168.2.131/ +3 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬─────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼─────────┼────────┤ +│ Latency │ 36 ms │ 56 ms │ 167 ms │ 193 ms │ 67.22 ms │ 33.6 ms │ 343 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴─────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 32 │ 34 │ 45 │ 52 │ 44.25 │ 4.31 │ 32 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 139 kB │ 148 kB │ 195 kB │ 226 kB │ 192 kB │ 18.7 kB │ 139 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 2655 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.08s, 11.5 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +3 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬─────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼─────────┼────────┤ +│ Latency │ 30 ms │ 45 ms │ 114 ms │ 128 ms │ 51.89 ms │ 22.2 ms │ 294 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴─────────┴────────┘ +┌───────────┬─────────┬────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 37 │ 44 │ 58 │ 67 │ 57.24 │ 6.16 │ 37 │ +├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 4.96 kB │ 5.9 kB │ 7.78 kB │ 8.98 kB │ 7.67 kB │ 825 B │ 4.96 kB │ +└───────────┴─────────┴────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3434 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.1s, 460 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +3 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ +│ Latency │ 412 ms │ 786 ms │ 1293 ms │ 1397 ms │ 829.09 ms │ 295.53 ms │ 1412 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 2 │ 3 │ 4 │ 4 │ 3.59 │ 0.53 │ 2 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 115 kB │ 103 kB │ 15.1 kB │ 57.6 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 215 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +218 requests in 60.11s, 6.19 MB read + + +---------------- + + + +CLIENTS: *** 4 *** + +Running 60s test @ http://192.168.2.131/ +4 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 41 ms │ 65 ms │ 189 ms │ 221 ms │ 79.49 ms │ 39.69 ms │ 385 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 41 │ 41 │ 51 │ 56 │ 50 │ 4.07 │ 41 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 178 kB │ 178 kB │ 221 kB │ 243 kB │ 217 kB │ 17.6 kB │ 178 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3000 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.1s, 13 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +4 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 35 ms │ 51 ms │ 128 ms │ 154 ms │ 58.79 ms │ 24.56 ms │ 347 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 38 │ 50 │ 69 │ 77 │ 67.49 │ 7.34 │ 38 │ +├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 5.09 kB │ 6.7 kB │ 9.25 kB │ 10.3 kB │ 9.04 kB │ 983 B │ 5.09 kB │ +└───────────┴─────────┴────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4049 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.11s, 543 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +4 connections +1 workers + + +┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 630 ms │ 885 ms │ 1840 ms │ 1877 ms │ 1082.82 ms │ 406.47 ms │ 2050 ms │ +└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 3 │ 3 │ 4 │ 5 │ 3.65 │ 0.55 │ 3 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 144 kB │ 105 kB │ 15.6 kB │ 86.3 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 219 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +223 requests in 60.08s, 6.3 MB read + + +---------------- + + + +CLIENTS: *** 5 *** + +Running 60s test @ http://192.168.2.131/ +5 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 47 ms │ 77 ms │ 213 ms │ 242 ms │ 91.79 ms │ 44.06 ms │ 398 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 41 │ 42 │ 55 │ 62 │ 54.12 │ 4.67 │ 41 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 178 kB │ 182 kB │ 239 kB │ 269 kB │ 235 kB │ 20.2 kB │ 178 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3247 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +3k requests in 60.08s, 14.1 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +5 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 39 ms │ 60 ms │ 126 ms │ 141 ms │ 65.76 ms │ 21.03 ms │ 172 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬─────────┬─────────┬─────────┬─────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼─────────┼─────────┼─────────┼─────────┼───────┼────────┤ +│ Req/Sec │ 53 │ 58 │ 76 │ 86 │ 75.34 │ 7.04 │ 53 │ +├───────────┼────────┼─────────┼─────────┼─────────┼─────────┼───────┼────────┤ +│ Bytes/Sec │ 7.1 kB │ 7.78 kB │ 10.2 kB │ 11.5 kB │ 10.1 kB │ 943 B │ 7.1 kB │ +└───────────┴────────┴─────────┴─────────┴─────────┴─────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4520 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.08s, 606 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +5 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 832 ms │ 1125 ms │ 2405 ms │ 2530 ms │ 1344.04 ms │ 504.13 ms │ 3485 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.69 │ 0.57 │ 2 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 106 kB │ 16.2 kB │ 57.6 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 221 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +226 requests in 60.08s, 6.36 MB read + + +---------------- + + + +CLIENTS: *** 6 *** + +Running 60s test @ http://192.168.2.131/ +6 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 51 ms │ 84 ms │ 214 ms │ 249 ms │ 98.05 ms │ 44.48 ms │ 553 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 40 │ 50 │ 62 │ 66 │ 60.87 │ 4.59 │ 40 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 174 kB │ 217 kB │ 269 kB │ 286 kB │ 264 kB │ 19.9 kB │ 174 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3652 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.09s, 15.8 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +6 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 45 ms │ 68 ms │ 144 ms │ 159 ms │ 75.43 ms │ 24.46 ms │ 223 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 53 │ 63 │ 80 │ 91 │ 78.95 │ 7.74 │ 53 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 7.16 kB │ 8.51 kB │ 10.8 kB │ 12.3 kB │ 10.7 kB │ 1.04 kB │ 7.16 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4737 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.08s, 639 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +6 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 893 ms │ 1385 ms │ 3045 ms │ 3336 ms │ 1630.97 ms │ 621.58 ms │ 4350 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.62 │ 0.58 │ 2 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 104 kB │ 16.7 kB │ 57.6 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 217 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +223 requests in 60.1s, 6.25 MB read + + +---------------- + + + +CLIENTS: *** 7 *** + +Running 60s test @ http://192.168.2.131/ +7 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 59 ms │ 96 ms │ 232 ms │ 260 ms │ 109.85 ms │ 47.04 ms │ 531 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Req/Sec │ 44 │ 53 │ 65 │ 72 │ 63.39 │ 5.76 │ 44 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ +│ Bytes/Sec │ 191 kB │ 230 kB │ 282 kB │ 313 kB │ 275 kB │ 25 kB │ 191 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3803 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.08s, 16.5 MB read + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +7 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 48 ms │ 75 ms │ 139 ms │ 152 ms │ 80.37 ms │ 22.69 ms │ 291 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬───────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 52 │ 67 │ 88 │ 96 │ 86.59 │ 7.62 │ 52 │ +├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 7.02 kB │ 9.05 kB │ 11.9 kB │ 13 kB │ 11.7 kB │ 1.03 kB │ 7.02 kB │ +└───────────┴─────────┴─────────┴─────────┴───────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5195 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.11s, 701 kB read + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +7 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 845 ms │ 1642 ms │ 3537 ms │ 3656 ms │ 1887.17 ms │ 728.73 ms │ 6130 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 3 │ 3 │ 4 │ 4 │ 3.64 │ 0.52 │ 3 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 115 kB │ 105 kB │ 14.8 kB │ 86.3 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 218 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +225 requests in 60.1s, 6.27 MB read + + +---------------- + + + +CLIENTS: *** 8 *** + +Running 60s test @ http://192.168.2.131/ +8 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 58 ms │ 96 ms │ 225 ms │ 256 ms │ 108.89 ms │ 45.19 ms │ 571 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Req/Sec │ 51 │ 54 │ 65 │ 70 │ 63.92 │ 4.63 │ 51 │ +├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ +│ Bytes/Sec │ 221 kB │ 234 kB │ 282 kB │ 304 kB │ 277 kB │ 20.1 kB │ 221 kB │ +└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3835 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.07s, 16.6 MB read +6 errors (6 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +8 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬─────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼─────────┼────────┤ +│ Latency │ 50 ms │ 76 ms │ 148 ms │ 166 ms │ 83.61 ms │ 26.5 ms │ 282 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴─────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 63 │ 66 │ 82 │ 98 │ 83.15 │ 8.78 │ 63 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 8.51 kB │ 8.91 kB │ 11.1 kB │ 13.2 kB │ 11.2 kB │ 1.18 kB │ 8.51 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4989 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.08s, 674 kB read +6 errors (6 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +8 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ +│ Latency │ 806 ms │ 1614 ms │ 3526 ms │ 3769 ms │ 1851.3 ms │ 712.64 ms │ 5877 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 3 │ 3 │ 4 │ 5 │ 3.72 │ 0.52 │ 3 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 144 kB │ 107 kB │ 14.9 kB │ 86.3 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 223 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +237 requests in 60.09s, 6.42 MB read +6 errors (6 timeouts) + + +---------------- + + + +CLIENTS: *** 9 *** + +Running 60s test @ http://192.168.2.131/ +9 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬─────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼─────────┼─────────┤ +│ Latency │ 60 ms │ 98 ms │ 232 ms │ 268 ms │ 111.69 ms │ 57.8 ms │ 1065 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴─────────┴─────────┘ +┌───────────┬───────┬────────┬────────┬────────┬────────┬───────┬───────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼───────┼────────┼────────┼────────┼────────┼───────┼───────┤ +│ Req/Sec │ 3 │ 54 │ 63 │ 72 │ 62.37 │ 8.98 │ 3 │ +├───────────┼───────┼────────┼────────┼────────┼────────┼───────┼───────┤ +│ Bytes/Sec │ 13 kB │ 234 kB │ 273 kB │ 313 kB │ 271 kB │ 39 kB │ 13 kB │ +└───────────┴───────┴────────┴────────┴────────┴────────┴───────┴───────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3742 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.07s, 16.2 MB read +12 errors (12 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +9 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 50 ms │ 77 ms │ 148 ms │ 160 ms │ 83.55 ms │ 24.72 ms │ 275 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Req/Sec │ 63 │ 67 │ 85 │ 95 │ 83.27 │ 7.2 │ 63 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ +│ Bytes/Sec │ 8.51 kB │ 9.05 kB │ 11.5 kB │ 12.8 kB │ 11.2 kB │ 972 B │ 8.51 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4996 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.08s, 674 kB read +12 errors (12 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +9 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 857 ms │ 1631 ms │ 3541 ms │ 3814 ms │ 1883.14 ms │ 730.83 ms │ 6021 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 3 │ 3 │ 4 │ 4 │ 3.67 │ 0.48 │ 3 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 115 kB │ 106 kB │ 13.5 kB │ 86.3 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 220 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +241 requests in 60.08s, 6.33 MB read +12 errors (12 timeouts) + + +---------------- + + + +CLIENTS: *** 10 *** + +Running 60s test @ http://192.168.2.131/ +10 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼─────────┤ +│ Latency │ 59 ms │ 95 ms │ 235 ms │ 276 ms │ 111.77 ms │ 57.35 ms │ 1010 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴─────────┘ +┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 17 │ 47 │ 64 │ 70 │ 62.32 │ 8.14 │ 17 │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 73.8 kB │ 204 kB │ 278 kB │ 304 kB │ 270 kB │ 35.3 kB │ 73.7 kB │ +└───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3739 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.08s, 16.2 MB read +18 errors (18 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +10 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 49 ms │ 82 ms │ 179 ms │ 209 ms │ 91.77 ms │ 33.92 ms │ 312 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 46 │ 48 │ 76 │ 92 │ 75.85 │ 11.6 │ 46 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 6.21 kB │ 6.48 kB │ 10.3 kB │ 12.4 kB │ 10.2 kB │ 1.57 kB │ 6.21 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4551 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.07s, 614 kB read +18 errors (18 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +10 connections +1 workers + + +┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 1062 ms │ 1673 ms │ 3588 ms │ 3643 ms │ 1903.02 ms │ 693.11 ms │ 5495 ms │ +└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 2 │ 2 │ 4 │ 5 │ 3.6 │ 0.64 │ 2 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 57.6 kB │ 57.6 kB │ 115 kB │ 144 kB │ 104 kB │ 18.3 kB │ 57.6 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 216 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +244 requests in 60.08s, 6.22 MB read +18 errors (18 timeouts) + + +---------------- + + + +CLIENTS: *** 15 *** + +Running 60s test @ http://192.168.2.131/ +15 connections +1 workers + + +┌─────────┬───────┬────────┬────────┬────────┬───────────┬─────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼────────┼────────┼────────┼───────────┼─────────┼─────────┤ +│ Latency │ 62 ms │ 103 ms │ 252 ms │ 297 ms │ 119.53 ms │ 58.1 ms │ 1050 ms │ +└─────────┴───────┴────────┴────────┴────────┴───────────┴─────────┴─────────┘ +┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 22 │ 41 │ 60 │ 70 │ 58.27 │ 8.6 │ 22 │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 95.5 kB │ 178 kB │ 260 kB │ 304 kB │ 253 kB │ 37.3 kB │ 95.4 kB │ +└───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3496 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.06s, 15.2 MB read +48 errors (48 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +15 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ +│ Latency │ 49 ms │ 75 ms │ 151 ms │ 164 ms │ 82.32 ms │ 25.95 ms │ 313 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 61 │ 66 │ 85 │ 99 │ 84.52 │ 8.42 │ 61 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 8.24 kB │ 8.91 kB │ 11.5 kB │ 13.4 kB │ 11.4 kB │ 1.14 kB │ 8.23 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 5071 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.07s, 685 kB read +48 errors (48 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +15 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 801 ms │ 1709 ms │ 3676 ms │ 3762 ms │ 1929.08 ms │ 744.64 ms │ 5846 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.57 │ 0.59 │ 2 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 103 kB │ 16.9 kB │ 57.6 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 214 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +277 requests in 60.09s, 6.16 MB read +48 errors (48 timeouts) + + +---------------- + + + +CLIENTS: *** 20 *** + +Running 60s test @ http://192.168.2.131/ +20 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼─────────┤ +│ Latency │ 60 ms │ 97 ms │ 232 ms │ 267 ms │ 112.02 ms │ 53.32 ms │ 1054 ms │ +└─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴─────────┘ +┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 21 │ 52 │ 63 │ 69 │ 62.12 │ 6.59 │ 21 │ +├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 91.1 kB │ 226 kB │ 273 kB │ 300 kB │ 269 kB │ 28.6 kB │ 91.1 kB │ +└───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 3727 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +4k requests in 60.1s, 16.2 MB read +78 errors (78 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/api?foo=bar +20 connections +1 workers + + +┌─────────┬───────┬───────┬────────┬────────┬─────────┬──────────┬────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼───────┼───────┼────────┼────────┼─────────┼──────────┼────────┤ +│ Latency │ 49 ms │ 78 ms │ 164 ms │ 178 ms │ 87.8 ms │ 35.83 ms │ 658 ms │ +└─────────┴───────┴───────┴────────┴────────┴─────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Req/Sec │ 35 │ 55 │ 81 │ 95 │ 79.27 │ 10.5 │ 35 │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ +│ Bytes/Sec │ 4.73 kB │ 7.43 kB │ 10.9 kB │ 12.8 kB │ 10.7 kB │ 1.42 kB │ 4.72 kB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 4756 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +5k requests in 60.07s, 642 kB read +78 errors (78 timeouts) + + +---------------- + +Running 60s test @ http://192.168.2.131/alien.png +20 connections +1 workers + + +┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ +│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ +├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ +│ Latency │ 806 ms │ 1613 ms │ 3348 ms │ 4009 ms │ 1854.07 ms │ 733.32 ms │ 6606 ms │ +└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ +┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ +│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.7 │ 0.62 │ 2 │ +├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ +│ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 106 kB │ 17.7 kB │ 57.6 kB │ +└───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ +┌──────┬───────┐ +│ Code │ Count │ +├──────┼───────┤ +│ 200 │ 222 │ +└──────┴───────┘ + +Req/Bytes counts sampled once per second. +# of samples: 60 + +320 requests in 60.05s, 6.39 MB read +78 errors (78 timeouts) + + +---------------- + diff --git a/lib/PsychicHttp/benchmark/results/psychic-v1.1-websocket-loadtest.log b/lib/PsychicHttp/benchmark/results/psychic-v1.1-websocket-loadtest.log new file mode 100644 index 0000000..8c507a9 --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/psychic-v1.1-websocket-loadtest.log @@ -0,0 +1,246 @@ + + +CLIENTS: *** 1 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 1 +Agent: none + +Completed requests: 1972 +Total errors: 0 +Total time: 60.003 s +Mean latency: 29.8 ms +Effective rps: 33 + +Percentage of requests served within a certain time + 50% 25 ms + 90% 40 ms + 95% 66 ms + 99% 96 ms + 100% 147 ms (longest request) + + +CLIENTS: *** 2 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 2 +Agent: none + +Completed requests: 3144 +Total errors: 0 +Total time: 60.003 s +Mean latency: 37.6 ms +Effective rps: 52 + +Percentage of requests served within a certain time + 50% 32 ms + 90% 58 ms + 95% 82 ms + 99% 114 ms + 100% 160 ms (longest request) + + +CLIENTS: *** 3 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 3 +Agent: none + +Completed requests: 4113 +Total errors: 0 +Total time: 60.005 s +Mean latency: 43.2 ms +Effective rps: 69 + +Percentage of requests served within a certain time + 50% 38 ms + 90% 63 ms + 95% 88 ms + 99% 119 ms + 100% 339 ms (longest request) + + +CLIENTS: *** 4 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 4 +Agent: none + +Completed requests: 4902 +Total errors: 0 +Total time: 60.004 s +Mean latency: 48.3 ms +Effective rps: 82 + +Percentage of requests served within a certain time + 50% 42 ms + 90% 74 ms + 95% 97 ms + 99% 125 ms + 100% 217 ms (longest request) + + +CLIENTS: *** 5 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 5 +Agent: none + +Completed requests: 5522 +Total errors: 0 +Total time: 60.003 s +Mean latency: 53.7 ms +Effective rps: 92 + +Percentage of requests served within a certain time + 50% 48 ms + 90% 81 ms + 95% 102 ms + 99% 122 ms + 100% 324 ms (longest request) + + +CLIENTS: *** 6 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 6 +Agent: none + +Completed requests: 5808 +Total errors: 0 +Total time: 60.004 s +Mean latency: 61.4 ms +Effective rps: 97 + +Percentage of requests served within a certain time + 50% 54 ms + 90% 94 ms + 95% 117 ms + 99% 142 ms + 100% 348 ms (longest request) + + +CLIENTS: *** 7 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 7 +Agent: none + +Completed requests: 6478 +Total errors: 0 +Total time: 60.006 s +Mean latency: 64.1 ms +Effective rps: 108 + +Percentage of requests served within a certain time + 50% 59 ms + 90% 94 ms + 95% 110 ms + 99% 137 ms + 100% 195 ms (longest request) + + +CLIENTS: *** 8 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 8 +Running on cores: 2 +Agent: none + +Completed requests: 6124 +Total errors: 0 +Total time: 60.004 s +Mean latency: 67.8 ms +Effective rps: 102 + +Percentage of requests served within a certain time + 50% 59 ms + 90% 107 ms + 95% 131 ms + 99% 173 ms + 100% 260 ms (longest request) + + +CLIENTS: *** 10 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 10 +Running on cores: 2 +Agent: none + +Completed requests: 5640 +Total errors: 0 +Total time: 60.004 s +Mean latency: 73.7 ms +Effective rps: 94 + +Percentage of requests served within a certain time + 50% 61 ms + 90% 120 ms + 95% 140 ms + 99% 240 ms + 100% 780 ms (longest request) + + +CLIENTS: *** 16 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 16 +Running on cores: 2 +Agent: none + +Completed requests: 5809 +Total errors: 0 +Total time: 60.006 s +Mean latency: 71.6 ms +Effective rps: 97 + +Percentage of requests served within a certain time + 50% 64 ms + 90% 111 ms + 95% 130 ms + 99% 162 ms + 100% 226 ms (longest request) + + +CLIENTS: *** 20 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 20 +Running on cores: 2 +Agent: none + +Completed requests: 5590 +Total errors: 0 +Total time: 60.003 s +Mean latency: 74.4 ms +Effective rps: 93 + +Percentage of requests served within a certain time + 50% 61 ms + 90% 122 ms + 95% 151 ms + 99% 247 ms + 100% 513 ms (longest request) diff --git a/lib/PsychicHttp/benchmark/results/psychic-websocket-loadtest.log b/lib/PsychicHttp/benchmark/results/psychic-websocket-loadtest.log new file mode 100644 index 0000000..812026c --- /dev/null +++ b/lib/PsychicHttp/benchmark/results/psychic-websocket-loadtest.log @@ -0,0 +1,246 @@ + + +CLIENTS: *** 1 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 1 +Agent: none + +Completed requests: 2304 +Total errors: 0 +Total time: 60.002 s +Mean latency: 25.5 ms +Effective rps: 38 + +Percentage of requests served within a certain time + 50% 22 ms + 90% 32 ms + 95% 58 ms + 99% 92 ms + 100% 105 ms (longest request) + + +CLIENTS: *** 2 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 2 +Agent: none + +Completed requests: 3647 +Total errors: 0 +Total time: 60.002 s +Mean latency: 32.3 ms +Effective rps: 61 + +Percentage of requests served within a certain time + 50% 28 ms + 90% 43 ms + 95% 67 ms + 99% 93 ms + 100% 135 ms (longest request) + + +CLIENTS: *** 3 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 3 +Agent: none + +Completed requests: 4629 +Total errors: 0 +Total time: 60.004 s +Mean latency: 38.3 ms +Effective rps: 77 + +Percentage of requests served within a certain time + 50% 34 ms + 90% 51 ms + 95% 79 ms + 99% 110 ms + 100% 152 ms (longest request) + + +CLIENTS: *** 4 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 4 +Agent: none + +Completed requests: 5290 +Total errors: 0 +Total time: 60.003 s +Mean latency: 44.7 ms +Effective rps: 88 + +Percentage of requests served within a certain time + 50% 40 ms + 90% 67 ms + 95% 92 ms + 99% 115 ms + 100% 159 ms (longest request) + + +CLIENTS: *** 5 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 5 +Agent: none + +Completed requests: 5935 +Total errors: 0 +Total time: 60.002 s +Mean latency: 50 ms +Effective rps: 99 + +Percentage of requests served within a certain time + 50% 45 ms + 90% 74 ms + 95% 97 ms + 99% 123 ms + 100% 172 ms (longest request) + + +CLIENTS: *** 6 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 6 +Agent: none + +Completed requests: 6533 +Total errors: 0 +Total time: 60.003 s +Mean latency: 54.5 ms +Effective rps: 109 + +Percentage of requests served within a certain time + 50% 49 ms + 90% 78 ms + 95% 101 ms + 99% 129 ms + 100% 170 ms (longest request) + + +CLIENTS: *** 7 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 7 +Agent: none + +Completed requests: 7086 +Total errors: 0 +Total time: 60.004 s +Mean latency: 58.6 ms +Effective rps: 118 + +Percentage of requests served within a certain time + 50% 54 ms + 90% 85 ms + 95% 107 ms + 99% 130 ms + 100% 184 ms (longest request) + + +CLIENTS: *** 8 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 8 +Running on cores: 2 +Agent: none + +Completed requests: 6994 +Total errors: 0 +Total time: 60.004 s +Mean latency: 59.3 ms +Effective rps: 117 + +Percentage of requests served within a certain time + 50% 54 ms + 90% 88 ms + 95% 109 ms + 99% 134 ms + 100% 176 ms (longest request) + + +CLIENTS: *** 10 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 10 +Running on cores: 2 +Agent: none + +Completed requests: 7197 +Total errors: 0 +Total time: 60.004 s +Mean latency: 57.7 ms +Effective rps: 120 + +Percentage of requests served within a certain time + 50% 53 ms + 90% 83 ms + 95% 98 ms + 99% 123 ms + 100% 176 ms (longest request) + + +CLIENTS: *** 16 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 16 +Running on cores: 2 +Agent: none + +Completed requests: 7173 +Total errors: 0 +Total time: 60.002 s +Mean latency: 57.9 ms +Effective rps: 120 + +Percentage of requests served within a certain time + 50% 53 ms + 90% 83 ms + 95% 100 ms + 99% 123 ms + 100% 156 ms (longest request) + + +CLIENTS: *** 20 *** + + +Target URL: ws://192.168.2.131/ws +Max time (s): 60 +Concurrent clients: 20 +Running on cores: 2 +Agent: none + +Completed requests: 6883 +Total errors: 0 +Total time: 60.002 s +Mean latency: 60.4 ms +Effective rps: 115 + +Percentage of requests served within a certain time + 50% 55 ms + 90% 92 ms + 95% 111 ms + 99% 138 ms + 100% 175 ms (longest request) diff --git a/lib/PsychicHttp/benchmark/websocket-client-test.js b/lib/PsychicHttp/benchmark/websocket-client-test.js new file mode 100644 index 0000000..3021bb0 --- /dev/null +++ b/lib/PsychicHttp/benchmark/websocket-client-test.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node + +const WebSocket = require('ws'); + +const uri = 'ws://192.168.2.131/ws'; + +async function websocketClient() { + console.log(`Starting test`); + for (let i = 0; i < 1000000; i++) { + const ws = new WebSocket(uri); + + if (i % 100 == 0) + console.log(`Count: ${i}`); + + ws.on('open', () => { + //console.log(`Connected`); + }); + + ws.on('message', (message) => { + //console.log(`Message: ${message}`); + ws.close(); + }); + + ws.on('error', (error) => { + console.error(`Error: ${error.message}`); + }); + + await new Promise((resolve) => { + ws.on('close', () => { + resolve(); + }); + }); + } +} + +websocketClient(); \ No newline at end of file diff --git a/lib/PsychicHttp/component.mk b/lib/PsychicHttp/component.mk new file mode 100644 index 0000000..bb5bb16 --- /dev/null +++ b/lib/PsychicHttp/component.mk @@ -0,0 +1,3 @@ +COMPONENT_ADD_INCLUDEDIRS := src +COMPONENT_SRCDIRS := src +CXXFLAGS += -fno-rtti diff --git a/lib/PsychicHttp/examples/arduino/.gitignore b/lib/PsychicHttp/examples/arduino/.gitignore new file mode 100644 index 0000000..9e5f911 --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/lib/PsychicHttp/examples/arduino/arduino.ino b/lib/PsychicHttp/examples/arduino/arduino.ino new file mode 100644 index 0000000..c0fe060 --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/arduino.ino @@ -0,0 +1,497 @@ +/* + PsychicHTTP Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +/********************************************************************************************** +* Note: this demo relies on the following libraries (Install via Library Manager) +* ArduinoJson UrlEncode +**********************************************************************************************/ + +/********************************************************************************************** +* Note: this demo relies on various files to be uploaded on the LittleFS partition +* Follow instructions here: https://randomnerdtutorials.com/esp32-littlefs-arduino-ide/ +**********************************************************************************************/ + +#include +#include +#include +#include +#include +#include "_secret.h" +#include +#include //uncomment this to enable HTTPS / SSL + +#ifndef WIFI_SSID + #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." +#endif + +//Enter your WIFI credentials in secret.h +const char *ssid = WIFI_SSID; +const char *password = WIFI_PASS; + +// Set your SoftAP credentials +const char *softap_ssid = "PsychicHttp"; +const char *softap_password = ""; +IPAddress softap_ip(10, 0, 0, 1); + +//credentials for the /auth-basic and /auth-digest examples +const char *app_user = "admin"; +const char *app_pass = "admin"; +const char *app_name = "Your App"; + +//hostname for mdns (psychic.local) +const char *local_hostname = "psychic"; + +//#define PSY_ENABLE_SSL to enable ssl +#ifdef PSY_ENABLE_SSL + bool app_enable_ssl = true; + String server_cert; + String server_key; +#endif + +//our main server object +#ifdef PSY_ENABLE_SSL + PsychicHttpsServer server; +#else + PsychicHttpServer server; +#endif +PsychicWebSocketHandler websocketHandler; +PsychicEventSource eventSource; + +bool connectToWifi() +{ + //dual client and AP mode + WiFi.mode(WIFI_AP_STA); + + // Configure SoftAP + WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00 + WiFi.softAP(softap_ssid, softap_password); + IPAddress myIP = WiFi.softAPIP(); + Serial.print("SoftAP IP Address: "); + Serial.println(myIP); + + Serial.println(); + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + WiFi.begin(ssid, password); + // Auto reconnect is set true as default + // To set auto connect off, use the following function + // WiFi.setAutoReconnect(false); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void setup() +{ + Serial.begin(115200); + delay(10); + + // We start by connecting to a WiFi network + // To debug, please enable Core Debug Level to Verbose + if (connectToWifi()) + { + //set up our esp32 to listen on the local_hostname.local domain + if (!MDNS.begin(local_hostname)) { + Serial.println("Error starting mDNS"); + return; + } + MDNS.addService("http", "tcp", 80); + + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + //look up our keys? + #ifdef PSY_ENABLE_SSL + if (app_enable_ssl) + { + File fp = LittleFS.open("/server.crt"); + if (fp) + { + server_cert = fp.readString(); + + // Serial.println("Server Cert:"); + // Serial.println(server_cert); + } + else + { + Serial.println("server.pem not found, SSL not available"); + app_enable_ssl = false; + } + fp.close(); + + File fp2 = LittleFS.open("/server.key"); + if (fp2) + { + server_key = fp2.readString(); + + // Serial.println("Server Key:"); + // Serial.println(server_key); + } + else + { + Serial.println("server.key not found, SSL not available"); + app_enable_ssl = false; + } + fp2.close(); + } + #endif + + //setup server config stuff here + server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + + #ifdef PSY_ENABLE_SSL + server.ssl_config.httpd.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + + //do we want secure or not? + if (app_enable_ssl) + { + server.listen(443, server_cert.c_str(), server_key.c_str()); + + //this creates a 2nd server listening on port 80 and redirects all requests HTTPS + PsychicHttpServer *redirectServer = new PsychicHttpServer(); + redirectServer->config.ctrl_port = 20424; // just a random port different from the default one + redirectServer->listen(80); + redirectServer->onNotFound([](PsychicRequest *request) { + String url = "https://" + request->host() + request->url(); + return request->redirect(url.c_str()); + }); + } + else + server.listen(80); + #else + server.listen(80); + #endif + + //serve static files from LittleFS/www on / only to clients on same wifi network + //this is where our /index.html file lives + server.serveStatic("/", LittleFS, "/www/")->setFilter(ON_STA_FILTER); + + //serve static files from LittleFS/www-ap on / only to clients on SoftAP + //this is where our /index.html file lives + server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); + + //serve static files from LittleFS/img on /img + //it's more efficient to serve everything from a single www directory, but this is also possible. + server.serveStatic("/img", LittleFS, "/img/"); + + //you can also serve single files + server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); + + //example callback everytime a connection is opened + server.onOpen([](PsychicClient *client) { + Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString()); + }); + + //example callback everytime a connection is closed + server.onClose([](PsychicClient *client) { + Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString()); + }); + + //api - json message passed in as post body + server.on("/api", HTTP_POST, [](PsychicRequest *request) + { + //load our JSON request + StaticJsonDocument<1024> json; + String body = request->body(); + DeserializationError err = deserializeJson(json, body); + + //create our response json + StaticJsonDocument<128> output; + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (json.containsKey("foo")) + { + String foo = json["foo"]; + output["foo"] = foo; + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + return request->reply(200, "application/json", jsonBuffer.c_str()); + }); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/ip", HTTP_GET, [](PsychicRequest *request) + { + String output = "Your IP is: " + request->client()->remoteIP().toString(); + return request->reply(output.c_str()); + }); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/api", HTTP_GET, [](PsychicRequest *request) + { + //create a response object + StaticJsonDocument<128> output; + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (request->hasParam("foo")) + { + String foo = request->getParam("foo")->name(); + output["foo"] = foo; + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + return request->reply(200, "application/json", jsonBuffer.c_str()); + }); + + //how to redirect a request + server.on("/redirect", HTTP_GET, [](PsychicRequest *request) + { + return request->redirect("/alien.png"); + }); + + //how to do basic auth + server.on("/auth-basic", HTTP_GET, [](PsychicRequest *request) + { + if (!request->authenticate(app_user, app_pass)) + return request->requestAuthentication(BASIC_AUTH, app_name, "You must log in."); + return request->reply("Auth Basic Success!"); + }); + + //how to do digest auth + server.on("/auth-digest", HTTP_GET, [](PsychicRequest *request) + { + if (!request->authenticate(app_user, app_pass)) + return request->requestAuthentication(DIGEST_AUTH, app_name, "You must log in."); + return request->reply("Auth Digest Success!"); + }); + + //example of getting / setting cookies + server.on("/cookies", HTTP_GET, [](PsychicRequest *request) + { + PsychicResponse response(request); + + int counter = 0; + if (request->hasCookie("counter")) + { + counter = std::stoi(request->getCookie("counter").c_str()); + counter++; + } + + char cookie[10]; + sprintf(cookie, "%i", counter); + + response.setCookie("counter", cookie); + response.setContent(cookie); + return response.send(); + }); + + //example of getting POST variables + server.on("/post", HTTP_POST, [](PsychicRequest *request) + { + String output; + output += "Param 1: " + request->getParam("param1")->value() + "
\n"; + output += "Param 2: " + request->getParam("param2")->value() + "
\n"; + + return request->reply(output.c_str()); + }); + + //you can set up a custom 404 handler. + server.onNotFound([](PsychicRequest *request) + { + return request->reply(404, "text/html", "Custom 404 Handler"); + }); + + //handle a very basic upload as post body + PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); + uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + Serial.printf("Writing %d/%d bytes to: %s\n", (int)index+(int)len, request->contentLength(), path.c_str()); + + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + uploadHandler->onRequest([](PsychicRequest *request) + { + String url = "/" + request->getFilename(); + String output = "
" + url + ""; + + return request->reply(output.c_str()); + }); + + //wildcard basic file upload - POST to /upload/filename.ext + server.on("/upload/*", HTTP_POST, uploadHandler); + + //a little bit more complicated multipart form + PsychicUploadHandler *multipartHandler = new PsychicUploadHandler(); + multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + //some progress over serial. + Serial.printf("Writing %d bytes to: %s\n", (int)len, path.c_str()); + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + multipartHandler->onRequest([](PsychicRequest *request) + { + PsychicWebParameter *file = request->getParam("file_upload"); + + String url = "/" + file->value(); + String output; + + output += "" + url + "
\n"; + output += "Bytes: " + String(file->size()) + "
\n"; + output += "Param 1: " + request->getParam("param1")->value() + "
\n"; + output += "Param 2: " + request->getParam("param2")->value() + "
\n"; + + return request->reply(output.c_str()); + }); + + //wildcard basic file upload - POST to /upload/filename.ext + server.on("/multipart", HTTP_POST, multipartHandler); + + //a websocket echo server + websocketHandler.onOpen([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->localIP().toString()); + client->sendMessage("Hello!"); + }); + websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { + Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); + return request->reply(frame); + }); + websocketHandler.onClose([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->localIP().toString()); + }); + server.on("/ws", &websocketHandler); + + //EventSource server + eventSource.onOpen([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->localIP().toString()); + client->send("Hello user!", NULL, millis(), 1000); + }); + eventSource.onClose([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->localIP().toString()); + }); + server.on("/events", &eventSource); + } +} + +unsigned long lastUpdate = 0; +char output[60]; + +void loop() +{ + if (millis() - lastUpdate > 2000) + { + sprintf(output, "Millis: %d\n", millis()); + websocketHandler.sendAll(output); + + sprintf(output, "%d", millis()); + eventSource.send(output, "millis", millis(), 0); + + lastUpdate = millis(); + } +} \ No newline at end of file diff --git a/lib/PsychicHttp/examples/arduino/arduino_captive_portal/README.md b/lib/PsychicHttp/examples/arduino/arduino_captive_portal/README.md new file mode 100644 index 0000000..f965eda --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/arduino_captive_portal/README.md @@ -0,0 +1,60 @@ +**1) SUMMARY** + +This example implements a **captive portal** with library DNSServer, ie web page which opens automatically when user connects Wifi network (eg "PsychitHttp"). + +Captiveportal is implemented in **ESPAsyncWebServer** [https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/examples/CaptivePortal/CaptivePortal.ino](url) and in **arduino-esp32 examples** [https://github.com/espressif/arduino-esp32/blob/master/libraries/DNSServer/examples/CaptivePortal/CaptivePortal.ino](url) + +This feature can be implemented with Psychichttp with a **dedicated handler**, as shown in code below. + +Code highlights are added below for reference. + +**2) CODE** + +**Definitions** +``` +// captiveportal +// credits https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/examples/CaptivePortal/CaptivePortal.ino +//https://github.com/espressif/arduino-esp32/blob/master/libraries/DNSServer/examples/CaptivePortal/CaptivePortal.ino +#include +DNSServer dnsServer; +class CaptiveRequestHandler : public PsychicWebHandler { // handler +public: + CaptiveRequestHandler() {}; + virtual ~CaptiveRequestHandler() {}; + bool canHandle(PsychicRequest*request){ + // ... if needed some tests ... return(false); + return true; // activate captive portal + } + esp_err_t handleRequest(PsychicRequest *request) { + //PsychicFileResponse response(request, LittleFS, "/captiveportal.html"); // uncomment : for captive portal page, if any, eg "captiveportal.html" + //return response.send(); // uncomment : return captive portal page + return request->reply(200,"text/html","Welcome to captive portal !"); // simple text, comment if captive portal page + } +}; +CaptiveRequestHandler *captivehandler=NULL; // handler for captive portal +``` + +**setup()** +``` + // captive portal + dnsServer.start(53, "*", WiFi.softAPIP()); // DNS requests are executed over port 53 (standard) + captivehandler= new CaptiveRequestHandler(); // create captive portal handler, important : after server.on since handlers are triggered on a first created/first trigerred basis + server.addHandler(captivehandler); // captive portal handler (last handler) +``` + +**loop()** +``` + dnsServer.processNextRequest(); // captive portal +``` + +**3) RESULT** + +**Access Point (web page is opened automatically when connecting to PsychicHttp AP)** +![captive portal access point](images/accesspoint.png) + +**Station (web page is shown whatever url for Station IP, eg 192.168.1.50/abcdefg** +![captive portal station point](images/station.png) + + + + diff --git a/lib/PsychicHttp/examples/arduino/arduino_captive_portal/arduino_captive_portal.ino b/lib/PsychicHttp/examples/arduino/arduino_captive_portal/arduino_captive_portal.ino new file mode 100644 index 0000000..fbe83a3 --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/arduino_captive_portal/arduino_captive_portal.ino @@ -0,0 +1,144 @@ +/* + PsychicHTTP Server Captive Portal Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include +#include +#include + +char* TAG = "CAPTPORT"; + +// captiveportal +// credits https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/examples/CaptivePortal/CaptivePortal.ino +//https://github.com/espressif/arduino-esp32/blob/master/libraries/DNSServer/examples/CaptivePortal/CaptivePortal.ino +#include +DNSServer dnsServer; +class CaptiveRequestHandler : public PsychicWebHandler { // handler +public: + CaptiveRequestHandler() {}; + virtual ~CaptiveRequestHandler() {}; + bool canHandle(PsychicRequest*request){ + // ... if needed some tests ... return(false); + return true; // activate captive portal + } + esp_err_t handleRequest(PsychicRequest *request) { + //PsychicFileResponse response(request, LittleFS, "/captiveportal.html"); // uncomment : for captive portal page, if any, eg "captiveportal.html" + //return response.send(); // uncomment : return captive portal page + return request->reply(200,"text/html","Welcome to captive portal !"); // simple text, comment if captive portal page + } +}; +CaptiveRequestHandler *captivehandler=NULL; // handler for captive portal + +const char* ssid = "mySSID"; // replace with your SSID (mode STATION) +const char* password = "myPassword"; // replace with you password (mode STATION) + +// Set your SoftAP credentials +const char *softap_ssid = "PsychicHttp"; +const char *softap_password = ""; +IPAddress softap_ip(10, 0, 0, 1); + +//hostname for mdns (psychic.local) +const char *local_hostname = "psychic"; + +//our main server object +PsychicHttpServer server; + +bool connectToWifi() { + //dual client and AP mode + WiFi.mode(WIFI_AP_STA); + + // Configure SoftAP + WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00 + WiFi.softAP(softap_ssid, softap_password); + IPAddress myIP = WiFi.softAPIP(); + ESP_LOGI(TAG,"SoftAP IP Address: %s", myIP.toString().c_str()); + ESP_LOGI(TAG,"[WiFi] Connecting to %s", ssid); + + WiFi.begin(ssid, password); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) { + switch (WiFi.status()) { + case WL_NO_SSID_AVAIL: + ESP_LOGE(TAG,"[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + ESP_LOGI(TAG,"[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + ESP_LOGI(TAG,"[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + ESP_LOGI(TAG,"[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + ESP_LOGI(TAG,"[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + ESP_LOGI(TAG,"[WiFi] WiFi is connected, IP address %s",WiFi.localIP().toString().c_str()); + return true; + break; + default: + ESP_LOGI(TAG,"[WiFi] WiFi Status: %d",WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) { + ESP_LOGI(TAG,"[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else numberOfTries--; + } + + return false; +} // end connectToWifi + +void setup() { + Serial.begin(115200); + delay(10); + + // Wifi + if (connectToWifi()) { // set up our esp32 to listen on the local_hostname.local domain + if (!MDNS.begin(local_hostname)) { + ESP_LOGE(TAG,"Error starting mDNS"); + return; + } + MDNS.addService("http", "tcp", 80); + + if(!LittleFS.begin()) { + ESP_LOGI(TAG,"ERROR : LittleFS Mount Failed."); + return; + } + + //setup server config stuff here + server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + server.listen(80); + + DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); + + // captive portal + dnsServer.start(53, "*", WiFi.softAPIP()); // DNS requests are executed over port 53 (standard) + captivehandler= new CaptiveRequestHandler(); // create captive portal handler, important : after server.on since handlers are triggered on a first created/first trigerred basis + server.addHandler(captivehandler); // captive portal handler (last handler) + } // end set up our esp32 to listen on the local_hostname.local domain +} // end setup + +void loop() { + dnsServer.processNextRequest(); // captive portal +} diff --git a/lib/PsychicHttp/examples/arduino/arduino_captive_portal/images/accesspoint.png b/lib/PsychicHttp/examples/arduino/arduino_captive_portal/images/accesspoint.png new file mode 100644 index 0000000000000000000000000000000000000000..76c35902b11e00dd438664abe730418f15c77a14 GIT binary patch literal 15389 zcmdVBbx>SE7vKxQ5+no*lHfsu4(<@#ZE%+f?(XjH!QBQYxD$fI;1Jy1b#NZZ_wCmE z<5lhZYqz$mW~#gA-qU?gpWEH%cY4AU#Y&`AaAc1V}0rs_P6N%lA8eaP5)r23Zyvm@e({7u*d|1;CjO@f@0m-h{=|5{K) zgsSNAF(HF=HT9c_H=h0n3frqaW-jOQU-;x?d009^LLJkk%4?Arakym?7`=Y+D2TWs zB3r?y2%Z8T;sjo0+Eq(oV_@8^L*W!HQJF$F)^Okczlx!+iJJAb{(qTy%;n4D?09Y& zaZ5@Q4tYPECmLZ~Ly)6(x-zMSc72Gue=f7oWS38;*`&^HwLl$$?nB?(+Y49&e=YQS zxPf4bh}3`LM(fIj2GsD}R1)nLW)A7VJnkB^-77;YM z-V<_-*5Uy*nJk^+qTTcgW1(^>Akc4uLowcw7kFajBkb1%0_8zRNPB|h%Fh=m+W?Cl zx@(YYEi&B^*99S-GBXp!q_ILTJ@ZMwA!gBC4D+= zM)e5}bqIOs2Nor7A)qM)aA6Pv`?HG)c|)?gB?i z$(>Oe5Lz<3(CU(%n4BopA5D0DyHIZd`t|$WWPzj+Ul!OB0q^1Fcus!o9Of&&YgZ7G zd(fL?go0EfE|$7`Loql?P@UCMxf~jX_Pk>q zgv0=_eP9&}$6!J~1b)1@Q~~Ic9AwG#+&}h!_Q!?CpkBLTbX1ENt-Isoej1xYuuKMP zsniS|JpLxp=i&}a0_pwEBu?e9P;*>mcfXuBA1nhE!{ef0;8r)WsQ8l{Z4F2UsA-y| zcQI_l9T-LxGmbW`)>kP_S|vCAN}dMLj2aoy#P=gm*OI-p6cz z`QE32)c0pOuL)fflXTQ1fj&u-daHB$(C^Rj4iGLaIj%xKmf{^w*R!u0jn?f$KW0** z3CAGKByhO*8@UL4llrdSeQGn%KTfVEVu7z_ao0*NvGo^GCM?t zZ{}SWtZ!l_!xwEjDGp#uvb9HWx7>XVAa=A$%|(WiTE6S=85B}ZrjzbD%Y-;I%(Q;=8KlO&fuyV0Hs_{6c$sxnL@PsJ%AAxBJh1-kJ-pD znHA}Eoby{!=Pg0O=RmJtfyLLtkn>HSZ5i1meRoL4OKfk%YQop5BdS*mUrkNpqSV{i zMq`Sdvls5L_#K*yUlkqPmRvd-Pq$@+iQpwvSeU-BfUONP8ME1SzSd;xInVBNz)Gl% zO5|Db9D7!F5oq>fu1rn7-4fheYVa0Q8z!q{ZE)yfn;4vaPhny{8hen!2Bz03>A3pl z<#RBXkPnSY+#KyB7telf-|Fn_OBG5z`?=V+*R?T4pHAN0Taqrmzb@iOQf8IzeBb#A zjA-co$MBeOE|+hXWlUxU|7Bq>ieqJ3m~zcvbOfKpX&;_wVXYe-ajHA0)fm5u`MB96 zJBlvEshXj|&uRwSG$u|W7Bq<_|Ex$4uDfC|3D4vZ97Tr5_2qp-ltqTe9E^Gw=FMb2 zH#>0j;6DiikNej1!`cs6H%1QLEvK>5Kj#nJt86~c#F-QTBv}7t1^zqI^0hbL(3#A& zYz?-QV*S#w;{Xh2B7BVNgI(b`#N0;n7_3c9=7zC;B?2>4h}CABlX_@yimPT|IYA^0PtLj3xgt6z?qu-6Bq2`c3tgFOmLn&9GA zgS6C`zEyRkYBbBTBGnf?}4vIq&Lgc6W(7QO3 zw}FJ~YN(hQtZyK}{`v%(u@fbVMwz8DGnG0#*X+Lz!K>PDBB}L8{qOf@kC7mFDoB6;QzVX^8|Ib@1VqQ~sO~B>f}+$rmnFAWU7|z7 zPn1R)SZfC49Y*ImhwlMPtDOOeq5^hftUwkd);(tyJ~1~p>DV*JC#}`{WXnGa8ntnf zd&@(W?=aio5v=roBA_$Gt{8_f1k|ocqnSMNRVU+9%M$6Ay%MbtU2Zv<*r+Ublt>n}5 zelq=J{UO2yga>8jfrs+~n?se)4oJ~mht7Y?RO;1Kj9=8E^z!_)joVpbN$zJZThh0g zb+e;oBXWHa)HPC9!&JTUJ1k)p2${Raq^L3U>d4L>Nr26i%hj&+NWjfUBf~eD_1IqggopGN+;J_o*+bWrD z*lkXn;K{X09+(1vzNz%FUBzu`?6^Pp`qUy6fY>ot?~>YJwQ$?e&9vwglkU}W%Cn^Q z)*tmJA;%P?M=%W4pYkUkxoTREKhdSa_8>fd4R=E*LZ$&P&$Y&KtKN76e)A+^&lcK| zoc0l|L=DjiC?74@?8k|9BaG_@O(noAe%TgO5OaT}@zhr)?JAaq;^`y3CT&&QMKym(d;BBA-;v4e zpx|c!Z`MJ8!jlmS!0^Bq|3L*qE>G1amLKYnOut^h_fIDy9S={x0c)cJNascLy>gP~Ptlb7A}bNg<$Vu_JF`f$-p z;1*$e1vGL)qUh`Ab#uDPXnzVbiS(U#r^68mpAyDSzQ=witH;w;09Asj4PAN0ZD4pf z;gfhI>Z3;R*!n6YjV2j1czmF~p(AWPeL?JLZ;tTYEV&G@qj3IMFuOABf0A+>m_ULm$kd?<&j9mf3FV53Hh>%Z^ z>~tQ%i;fpxDAJTH0cr-v&q}VWENke;kfZUw-8?{v0a=qUZH)G@^zt z{veq_q*>W(0!ZR}!lNy-kjy+!nyG+dk8*A|^O7Ci z2qed*U$V?j#Ks4va=;%i7@NVfY!~duSPCl00&*^W_@fC#fF~aOKiPWRm}s0G;o(O2 zt`?dqOd`|N2!rfP8GAH_yO8Dku!&%_>;pk^T=y0U1b6>$$V^bz{c_TEJc1J9coZophLokB04`%_W zxmtES57HdEh&{%(qsqqze=hcmRVLSh*Sc&vB(vSPEnTxQ8_gC~OW#jmu#Ggr4nRm%t3zV5!aE~26N1)#|I=g_k#?gy z&FYc`lMNkBH-W-_o%Grom`lCus9gVMwPocW$>(uXfN7nqNKWKAU)z$#2y7huh z^43ORbl!h;w@cYKgy_DIf&aIU=k1Tm1M@uolsViTBfAMA2Ed7sz*2slzF zQps6@J9*cb(;Y}`qX74(!zFv?`3*dQw~#CY6MmQTF{WPrXnN!Li-lAX1{;+^@4K0x zN2%wDzR9XgisX796L8sz7^TymzE5=S9JQd@L&?V=Ywb<6A{RP%f`NxM2mXX=8<(!j zwSbtz)1wH6a0bu_+pV=D$I5%|K$k=*_AKFO1vhs%!>o;z)V1| z-Hwm!fyWYxJm0x7VO0A>11Sbw0H{W;jcUgXgM6S1T`>q&?+taFpcry0_UHUTR)e&6 zZ&634IObzj+v4D<3)WGL_ z4~gCSgGK>V>+hN^kXD+(^KdpkoVRHSUKgv|OAbyvNT;!o1BtiePdEGYW<#QTyl~l6 z$2?y;ba|Xk3Q{U0`Sx=OFu*9e?sl>9K6C-JgX&08i7aT@y{;vpGTf*{mc;@XSFu5l zu>NRgH{X)1_o({`&#-Ya%cVmm@0OKxOsD*bSw$*hjgiVfC6B|QmP84OF~H-FH(x@p zWy31X6M~fB$|I~4+;es@A6=&k!mu{IH4KQ7yKB{{zIytjKHdUPS7sPxyQdV4@8ja^ zQ^}&Y`%Nb(o(@=ZZb5FK-=WtxYYd)> zn-GTa8)3TA9{i{*IteU>=bcWyANlESD*z7|{^4Z)CQ)$7uAiEabB%-H z(c5f6un1##&5U<;&~ZP=Z#6`aB*c7SZxpj#C^tk>;H_9+)I(wAPSBM}*;cP$k3H%w z!iCKBuTxaw@eMAy6`e?dP8WzMrX-P(W%~w0Oi`^yqv9%LE&+Shxds3Noz@sEp(q|g@8Pxj zciu7!ZLq$L^dWwmLP^w6N6WDcl(w4_l#!dqn-Htq#2!mHKh!tmHayWHm(0-(Oe+mv zOV5UHDgBj@;YU_{Ay*ghCDuV8892+XLm(VX0Xj6+gV>ifC-8$I{iYn;iMY!HSVp7W zyx!Y4fE|Y#UtDctvp-60?l&Ue8e7pnGf3y1Gp_8}C2A?)yE9HS9!d5%_3A2hHdfp1 zwvZ7XQ&LA9b{7z5dDs2MAO0#Ztd5k?pLX&UG?Yuzj#TNBSG#V6g*biiUo$*edI3T~ zEcGKXne>XhoK>#b%J{!Vh>PgEU|eGD&h#C1Ceq&QLz32hbt%J$JT(agz~jFP-$3kb znfK+682mMroHoG9wAAWS#ZV{+4MEghEd1obGMU4qkicF9so-{oeOIbL%^cbCv-vlIOFxf^~bRRQ`Bg;4Zpi1pdGgm z4lwyA&O2&Jsrq6j#>C0o7Ufz6ws+nBt$cwG1ma9lD;XcO)9u!BlD9~F0&lH(3zotYd8g@h zi*YLg0uJ=vMia1ohN4tR{%;{`mw4<=3oj-+uZ6ZV+UJ~_9h=(IYQG5Spz{~m}lLt}_F>p8X8?dxxz4;o~_Y#hWR1!?}3nK zoMEiS6=YfQ=WhukMP$BnrsH3T#4^6FYJwk5(HD$2T8Ysn?6QAAEI&l%Y>{d8`?rPP zS;avzdAOCadhBwh2nvXWX2Ph+VG#>s9w-r(N=yTdt>x;PndA{`-#Ia)FupLwLr4^m zu=Z~b%^8)!So(O>!FqM$&oyN$UiT$NU~jevk~4&e;-;Np77QRp(|v8nB(#UnzD-!V zw28EJ4pq_02+TPXC^lISOLVaoBBFRquR0^2c#&sZquZXBNOZqt=&Ptq^5I*Deoycy zw{VBgi+84pkXqea_VFAZEh~m4g(o`QPbunW`Yu}o23&Ti^Ib6cAG>73D_74Pmmb~8 zv*I`R*$9rdE|ep+am9RvUa2t_94eX5UvjPgw^+l}MRJicE4K2FnmZaA{H=UVx&NOT zY;kXX8w@fsaQG^^NX_{PNqB%B$txcZ`5#4c0RcT^Gh?)mO zOG`U#tBo!q;%NA-G>DI>i6lG(77^ob3JO*!_+Rt#|36H)8R*{OzxmKL2^v}&jQ}s(I>0a*Ulr! z{=!yB`DC_DnguEx5?d~0~w)Q?Q|IQuZ--UVV z$S5d<20hAT=kU*mFmQ#q+uP!W`Y#MP+Sq^K{?jhZU$*c1#;#W~6u`8{j!W6}Zya7# z4x9T0?p&k*9*&U}7zOz6<(QstP?PmP{JmGvoD5bB{&n>JCU9^Yx%f~;zIe>goE~vJ zV`C{TPKOc(281Ta6tym=x}|DW{L+78 z6mrITK01^k=)?xLLR)?wZ=i@L7*g;5Hj#&wsAI2q@)`&sULtQ&qvn+N?biJ}Omf&I zA4NsN&UGc<|3`#E`A1P~)YGIb5hT2A+G?i%c068PH2mMFeoLsQRpPlJOc1c)#>8im z`)6KkT34s~ynx}LwOc|DhXx*-{`5)a#2eIUV+%{&i^p-AqsuT0_}7LpkLPXm z%`2SR}jvhl)k_nb&hE6%#eN)BOO~GX3A%fh`*Lc!I8|G7 z7qo|CgL!+A86aHk3gxR=q`0`*M>1i-*VY-$WB4f6gM6b~i_o^iNRH7%8dpSV`Tw8RVKn(nF}NpXn%R z@eWan_;uDu&eS7HIKMcvh#BX}PblOZhI$=Yov4ivI% z_blsP_+@Xx)7hVtK6 z_h2L{9X>J2)L(M^;JiR`Dm2t{|ETiZ$wr-7<9ZZEw;l?Ol- zX#{7nQR1y~^zKH*(w2w%xlT#KViNsdy!90?XUV1|4tAMoF3S%tg+>h+_6GnqtM->3&yF(T!-=IkskZF;%&Gl!?(99}b3ekLiGsnL{ zXfmv|63fW1B-Ej8Q_M05olU=SeYgPI^Q$0^KlzfH62vQl(hvSDrn#Xs4!}6*++~Qr z^~|IVYry5858LVG?yotC@rBBQp{7f$2-p@6b^<<_M^mRO$d@d+eSd0HW=njIHQ5{c z`84Y+i=Qi#Do%E^Er=3!jq{X~hG@2JLBHbMPM!{(0X?PG^?Uqr;vQ~*kP|Dq=R!wh zC8c=%%s&tH*-K-1frW?*mwKNlcNh-jLZw(*Ic5_B7diapq9TPMad9pqB_a{Nm`gl`5g>BstlyWt`4O zAlP&&Z*TU_yTiz4PSR)EtxXmw#in1%vy9#Ri361DU_jS>eIL5;eM_@uHNBzYXwGkJ z%y-cY<#|UEtw?)<0tx515MDT4A-&b$>HR%amk_Q(2XeIW%IDGYQ+rG z>@Dgd5Ozd+j$I3U*pF}KHVi*kQ{r{!`TmPTX5XrATiutml)caWEdpRD7tTTZ7J2iS zukXWEbK|7f=VsI0n^8!aF>2yOTx5;%P|c2&MtEele3(Rp^gS4LN7K!t^ues%;uAeX zTu0{gOLGK;cLO_w+YhQW$m6p+TR&Nf6%8uO#8_$82_hyeJf$$tjysl5TrcugjCgfD zg`^7t`xWqaC7HpbPj|3MX1ZSirtWKNWaAxP$!5%+d5_4u)VbyzxGNSg%0$ixs+1LR zmjomgG;P^b{%vi%yDHyB5b(q=k;#M^B!Ry$u=sz2G9pz{OC=Ujo*e%a>$(olUhvJ6 zXf3VYOCC(h?ZW8D6fq&ly+Z}3G?g(Gd>LT2S*3kB(>h^o5iL!oG!3vAB1=Koy!*#vAR@2)41*deSo_!x9!G*Nu zvVfGST^6hBwg>A-VnLCxk9^*Pa_45FkYc@!bC+}W3FFr;uml<(Qvk8*^15Hv+UJkf zHcnCaQmTAdGU5QF2~j4i-I?>EEo-=PmywUatdH861pjfvmv3|OKbfl#l@=PB+Y9Vu zjMxF|8?pjwss{4}%6KaoCQPX+s;BTM_~;#JLW=|Y%awZ9(cNmR8Qda)H7QD7bgUK|2Bdbk!)Qh> zkAWx`sPex@d>7_QD=~uvPutihhX%ip@BQfj1vui5^Zd5p?2UB986+}s`qIbRn*V+!V)}zNuL$`Wmz2+^$hj;uCBq>&7 zg{mQ{^O>L}X1Rt#NuP?kDeGbTDeYqJG=if8v%%q6r$m$`v=*P|%k2H`s&yCc!g_e} zau}mq(N$aBAeb4&M}RtW)|$968Qe)@Oftvto9UnYV`lR4;UxW^jqsgI*_fM*32SLxhu0;5pYe z4y&wzR6qYuyU(c`z%Y%=LwtS}!<7{L7Vfbz+OnP7P~>lz@N{Mg8{QsQ1SKqwEFa5^ zsI6(}GpKGY8W@tS($8{M?v|o!ya)pa1oypa_=r-F^(p1Pc?8y9?6oHyV_+iJo?2$Q zh)=VvCGu-?^Z*kU3dcYqv31|?Tx;o7Z4^&}S5w~t@}!x7`Ub&F|MuxRcs!h4r+PUN z3$9U3=j5KHDf+hxdVwt{izQ+nJjM%qc4RS2JJYh(6jo!(!y+xsUTWU!$`!fpm`6sE zW=XV2ilS$BhZr94T;Ge+Nx|KwRvFeOM(?hkf5(n+ol7}(1cWC&d+ZfVBQ<5a}af>F6V95dxr(a1fP)?e`Z%De8;MQP*xH!@gh zNnmRrw7>5#l^P!Q#zKt-Y&l)AA9CJ6{8G@ z+`~kr2}L!)cbTD?%*pcU!GyFvfWG_Kw0^gXT@N(0S zq^DpaU5K@X7ofEu$NtbHq#JI2MYzW4RgiX*c*7=!fJPj_%S+kf4&3;=ja8Fey|W>c zn@Anrb!6I=5wEhJDV@aD_h6oiKjMXQMLYR?aab+5ZB9CR$V$Y^7BDzvS!XYMxJ!~9 zxDxYLADd1;C6D}$6A$a5s1$*^{`u+UbQCWluYiFoV62A!jZr~SNXIe^8nx-- z?qDC?F5>;2Bjctn<*jI&MHg3x#6wtb`_X39+xS^a9r5`j>Lw-tj`0>Dmy$IhpF6PQ z`9Ah>Y}ChvGNLYPZpux?>n({p|Fitv!QtAn5NXp7mwuN5lZS*%nnbicJ5&4CEkKnB zmw{xdr-yy|?{*MIX4iY7N4;H@mRz7P0#P0y;jyNHpv$fE12yGT32&vN4&1&(S*Nnx zUC||NW82F!aw`9m%ew#+4y@Qn0yY`2+^`?N}P!+fX=C4KXxQ9jZ| z9##?KaW=vH*VnB_3;yIENhqJeApibZbwuw(O#gh%SNY05x1lvh@QQIrEc|SSNn$IB zQ{cB)V#Lwm?Kk{I(iSOP42)hSjqSGqcLp6H0jz>!O=*d<}pj{XZqlrWYN zg>QNOb6z!(n^1pidwWolYWwG3v9Bp!Radui^Btj@{u^CvY;1{uWa{35x&Pvo4}W>o zdEk4dvwkAEc!q$X0)wxu^#1{BioDd#oVIH@?x20wZ~ZtRkT|{QD}RG;mn3{;TxdA% z|G`?}2s3#{>#w_!;^>nVH6*+rW=J3VxBt`_asT1;)|OTE3(1oFr^T&-DWCCZ9(g33 z5~3gyIN>i{Fp0(_16@8W|F{%}n+kiqV2*)W%;eNEY;bqiUj!8z>d)jCwEpQo=nA8d z>>%XkUv(&$+-7?yiLU!!C-M}wG%RiZGcKeCI5hv*7Bbp@A=m%bj4T8mFQl>at8%bQ zT#*xFN=l|#V$aZO{CX5h!AX$(FoOwZ>6E=~)CkQn)$u`N%A^xSSj}moC+)WreB5anQjtg^g+Ly-k17B5S9@YfgcWpx$e~s>#>>0`0EcEg70xcEu2nJM-EhRdGQQiTKsDj>4=(ag-F#jNFdSiAyRn z20CK*0P{ZB)awzXNf&i_6YV?0INs^1%xOl!*2tF0wUO+Ba#pi4)~lGs${dGbpbYQ-)eDWC4d% za+(JX@#{XfZ%_t{Gz9D#6xGshJY`hf6H=y`KgTu(HzrqNaYzPPCWH~z$YqJ7<$sZK z;8ky|E6&&{*J|uey-3hVpT@wusmYknsm!t|c52$|3G7+iUZ%<4-f+Gy^xEfkQa^ZP z@2y2p>Rh&UJ7)2aMp_|YsA!c-l*J)iq;#%p41!bGbr{W1YHpBLF12{Sw>_Nc#U*Uv zly%IdXyry5qt}kqq9{;Q_LMcNEQOm2fB*K!PH0c|z}3LbvBUIft|;a_Tm$r5>6P;0 zSB0?q4fLMEB+Z7C`Oa9XkUsZS%#1l~R)O}^`$ZY`e?f!#V4MMO3v-tmku=Tx6rO~{ z?fi%eKZ^YFB6F5&qz>yOuBnPA<%$!Nn8pUq{MoE|)fche*%G(Ka$)nU@%us5F+_6@ zw949@lSW*>`KOwg=UGdWB_*xL+m4^*zYzH<2FuV(Y5*3{trOLb1-$@L8}5h2{8GGJG@m$7o!fak^r!lofn~BwyAZ}#EGvrEFnzk!5t5`y+Hc7u8q(axysVQG5 zl5OWe5!-)~h+v)#AGlZe-tH``T=pWv!^asMzageNHdsdg@ZqC+e)tUY7OoalG_zPr zvQWcDKrcK0`hIlb`{*Z7XE7Sk9aiDp;6Xe9gNe zpADs-aATWIuDfs>Gy36?l$2dnNotb#ItP^$q2TG4pw3_NIU!a6V%5!~%-P1>+bx^f zT1tlsw#&M@20>Q%KF+`$&%3?XqsfZWnaqo;r(ac5tM_C)lVFT?zZ^b3dX^PK&bgfV zK~7(>%c`NMI5#DZrIGqx@@oO8$CHY(GYuCNmsF|8RrOMuq?aCz*EK8%M~-#XeN7I( zK`a&gv{rX&D&HUciG*?Z@n_b|D$REbE>(i32jO{~`DaFH6Tg)~Q9U-E@!KnYJziD} zjdJ#{fJNHv%js)0OY1nA4gB8!u=FTT^*T5-I^dr<>Z0lzU#Sb8R$GjO$9q*P|A)hD ztEd=}6sh^c*)Ei?aB^PsPTAnq&bP*OGu{VfUCc$kX0!O7V_d)Nl`{n`|72%KLTV)E z2D$e_H=rE|Tvbv^u!zxvKl%SgXJkK?gk!UQwhOU7^4janiqk+68G1QuqdN&=|G2z$ zqBZuh^fqDSH>w7yJjC)#zBQW@|1&Ad#~0-rxz69Es)pz$&+Z9X4~;R;;Fkxkv%XIk zilQi7FGy&aG*Xv5Ah%Qs5Q?~zFina8zyt(?bq%X3+q`r&N4wtHAl{O$%ooIXlcdK+B$k={0b&K z9k*t>R1VL|gyp-R4Vo2u7*$?6;}NPlP2>RtURtBFbg7pF>LG?^Q|p#o{rD9BnaGjw z;`&x_P6snqeR2VVhh$cCnlD$HNu}kn&}T2Or4F{&WT^2;H1frMh16_bUBQs; z5&9h0uu8oJbQe;l&OUIDHjsS3H>m;n^)luXawcqBN-V50BYDXn=_*xce|J>BK3QqN za(6yVHrILIu;kb9TZolcw4?M{1yN1X+^hAm+@!a56iz^gNM& zaQiTJ!vM}LJX@SRADPN{jh&L^ zr7^nt<~n||kJ@`KD0-0~(&Z4eW|v=6DY6H;T>CuP6ugAa6&?=wZ5;EOFFr`qe)=O; z9(%sIYNx9P;_zLjv>AimC{om3*Yt0xI9`T{Zjo#3xKUee`E)v%%7-X(1%tr}-%XaS zY@}zDYgLFmj#M6Iu$Bw2orbq+dp$sl{_g+<$^j)x=_HzkOBIU3&DV2{Qd8lei@iV4 zt%%jggczhHdc`2Nvx(N)5~+Y2xQq02`Qgrt8sm90e%Z8KPNPLVP^u4eEVj$hQJa(; z-flg381xHX&E)OhO#_>@5^S(ztCqqisoAPHBVn;-KsVZUv9P;T$D=a)apyPdh=cT- z&-*L&9Ro<{gE;6f*`hDHB+PxnxUtTvWR z%0a0kmIpj->vLAWIjLyN`@7pU=lv?_Y;-E8n&dnR`dRUOXUG<1-E1k0F0*GRd0crm z?NBjX+1%H1^bl1pCKto6Id6Sa_W7DzI_H-QmZSw8DDnY;%f^k~k6U<;dwstQ>j{4Txi?-hJ!Dw^z@wk`l-O>DW)bN0b!sBt zDo$nk(#@nDP%r)S82xn}rK_PtKQ?D`h<+0IC|{eURrJw;KqjIrq!O-dfm=)#_pQ{`R~6SiR}lgWTx# zG$Q(nJP|%q1lh~y=2?{%7bdkTy~UzZDB~(E!yRYsQZA0s(Q&Df);eD>?df8tf94$C zT%v%J7rI$aqsIJM#^2@YyEJ##INzIV=oL&sE>bQ^z6ZL!kXiu+F2~RC@hCIzxS5-U z)Y(3Xli~d+q*OkWlB0UlnEL81!!ivFfXkh#;-W=GYSPz*A$CockW7nAP17o^+m{ZL zYr@VP04Edv)POIMH~0jLk8GX~rny+{#vJDb&(O9dX=WJRqaENXk+|b}Ll>F-9tTy& z1H3>5QR|kjb3b1lFKJZR*s?q(J`t*IamjZnjhLzKF2&Y z==qSDFQrv}p4rC7)%;o0Mz;0FWAvh@6)Q`6a_>cdEjRY5orPS4lLLl@qD_rtkm%06D39r~m)} literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/arduino/arduino_captive_portal/images/station.png b/lib/PsychicHttp/examples/arduino/arduino_captive_portal/images/station.png new file mode 100644 index 0000000000000000000000000000000000000000..525a530a5b43726e51eccd3ff544e8819818fa0a GIT binary patch literal 10008 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 literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/arduino/arduino_ota/README.md b/lib/PsychicHttp/examples/arduino/arduino_ota/README.md new file mode 100644 index 0000000..d5f8183 --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/arduino_ota/README.md @@ -0,0 +1,138 @@ +**OTA update example for PsychicHttp** + +Example of OTA (Over The Air) update implementation for PsychicHttp, using Arduino IDE. + +**Requirements** +Requirements for project are : +- OTA update for code (firmware) +- OTA update for data (littlefs) +- manual restart of ESP32, triggered by User (no automatic restart after code or data file upload) + +**Implementation** + +OTA update relies on handler PsychicUploadHandler. + +Screenshots and Code are shown below. + +**Credits** + +https://github.com/hoeken/PsychicHttp/blob/master/src/PsychicUploadHandler.cpp + +https://github.com/hoeken/PsychicHttp/issues/30 + +**Configuration** + +Example has been implemented with following configuration :\ +Arduino IDE 1.8.19\ +arduino-32 v2.0.15\ +PsychicHttp 1.1.1\ +ESP32S3 + +**Example Files Structure** + +``` +arduino_ota + data + | update.html + arduino_ota.ino + code.bin + littlefs.bin + README +``` +"code.bin" and "littlefs.bin" are example update files which can be used to update respectily code (firmware) or data (littlefs). + +"Real" update files can be generated on Arduino IDE 1.x : +- for code, menu "Sketch -> Export bin" +- for data, using plugin arduino-esp32fs-plugin https://github.com/lorol/arduino-esp32fs-plugin/releases + +**SCREENSHOTS** + +**Update code (firmware)** +![otaupdate1](images/otaupdate1.png) + +![otaupdate2](images/otaupdate2.png)\ +```ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x2b (SPI_FAST_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:1 +load:0x3fce3808,len:0x4bc +load:0x403c9700,len:0xbd8 +load:0x403cc700,len:0x2a0c +entry 0x403c98d0 +[332885][I][PsychicHttpServer.cpp:236] openCallback(): [psychic] New client connected 51 +[332895][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 867306 +[332908][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 862016 +[332919][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[332929][I][arduino_ota.ino:133] operator()(): [OTA] update begin, filename code.bin +[333082][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 856272 +[333095][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[snip] +[339557][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 416 +[339566][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 1 +[339718][I][arduino_ota.ino:165] operator()(): [OTA] Update Success: 867072 written +[339726][I][arduino_ota.ino:184] operator()(): [OTA] Update code or data OK Update.errorString() No Error +[339738][I][PsychicHttpServer.cpp:236] openCallback(): [psychic] New client connected 52 +[339747][I][PsychicHttpServer.cpp:262] closeCallback(): [psychic] Client disconnected 52 + +``` + + +**Update data (littlefs)** + +![otaupdate3](images/otaupdate3.png) +``` +[ 48216][I][PsychicHttpServer.cpp:236] openCallback(): [psychic] New client connected 51 +[ 48226][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 1573100 +[ 48239][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 1567810 +[ 48250][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[ 48261][I][arduino_ota.ino:133] operator()(): [OTA] update begin, filename littlefs.bin +[ 48376][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 1562066 +[ 48389][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[ 48408][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 1556322 +[ 48421][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 1550578 +[ 48432][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[snip] +[ 54317][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 16930 +[ 54327][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[ 54340][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 11186 +[ 54351][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[ 54363][I][PsychicUploadHandler.cpp:164] _multipartUploadHandler(): [psychic] Remaining size : 5442 +[ 54375][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 0 +[ 54386][I][arduino_ota.ino:128] operator()(): [OTA] updateHandler->onUpload _error 0 Update.hasError() 0 last 1 +[ 54396][I][arduino_ota.ino:165] operator()(): [OTA] Update Success: 1572864 written +[ 54404][I][arduino_ota.ino:184] operator()(): [OTA] Update code or data OK Update.errorString() No Error +[ 54415][I][PsychicHttpServer.cpp:236] openCallback(): [psychic] New client connected 52 +[ 54424][I][PsychicHttpServer.cpp:262] closeCallback(): [psychic] Client disconnected 52 + +``` + +**Restart** + +![otaupdate4](images/otaupdate4.png) + +![otaupdate5](images/otaupdate5.png) + +``` +[110318][I][PsychicHttpServer.cpp:236] openCallback(): [psychic] New client connected 51 +[110327][I][arduino_ota.ino:205] operator()(): [OTA] Restarting ... +[110338][I][PsychicHttpServer.cpp:236] openCallback(): [psychic] New client connected 52 +[111317][W][WiFiGeneric.cpp:1062] _eventCallback(): Reason: 8 - ASSOC_LEAVE +[111319][I][PsychicHttpServer.cpp:262] closeCallback(): [psychic] Client disconnected 51 +[111332][I][PsychicHttpServer.cpp:262] closeCallback(): [psychic] Client disconnected 52 +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0xc (RTC_SW_CPU_RST),boot:0x8 (SPI_FAST_FLASH_BOOT) +Saved PC:0x420984ae +SPIWP:0xee +mode:DIO, clock div:1 +load:0x3fce3808,len:0x4bc +load:0x403c9700,len:0xbd8 +load:0x403cc700,len:0x2a0c +entry 0x403c98d0 +[ 283][I][arduino_ota.ino:57] connectToWifi(): [OTA] [WiFi] WiFi is disconnected +[ 791][I][arduino_ota.ino:60] connectToWifi(): [OTA] [WiFi] WiFi is connected, IP address 192.168.1.50 + +``` + + diff --git a/lib/PsychicHttp/examples/arduino/arduino_ota/arduino_ota.ino b/lib/PsychicHttp/examples/arduino/arduino_ota/arduino_ota.ino new file mode 100644 index 0000000..47f611f --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/arduino_ota/arduino_ota.ino @@ -0,0 +1,219 @@ +/* + Over The Air (OTA) update example for PsychicHttp web server + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + +*/ +char *TAG = "OTA"; // ESP_LOG tag + +// PsychicHttp +#include +#include +#include +#include +#include +PsychicHttpServer server; // main server object +const char *local_hostname = "psychichttp"; // hostname for mdns + +// OTA +#include +bool esprestart=false; // true if/when ESP should be restarted, after OTA update + +// Wifi +const char *ssid = "SSID"; // your SSID +const char *password = "PASSWORD"; // your PASSWORD + +bool connectToWifi() { // Wifi + //client in STA mode + WiFi.mode(WIFI_AP_STA); + WiFi.begin(ssid,password); + + WiFi.begin(ssid, password); + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) { + switch (WiFi.status()) { + case WL_NO_SSID_AVAIL: + ESP_LOGE(TAG,"[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + ESP_LOGI(TAG,"[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + ESP_LOGI(TAG,"[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + ESP_LOGI(TAG,"[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + ESP_LOGI(TAG,"[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + ESP_LOGI(TAG,"[WiFi] WiFi is connected, IP address %s",WiFi.localIP().toString().c_str()); + return true; + break; + default: + ESP_LOGI(TAG,"[WiFi] WiFi Status: %d",WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) { + ESP_LOGI(TAG,"[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else numberOfTries--; + } + return false; +} + + +// ======================================================================= +// setup +// ======================================================================= +void setup() +{ Serial.begin(115200); + delay(10); + + // Wifi + if (connectToWifi()) { //set up our esp32 to listen on the local_hostname.local domain + if (!MDNS.begin(local_hostname)) { + ESP_LOGE(TAG,"Error starting mDNS"); + return; + } + MDNS.addService("http", "tcp", 80); + + if(!LittleFS.begin()) { + ESP_LOGI(TAG,"ERROR : LittleFS Mount Failed."); + return; + } + + //setup server config stuff here + server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + server.listen(80); + + DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); + + //server.maxRequestBodySize=2*1024*1024; // 2Mb, change default value if needed + //server.maxUploadSize=64*1024*1024; // 64Mb, change default value if needed + + //you can set up a custom 404 handler. + // curl -i http://psychic.local/404 + server.onNotFound([](PsychicRequest *request) { + return request->reply(404, "text/html", "Custom 404 Handler"); + }); + + // OTA + PsychicUploadHandler *updateHandler = new PsychicUploadHandler(); // create handler for OTA update + updateHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { // onUpload + /* callback to upload code (firmware) or data (littlefs) + * callback is triggered for each file chunk, from first chunk (index is 0) to last chunk (last is true) + * callback is triggered by handler in handleRequest(), after _multipartUploadHandler() * + * filename : name of file to upload, with naming convention below + * "*code.bin" for code (firmware) update, eg "v1_code.bin" + * "*littlefs.bin" for data (little fs) update, eg "v1_littlefs.bin" * + */ + + int command; //command : firmware and filesystem update type, ie code (U_FLASH 0) or data (U_SPIFFS 100) + ESP_LOGI(TAG,"updateHandler->onUpload _error %d Update.hasError() %d last %d", Update.getError(), Update.hasError(), last); + + // Update.abort() replaces 1st error (eg "UPDATE_ERROR_ERASE") with abort error ("UPDATE_ERROR_ABORT") so root cause is lost + if (!Update.hasError()) { // no error encountered so far during update, process current chunk + if (!index){ // index is 0, begin update (first chunk) + ESP_LOGI(TAG,"update begin, filename %s", filename.c_str()); + Update.clearError(); // first chunk, clear Update error if any + // check if update file is code, data or sd card one + if (!filename.endsWith("code.bin") && !filename.endsWith("littlefs.bin")) { // incorrect file name + ESP_LOGE(TAG,"ERROR : filename %s format is incorrect", filename.c_str()); + if (!Update.hasError()) Update.abort(); + return(ESP_FAIL); + } // end incorrect file name + else { // file name is correct + // check update type : code or data + if (filename.endsWith("code.bin")) command=U_FLASH; // update code + else command=U_SPIFFS; // update data + if (!Update.begin(UPDATE_SIZE_UNKNOWN, command)) { // start update with max available size + // error, begin is KO + if (!Update.hasError()) Update.abort(); // abort + ESP_LOGE(TAG,"ERROR : update.begin error Update.errorString() %s",Update.errorString()); + return(ESP_FAIL); + } + } // end file name is correct + } // end begin update + + if ((len) && (!Update.hasError())) { // ongoing update if no error encountered + if (Update.write(data, len) != len) { + // error, write is KO + if (!Update.hasError()) Update.abort(); + ESP_LOGE(TAG,"ERROR : update.write len %d Update.errorString() %s",len, Update.errorString()) ; + return(ESP_FAIL); + } + } // end ongoing update + + if ((last) && (!Update.hasError())) { // last update if no error encountered + if (Update.end(true)) { // update end is OKTEST + ESP_LOGI(TAG, "Update Success: %u written", index+len); + } + else { // update end is KO + if (!Update.hasError()) Update.abort(); // abort + ESP_LOGE(TAG,"ERROR : update end error Update.errorString() %s", Update.errorString()); + return(ESP_FAIL); + } + } // last update if no error encountered + return(ESP_OK); + } // end no error encountered so far during update, process current chunk + else { // error encountered so far during update + return(ESP_FAIL); + } + }); // end onUpload + + updateHandler->onRequest([](PsychicRequest *request) { // triggered when update is completed (either OK or KO) and returns request's response (important) + String result; // request result + // code below is executed when update is finished + if (!Update.hasError()) { // update is OK + ESP_LOGI(TAG,"Update code or data OK Update.errorString() %s", Update.errorString()); + result = "Update done for file."; + return request->reply(200,"text/html",result.c_str()); + // ESP.restart(); // restart ESP if needed + } // end update is OK + else { // update is KO, send request with pretty print error + result = " Update.errorString() " + String(Update.errorString()); + ESP_LOGE(TAG,"ERROR : error %s",result.c_str()); + return request->reply(500, "text/html", result.c_str()); + } // end update is KO + }); + + server.on("/update", HTTP_GET, [](PsychicRequest*request){ + PsychicFileResponse response(request, LittleFS, "/update.html"); + return response.send(); + }); + + server.on("/update", HTTP_POST, updateHandler); + + server.on("/restart", HTTP_POST, [](PsychicRequest *request) { + String output = "Restarting ..."; + ESP_LOGI(TAG,"%s",output.c_str()); + esprestart=true; + return request->reply(output.c_str()); + }); + } // end onRequest + +} // end setup + +// ======================================================================= +// loop +// ======================================================================= +void loop() { + delay(2000); + if (esprestart) ESP.restart(); // restart ESP +} // end loop diff --git a/lib/PsychicHttp/examples/arduino/arduino_ota/code.bin b/lib/PsychicHttp/examples/arduino/arduino_ota/code.bin new file mode 100644 index 0000000000000000000000000000000000000000..f9842f4704d38260b3fa3521102a8467f506f9cf GIT binary patch literal 867072 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

cts&2Y+gU+pPh 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�a&iBL(}{+9+#0>}(()tQQqRa5! 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 ixz@ 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

BQ_-s78@tSeX_|i4LbSiU?WLe~B@Pj(*%xD3V z$QMyJ)>1dRan3>&A0a0~%&&QDq~M5S`8eCh;{2@}=Pa=nas>4}Za#(d)O z9zo)gdG2frbZ3k5gfuo~9)*z#r6ks257xAEySFeQe1`9Iv#ord=wH_;&bF)^qKKTW zftpNAUPYgPQSuQ>x_gdxv&Xul^wiTEM%&3zDf%=@3lW4C2_x6@L>j3;SORh?Ow5bdKgf8Am(!{J7%g^`mlibFK*Imh!=&yQg?@1*Ai z&uV>4)t+vrKB8J5TV?Hb?C!4C$5olTTlCYN`bhTo-Huv>%&PhrAv2u%D7L!W@q2{K zuBt=G&z$;b_P5=RDul#W?d+y;2s@`m|8u8)rd9t!hW>!S2YQEZyCbw!jVO&M+8V_ z6tYWEhq0qf_eZ1U7eIr5psQ)!Y+AR){m^N-#_T2uexR#17W(FYpsSydFaM&e=>-l@ zNYK@dd?G-~F|+w}J06C~wd+_B zCJW&acowgmPsy+2meGf_*Md4cX{g_fM=U{y6N9u7K|wPiVig{)6+%;S(D8^SQ-r}V z^CpCGx0pPeLJ|$?h=4jQ(^z+nYN~o4Wq2Q!OUPk6E!^elwXo6lyCw8wvvxCCZxS(F zRqlsh_L2Y9;Z#Ir#8gjZP*`xN(4V>W(D9Fls#^~Yl|D^}e1c3t6(4y)p&D#P5&9GH z{%6j8R8zB0%U;}`fBDhf?Z;{&X4S;ZzD1dWbBT_hN|_YQ87V`g`Rx*uu+iQi{NbJ* zOpP*IpP`c5H^rsr#6aPgbTK5i1GdhKc|n+zO2z8EqvJ}>7aWjClL&uhN>_M72xpFD zRsC#e`qwu8Vc(I6;@PgW==>uwVa2mt_E>nEj<}8;v#NY9h?wCnj5njoXV&^zCR}zz zEOSBVeGXpLtg3&m+20;QGEf|e8CTrsikL+b+Gi$pbH&G9P0{%#eZ}($K(INd>C4-X zR~&ghx@pJl?VDYL%8M$W2cEE48%YE-Z%%*};o8W|O&%yRBF+WzHoec5^|O{GwG$Qy zwTXGbAG`MUs)uaVL#O$5OVvZ>62S|3n@T!zuFSj}yD9qSk=r-#;LjDisp(5sL{~nL zh;2O91z)UkwfKyGX9&s?L7^YrZhXkThn(IfOqOE!0Xgfo9^8-nD~uPG+Gu%KsGXP1 zg!P;hS-*@=2(O_fs~rXZ;KHi1ONyeGD<>`Y^@jzGYL#bDc+K`LHB)0H>ixKX_E59& zS;7Mn+u9`yj~#j8Rdb<>UfdzND#yk{dcgxbIS==@T;>{Vd6;~D10Y>Q}W^f6U6 z2v2mR?d8Y9F)|ju{3xQ9F_om9tuY*RHC?Oua!wHY@n&tEv$Axe?g3D$79 zQStyje+NDzCd)}#LW?Wa&Qy?gtu4uE;N_3?RGOg%`wgR>%Kbiw9zXUH$Nd0Ahm2k9 zx!a>2vQZCjv*a+5aJ`SY%c<0~Et#GILuHI+JTB*3eHcTKJhQdkHa@xnpd88P!Uob{r8fREwCo{GdeV zcbqS>mol?|_&IofgH`>!iAu)o^MRj2A ze0jhPR`l?JEa3)@aIx%kb32uz?bJi_-p|;cF6&Fwhqu1^8lOn@y11P=kIf;UMOk5D z)p6zm+?Xn!{Vul_dqACBpz5UBB z_KPmZmtEyUT=Jgtd)vS0V%aXo7hT|2x7Xa>dZxX#i#^fhXzjx87_UR$zrEGo?&@Nj zyBw}AOtjjy36&)1PhISxF2~6(TpbwA@9m29Za?0|{;|t(ybBsaO*>G~(Jr>J%W<>| zZ{JL<`?cFGiTj$p+duDOZC#Gfy1*}0933C(>D_*)i#2yS4s}5i#&l`_mC7j79fekG z%<}-N4?Av}9xU%lC+uK#Ly?}+e(3R zGF{sOVmgi4OTILeW`>VIVu(ua+r3hk_lz#s2PW=P{qHEfLISzH%+eX_8p{<9f^kM{OvpPtMi^C94bGavPzRh8D9+2!sRokr|oU=Ze()Eh?^FU zMK>tKm8NdN-QzM61ok9onbADol{Vj{%O5-cvXEKgUFc*(R#(N9XhygkacztNgQt zWAp-8We_8D(=+tm*s0>R92sG-N!N`7sOerEqE*j(YazSl-{}!1!Yx{ELT=ipuH5-P z=3y8uNH90J5)yK)XF1)e=Mqx$FPg_mYM`FT@Z@40EkQJCL+~bb@ z`Kq4rCjE6qkLCN?`fFtQ{wmrf#W|i+t+al@wM>&Zr^(JXZfbOWciVOLwyV+A)RA>hX;5X!cZG76&<2wtB$->T6J03D;MpzTU1|RK+xZia!^H>N*9UEEEmbRfgC|^^>QNm`JO1*Z`(0PlQJ>uP`6m|M z_7hf}eURHF-`*9p>5~r+?V9t#MtO?hf+voS9&nj=hfS9@&EI6IykzPFg7EZ_H)gkk z$Cv5_Iz7e5dxIl971sPV2)`41Woo^*T3%|DuT$$i)m3j-T4>Lc;;<^T8H70&={ z>jILew$6W0uh$jpbHU}{0D~fKIZd2nJc^ujWvobKA+Fjw%0yj zYadvaKXC3j#MIqKM*o}yQ%6*-v_nzX^I@U)`j5Ta$Z@Reusq?o3r6IPZ1OQXatvDo zNllBEG3HD#s^Zl-<16d6Rp@?oj!)$^wQ7#@+;kGv6xL|e&rz%7%*F>{(2-ZC%nn~Y zYh&uwW1VLM=Fgy8^sz>L{F$mVmNU#ruW}dXv|#6aq+ZyzoHA8eRdaHi<;cG-{lfU+ z<3O)Aednf$u-2*YpXfc`wO5|d=u)%EWZz*mR$7epK3U!g94f(Yz{2BhZy7W$cuik@s znKUOfnyO;)#LF^Rk{uqGlf^`-;xlr_qt!B0bGi*RarNE9HL+D;YYd!ATQ!TzZtT#f zM?Tn?igu(u0<;D$xK6oxR+uG29~&^gk{PO+GhU^A@TN!EqmMn>oSdo3+*s56vn`F_ z%bFU4oa3p}&pQ7uavc{m9SZ`QXu-Plt0kR!zU{7*bm(_(5}fPGRCWg3u=kjoYL3h~ zPGq2I87xo!NS8L2l%(9pf#(K~Fg=`&4w}%WsWD<&*5(5y-M^X*Db~7-W>14U->Q;U zreEJ=D!Cxs)O^5;p~xtOS$98LZ4TaZKo}WG@=tc) zUo0d>V*F)HG-njL z=y0A+=&5GWyTWPHK3A_-PYswlTi))z&${nB&4(x2Tm@_Fud9 zP4jZY4cB0F@r``5-PO!q7}t2PQhrs%>?iqLQJLg+zvkd;CVZ@An;O@G@?NWJ{zDzx z5d778*O7Xk(~&nID*$RJci-ns^7FGYcMTozAPpGm?cM%g_t}r`J1x@kD}T)q4H?OPrX{JVZwlX)y`ihsFNmG1+JDa;3}XjoYeZNHGn@2!gM(h&!cGTzQb_E~ zkk#t1Z@T#9|u;Y<&Oax=Lk!uMeCy>5lcmGzK0N<${YW2#Y;x+T;_e!1+*z*stfZ1 zGvaiKU0)}_jOcqHiTK)u`H<-LCd=*ch$;Po(C&HCOY>yZlpY@4;F;9P-(gwEP)PR6 zDlB?icgjC^e!h3Xn)zunsc6CHlG0Q6iwpBh*{Nki z)eEQvFRZ}&d1ZC&#;@vJtKwd!pXm_Q$v2WsB(XLsETFipsnutfW_tOMc%sv*%ZBP4 z)$H^-!4vv{loWo(V!Qt|($R73xce7bEg5F+e{h+{=xPtDY+(EPh+4n0)5Yt>+iP8= z-$>Tfxz-D?^j3X_fdJRMFM>Hnnh;4FEcWP=vih+zjRT?6ICuu$&W#mDQQ(CE@7i^TT7IcW8;E?o&2dP)3zMyjy z4gRXkq~E%le#Tt6Bhzzy2D@^jJX=6d=oGfnkP6?*NAUzpf$Cf>E>izWf_lzpPWCgwG!$W1mM!iy}574xa z)-tpdqN8;^izRipRy)cDS6d*qrnjxhD6*ij8w*!k=#)G*s}421vVPFgA$mHb{yusG zl~W8;x`X={d@L-lpyajy8DMrA#y#8z%jg60<#` zp{hZ8`I^Wu|1;x0gJHgp?&}eHastd~Camj-&iqSIvdGk4S9U?1zWQCCDq@XlZg zw6xaAtJxKGf(PTCc;3L36F>S&+O^c~q9vRF$^$GoNJwyEDxc zK`%P#QCVgC(`bX!F4bgbWL4E#vgc)}ltqs0{0voq=~jJX_S&*Xc}rvs(Z+d`vOV4u zzbZCWEtX;z*L(Ky1Msh8h@nkC=SY)fp1ofrE!{`u?)}WXy}x*)Pv%r{ts{3$&8`=lY#8{kVI%^MlHX0A8rL4&^0m zs#z7YJ$H>fHA#w>`FWp$=cQKdng$Z)WQ$5+a#~MqlwWzvRtI#dDWzsX z?tB;WNm#Xcu@sr{JJ$l+=+%PUWW7%us!CHwcnmDxa-(ou4c%ye2`Nug;<_ z|N5dDKWEjHxu?QZW)eqBq%Uz)sO0Mf&i0JIJ#Opem`^(~wpFL@Iv#hb^&)k!fJh%f zLsQSmyJ2$nY7}w!Wm^!>N~1+VYapyJ7k2 zP%m}9heLCh9Kn@;v4>0EyN^xO$jN_HN^z;M5PKHS#A8JcFnEOiay6Z#JbW$j_ zr?gdt>I069yC4@5CV@oByfy@TZvl08jd(<_#!Iq152MPvIOMa8BC1gRtKw@lI6_pn zPOg}uWJWr3Z^+sEok6#Q4E$lC)O}#|UD?iAVyF4%kb!0rNsiSjnUS4IPcRQ{u9VkS zF|l{sW6*yOys$)om0I1NeXFj=y^}-2Mi>WrXcSG#yfRYu-3(|Rc;m}*974Zv>#4qi zi0qO#g^ZdXjnCetCvFgGeKYiaYQ5J+CQTD$Ws}{K4A5+Ks4PABZe|9Ju4vPc@x_1h z%*YIvJ5|2RtLY4tpHuIdUDcfdBen;`0cxCtXI_&!RelG=#2YScVUwL~!~v-dC&H-` zJbN_C)fGoRV*b>b`;DAE*m;}PqSA;0wy)s7MlYYG$~r3Qrv&Kwgh9euPiF4I8mf$oMc`8O^MHY${PCKX$5Q7y+d8)KRC(d(5AN zeR-)+8ahd;6}CZ747H(!dC-}*-lt+x7<0dKLyKX(4|A`xV%I-9E-L=`Bt>xLHYvr~ zl0fjf4of|cliXZ~Q!$==v;O$ab`ADiN0NxB2bnI{;ey|cQcp6fCswN`&oben0jI|i zrrXB6iCBhc(dp)sqOZ2f!KFpzO)};w7*_|n*KxiR>?=r<&CSIqy`%CJ_=>bqc`%U6 z?~$x(g;70eU+g@idUEC!FQ%F#i*$54Iy<8Dc=t_8?D<1ygvk)4RlM%y&9%pGq)K^Gp?=1&BeDLY7B9-MJ!IGQ7pv71>rK__fbFK+m7*xVD4-u; z@~$)HO{&hD9Mc@fC$J)cA%yZwFq^AD@-uVz=7)X}L<3^pa3|yB6!mxrNxXg>L4z=1 zCrR#_cv-rIhcta05ip02E(}~TLi0yE zwr^jzwa$2UslmCq$*}Hzhb}~Es`+%Xe{FDpiS|!01$ozPsWVjGHdMMQgX#Gx(X=Os z{=CX1DfuaD!EVfuwUKKWaxV%hiSg{d%b9b6uFq;N!D_Gt?3E&Q9N|%L8Znb19EvfR z#&&zObLJElHf+8Fok8nxGB}Yi;|> z*m#7=jm5IO)&vOJqB(R)F1`(JQ7LS4Yj*pBb<>L4vo5W9spwGd{Iz-N=N5df)Tslq z`*eAe30tpuz$Ld{pOvodOq=hM(rRk2tjemoF0r?_2`E)?XVCnhlvXl0sjOB<~&J>*hgPU;W(CIzd|kHVooukKWCoJO&?dyXATV77&pzWv~1ElMNDZ&@+G`a%R00# zK$xVWRxCPaVVLhQrcHaYZ_yJg+p~L2!Yp6Byvxx0o77oR7#11isoF%G$p}rCvJNee z4b5mx;HxA-zia+6st6Yw5ALt1m+=>Aniftm%o8Mf%5fI*V|b4;y7Ss7`9%gabQbL# zev%~&l{5iEy$7cll=FDHvEM0x6vJV&CPj5Z% za+#Y(^76;wUIFD7wNCn{ygj9ZP3dr?bXeTqF%pQ~HL{6A1E^g-*0VY>`M}pc1)Io?Ek@mo~u|kGpk}2IqhiWgaI(lsNr6eMP zX*#s3`A?OXjArVhS5b}>8n7#GpU7#P_?bxDy7_bHZW~|grQ(ZLQ+uX%;ECCgB&JZ<4hadc*Sp6 zb6UxSa(^|NIXr-gQ<={6e>ZrmGO^f@YcK^@Mw-};b|}K@{;s*_f@~v5dfAf$Lbcv6 zPw%fWj>2T=#n#CO#$9#TO4F}6u5SX<#s2d<;}xmPBygP$B69Q?;vtxxyS z%ly&H%||bDnU~pjN%a;-pFoxL;Ky0Id1wK=Pb=msyA6Ao6Ghs`@L_gJC!OSsYjZJoi@C9l_#QS+2aeCm?l z>h6@$tmYNpXYsVG=L50*1DAQfs0#jDf;0K$@9T~Eavg%dhd+XyB1ei7Tp^!nu!_=G zwSPK;>Z}wQJsvk;HCKCoh#+z;qWR2%%58-jnxcO)p&~%H;JPiOHWN zrvI3J@Pod0{ZLd0LgdQz{PyG&UUtz1T?-`=hos#Fm-&Lr%>|da+n3m<33Yu4;5s?@ z+u~)ud)_(G$|IIZW=N~Y;oQv0I!FgzExqnSUSs%AU7vOT_A;M&xp~E9BFtR>6*(vY zlRAy-@fAg9-jgr%pky$*yhu2gV=kTN6vdfwI%2_65K9*ZQ^ZU(#~vop<%{~~fd_a> zrw;g9F)-un1vo0}j^0i&>iFc8jHEf2Z+E98U1^3aNSmTcfykFkddB1j5m(-l`=}>a zX*eEAE7BK~BqnY&Ui{AR^-?@5-t;Q{82iH}88j9!$_3X|i&!7hOoKfd2CFuHQC&)g z&Q2T+^NXDN3^T^L7aq&T)%uJ=RR9_3n|+Z`XbPy)XS^=IS8nGQ3G4OwiTC8!*6{uq z>Wj!^%Buk;wR?3yWOCxBlJ)C18kb+dIUdi(uf}G1PrK2=XOezuxNqI7Bw{CmGVPIQ zELrS96n_e#@S&Qr<>mET#wzxAPh8@u%gr8_JF8uVs-Uu1i+$_7EtAV`W~$rd_2XLu zv%=}FDw8+c=R|9&D=ni1HoceO18n>O`XHStQ1R*Q?F(A?MNQJ0O7-lF>Pp34-vSvf z-F>BbYetHSEG5F2s+)^88Ot4J>;0GGBC$wX zwzfnB*pBrVvq_4_MF-iW*3~}mmX{rD9Xn`gulBTBa`(hhvSqGpQ_`Aco#`%7diIa* zaXKL*;}nZPCx)qFg$NyH{OOGG>mG=i7PxZ4sE|$c}h}9&?F)0kD zDQZo|I=S)cDKfOaM(*A6?+Td^>z?-YuO{u8L&@g4NEL06M3YLeWd6(5CbNFB^Wgf8 z@5w9n7ak8?Z!AeiFx{U{(jGhJmvlzEqG;d(%CK(`@>xc!E4KyCtZ%k|ATGKqL+BBU zy!cGr^7nF!%Xe+Ao_~18;itAYnpJe6g4$m_X6{tt!KQd*|7X;KMpuh$y5i}p*3IpO zMymMOgBajt9LWCjeG3P|%P~@9Z@4zM9`V@>m_nH>f98QLANp+G`XP3C(O2);P|dbE zwITogTDj%Jkj>=!z>Ka&0K*9}JD-5&Gt2CqdQ4GrY&-;3#JIu-Yix$;Qi=&8rU`k< z2AyOI{h02ItUXx_jx?qtsE1eD`JfhleUmJOjjyabL@K!I8AT%rA`$ipQt)CbrV~1X z#C_q6z&wkJevYP!pV(OnzaHE7&)F;8;wk%v@C|i78OC52<0;Gc|4#E2JNeQ!>O$3w zV9jUM4Koi9X!_bbBW^J>LYibuevtYJ)CB+7UsXPFsm*f-Sc9+7zFZ_KuHFG#->S9o zL3Of>I8XhQuE_Y(l--&Sjb<>vL3V$j}UWy7bLlCNkwC9)fu7r3-mm{g7Wcd+(E%qp+s&A(oS3q~g zN0&oGLhz-_&H1cHTTxGsNnFjYAzn zjom*v`JbH4KRdZUJK1(p^DHUkyNf9(DivLTOEb8*&~z%5yNqL=ZldJtewHu z6wN5<-}LZ_562K=f-uMTrpXV>sq-&nEJa=r#u9u|*Akq7&L3Q-4rZf+-p2@4B;1k_ z)o+}UWoGvtD_%@tPmB&z=2*pZJ?P1yf*Z|f_8pSlkhCy+NpyO;G{+P`duCyb=$&Kf z73GTMzO+O}e60yGpY$dv6oQQ_d=27-#Bz3mjHy@{pjXQfIMaH)tsn+gT z=BxP0laF!^%g1l3>j82x(Kc{oSYPhxfShJ?s=H2P~*-lBpvvBp#`<6A^c_K%Gya$%)yB%J(;}h0RB{*AEALahygg&LU#<%9^*Jr-IShdf! zC+Lexe=@G1#fHui6&p*GCBEsIzKXMy{U}6M=_n^}^MrAQ)GU6va)Z;&(*fFSl9~B} zvJJx!Z3RL4M@XAad`QhF#OZ`DvuvYM@#?jfa2iD%JeV!|Jj}UGIFu0nB}wFF%!Tc( z)wMfPTye*O@W{o*yY>gk+F+!tzJfl(>?1su#lEK?-EK#_z-Z;|zi)iaqcYpMovl$} zpP;8zZ#^l7dmJVN-)oYpm{7=ZGhm@x=2v5?Vjin9c~%;oxUa9f?HY1e>{U%Ju#{zX z%T2f9@gB>hhgDbqj?`IP{6WA%D2#?*<0n^LO60_qPLY+`c&)xJy&RN>e+y-{fh3#`egR%cEI z)|HPs84%L;5(627*STN?wJY?BNmuG3qRE?De8zm^F zsVJw?;=4eQbfJIUNa32<%*YqZtL&PJ%zCeFJ0~}IGhuMh*i}emIUigAO$lTBp4Z!Q z$Scs-U}{ge$p#O#mG^hAkwD!8>*cn+lm0sbwt>$V>&{59cVw?pLJ!3Hp|ZgQPXc8B zz!1sW;2aRBg?ob(?d#MO=X{#V$qn3|8*w_~2PQWlt0V372_CGJXWn)!XuYpV2XbT@Ea?vytu|$1 zqcUmTMXWY9lNTgE^jtKBpqI5Fg3D?yt6V$Z*@YG@4{kLk{5(qvpCn;{7WKB-8!|m~ zGlLcHOfuv`*4)yOs7(F@J*-|nv3wT=hg&oSDJ0O`lSj!Gij?7XGyEwis{;ohus_cq z&0Ke}`}vD#=P3qqq0TF=?x`1fpNpq@lw%zl$eqymyeB8n7 z<0}+j;yy$6AMs_PVO$DJr1}roAVNo5Zh*0I zyhvAC4J05^V3ijQ2ksXqD|NXv@%rfpLSRqPe~`5=;t=F65C|&9{)3kG?7M#|IT!8i zR$K&a$HD^m(-+V_Lf^WXSdw~QyS|{rwe}{~{k1Kf#jdrw(l@{li3Q$YolJJ`R64E% z_CTo@`P7TesTa9p7g#w#DuxMelOH=o6r+6~yki}*9Ma`XhU;+%w{5HHKG_*Q1{SKLz&uR00V0^oJt696rj#;E+G*Z&w@{L{}fsTrDiW|L2J^p9)_X=oC6q1iEp%L7ZZ zw*Nv(2Cb$8$M;^~&s-RmrzByK6D|#~kk%RRuH6S}edk&y!R<-cwm5sCO|(8%8E=nK zP5z>!kO`ghSv30xfqE4envagfYtfzK+b+bb0KoGX;{DL1>ptU`D$JFwK4wO*ck5%TLNSu%kV}zJeBebZBnp8*}Dk;SGj|*EntiI zvL4)Vj3Mt%s`EDxa6MXCHsHQ7^_U(vo1$L#Zq;^e zVqY1{{*xb|?bhl-({JdNv}bS^S_G;Vnx3e8(Y1LmRT_o49vW|(E0=xd0+P6!_{0UA zmIqO#Z7x|0d2W+#n@zPG71LhOSdD(OZvC!L_5{)9WB1n{T5B(k*;!ie>hXKj_?oD1 zmvpX7R_DssEN6c^hHY!iTFs}8`F>w4T*rdWbzk1w9r+9JG4tyQE?dA8!6tpeA~qhm z)w-p!cMKzHj}Hac(n2hufqQ0^$SD8WrkB|=DRYio@#S|5K5!|V5(C!E{i5_YKiTXR zv*DvLjNJ9hd<{8thIY(;pnz~1oDSWXjA21|NGJ$SJAU-YKQP)ByNr!DaDLY1$iZan zq!#qJqG#952%|6&F&@cq@4;j;9v`y9*pr~BzJ+DaczjS;HPH%nmW%igjsXv{n#?nf z@L3QZw(z)?!xrJZGeJh*@m<3h`AAaR?~(EDh9lF^7nMNZ6|c>^J+Zq{`j9811)hv# z_eI~Q`2kZ-9=-3(OC2AW89@tRzt?w z=%2=hVVNN8W!XXwv@uCkZ-eh6MLKNIH;(Ze$C@{eaRZ}lD9Q1FXI@5_IblyEuz3Ll zcJ&zl`?2QVk8%GVW&M5ytN$;o8<#xabYeuba*Q{PHJiq`j#1WwfD;Dkk?xnq_?O0- ze>VnB9PONP+wWI55p`l=_a*46SK{ve{sVt=0=_;32mKhIG1i2d{J-$4-0BNsU#>fhdn{n1mb7zo6e z#CcO)*N*G4vusI`=&aV@KY(Xz=GK>DUB0C(s5zF*zr^LlZQ2m%G;9 zA>MrI{n$3r90L=I`>g{*3!gl1z?t;G7DG;u0p%?_@(GX@;q-bBWk%7ypBLNsICiU$ zhfmKDzhLb3G+V8v`q8Wm`+@ZaJ}B6NHQ-nqx#e4(R7b=7;kH>a-Dr`S{luozqgaE{ z;%ul6Ii95zT7k97y=NZ(GTuf{ z6Edt;IVo4BB+F(z@_i8rxuPz+r`Y?`g3riWV-n_J-S7*w^6vek{QlAA{iEEi5jKG| zTxSyFlnM>7G@cwc^IEVDd05~4Bww@Q;TjWTF_)<sQJu2{xJghU#dhxpCY~60A&bx zWO=p1CYi1{GLcRE8hW9Z-gU)X|(yJQLb@>y>=dG+N`2EtqCzX zA?_0~q4(~IYvtRu-JzrWyCc^n!YSC9b#Hg77-ck&8-1~mRu&fTr@r!V{rIA)9$IE` zsjQup3iBuUpDe6MW&cTDElh>QqN&isCKPY=I;6J>U=;f)`7g+3l0-UA)CFr$OH3xE zeQI>uo1XQ|(4BhSwKbNQJy*Qy%@qlj06@nDG*M7Sn&m;dbahE0BG%O?@5L+2c3!(eZ*P+3b-Zv=j((E3>+YU5>R#>hC=nYn3fTa> zx4lEKLP7A_`Cxsl<6jUK60+aH-8twKV})va-4`#+mBX|yAvfufQ7E3m2!C&cn;2=aX0mS(%yK9(<1a4cM3MP4_458{sv9E( zlY4{TiFDY@Azqq(gRbJ!sZPnM0 zt!W&2|4>85j(1f1BUTi*Cue`Pv(0nZvlPMEL$<8nkqlB!-*KSCx?^L=aM$V)OMCGz zJ1gD@+5Va0u{H#KeA{!|-+SpvEtKfWhWd<~d=z3p%#w%XcaFL&TE~{n`z{urZYQ?_ zvpmz1WrqcQee_TLOxRT(AK{OWv~V8Wmm_Qn!Tc2b#B`37Gu4#Ee+OzZC(UW4yE{hs zc7Vx<&KdkT?2QFAHQ;Em9zge00=q+zG|`$^S#)#8moV2CO-@7vFvjjuCi_-wM9ZdFUYA(h9Ze&&zHO0Fabu^A)uGv($6_rm$2fb{<>X}OTJXk>iB2m#|>7>O50x7`-;ru(1W)Ns9pO_)xKzxNSJ_WV;#?`ji=s3pKj24T5l4)rG#)bpJlHF4Oa;Eq z%uzoxLF}`n8Z#%!kEFqm2;7JITi4ri;>-8faI=2}H_Z)Kcj?z~%I_q~SyDv4%)5RK zi$rKwi#>ylAh|-1tG~NeVOFU}=pdj{hB1V3pbZ<*Q$=@))1xVbAaD&_zOOpLr=}*s znBvhIIpotFGQx+BaG@hDwinnp2+H;+Zw!828@gl@mIYU z9atBC0;`;q6kD+hI=H`ex0{xd`&87er{es#FD<%DG-7X)ZzFK@_GVlwTv4>%3#cxD zg}G>i7h~x;-(s_{*#soMfDV94?)T)m0vipxfIMMUB|dLy2~4B0B%|y&G)mVI*d*N| zDF|Rk36N!8(YH7v(g=c@6>H9wvcn{`-hX*;Y~IxCID9}Z!uFE18@v51Q`Xd2Nn9v2 ztU!;SEPc^WM*hEb+X)Qn9(Y)H7^(Z8B$d?tRGr_$ zx<4an|EKO8O(<$L6od-38w#eO`U3=xRPRY1NcG>jtH;=n=Q24sp`5Z>AP~c#O2!x-!!*Cc45Jgg678Z&NbZlbD&N%3SvGL^^Y&e4WJT@L(?qWxk6t|2kfMp5J?(c+R`-BgjTkq#?`|HoBL)of6B z|0X6vWIQBgPY963fUF*W{XB05`VTxvMc#pRBjSQ?BBQ8-fGJ7^H_h$jxgxenk~iQ| z+!}B4gs&!Y4QSb*@z?8T?h0W)6$<@53avrq1*ja}eAb>PqHPU)f^9EEOIQP;u|A;5 z6RLfwjJ_o#Ii-btfh5*GnH7fn+xgGjj98}1J0I^6@s@_mqH3R79gC2-3y^G7WuK3i z#c>g(t0j6)wfuaXG=gM_?cC=Q@x8+c9&3YVs!T!>5~FqvTPdKl04VJnpLd>51xdHU zD~RA9Cxp;ch8BoK+>_@qUfIV98kI5UnFz|*;VP;ofyOrI2!dHid7=8d7ry98|a|dDIX24bCrMfpr_JQV#H3R#+Nb4 zjJ9#bMt}B2p-p~h(`Vz4oacjtp_k$(c;9&hZSs)qa9P+gQ>VklSAr%m)DyD-0! z*Ir{63Hin6t1ETZ56ADH<7J>qjVc}*Eu;*m_np_N(pHyfyS<4ny8Cqo>m(VVxRkNQ z%lMgBb!EI)O+e-Cb6ayGBsEK3msm_qbaf_Lp2-dq$m(Xr*D`MO9HJ2Qk@q-?J9mz_ zYf7k&4XLThm9VEta@ALXP2NR!5o+?J0JX3>Bl-j1t`*TZs4ZM9-_f9Cza}3P8$}p5 z>K50Q4RMtgQ%G!@CBCxEG}p89Bht)fc&osw7(_rZK}1_N%=LWRQ+B{b`$vbDz=xiq zg^+rE>Q17RvumLH&($&L_UhVf|LbuPkh z`~zgV&+kJH_l$LZ4|s{}2Uh`=sf*j7ml@Ua-T;qGg(VO|^cBfg%rmm;>NG+0WQdZ; zV*%N*kWl1={;4|w$65g_Q8F#SWw>qP1#X$%mmcA~B zMfPt5ocn&3^|;Ta#u$n<3+T@Q`jO))=lCbjF(wg?qhylQ^|?DqZ+Pa1IlIEy%>rl? zKp!7}_8kA*ulw^1=^tpa3H>88PWIRxPHkTTygb-PNv%a!z$O-3SpB>v57_Xf%wv5> za;73K;J+OhOS*?JbNaAnesXtWO4u_$ke=zpMceM#LWyL_zC$qiZ;?yfMJ{1~CdqnI zB)L(uf~6p(!6V8uKSM}B9&`Ok@shPDo)A1mvVfSlkuy3O=g$cZ{BfA!UK;m0$KM=w z?@gFl&dvvy*POWN ziXS{#jQ~Cb@Xf+NxiKsZlpDhfesIm`c$tpOKkcwkt-5M2OVF{YH>&-NfAT0VBdw?a zs#ThxD4&Bhq!m@d$VNiw|4EqId~P@zUIA)+tqlawRxmXsC%zEmJB66YlDR^hT{;-H zNOZ{0$zs~4Twqsf0eZ~)V6n95PpA6*4u0W#>T{C&us42My^FqpUnsC=#oI|@8A-HE zZSD{%viL#*j)aD(GN(YtB+<1()z)Dl>UM1j``sWqq)py^c$j~Gn0tSCjQg40O8T8J z4x&tIyRN{4{X`facfq5)Nhrh}7`7;^`+Vbcn#nh+{36{PRenGM>WQ91Up%W`7}8Mec9K9mPYXPwf$>b*Hq2Ru4XZ@<4^}0;h0Umr zJxLl~(7=A-3N- zQXfvz1;bpy@URE_7AWt9;vyH>NahO{^+``dp8ENGV|Imu68BExWm?$AQ2AeUg$`~$_6f^Ba ztSe$)7*@T2cNt#T=Z96#lV@<;?BZe7V)9IEf=JwnOjPHTi6M~Qfx$u2tlvqj7#7J7 zQ6Te_h7wX5r!*+pjimO9aIi*!Y0X$?o*`fwRhA?o0KT)r*m&~ZbsVnR_rK$*dTdxj zF}Y$ezIimM|SWzV_vP3$oa>uUrq^ZM=7Fbh-i?fUzI zGn0q8$yfr!>}O<{VGu=i;YI}xG}uJa9*soVm84Yuqk;$D(_+9 z&YJahQ$rDXj~ypPpiI#)M-2~Ctc|=A{(#WQ`uG##9!t$X#vI%=_e`QE9l^Ff9m zhq&8AV4~RF50KEL{;UgOhaY^tHpGn&v416R3?~&-T8KL#o+g23{(1t=5zE=02e6|< z+~pzET|(Z84i(}IdL8>zFU?He_DL{%%njC~lJI>)+_@oks{jXXIYm|`DTV$$XvYqa zME2Csu(WnfW!F%Zw^zltIH4nqF?*VPBI6J5FmVJB-pN=(8#nMnnN-1)6-!@!(u18s zh-9O}6?=W6Jgm#wVbzn?NIXe2+Sm~XzL}};^0yF|&#Vj2KlWk)RqvTrbrOCV)(8>n z5E^Ek+@h-1qHXVzIzZRPA?}l*VHvwfzyVkI@Zb%FGGzmWn+k6d#jI|&I7b&wvY{jy zVdwr$s!C68U+Y!;rmv!3O6pm$^MCcgaoB@;*08L*o?#ikV~DFA!Wz0#E+dY`aj!zJ{9bPsn~btTWo zP~nT^8C!=GVbVCN-Xkq1fvBg-;1g*s>ATzq?JFGO-WVE|uz^DRAe%zXb}I?%t)3~i zP%*l2^D$)^8hfHS^oP7i@xoYLT1=}JHhB_uJE=NKme>hj7peQ$9pts%Ba?!&6v>Ll zx-sHj^$45tpoJ2C`4E>kREo7&lo&m`VjlaEJ3}0GQrfCbXoO+3g-X?@P;jg(%zcEG z#OQS{JTa8Y&Ut|Ef+23%5c@6Z=xH}px**M6|66epV1kBVDW^?t*c zyS{MdRJR0k7eVLQ3#1`!@?nY}ALRUoIw3U`rN5dBNKZRI8~yK02js^Nc7OmvnV-Kr z$jOG3JxceQpYfan$m>*7@JqrDt#D(HI*HoJ>^zD1wfyz5?L<9FB(kX_2vLC>Q1&jt zny3}uW$mP3zGtva&fOejIB)hhfbdVn%-m`f1@VF8azOrZFkAGXww6Kuz##Vtsm*$sjUuT~eROeu^(2V9B>^7&TlD4A$>8WSULG`04(~d~ zOxA=j%rXsgR0&QaKWJWrDni>NrgI`pW#W{ilnm!jm^7Dyr?aXDrREOuO9#1Jf!eL? zDN-)67ci>>QuVsL0HTM-s3)J4T0r$Savy}glCypfaxWR=lLxtFLT)?DyIWB@fZGEp z>}(>FONl?VU$tpIX_NMu!Nrt0AUr=PiPlFXMP#@UG@C>S^+m9o#{NP|0{i^jARjfz zJtvI3Rcr%Eg}?o+QvTT`xL6I0!P*jO6L;cL6n2cC5pzWr~xDgeGngY_kuOGA^V35D( z;QR#&ZDY5%DHJm~JU=Y0S0`itMG~;gbo&hQHyxauz}ZynA4sw`d_Y)hLdaSpf&P;$ zK+LLzSu{BuQ9teXpP8pjD48X}z4s;mAogQ-m868Q?pqH2oP)dNSa8jSX-3+rWH0rj zRERs_bkfF0#hcgOmF~*AM;!c72Y11Nam>}a7sCZK*7&Hr6Rw=e;noCYzG8l_{4V?E z!_EUgf+dQybybw``LpXF*D*WqP~y-j+L)wv75&35l`aC;o9#bexuO1qLB zCtqKexi>zH{#_1!orBxyAU4rs+zIwu@_m*k8{^JTI$G0REMbTSm(Jj>{+2anGCv!jj^~Ch$bDKaI>Kq z(pAt8I?~aulk#3Td!PIuf1!idJGer}80TcaB)N-v*+(9HTjAi79Naomwrv*s&j)2c zj|D)H;9cJg;cL3W)rtBCJyk`wjUyroL`>E~xI&#JIw@~$uFt4@ABuq(RkCd^_7Ewe zO@5`apXDzNaNdpyPd1mN6@9yB3RU`N7u)EDz~~Y=_~8LgjPWzZHL_VGT~^={<@vcf zOMn_Vg9*HIV7%7oPqDS`jNNhv{|za5%$CG1B55%YjuMT;q>}@*BxY$ivud)s5lg}D z*X~=ijdAhXIO^ji^|}+ySpA^c*(heq+(q$i1KjNa@YxAM-DER}^RL}kuM+?f!}AR{ z+^~c{G{F6504HMY_X8GfnZ&skj>8%P%EHQG%jVVVEKHjubP-Si7oW-&{x?_M0C!=a zRFGtutzz@iaC4yMpvBL8Y_@q-fb(yN6(ccQssEFXxJJ7pDIFtGY`h6nqy%TjIQ!y% zOWiTReKP=2;avFw9w?J@TGznZU#j$?sssP5(u=DOG*#)Tssrb$^b&ZM(|c9vz1No0 z-c@>emENaH?@^_fRp~u#dTHgq1}udPrL6I)D6G;1R(+jVRS@_;T?5>%fxT6<*PaA~W*s0V_Uu~%+yRURQV1P1C*kvAB^1%!^;`U_ znH`#Tr79cR@)c4j*_RIRr2|~)03;%;whz5s`Q|{GMEmBz-s(NEIA{-)37LVr4dJq` z-N$mSNQKK7=XKXLIh!|7?G@6{;B~4DO0DgcQ~N=`y%wC0j;$|a$oQT6usn+g~d6MlSd#P&W066=V16a>-el8+OY$y3eY8FXlHy#YaZ(hcArY zf9;q}@5|_E05J#b2EB(tFEfx)7hup&GUy*M=pSYDfd>6#gI;0KPqEr1Lhh-^ZPN!a z`d}erhfN>C=tB+qX$Jjc2K{tvhZ{^KfGHV0BY+*X>BAU(xIwQr=w}%8Gp$G6U=aeC zhS5g~U|-twk2CrxgMOAl{~Lq;3G0_`Fs%R<&FEtUuuhvkmeJ2P=$|y`pEBs@SUcTd z@d8*JqfZdPnr-^08T~T`{al0oS%dyLYqJ|HNdQY^^g02o#ipOf=#vfl`3C(0gMOj4 z#SNAsfaw{1ssMJ#rcYz^iwye32L1B}{R`GZZmdM0{ujY0o=##@>h!6fS$Z;um)ndB74TSn7Naw_BP zX-HngB&Q)4le`#NnB?cvHTZoYl=`EYsrNB=B|=7B{-(8HqfM{R(Wh9m$+*eUr=q%bm_7uP`V7cDa)(O@k4Qa+xn_8o`#}Nt%}Yydn98 z1<6=&Oz-v@R!`1Z9cYXhub)Z=VlwF-w9u;AWJ9}?(=sFef3sUs3}?gy%lRp&m6@Mf zjMmRjGtbwzW*yXQY?;4^nZNj;a?`<9BtPFW|Ap4oLUKAYKjUGtp=JJ(>a9CSAjiaCwRwJ8r!Ub5V)gyX zYD`5Db_3;(O-jusYzT~=Y?8v-sxXMy=lab#kiYMGn5J6MFRb+yRcvNIS@~;%h*pUJ z-zMq0*}b%%U)s+t?SFAQJAM{xMD08hH;?TnE0V>(fl9?lNZz&h6*t_H$1G zriDE%z!Y)`{p?~=aI1ui>t|ERBR8j?)sqKg1`SlR`yo)6LsqlOn0{D)`B;dIF-3H3 z3;RTWt5?N$P|b@L7Qm=}dMb8Lc9!tw{*5BfM9%&=d3b=2DuR^Bkd(@q{T6Q@OPTkz z%u01XH=`e?Ad5Hjit)e9yE3ew`&aL;lFX}#cq8c7WznRpM@fn{sNXChKDy?Kv|jxd ziSd}IIk5l5R5p-&ZGciRD|hEayR`TFAmtO2_au2gBNh2DxMyaPaZx5ty3>b;q+5OU zQub9ssR$wwyz3^)Q==kI+;8XOTC5`H#^YL7AB6RAn!7iceVM>EPxOu7?c?wDkt<|G z@bSBiRvY)a;zpZq_u2Vpu*KKCf1Jp)9#J1(%zoUffunBUnQ+9d5jBtZ(K$WD$T=Bc zjLBDRJr1VQ7)&!N-x)bnsTOCh)VkYOYvrSt`?@dpHDB(t+f3|Lf*PD~@yYniX7%Dt zvDbs0NAO|9Db4+T5zUNPKR(z8lXLMx9{(#D@d8}`v5a{Bb)Se0j2L0DBTS6=g@_DB z{Cq?bBVHV#WyFgjRE#(+!jBQBMu_XAw~KBP|Ax(7eY*YYfhah1APNsgb?=}_ieQSJ zA<*E9zVY@xz5_IH>uESex9dmib{${vg)2toyD(rJa`({jhKRU>|?5@8P|9-rZaE8S1|t2Z?Qbu=?^aJyw~W4+gHzO(?K-FMIC{`A~(D#^3D|E-GvL$Pq~-syn!t zU)bCHc3=B(Uu4KzL%M)Ox%!Wg<*h#4AdmmWoo7d%?3@cgb!!En;9Kk^0jNj-ddm&8 zt*`TpAiu7oe?y1Z_OEBBkj7vL91*h)0e*cS|AsJNh{W~#TWlx+KnOe8Z)iFDC(;JQ zTcq;(!K~8ZU26xqaQQ^fj+Sqc5=eBtw;-hs{q`WuU73 zJZ%OEx4N&&Acde3!9D6;c*5sRdg?ll;?+B|i#JvjL}%?xkKWidZ$`L0KiIR0LcHy~ zOR$4t|3R=5)-!EPAKJ;zCPk~F`*f7ZxDkRg@|v-<&+H*G_KS5^F#odSnN2%3?trwTx1QZ zll(VL^e_#Y!EQJxr9?A;CW|atvuF3w-qB?{g+-UFU)nz4k61RUN7mnhSvzIvzAh{C zv+%@Q&jTgn5+%t?U}ynGbY1q)+@x+BhNOT#%8G9P5!AC`-v4FlLp+q$jdf26`GMyw zJOD0lw$fS-rd4c?lvP|oFwlwtvqk}GSk{;|lZpD`+uj0$4%i$VXsjY-X$e+U7_FR? z4tsPRi!Lqc+ESl0OXjrUrV@%9h`I|{=f^#$T>ZSVZpv9902XxHPVmSD(}mEp7Ptqp zsifvrBIie^8v^Sy@;*9kF*Ssj&9-c)=gN{Tm1V2cji=4xs0Qr&1Z7et7Crj*Hh;gF zagWVld_H9&82cTh{mdxqVnyoFGu&9OGvMq3sYvup&^G_8+hV*J`%?h;RCms~gz&Z~ z1JxS@HkXul3IWLz+kd1JW=xaN)96(77NIIaU)9;(@o#!Zzv=Bh)ytpiZ9dhDse~5Q z?KuM{Hr&5^X(}&KDFg-sS^r1(ydS44u8N$_={SR9QrOOrYf0x%(<>V5E>3Tg(~%?w zyr}M%>9C19a$kLGI%1nbTi-cGKBlVwc4ih)ts!HHg`9nQ3m=SWB+pBL2lnZU<#o5G zuav95n{JU@m*b{Ioc06hd@Lt*!)N}7gEB^$2Y>BH$Yxe|7lHvozq+C6j8&8$d=obZ zxV5bS+ej3To5%P0fwW~M6L4IB%Kjq}-s%mF%#2rX1*Z8WcdYLtTs!IG$7JjFw zBSrNRXX#n^vQNN+mwQ~Q-#nXVKI`;QVnK6{X;w5NK%O5)bSF-d-pV&bzOd2drZb{?j80>`_x$>X^=QS zKLa1P#^ZOjg}pXJ3tu5}Za$lnD>AL`ZJ8lbJT1{+`4czV)P-k3jU%@+*7T~@^d5s@ zGCc{)+m8~gWml!h3970$o@H`HvSF9W)N8jcZIZDENTs1qH_6nGON>{4L^n2Q)sHE! zXchiHDlW^0DbC(S5;$E$|EpqpaeRf_ViaJ;#SW#?K1|4WCvcDV)`dt^Gae>y zf)GQ{K*l&ViPlkwA3P&)YSGR7@aajaUKlOpGsHlv=%)IPiyXX@LV7jQ8U9jLNH6?& zS{H45XK99_*QORJB1L)GXBla_QVA#SlvzgUtS(H6h1I=gnMC0=q3gJ}ZT4}qOscc4 zTv*Mwblm;zht3se8$3X;Yp#}#`;UL<%);veToH(fC86OW9iU% zY2NS8l>S>-&)6Gh%{l(;fAEU{3uJ!c9&F!qcr1OO@VJ=Jcb5DMM~n3g*vu62+Tg!f z-gO*%(MCY^A@FjVmt23-tR%J1^#-EMLsE=z-St&z2RqA>>mgJI7+@^7@sHq z;l>CB#?l`wAXjW^jx^e7oz8}nEY{;n#LAYkR|#ajzmAL2t$D)fCHklttKWa7w@GOF zBQEb1kv%x7C312@4n2uZaH^E3;~2?N+99lUNg@T~VachzV$)2<=x2HI&5~2^MJ#}X zl5^NEgf#!!WA#^v{g3;t{x+UJ5g`28L*zdw*-ZYUn(0b+=tZ*b$K9m8wgt?J8{4*1 z`3&g-p@|J+bLB<1Yggp4Sbn4E?fcy2YR2e*cz+tvfQ0>+4x&9YV~Lq1^y+n973S@Q zwRIN%3e7B7RFLo%P(P{Bvik`&slBQ)=Y>iTXi z4mu3{54*Id(3*qx6%cJV1a0_v`DeL3QUnFb$dSs5hy6m~x6H-|p2}CU3W6hvu??N> zQ1ZN0+!Ch9J{5kFn((-e(}ubSJ7&?(LkSNuv7TfA(h6%%Np^O2D7nWb>hDsLH{&Tn zTX(o0k6E%gfgbD}k}ixR*v;9a>9;IQt%ht2gmeCpe5`ow1hcT}GT~onf5O!XW0}Bu z(4~O(s_1}%3HS*13IXquXE7whMbcJQWSS~eDMP(*u+}5iNoyCbO3U=8*Xq=BX)j%p z^xb3{0mPv}n>yc|fSErI%Q4OOO1NwTQ*Pcn>uSw@QKS`PhabM=`f$@ND= z-%jJzYsC<>&LU%8zX3mSXwhU=La+$+If09yo?gd2xAz}m?6Bo$Z~%sahS?;JP#xB) z_tQuR5B%nIMWe_TiNOe7SEev5-Lxku*8_;-e~f85T@tU`Iu;|X%{lPT>{wq~ zksYgyNv_pdE4Jq(>WX8SSJ=Wc+azJcA6?!ZT#gtK0tGo$)A0*4rkU_xpcb(X(n#E4 z#xQ^AVMN3w$17(0+ISV8dnSet>^b>j6g+Fdu-LxnI&o2x_tn`RCg0BF*_c;gkUZ$B zIBsTM(JntT$5J8OG1zj}w=g1Z*%^p&)gPqgFs~$~pIIpLw^=K!w&ML$yvdknOGvGB zpw0TRF2~PqEfDmu{!9{eIrB^5l;^=XsOk}$y=#fEUTIFLg(qzt9D3R4yW4H*UDF?q zl^!vuKnE2I?WgC(>*D;)VX;H~dI}PiT-fEzhDozj{VDO$EX_MA{BxdgMfuq5i_`@@ z6|V{9{T1akR5i5xSN5+9qZAh@bW=-iOiQjG9k__Lb-?B`QaH);$HOdHijgL8JL+3-ETOa$!aK{p2fYCv+F2RcDrK>R*Uy|A>1>Uqy zi6J(xcG?Ye@xb$9li4*?O)7_?T5QdKO9(QhjR#QKf4g&w+v zvRc{}_J_Pz(yXl;TNZlS7JA7faCBOqlK=R(_JyAIgj2>M2p0?#KwQ+hxYpQ76 zH;}=W6BKP>&-cSqL-f@Pw%m-iN9c;CN#B~-68ERHE2bVUoMe2Su4tRo^ayelkx%^@ zuxp-F_uDi);KRH7+07)G4z`Tja?>hWv50utzVc%?kksnYio<~Nwtbbvt`ibC8eXJ# zl!NLsKy{nG{K%GsTJnx4a|YzHLIBgqJnP5dNi_x`6^c-liI($?UUEuxvzi2b*jBX73$OE z<@}{vamSNvunLm3eghjw{;Pg_N?7%Yf{ccm8L)v}Ns{{ma=y}HOFlWfNruo$8SHc+ zH3N&{$#8XY8n$p5SR-~SNw%0m>Ec%KPA{^P$$RUDlOAY{KY70HF-+Mx5%Tei)W^E( zH{gN-v^6ZuVSoGv-%CHls))H{5CRH)M6^s~{a~wK@#iE)W%=jVC8s|3W4L2=Pp3sNvE*=fNW;yV+zKB^k6UAQl_)C(DD$AEhnd; zr>x~t=I^VRb#KcjwnRV|?iL9(xzI-A0>Je>SwBe4L&%pdyhX>ZvjNBD>YnM!`B0 zcv+fBpGszeE4@NUE#N&WN;>~dp@4UslvV&D#uO0aS)1uk_=APDI7S%a>>+SW_Tdsr z7A4AI*2O>wqo87POz}POmGN906%*!fJS~sQI~eO1)1gmz^7gVGX5F2A>m>QJ=9xWf zLuJ{pd6kA<+J2yTRmqyh0n;xV*v$lkb@G1G?F|cqS8T~^UfHvLec*M8oo5P@7+q4B za{$*dSoiZ@cE!M9JgzxSRE%1d-Pk^6t&jI>5MOo1>?=0xYpiA!XTZg<*`$u7{ojyAsRBsjr85JjvJE7;bs5o4`L27nVTyJ&dY7Ug?37cFPrpQ-l;{vAS zPKlW$emr*bzDIK=iJtLTzS}clxN4HP@M|gu+=oYYo^-V-D`TC(iHZ7L- zCn=75Akfhyb`D8+?StBVmW*c4p5h&WRY}Z*jP|}RA>JsNDiF8>1jdG+;fl*Z7}Tu) zhpu;zYwF7Shfhu}#Bj6(G-9wV1P(WGhKp6Sb_P(n>I`HwqN6h;_TZ&;D%MtOafSqo z0qHY}Ed_=?O=6>giWhoIvCNQYDGF4jEn}^8)Ha=JwNwbx#IU7&E*A}Kby*wba+-96a*AoSy%Uw8z4%YW#{frX3@iz(-S9XdHB zmK7KsrIT2T$!Gz?*OW_K#Ggco-*y^fBh#m&LjiX4EbQiAUFUcxh-J+IG5i&mz5c)` z=wWR0CB-2yKHcm@8vTL0kO)PJxW_+0VZL#GolQcHZP!7mV@|V`wJPzF4b792vqw z74jusz3Fj;SIXJ!kNg35mBJ!FVuE16@wd`)cwXU`Sw9Ep(C{_=< zJEnbdk>ssT@wrb~Q)+844IOpTX0Vf5+L7|0A8t&Fvq~3{Y2zP-)ulY!{SW_-k3_^y zV5>g%1K?2LF^VsfQ7&3jT$Z%@0m4$(MKy<$yoPfl^AlFmQ z2*30z4#7EX!?u^%?hGdOWj4EwYFp{MV7;Iz9_K~kk+Ih68rN8hPOZrudmjwQg>Y{# z$vSpnyIxbQa_Ii#HL`*)|3&(&PH~>kx2E(&T~WbEle|sIfBcZ9L(V}v(bxZw;$^f_ zY(XqMZCru*aLk0__C1T(>~eqcfM(H`*#{A&*QuN5{b##h7CWgu`vq#oqPx_LnWK*72Bafdx-HrKk_{6jw`S;8(LqfYV0A+T}lRwz)5^L>A;_F~owMM5Q1Hp++b zpuJ*?RFfs=6!0>ksI!^iXkzhulE)Ou>5<9F8%B0R(^mVUw4|o;@~u>v(Ihif;Pz~K zr0IiKXH0*nE^e(!CM!*tRCj=Fs_#_p=-Aaf#ne4oQnL#zIy$4EU8CFTZ<=Is3wSG~ z>XKEF|NQ!EbL2-ARUy?y4Mji{nSIB&`0C6Vr3x+_mVRKbIqpxm^gMQp<7~Z^UA7H| z8m8{ETRYyU)NGbG;+{J6=aVHnV&1J`6~AE)Ri4C0>CBfJiZnCB>f+Wk&uq89%GGwf z`?uH4uhWA28rS4Qerskai~()}392cLXS+8ub+(Ef^&y7$Zf^UU4C28Nki+p5^^CCr7Drh%379C3nq1^uJTfJ|4+%m zRJBgtY&r0+3S9^kK6Ue&i;K8WPMYxVk`=PT=*rbU=~jlo4);;DRQ>Rnex)-h~{|Of6?0b z=QnmtH4aqfxI;lF>*Ah*LN5dWcD%8xJ?;t3=XbnuPx|biXlgD%s$GP%s>F}+G(2}c z`wv*^i(@CTie>&Py7Obs{?2~+1Q<i-}h@4mNACbyAV1sNdHaj z144!72dU!~V;F0ZVbg%l9+Mc?#{TUs;lt7NOox48>)zR%;pw|j&Qno3unS=&a#T?( z@*XU<2jbFyX+Cu(yfg<<}&IWY3&Ai3` zKYEl-{X@7Sa{znTB>!Ox3)_0%%*cky+UEisa=@uE_^qYZ@V;+=YaOu)n3X^l=sqwm zwpPpgM_r|$UVOJkTUbrg2t0X|(%j~!F`6?Y3eW+-H=ql2HYQFh=SN0$Hqv=D;=VcA zG>2JllbCGqBr07$hjpiNiN5`x^eq+DTOrE~5wSTkTg6Sw9J$S!X_=#N%n5p9m)kqGXE#R%6kd$BTr(=V^-C1 zL0ovrw#xOo;EIwPn#E`lx2`mDJv*PiUuVSc1)6P4+3h}jmz3OCk5ky*(l}NTgR~VC zw<*(<&j5%iYxc=a$H(VCz4BdF@pnH}4r4KZ3r9$~MehtpwE`*vzaNd_Jy-8cU_l_>w$f`Y?d?WvZV#3$+2Bo-qEZ%lI|A2DUaoTH04Q6 z2lkorWb5%*}iU6vav9(>|bJ(DLs9>R_F z+A4^p<@?fKzggOvmZg&feZ^M&8Xkv-Kxtt-v*s|%6y-7QEd3l71DiERwtkLWKPPDY z9C()Z?fB&E@$%uKYAQS{Dvqj<RhO#jhg02>eu`PW*Qo1o=g=d&+P&!KaoBirU{zOH%ThPJcYuXD$Atl zKIPBKmFaU?Q?7J@t?TOuvVdPZv4c(vrQpv`~;M2PSq8EY4+o;otv__gpE!6 z8g!fN@D@~OlWBgsjos~)pp|tip4wzCjeTZSUCGK2Zl>nhTdqUvKbF{1n zb3apOXI|O1GiYTHmcWXqV6BUbQ{45hN+rU_Pyx7yG&0+Z3)lJ32PR40>6Y43RJ-M87QUox|m+nOfi#o^Bst7WBB zeAmpaf=LAAxvCks^Ny*Y{V017+(x66Kbmhge&kNs))elLDN41W4p~s&@w^~0SM|hh z%H=pJU)&LHv4zFCLy^nZm2WnDw5@5(6I-iuaFg)P`oB4jvd(|xM-_Y&*A!wos<5mC z-6XN}j8w0z`j_8afW#q4yzyS-wrs)yQE4AR7!VFU@aL6#fA)5N>@UAwMXnFUzK{nx zto&A~H|z>7{_a9viQ_tDAPa@3*?@&K;Qw|TOGiUZKF$ApNoqbxPl@po#0wMh12Ks6 zC>^h_inK(Av{t|$O6uz+F4e0++O)DQz3Fr#v?-`1rTz8UF6GRqjueESlMW=pyz^|Y zgTdvDtXk5j_HAXYk_PqQW=Z*Vq{x+~{Hd3)3~ou6n#;stjc3{&vyt&&W(8%GwT-Ek z2xerfZyj+d6N)$;q!Sa>dFF&e38%7Ne>^@)J2zqHt(17#1yB5dXFLa%Qo{M?;IU%0 z$GP?-91QqCT2Hqt>s~K(yq=_Od@-nDOlz6_x4n&JsJcOgTJUZRs?&u+(P%tY^$u_Dg?P2K z?4r6lY6<#%z>`Gi%J;3G?r^^q;bARN3p?f2W#*|=e1j4k6TIt;1t=Mc?k^|6HT5TP(+%UOpJ#}a-y3zOK1 zzGVoixmg0eAMh<^TRcuYP6>FF`3!7}*@?$_!N4}ax89Cbl`T;vB^&ytC$Flk-NUhK z*@R43zIs>!${jxq?~FDW={``5LqHo35btv`P4v>gTtP=J6;m#(_(oig8ff1vzM*0~h* zeNW}!$hZ(w_Pf&_AYN%(5Rc5mS6nc{3q^Az2ts*CaQw0e4$JG=devoj3W15T0Yu`r z0M70@3iqB`Zy`D}gb#nYz(5cqzF{NK1vp~%nt?Y6gEo*~)%B<*2-E?b}f z?{?ddu2c6S=AeCFgJre{-m&t%wjYuBf@l4ZOxveD42>m3c0%56y>k}ZlBsEe(R$}6 ze)lz3rr2rOs6YL5iaV1y_5?xAj6k>XS*U+{pxb=-R!#4TLc65S4AlQfkC`AAGLe3o z%ZwsV`0tN{kPCRyPM3*AN`)M@erc+!tWzfRQ>|rGtMF@-mx5Zwdjeq|`Xf0|t8W(I z`DLJ1=cDM4Uj%9uW@0?1_ME3$d8A34>N*cWh91BB5SE+V`55|tO1~5~KwWvhy?XZ(mR z5C_s3C&tZZ9QH14>xBcGE?q(mt|Gt^7&~DRPF(n=QAZPFC^?u~b+)&HW3-Kl)ebN( zifp#=Q-fk>u~oAeK7!o>AT+9RfllFLVY8I-q=E0sO~J&QMnOd~GZZ9FTZJ!H{6B?u ziy+}mf*K3!9(WIm|6&N_doTnElf}4)ak4|wI+)u(tZl4$qo&%TobFID6|Kqe=IzYQ zZz&3_0Uj_X>Z$XcZB%%m99%*9CgR7;|0!AO(;y-~zQ|!u*R}>aI{(s;405V8i=A`x z+wJMVcrf~n=|v;R1q-39YbO`sQ$wDEixAYTD}{^T@iKD3&JfmRM+h$u7vX$>b)7;I zyfTqt9P27W2rn~2V_DaEBpJ(M?ga5;A>D`&$2y`yxnQ1SU58LwFoJ3XgIU)#u-;TG zFtuZUyEbk9l-Aw8RVPgpyrC^caF1{0Nec$l>NqeRL6@)eWF7t<*raW2w{5kxRQQCG zDStU8?nqDJnHtT^7(doraSXVf@N3oH(fj)fU2TJqFh53k|A~d%lY6w`5MGg)XN!sP z7bM+G`o>0tfXqDo-VYW+vb5)9%7>&WD~?@}p`vtj64hsXEmao^m%*+tPi|FLmwjfu zSmeAbS+5I)SPOi{{Tmnk+B~~Cn~K_^^4*khlh)0wOs?2~%Ld~&MF!Wyw(3el?!&2$ z3b^5DJ9+#_=eV}9jhU&Q>0pF6WX`;netpULq!&y^b)7LZ^~j|qhu%qJ>tGN!j8mj$ zPG4Y&J~LT5LTt+^k|XBbIle?dU+8+tGINx@Xy?FVB3;C2_s2yPaR1 zsGl_vVCy~Q7oyd;HaYnxINCm)=J}-eF7d44&GwZO^{NS=AULThUOhweh$TL>Q{IqX zQWY6&t@*t}&eSY{V0^W)aA{YYyfO2=BL1QL!n9N98P9Tg#qD!{pEb^5-LtW{}_|Acg|onAgHhor(n698W!(dQ6Ur&AXx+n2;37~ge}>9Z{eZIiRbaUDVdXcS z7bW$H*Xt0YXipg5rr!? z$l+j@ahqK`ol{TNcZ^~R)HyOR2io`lLfhzw4;xau@3Gx~W4^JU zHuoHI?*!}8_hTL^(l%bVyD#h?Kd@iv*ss(2FI_zI8BKqn4Ch-X>JvjT3XbRy=XD8M z(INqv3@{#3UAJ8yKufI-yW6Ag)%wqzP72nx3Z*9oYvid1{1GUMBvL_J>r<;CQR5?m9Y0pOrP<7BSr z=YUC0j?v5^IJ`E9*<5drS10#%;c(-;oChe;7)!Jo_-_9%@Uvgp@t(G|x{Q;Wg9)ct z%Vs39*`N9Gt5d0M$q3d3ZK4{?r4UR2RGUR@@*mm1N^liTadt|Hm#D{3Q3F{@oH7NX3f-ltUhz-#m z7Cx8gFrb601|a@gl3FQF1?(5CE!Z!3=}DsVgPpr^KA;_FH*gPV0f)r3bE?$*I+cpK zZ}&Yl0RMGRZn8^K{{5l23jca<6{^e^DOLFoRnzFEWV%_F`7cV58bhbIRO7Z(@IRx+ z^Y2LX$HzTCEBTS#$!Sq~2Tn6H-hPl%6nzpXZW|Rxf|#%k63Li9p)!pT$4Zul48Fhw zFDkDpSri$u$YfI%mUfwpQoXG-c#+0;2ZtcAdVD`%0^iA~h^e)LAeJJAKb{x(#qX4W zVXMi%?b|QGjbm_|!uOH{-s=g$9*`K0k2YXO22%31N(P7F>zGQrb^_LMs4ba{-X^5l z7?U?w|AF7|DQFe4X1GIAeq$sqR1DdqE zsLA;qG`}=8BU!po>sC#C5Z;~Ao~@3U2Yfpv2)MSk$Bge+33Y)0|GLC*=9U=GJ2B4; zs#}<64nD*%OO6i{9|{SaEPjmUD3yeEo_@(26Xu8+2T(!g=HNm`tUE_NDxBe<;gJxB1iyqx=zVc}2kGKu~I!eV{Fc_I%mk;d@e8MZx| zco~`krYFpK_g>#>^g*e*P|XhtEIa`dFnR(e;PZHEFd;WfTt9%o!s|=WOV3GMmH3I* zF3?AtB(66>pW*c}=q#7SwHuLpC9WgLw-@;*3G)XW(ORCK6V2DV0S4T++tzVXVa`CR zNz~5;teQ}Q32H&E3+=dncv|Ang`uGJl4W2Z;yf(<5BSll5m1=x91uC2NbwF66d4Tp z(C6sp$X>a#LE?Bk1RJJi4LkF?vl$_^BPIkYQLa44;~1O}p5nss;oFNC7?>b-&kDqa zYHIGZC(2Sy541n7PIWNv?#5{xHlRe^0>(9q7m#RzrfD=aCKN@vSGa!H{&={PvT8K5 z?xL)z@~Urvo~CC8Wf_Aq{oU<4*~}tW>SH8oY>Hu?Bc_kZ_%ERZTeI8b()<1>;U;s+ zlG@4^BRHcCDSf%N+DgqW@Zla~7qH7&oe@ruD}eR)y>D}YM#yk>owye<(&}p8%{c#Y zg7#{@1nU9g^GAuR2;=idNt+x!_y-BCGvN;sB9(p1B}Jci1Awfke~D|He8l~RF`uPj z*_5SoFdN%h#d&`l+-V%k^zF)vh-*lRjLKYcXy&N}s?EBd%-UnPy6~9$DMQP7E-CgX ztN7Hf2Yibz?0aiq-#)kKBxCj^-d5=DFwu5t{Y$qVs(PCj*vExr;S zM}XJmz#hv$%6v!jyG1R6U!#HR59t@frXBn`QQB>ln+lX(eP>f>l4zUm=MJ*OUH%j6X z7l7mkMRCl0Y^OPcX7L{FLb8V47UbKU_ZZw%65~AHJdb)h_DBYYxd6qvNK+)?J%x}- zn}gJsLBlH(G9*PpU|jEUzA&z7jJcFD=fP}M_eHukHF34T9w1IN|=t#8l#g=g9JfS+;r3Qj51hd$u*URc-d5? zCgvyfiww3bg}I=te#AkLuG4+LLNDD!KxCrM2AzC^m`wlz&=MKWo%tPxj&nWj&qZ;z7f(!sC*`rmN&3P{dz$i7}Cys*j~TO>dl{ z*U8-T9K2MRA}P+=qH+taMV#E3h>5hW7%VLiGloTst+-P20`5;1C>~Rk7$UJWBKWC+ z^^zpP>IvqPs3(JbQzYVfbBd%WB+#K#P)oYJBsP-P%C8u+n)?maf#NGBeBa%&rvQAAcUiB=aX9h=a%~{^ifI9n{XfaA3p0fhVtfp1kzcnNI@4 zki<_oQGe#9^Fj1KxPqQ1g@@4pKygAOPGy|3u$sCbXzm+Xa>Fh|wG4zkxLY_K_sbjA zLJ}CpYsb_ty^d~|6AFyQ7+j+@nY)qrybmgU{Ar1QevlHCe(bst&`( zYS~P4)hRkjVmur=k~5P!qcN&?kx?NW9L;X0t%CMpF07BxHoP6-7?cNzbG*VOE}ie! zl3W;PKnbPP|f$Ig`^;jqv`xX`!95(sQirmaREtc&JNlP?H!eOnDo7Z9o87@33nIF4FwfCJOPPtaO z0Gd?fE5TE20X1Wf6xN&oQ6?V9wzfK*t548$g|SS$Wc)08DSR^}u)36Ha8E?9e&>%j z7w8hqFm{F(N31L{W;%bBx4AU;)atTT=d+BVxki(5{8Ux#(KZ=a!sb5{d=BG;ST74} z{U&>)w)Db*Ncw~XJMKYglaXL}UK*Z5n0r~ubHTlKMZ1;(Mzk2&#uQ=MC>i{XuvWz~CL9B75=ZPXl@U`R8Qg_%A^y;TO1fGUq`*sI!)@)4w!SD##Kt_73cG@SA;zrjj+v_Lmgf#x95Q z$?}Wq{vzMe8nTWYJU!`%pCr6>YCR}orDf%$ZJH>^cSreFpL!K$j5V`< z*yv)jPx?_;UFoD%(T3?aP0=z#_D`v7VU|sj0ZHMkLTq@x90*Qzi}t*h-qros7QvMSsCBn>g3N&GxoF9NKKQb&qclUi*ARX!~#tG{S&*SHLDQ>K2atXWh;oCRRpf^A$(^fm|_R?hCH3 zd?%Rub>l$behYljPnkg7LsE*lftY@#e$5?V=zlSWOH`-DSvU}*)>_-&7(tJAxT$SN zsOoG~&0s)+zGSNwQ{RHI3U39P*ID6qt|@vF3>90VwU@Ko6qj@Rv7JF)*F2s2cdd-W zCZALlg0A6j<_6?;T$`vOkIH|lAO4~f*Beg?Aty)%4JG2lZT{g1+F57Y2QI|1NGNuA z73S|K3dK+6w1*wuSo0+cL;SWK&Lvx&+zBp7v6?1JZF;BD_Y1$~>ua3~P$<9f!@42E zB+0V(-@_v&to3KhZ;_TIlDCIGgc0E%9<|sY{Uga(RcT=hak&-rjk4B2>zPl4>}x0v zIPq$JS}s02Ro-~D%R0mfdl}+Aq0wOG#D{3UQC>Z)#@>-asrpp|@oKm0%o#961k94- zw+v3Do-Kq<@TaWe9PteG{G@FYrnKAWCM$dt5p7aLxhF&%K6r40FmZGl{2%!=fgkPs zBxjUghuLC^-lq`r2opx#<6%gIQai_wF8ds?jvpnEeGdWMLSUGtT08**?RxK2Zd=+^WH0blLPBFf5WJwfINHO$k@PcQ~uVWtMCQOwVIgb#2xo>3}l-j=;^Bw|7FO_(u zOvNH~VP9k$>-*6U%yMSL8a3tVT4jrJRZ49(e7q@ED@{?6`Dp-dB-Ruk!p-6mN}6`9 z9a9&t&N@1}qtue~PQ;p=j5%GCf6RGDy(+3!@tndGCDp_-S*1+I#tIDTx;b6%zRp7I zqb+~y4b3}`(5w2 z)qDl*9ysT;$Euy1nU;4DTJ?@*@Cv^P@IZ3ZGl_qWv8uUAB_c7oMb(#XtFElQ1?)%!5cU3QCVbc(!`AoB*`kKooJ|^Y>A50xRhd+h4-@*Aj>5{OIRO(=9Nek-Ml9_ZN?sr2_=6#qkbG*icFiSOqEG;Q{Y2RmWrOJ)TmCoFZlk# z7;65BFi^7L#yA~Hhluy)2Z|DoD2n}8UsbT}mrlp&*$8wrGClJBA z8O@p#Vo#~Bma4m5gxh}ZhZ+7yHw0(nF9f1Zu@js3V6e?YUX;D}rKfRX4x;dDRF`3v z0-80|^viLS7?Nq_N{JhO@p*B5u`6V#Jkv$%u>21$vhXTPu4?9a2T0X3K9GnUur1LrL zp)7VXLPb4yEun1DXWqr((CM`3S&J@o(*=Ee$SUi;4{r4&A$hUuw$V3~bIaHwxf0aI zLSz#ZrSI%r93WU*^sYA6W-B53Zy zT^+DHD?Qz#=^we}()?va=b4;yDRTTb`SUBU0k+E*}ZgZW26AHg0Z_}NEoiRh!YFAs+HG(B`WCGZ|0y&*uZa86@z;s}PgZTa9d?Tdd3>iagRQr^zkq3Ck!vIuW;6A(ZVxHMkb*KTy@hQphbg~lTa+B! zN_T62OewT&K(D@%N7fItz8ExDbLl3ee~^vvW;y_`*WN7u2HUBkVo^x7TF3#K-PXi* zf8;N0O@`<}VsfyKZp?Kdk-wB@AM-av!MaZ3`#mX%)pbfHvDShuRaa4+Tu~E(r=@_Y z`jXbidB*rXlp)dBVg(+0W0 zb&>t|5V{yJB2Cd*kJxD>kwl_VMdmL&=&(1;iSfZO`Nc#=jxSB^0r!n9jIV?QgrWUT z33zS#J2-$SfY={q>YIl9<&Z6qJAVNY^xHTc1>qDT6`~jjYysfFJcZp2ucu9hT5^@# zB&yOlG0PKCHq{#NTC0vdkY)7N*Y)6j{z$YY6Z_w2@NUu)3j)DxJeWn_3v$* z>5YoS#0oM$OaJN0c($+>B^5OXdSIg117v67*XCC#$>ecepW_-T^uhanMPiDZApM zpY20|Z_*3i#1m8@SOHo-Gxz?wrg(wkNzJ`kOI#bim8q=(FC~_uNg@+vw%a)HgUm}Q z3amgBQsbMQOWh?j-p3JGj(^5-GKW*5d*v^FOL6Zcj8mMjn*sl4@3hNVHfC`LYn#_a z9yhS>OU;Rt2Zh<0{4~&UB-!R<4EZDZdmy8U`r(v<7_DDIT?rUAei!hT2-m%MEwOI@ zcRV+aj?h3(Tjl@fAC<+={J(i9-ucDnBD5u;K8254XK2hs~Vp zeZyGNa@4Gr0J}$HLI+@V{u>0K<$!;m7L3I|Go9iY9}OilWQ0Z`eTchrW#kLHDk@6u z*;YZpgi)TyYD2+ih!HI8p-qhs6f4&MCh>$p&Hu-EV=Sx~>U(2HDg@4bjKv17Cz#mx z#s;Vj@%$VAH=fe_9?o?fhpN}T9BAhS8*{4Z2wHpY@0gl=`2X%dGvj~}=7?MvY2hPs zy*6w+6w%K#t8@oO5$jtS ze^&i~Pg6Ha~7k6^DIhW}8kGXc*SA%Us=1klA3ZuR^FO}K}X(b6qw4A!`~ z_IzKWCF6xJb09B{58tmDkh6wi*%ej6fY&p=_s#8gAx#bk$Fj3a3BlqrdaY`KytZch zA^y7@ZoF;YE?S?srN;u(;gINy!h?DeT5IOrRB>cX2rM4SsEm6yTmpNI^I0+>LTCj% zcgFmZms3?#Ir7RX$}+EhsNUSyRn_&X0#`YjitJsRx0ap%$F_G>>1E~D6QsHWuzGjS zs*bR3OB?f;Dsp-ygraJHms?gX%f99BlrLasE0)VXOk45Fo4<)j(+A_U2CMn3DOghI z$gG2a{}_p5F4N^y&z2XqhLkLfTG~od5kZKR)I~{ONn;f{=3J@TVViB+6T&MH&(8ih zb*{8vVR~64-$mYFb@R}pRoH?Df|>Qfl9P^1eJ~iP?M9i`C?7B?d@R**+W2T%SDU&7 z{LN3I&QL9aglajrS3Uc~b*&+V&qi&gSa_I39fh`q0)0r`nFVtnJ={~&kOFmEs+rjC z0i4kWD>?lz6naI`_v~t}O(8Oju?av=L)=$j5;gV6$gI@3Hwk~aSOm-(n!*cIt}yd> z`f6eG!U}ZcL=5YA3!7Xr{za&zae8V!{za7L+b)|Kny2HGq^3c5@M8umM7ZU5 z&|Ux_dO~M3k4O4GNUU&S=1Ek*OcvISC><`&MG^}7`Gn|@AHkIGJnz0KE2sVhFsVoV zIge_oOmeiO0H-HcM}FGp=E@8Ufm-~Ak4ICFNRl6Y8uS!b;4~A+su6(v{JA4)^Nqh# zSK+wLQv1!+{)zy^tU~m1fjXB%G>2d!Z}6M}Q&8u+Qo+7T|y#m|&ehbzsOsf-7& z#z&KLdR;tg&@)*_{f19QoG$+;}-G^M#RRKHOuF1K(+OXeG~- zbC3vzXFeOVcl^!v4@AP^oL{qaqze2cz+w&Ex2aCYT6+BF;?bdaC|tE` zxgx`qxn_~LSq%ZFHGgup**Hsx(b2zzT{K*EjH8>)T8Iw^_u@f4U&yA#^%gCSiQ^Ut^mE&>(|uW z73aMxG^2ye$+Y*5K!uSpbKv8n3*nx%yiGlOeN4Y z#_%Pm{kfG9bD0Sl%-`&=ebK07q!|ZoJFTJHrhSop&YvXZRE23>bnd1xOMW}EjZNM$ z_uX{)yA9(sjW`uE!WnXq0~!txyQiP)RddR8?ZLFJ8L;vaTouHmC!YI8ef;CoV=twB z2IJ8MeOQl(>ro$1dk^8%M9GZw@A_uKx5*VKE~jg`u{-*@g41H9m)(i9__lweM7Nzg zxD)Pu--Fe<4CV`a_D_DQ?f8phz#Bjx8kuBsiNy#HFQiPH-0)_>p~oa`Y$G&iW~K<{ z9I=%1=pA`e)Vvh?AaPfsGKWTZ+@;J_;gUyE^gwC znUXEz&NMa#uS?#r<$-@9q?k2>+&V0^auYStw~9j}9$!37-Kjvq{C+>=xiBcO@@LIe zE)U9|lc$_9jn!jLCvPaQE>OVPq&Q#nF%pB5afdBFJgK~t=I*MPbI+(Z2jDdrMuQij zf2hP3KT((+cfHXPB>3>{$jHhrZc;^4<&KIC&G9asJGxLr^qUWFZZ&_I(a? zW4u)db=7mN_ppe|{2~40lx|&5^y!#am0Z{Y<;I5C=Ej=s^p#nXE3zG}T{W-X8uP03r&(;qOOclvuOi~xmT?7E z+-K$~BNmAt%;H^d!B~gb)>TrgtfIhtf}_~yU&U+&<-3RhIF zW~-ocdW7g@8l@}#v$IM{N`P%Z?*h?NcL4uv78eZW@_1LgliB@)*010V<@;SI&xt*L2uNUjN!id zIdQE9QxqFl&so)m1}P_&(I_D_*FAcdThVJu{py;5lD-!O!(N zJdgV(%5WlZL`l$PLOtccL96?+pN?1Y!po3MG@QBP&O~nEB?_qSxZ&F>W6I$rr+b>95_%Zj7`@?=eqcW6yYG{Ieycf zf<(>(6d1lyd}!5q1WTUjIvA(!WU(X=UGu}ajcT~(L^kK#oWU9;l`zfsj+p;iaO^pY zI-%%XtXk*2l~5M-o6g{85*L*SUDZaAdGSRMBI|yTL04%E5{O;5DV*D|fTh;Vcv-Qs zP-jU{=%OT=tSbaZJX~|0J)?_ZT&OQT!S37Y@4JLuQtf-sZ_s|5u7(i+DI5{C_-CeM zL%zZi1+IDgI%`E;Jmd9D1H-0K*%s{l9FS>_-{^h~R!BSz*IdSPaq7euS{rrVk)AP$ zi`+_GRA~9_HM$QEPjt!4Z#Q$?Sg_ptiCVErYJ!k|iw4;DXoaiqRMIA%m(nF+MX8$dAQE@z-h>S<7nl4xZ6RO@OI* zIS~@=p84=Z6cyTrOg*f#S!T%p0WuskAAXthwe|php^xbwqMS`K!_sRf4QeRL!wl`q zFfel!E%kclqj~-4zwc?>;{}(0a2O?HXy)_l{f5Nv0_p3>AkF>s{4u1zjN%DcSZZN| zHv+MCHw4xm#5m?k-ma)_NdLKwQTNQE)haaLVPNthvzbb~w~5s!plStTxhZ^GYJ3hCu++ zF{zXSiDS0gePKByb2eTG%fUgcRb)f(AvvAWeTf+UqJ${O0g!O9(%IlY+m!Ln3Z!*b zQto`5*>I)oG$PQgX13e;2XZ=9IAzt+-!&qmVjN=VJhdHBmuDe8eoxG)uVJ?z@*;CE z8aq{#Ndq9{r%t22jjH&3EZp(FJU_NsG-0q3N5D9ab7gc9Bdp%Im2g1!lsIRHG%;3ADk5X!0xP!v0)g=A+sNbq4 zYOg##o5oHQER?~}MtLuQ?tThG$xWa`5_)5axq4@{eaWF}PxYxK=c}(-L^TeJG1OuV zTT)aFJ7#+0c#Cm@#i+sEti>2^F;3DOwH9N9#W-1Se9&Ti$YOk$|H9v?GBiPF2Sp0c z`^%>oj>9f0)ey-K(!Vny?H&ww5Zy3Vk=T z>uWU7aBd6sdY19^RG134ap@R)FN%DbB8C-S&jvV%!OVF{4CD5S^8N`3Ynav#RsJWq z(i=~3uX0|^9?XZU(^a%?W*|dVaDexgiwn0BfAZ73ABiIuhv zi!H$P@NJ_?a4q8VQ@wNH*%bCP0w6B)Ef!NB|0y+Uve<30uo+A+K7^AEs`4!g{J%+A zH~1Ec@Lp1U>_>aWJ}VR|0`&p&ZJ=I_^i@!zOLw}DA<;65N^<@MQHIUekcqEB&rCUgW97(_X=bDIJ2!)U2bwqd!V@I8_LcoENNkTqFrHb zUB;rwAsKFx_%E`{Z={D6E=k>&eHeN(M@u!$T_bruS(00 zEUxE^S#ENnJ*s$&Em>Ar6oosn!a`Z$ijdW^O`*wWyMi~C1}W~*K^GEO6HejV)HUDL zRJoO|ag9ZvrBoY_WnKW<@6GPXbju2#>`E>Tv6Y4#ERD1pzbRWSvn`5rk8>&cW7)uJ znYI^03g7QKXbmY@6)FB}D5?_A8XlQ(>f!YVNagI_Mc7~U_KKXh+3ppL!`AGz+erA# zJluip+(HUlj{T}u7boZcD%EH1(`UA26ldx)_s^E=GvDllzRFvvwq|Xbc9G*qc)u+F z5&eORtjq)E^ORjbhwdfyw#a8+2)1|HYr<;Y8{nAsV8+rTIe9p_eLmyulT`fI ziUE(d?pR30cU}EsajhwBgxvw>C(fa7IoAz@WK33w`zZA4{qWTft&uPx2;CNF1`r&oZ<3v~c^u@X+;|5>QL!w8QQ zC7Ab8X^USP`k$`yZIPxrG93GtY17*6w$|OkTB)AGN!mrTgIk@#lTwuKXuG5>g(=cK z!V^+tb+ny0Y7prL9;FIFi=StT4tLDv z@IO}$D#YQLOi7_Zhik3vl!lX;Jd^VWCy(J&b;tZW+AA!<4D>i6L`P`_Flo8t-Cd*? zxIF~I&<5AtI&Wm#7`TgpzJjkyx?I(eGSZrLG-S4F?@Mn#yESa-(+&l*J_t@XB({yo z>$4eCw?yo<$&k?WJ%tQ?9n`_I!PPZU{q44703|G!g4`+E5okylV5D1Sw4?4`JB(6u zb;nw|myK6$D#c4@T;a7>=nnL3oc)V+&0ULU#uqkst!wPcnfb8TPR{&0%Ib&>)y1ks zXUp6nUlRTGbD9yf2IwDG0QkM!<@cnC7}%Ol^bKpYMWOLfiC+ONP3 zPf6U}FEdZ0r1?5>!H%v71`!8R=2GvWHU;cx<8u}lmRc&)+jn5H?=Nq+S3sWmw^GH0 z`}{+PHc0M z>*#`mXdPDg4wce&0~;7jSw_c;GPEi;h;$9nuhpdcl6TD!gIc-Y1#Yz8}E{hmCnUWb8vNoA>p|r0F(;9a+plJS0W^Z$^;gKE$ z`%4&}L@JlyJI|P-I<^GH$MSkWiX%*S;CPOk&5mL$b_OH065kIov+3#rpqamY4|%N{ z<8$KTrSXgRU56k6=mxq@zedP*Sf+=0b(x~wGZ}Hh$3zBR?nuvp?cAJhxJ`jRV)xxZ z9+c*Li~jd@ATi_qfrQhC;?{#hestOO3@gL*qAJy$-({5hXe~?Frs^9 zu<+HJR?v>SA-j)3cbr`VQ8=md^HFZH&07f*=mjWVAOaotUv;B_Io2s73mU$=qYG38 zy4p*`8^uy5iD3TD=-GV8uM9SfV)!Tm7W%ZKRp7xdP$>d>2bqk{E5peIe}&@g!KK(J zm?|kl7s+o@r1Qe4=2C!{hdv3I5u~&?NBhL98SzS3>_d{k#vk8a;ES_OS-MGQw-$H< za7^(V@?|N%dLNu(U4nF~{K2ca()!0GZBo7kQhb`LbJcs_3NL!({VGvMpV}<+QME*A zGhlyDYLlLmF=*W|&Qjm|sd-3pWz=wKG*>$K5`;GpJO=B7qk)$A3bN}j-3lag!v5gq zQShIM7gu2CXJ;39ui!qLn}R-^?x#DpvmFJeAWc9zO+TP-`;NCC2}cYG-CP&@x;)zX z{3w(!qJrP2J~!h4<(!dCWciGrOG8wOSeoII4EeVQ^3!ZGoRK0lmQG*_;R+%yA0P0H z4^${zM)>S;FHT#hI&rZgvd{aD_r6IfNF(hM{?m!>X}XtAJ#d|WS_I_cVf49~(H{vX zwvF?>(FJISMjM2a-7g3*=hWcjasFFF(2$Jx8cagqkso6oXpd%q1)V?o;pBZPeCj|9 zgO1O)SZqvXU5B9yc=d60eknK~Yie3>2!ec0kPw}1HTG`Y*zg09>^cCR&eL2eL_YdQ9)-&T#|%;ox*dT}-)dl==RN<0NQ;lv|m& z+$?D9ANR^@r`x-46nGE5U4g=VG5CM^xV0>JuBm5aO`7S08-}lBIKfbr>rlm6)qp3R z25ByE{KrVF8OE6@iC>16ru%-6=Ui&)deR8nEOAgo5f$Bd(S%z76PD>F3h%v}4-|z3fK*V}9GvJwfuopW5d}|=t z@mp?;%iP|>7<$%soT8e3qNsF=qV7FLaZ7)6>s(Uja_X6;n=Ywa*W2n`Mxl-yE-CKb z9LK|*g`=8Jeh8imhhv!cCFX_D*~vP}t%o?vD`QJO%%r2S(~W@zX`O;Q&{w)(f~aN} zWGatB;vVV`b0K|&6Fl_Rjzv0-aK1VItfMj5StB;CTeso<{QyAX^G6+C&Y6d2xR7@b zC*jmXsNnf~NCG}N?(cVY#`vql%h`BEn*_K(!ArAy?LHP`9`2-P3=glQg25Z^JsLj5 zVS|2-5vxdKE!Mt6R=HDPr@O|A}~A%&+YApiD~PBPNSs zX;80cNJ3$uRSYXAL5!HdMiKe)furFBKUM3Zm_&3A0{k}!j4Va*oZ?8MTS6=Ybzq)aSpvRbrj=~gy|mcq-PADgdrW01vZvJ z4fLzeNd4M0QXrJPBbIVw-e98K^Z}IffwfNIIdE%QqsdS?Ysb!JpyuJtx9>3%ayKJK_9?`S-7RH2=Q9GhFy$ zUSLn;V7PK7D-0nrgs10Z=UF`Cp%Lxy&i8#jFV&<;HBC%4g{PV(rJA&<`uZoX3i`aq+Trs~3D!zLFv_z2AO5T_Q;$pSs&`MXr8a}o?~Jjh9BG(Tr81CmAekVE(2wlts9#bOIa8?eu!8NRsDv#Q=VdmTdD zXy0aRAZU&=7R?p+R;(wWlBuJljN9wL`1FA?pav(2DFClTQnnhjc6<;QjCB>X{%Ckx z5MMmnDc7hTh=0N07z=8uZytt~r)P|_7tdsYp0j<&@yvD7b0L3x-cV3~5N5l=XQ2Vf z4;sQ`q#;u{%@i>Ub8!e}#}KX%!*UwHPsND8P(-2(31k%JpB2NveBr$d19^NK5OsFA z6M`ecZ$=HXM2>m5bHS+Lj3`|SAxzQoG$e7HGMa@vJ!5=QJd*`_&gSWv>tyjQ$uSS5ZvOWt1WUnhQYxKv7G-rJAKYOK{a(YWL*!?&c*F z=(va`0~rX+X!dUZ^z+gegSN0y55DX0lz>K!XY=f8NxVnxe@|1Reo@7-FcB;9{lg!jg9*qoQ2&;mm{(FS;=m)rwu#my zK2V%lSZZ0WS_$LrNsPrad##e=bT-LJy*hCb-3BgPqPVJ47gJ_jdzLOv{mNuN*}nft zZE43lZ9x_zBaXc#iZT3uw7q{^Q%BZ6eslAK5UvV{5s|jZ#ULndNwf-RR}v5rZ7XO| zY26hxh*Wp0wOi}jZEoyhK)MfNw*|`XCaGx=MIc&BwRVNJ_zSyge_4yI_1Sc-)vcx0 zDnGoR_qjKy?X%DGdVT-+=JlG~xpQaEoH_IB%$YN1N_9m@mmg93@VP+5iH9SaG#CF_ zFh8s=zaSi^2x?a2*u+bdKTpUV-S}E0+c}fTli@rZ*0Jw(bi&_A_-INBD~nyZw&iy@ zm*0)H?AuCR&%Sj=-XSvG0CV*-Cg3V$HftJVeB#YAj&WJBANqO+Gdi7l)6-&Dd%_t} zV;x6hNtNk*#dCUI1IvyOv|yd(t3VcD{*Wz{M2UZpy* zFrtEro%8SlJ*)XI+S%|eVJj`@1+Aj_Z+oRR))V-L`Ai%;EN2AwUMx&nT9=&|AJO7> zU<=>TVA!_@RHy0ErO@~OnDyV{=!Z41mU`kyL{*mqdL4Q1b)Wm(aPVCRDk6kzbbf8N z?-gqzC_z&wu1$4bf?3!T<02Y)*svZqCQ&=vMVAx6Fj<&y;|xygc5ZaI*S2}K7ZQm+ z#=0ZFLUq_f;_GRFd(mwB>kb#9MdNE=A6_pt)>ttRx;sZ7iD}GpcM2JVb+I0ai?1kZvpQV;+u|7U1=_Bes)+j!CM(q+qj*C z+iU)HxbT3Is)a|C{EDySB(xr$Mgi*slpZpBY{gWIXE`qP!~p{egeF&As5ke10%qg}YN!mCznxOSp0`MjE`Dob`a zRF2=YL)8OqvdR^)*?IOvhz5erlJb8KPq;eIjg2c0FV#Viu&Fw!1)~!Bha73qRuf{D+lExBpraR z=63_oap0i~A}Q`-ZX+(!^u&zH0?*rRG?n?^;V@YTR?mPH8+nd1t)7A1U}yzk4#R$! zMCnV|GSgQu);HQ{G0QN3;g}IWj@68ZB23^2PO6UleP()w^|eW#0Z*mx>wy z;~CShW?(*rKj0t0fH9lbpMm^6D)6@fvf3h zvhSY*uf|NXxI?FmxW|tzEw_TUv_i4I&L7t&ZMp^QGxFE>cH&N|*8gfF@XKaYHnu)b zUemG}`4#Q_Qk-9`+AOLzz@akffswfo1I7DrotH?r+r7E z_9Zi5!2zqQTnr6SzT6qac5QA&>2)IV-WNimN+5)QPzq?&WEbVDFmpw5)3EueAOuU* zt9049FV4rD8wE}9102nY8j5|Cf=+eBVgey4`2*-XNQw30q8Sh9>MvR!X~RrPXGo)b z9}YkcgO8*j8n`y_0Jc4PvAo?pP_~n3D8UIo_!hA(zMDiweeZS6kHpIWv>o7z$4!=& ztJ#Pl{RBWelLk60^0|8eq>nV*g7-szB#rIm+0YZ#*9n2e2jM2gh_41F+X7E z0&z`U11~n>s?#}$@!*UXj}8cyHVx?ywzIQxZN`OdxwBfWt6y&-0amu^Y0OtNiWNQt z^Eg-vt=EI5tnUo@_l>}E>X8|%W82V*vSvfZaT@1a|E4JNqk&(XA60V0nmMRYyLc)ZGH5sp$ z)}!Ij%ANIng;&Xlh00fsyt0%G#wHwIv%iNoY-Ty#rz<>x0}3hu2$9*i4TBa@1;r`G zA$|MGf$o1%6Uv@Z9HVy{!>)TSY&o1;t^nVmr0U;*!kNQgm!^-0r0D~qY@93I&IsYJ zV<>L3V!dpd;a;*9wl3)yGP>(BsXRDN*(C+2>`N(N98@y0?L0A|0VDpG_~?J7iT>9G z3^Tki*3f9DXBBLj-7`b3uI5KkU3U+@lr%P&?xt>oZjy~E|BwJ*9C@len>eufAjn4) za#SxzS%N6XC}7`+Q|(JA%1Sig4=AJ#tI3re?eqQZ>yYXQ1(gmz)4`(6=k)@L312vxx3BEFjY08Sz(hf4)#RUPeQ zorkqke%%E@04ZlxM-K;;BIpC^H82yCF4^4kHbdP8RjJ$s8|9)h&o zK1@7sjHXy5NoNCBB{c^M3c)4-Dl+grLb|ah(1BY4@NT*b?>J{Ib>aOt-4zBv!5vrU zZ@SDh3S%z*k2HZo^Pn83ok@x0bbB}2xoFEfH_FYaR<34B%41H5F?30lO7(*m$U`E6-{m0V5oO*$gpK#z_#zO7Uf$q8*=vZNg>^bL(@Yv|K$Tw2BV1CP`{LZyS z?0a*27}zZAH5W3RGWKO4YYj8qHE=)1j2sugzr9A=cKDd57OHlc^y*I-frV{v&>w|- zZUp@!;ivE#^Zxc)tzQx5iUz21GqXSL7wNIClF49exk_Naunol9}K3F$-({v5GQZ}QRFOB zT-iU#^0CQsmR|*d6TpAo_WS-j=nn^!^OB!PXbO6Pmi^0M462ZGK|k^jd?Ck1Ap(QJ zK*e;2-%e3vw6a|be}dMI2@+cNvv&YIbK7qk^hW@JPvwk=`ln?tQT&r&Y@+y}ANkuq z4u+#Zb_$pCU+60NN&e&G<@`Si#wH@3y6ra&`Xd45q!ivpwRJ-F0+ql&g0Vpg)1V*N znkb5#XNhawPx5^19w|@L`@xjM0q|JRPvk!;`6(Zh?3L=)ek2%$P@!-c_&S&CC%{L? z$OSqajE&j~ychHnT7!Nf%e%pFAYi9(8KTQo^Am`_A0earPB1nI?K>Fs1DC+xgMNZO z=%=Pjab$=!u7;mL{489C_;xTQwc`CjKX3``3;F@wH0Y=5r8qLg23OlpAWr2Zh=I0X zN@@UmgMMnSuLu1EeGmM{JpKgz{`LUE%{4r_eEFMz$7vlq=9QbbAK?x+M2>d7*2b2_ zG9Uk*!FD^w&(||2KnE89S~APNx=9i*kEtcIvsd=on=>^WG4_bCD^n9Mdrk1p5`jQd zre-;wCOn<=zB4oHC3@eP*}0wGcVuS0MejQ@JKq-%n_=Clar+S+4k^iF=4ffm3>@jl zn7MvRaLjbkm>GCYDq33&N*wgI!P_8*qTl+0p{Ie^t8yrst)0C8X&C*?EVrL+x4;HG zvbO=~1-$@k^2(tA>kEbgY``stUPlD-%C+_L;dFG8T>pQ)1=#np>;>w9I@wG3215zo zpcjCB6hQ|3^Kmt7tc-lkEoi0}Z+Y#1mc2mU_rfi2V5{s!cJ`nbdDw#yT!6X{`{H;7px4rgCITWSyZI->r!xs!~00qe_XZQ1A^Y^3V z_FjGquzk}lZ=g)}0`Ww+A(ylcH2Dd$%tM+Rw4K`&7D{aOwsgnxAl#Q*3cLnRPCim|`FjUq3W zy};i7i0lRSfr48ge%|h<%8kKJF#rOQeT!so4L~fAy{8eHe+$IV+w$fRxjj(~fS||^ z!;99?u)p0j?^bJAaYt)-np-EghD>S=?sHhY%pYtG4`p`#RUYi^4`pf^Wv}nS%&gZ@ z_ofFkVX6&TJdg>C`FMXI6C$l!<2y{Hg70=k(y2k{3*j!>)OIc|wG{{1EbWuhT}wHA zQWuzaWx24zvto7X7NNTZymyUhop5eh#JNKerEfx;&n&!8q2r&bF+VLluUa?aDX1IW z#b6oJ(VhUs;f*kZnjfQ~WsAB=?Q81C>o~P*!~q@hK_*FEl!5Rb0dWcIOgOvMq%3^{ zd)>ktQkXfVwPIqbkTPP*r167Leb10_)qN6BGV{^^sI=xew+`o{5<|f6pB2wdib$K1 zh=Yw~S4ghaTC=;uO-G@8S7=*F+LLR20bwBsvwwR#tO{XTgqi*w45M`e2feXWb6`>b zD%})Zv^9=N;g7YG3<$EXg^_`YnQeTN5}Rzt5=;{_U2zR&2kPNcy@c<&_-xLxUmziKbph{fIv9tFtm@4VsCWdA8qj7)^RI+<-5*-q6 zT8fzBrEKR;=Gqg>iYKz2E0}8;Ba4T#S?Nr19NU?Xf~T_9S-Z35fwd^kV6z@)isxO- zE!u4^+`Y(>Tev$mYEd;F*n!O}8j<_!-Q4h5{4A%fC~B5}*7?-P)HRl(;i=w59jT|R zi*|RWFx(3>x3Zlr%#=x4#ZRzhe`Sg{zMT8y?pPI|twriZMY&Jxp2Y*`n%%SV27A_b9dHIls7kgw{__5ydq`p(A_pFU})};-P|$)zyZK5w5+<_KPl;~ z9;*f!hTDoFq3?&=7xVkQMS5&6+sQ*4)|`rj;<*fASDh>R&3*n^ytU6yp!@y6b(RGO z;n&{E;V0m&Klq_4ocn{{eASO4UiC{ru2M2e-{b#3>fn;U^Aj=mvfum-CB5Y5-bMc3 z3?{w!eKA8Uw_+d>k`}R2KQd;R|*T=Y~K&LfaSHaHu`MQF+X|SlJd*WYy(a!!}Q; z@5A;gzntNcc90TeBL>;-Qc?gX+W1 zW{l<-kS7-^tWP&{YJ5pit)hpeFwWw>lx%U>n%`aHhAu_grg$^o{LWn4#yoeGw|PU% z!`S-tyB&e8Op_|NR0bVK0Mw-WnkhU+u(LB7hKFUQFWAVbW*M`goY&!Y9BYM!A*fr> zciLEg|FQNZFx<%E3}^oOK380gvCdTlk*k3RQnn%!`MIiP< zafm7QtZq&ij2~B^g1XnQF{M4PTdaG^lnupWM1+`8=-CC(b4bV5Cos82nL;Ba3QdVh z6XGc6!y(s4{`eO1xjwe|9>r1D=*BgNqHtIR<65}}H(GQ?Nj7--=67)>lYy4-!On|8 zJzAyLeW}A!bwFt;N`sjc;S0)cGjwr$E=4t;7Mt=?g6Uj2qp63lqSuwPM(R^#&|olzE3kCqXlqhQJUXV)#!zJMCe+RiVe+6 zb3&V-6M9?)lxC)?OkasKqs#kIlOeoah6cWd5$alN>>@jnYvWmh&8zN);b!KU!Vl1#4cStwg%EwVgO)X4&v2|=z8r~ z?ZRRW-gNP%$sjpRn7?KqVAbzaN^L$Cyh&gMll#str;&QrpfSz~xI=V-@DAaF%93@0 z?27%cYkzJ1p;^uf}l=FIfIp3KyL-TgA)!s#q-gO8veKYYVsRN*_bxgw41d|CV^@c^s>miE{suQ;IXFU+Qd>x*q3cX+15vTH-~9tX;e zVt$3~O6cg1$SWG2hmQ|Cmh3eo`yBR%nX=Ds+x?S7+kv~7ON})AWE?AkbuJA^HihY~ zK*;utFCQ-1b-kVF6YQ}|+$b7HQ;1{CQj-m*8Ezgnw;9p@Q^*Dt`2wj7wD90l z9%qGtCKN~3X8($K)!Vx4k}_kkAK)CqKjc1Yw6scL&$C8zYy85fYb7I$zi5q53Bv~% zV5oL8j>N50cwO?R<=Z72--O0T{Y^i0~H4y2HsEv|f1>WB81Gw8G^ z!tuta}ATYo~(YHVb0QOZ#Whp2nk_P07sm$o%z;aHFM@6w3qi6#4&D!jd}nX#DOH z`!DcWue%AG+0wQG!q%E5P}tFa(_IRfr2t-j_qo7$3ajYzO;u!lEBck8UN`F-$9Rj_ z{1_~Zu^Q-<>cMlarJi-Mm4{u?@e3ziD+!BF85N&xlp9w3I?LX5iJnhdid~msiz1Y1 zh&1I$cdaw<2ChklQLwMn5b4t^vU)_s0Siv=rp*~y|E7AT@y>JTRvy>Zan}~!dpi%V z+;gyHQ|k9fVNpos1z?};Rvz{rDGvliDzYTIB?pwA-9LpkL4tMPrRj_h2LM^1oy$@}{GouA2jx0P= zKQD!^$2myo!*?~hFegetO=oY7XI4g-+HyeYqD3l79h@hHDJ@N`ON#&$5b&B{!?1F< zU}@pPkZbS*Y((UaXc{~NGuwkNeBtQ8QJks>93zG!2U-W3*&g3XymYNUphEZu@Xq$w z3o;PdX{6g07pfQF*h{F4Iy2eG7BWrJ86@ygn`M|&18RkIcHvPlIp862dF*Bn!T_~S zI!L4SY4BokU-33a!)Ctw6^CzuoWoS)P!!LQ<+Uq2<>F9zkc#$L7oCH0(kvW5p|GSR zi3Qk8(d@HOaH-stP6`)VgNYHA=RSBq>1_Vff+&--`Psa89XMaUQrrC`6p(B-CjH>r z@m`^xqCZb8YMxC&qYcuz*e+KpF(?RiCEzb1A)lv8tkD|JHXCZg;jPu;FEs(3hvpg& zgI!eRGwNBmjb&0|A*3dr)YiE(4^DY0;^YU-?H@GzJ##&$ngs{t=331~u2Rk|+US7U z#&|BS%Oiwi!~(h^YK-F&$L?i2-(=EL4X~!fg>o^;+c(yl-N|b=?M-r7 z3q8BGcy=u*aX{y5IV@CaF(QMmn?;htUW9?r%sJ$^1mK@rmZNNrpH-S2-@uFy2U)W%38FSRx?xQ3~%+J6{Fe`tQcY zT`-?q(cET*=HqeIxHMr6(HUX(<~HMTERuxZPylIQC78NJIv$_IcU-l9CXSn^|0u-s zXfsW^a^}Vdr57-;A%u%LznRX~0)&!YJ@chkfuR?d^|(5x4WGW>fwz;hnmswqusAU^ zmJ{+Qzmu8Gt=Y}=PGo(6f?93Oz#;H~NV;xGIK+29%fEKwDZf?tWiuCvMwO@)?x%%A zqSkK=^BQ&bFVV~2(h2WU7OhFm?US1?hn3{gqJhT~A4z;D6 z?LrHsZde`5VGtLZ=%wrv%&y8!ds*yx!Gg*|;wtt1YJK49DpjXj>YG+3-;dL`5-wMF zwX2(3zA?}hK)89R4z2U9E8P=?=cKQfc2TQsAJ`>vOR$Li!k5A`(wDE~FV}aKNvsXV zh(LXu*L{!gcW;9MqA@A9*Y}6F124x>_oR1oU>6Pv!I-O0~)wST_LgF7$XdxhUp zG-;d)a>Czg)w#0@`o7j*wwm@1*XZCCr-5+#6e*=0FR zdh(=doEh4_6{Ac3Ms<4X#{4)a+SEpvzO}6kfi*GA;kcKarNPufs07voQK^Y&3N<|w z2kG!1;tm@xE6=Ky>?O@a(+=QNS>U@U(&6|fb>P`J*cw~Y{JC`W3baMl2?q!vp?=qP zEgG78r%PL(AVgBw0bTn`yF4%L61>!jK&L{6&S$pSI@}8Po(Qr-8r`=TXYRV{ptFM` zM}hi2n(0h8dgZz+M02WPW3O{#xEZTp;pG|SWo6ZmPQ#mC!*=I!+&8-K@8#y|LiT*2 zLU@*H2)nsB(mrKs)EisbnQ23F{9Me~sfwndX@@3+9bKRPclDK5BCfm};ht)EfSZfA zS0k>x7ZK)K%()*k8y?JarbrGaA*B%Pm~y!G(KVa z5}#V?B0SMMHpIV_7KU*{?Ypnx`-sFBlKtbctBkPO+YB&_LhAnaok4I0OA?n zJ5YD9!=e>8&+&^0nfT636kW{)q zMagR-4jWIK^LuZ!UTJLE8S&cwg)22n9u6-Hk{od zo3c170|`?a4wryW*Z626?Jj6}17{(81HIG>AQ^;SDcZ{f9PJ4lre1)$;G!2RX15b- zUk-zJi_EhW;A4Y4r|H4Cq_jZ%+glDcZMcYT?Fx0fh$ku!-SQT3=&sP>=pUsvZDqE+ z(S+`bKxZQ3cG1G3JQRt?zwOJpHCAqExs__muBO&qO%1#h@3_)_-GFd zZfCZItl<@F4k@0F)W4@*K4}q-#~n}>G#=O3-aVKzL-=qz#<3;YZSa2v;gLUJe=1LS6m12K@|IW;~;-KLa@%%@$lp z*}ui&=npt8#8F7L*>5jkAS3g4XafZd>_S6O_f*t}Kw!zc2TOO)ZFC5GD0eU%gm;I# zL}L8gC@k2%2E%q!7}=6RJktU~Ex(Pu6SpD2?fH$!_keOhdwEEQJ3U}$?$^~fnjU5* zK*y#y`t)tI0&^LzmuFd7AU`#=yoUo{P*UZ~$nrX|BuQXn69(q6n{5<#7MtwW^)g5J!V`uIovs zRXf-5gVP#1*JX7|viT~<{0{dzuT?YcNk`YgP4}s$RpQzD{Z0&@*RVA1Ue8rx4r6`i zY2DlyJ7=>W4C8X5#ROW@EHvXUMKuLW&XuIiU{7OA)8uyWJ`lOp4lINPT94cg)=KSQa-pzF!+` z6R0n&AkiX^tHwsDeBa7r8S3ew{~znA0`&xokGJbdmTxpBaZ)`EH2jBpk_td$uod*3 zlAq05`1^{V80C#z=^M|GJ;gf4QZxzD_M$1WVIpq0rcdOQ#nIn+`TOw6kA0~z3>G|6 zJFH=g+>WZb%W}%wBGK8=MMu7r!s4X=zNgCBrz4-L)6Z-hUc)z7Mwd%p&;uKvD(9cJ z=NwBke;G_OhEg{@9jq@Ft^7rrdWmxc`E8% zWG&TxL8Gvna>g+r&;*JQQr*8nLBTQLXLU=5xiJn1jP-Ow4UOUpRSgzA7Oh?w*~V5q zZ#vN!;chUTsEwHWyvb2(XgO{=@y7^9rNMpp$qp;$KKMh2b%@tG%zfBe)$pX(8eT=+ zG@{x%ysE+KsCn|lBk)H#s(z@pj&NE>I%?#;8!h$SQRurJ)}c;oOrCXgi(^>_L><=1 zYO6la{adLEt2^9}Qy*4Q+qjChaS7&?NUmfGP(w)r5qSQ-X={GO!qH5~{X){>khj$oy4DwxuSDv=-xP#-@z$b|f1+b~qXphDO7j&Ay#HET>Kx`Gqh-h*!URqxT{>LbZaV}zqtVcgnK)o4I!OdL)xGd5+B zr+J4PiFR9C99P{nQtIsyb&a?s(%o>7R$ebhIGo;nwO48*s;UW`IrLHc%MfSYsAUjioD=x3Qf?#J+|KDN<|E zXq31%+I(CsoFc4On4zU#Sou$3=?>4*9Tm?xHd);b-tC24Xl<@5x2Eu=8eHq0RS4+U zENt0KX0MaQ_M_A7@GRQFb-A}K-EM94IT|~J2X|~=jgG7p-XpNj>pZjZoeRT_pY0TY zYdcly8Y>!noA%`F9aRVSr0FZOz(|8r{+uez1`Ow+92g4J|); zKlhsa?k!Q^}>wA*7KhD?Wx^Ps~k$JFUVr}j->a0f5WiEVudufUA`u621Y}U)7-uG22y92_k zi!{@%f>yy-!h8wx_l@lXjhDk@*@mwYEecy^57MWy5^3J#9;Knvq#W$iHg{N_ds(n& z&+Vzq3CrxtoTa255BQmsvQug)v@dYEaTOHC5?3dv^ZlVbW1O8pKJ)-RCn*D>QK*h(f&*l&hjyb+4!Vz z&krIu-u@u(^st=>Lug0qccSMz@s0WmiJ|gw9%kDkOgfG87>$Ji5#*xkLEN{3;gWq@ z2#fmf_=fn&qy-TeDh=;6p8K$YA|)jMSJqp8`&Te_Phlf(f5*LBFh%b;GM)I?n ze4L;Q^A6sX5|niS?uFZM-}>I%0wwPlqiCNjO;T5%Ju>}6IKVKr<2GW( zU^C=ru;vOF4RtA-!dwZU{dTrH2Icq5X}=eJ7G8t1-u5j_{YB%mVhY=^TmE!e3?#!q za)T34Dt!T_)jxlphx5P)*G5Ww-Q;&QP+Why{jo789Sq zcB&X00n0C7gxE&pFVuWPA>-2N3$9QBRnr|j}Ba* z{Nxl2lZbpLp}i)NgFhM1HK3IN&+bQMI{ql);DTeR1Q!c`0$d6Fa-E@UI6Cb4Rs@r7 zzl#t2C|WOzmNCrOtwrWUahlGhbkz|3Rb%T$Pz8XZGNJz|+GC{pGfx&%b>=U{)EINB zm^#jUPE1XszZ~-yVrl{XmD1mO`l~R1E~eJeUnBkPBhn-n9+|F(x$orjBtOM;7pn+&Ig>p}W9xJI~6I5*sfaMf_@ zkp7o&$#8Sv;^7M5qTy~XJ~I7XxV>AY(k8*(4VMNN1(yw{fjfb8op5i#|acA zqgr@D%nC3eB_YCA9JNFmp<2{@iRZ}2W^^qS{!8pUE5rGdnDv1{X_V%bL$ zoN_Vi0||~x)SM(Zn?%h&@MOk*qZa-sW}RSw*K?xgD1rztrLfUJF{_^7ETaG_A`8P5C|{wdsm42%@7H41e@U=@C2F3(4eJpRrbeiI1!CD| z3DzPptDImh6tgxlvD~3Q>83SbxfvdRFWZTdu2OYn3wdIh13*KBTrukph)dP^kXZJ6 zhC5PK6J9(^Bg_#s>ma|~t`=sCWlu_QbHuDC2<|K~Yc-y7S+hjVZ)E&th-HsSfHK9b z6@!4(f>rFaOK{S~vZWH7>7vA!3cnPyehoNO*3DGbsbbb6c&D64&^kSJoD&GhZra^C& zFH-D`k>Es#WziBo;bNAa@EIm%MKSe@QFTMavLO-*oR}5H2w`GpD9{`Q_&QM&g1Cg9 zR?Jc{LWtOjd79`uk@8oI<)Ij0>AO-a15j2hm)DfSA8ukyhh3`pNJC{_Ja5rNLU-xW zn;UL8BbAQYiWsGxgZn$0@sjhM*Ptod~?QqJI zF-}CddWv@F?Qk{mCc^B&JbnH<^US1ReU3C=Gv>IPE@fBBhMrDHQ-&+MRk#~Jn!6J;O zcYLI8W2JAAUA;ocO_x@m!0=jy5GO~V@AnL*y+;rSYP1tVD{nZpDt{;gR^*K~RaJs} zJTHvA9e+4^4RhDx>NM;i>=Swhve)HhM_aN-2}9^pC8Xa?YA6aKf8t9{IV3*9cwr#L z_5gcnsW}CA3fyL-Y~49Ona8h+fxj53T*joEiSlKNQW@JK!L+mX>0GePUKRy5@lo{3 zWMl57vfLCCB%%7E1B36pLG-`bI|hTBHbiM7S+~ z!HNfL%-Fgp;k4*XVB%9WcCsXlmIy&zBs^N+^{9+1%x?N#4uf2;tJKshf>XstM~v<0 zy~Wy*NprZc*^R=$D*!%+v;r*oE`bH)RzU=CnQL9R9%KFlXLmr)n`Iw@%fsoke#$RljoD)_PS- zxM=snt$DT$)vCg(wyl2KGhS7ZKXSUGE_}m9_l|IgKfHOd`mN!C^PV3x`{6(V4m@~0A z!WJ%gQ^%?EGL~d1&?FdV#*#etd*5Bd7VOj64a2+{g_61_h21mkwfnw-CoiMW$^Ni6 zK}uFKn5-m^y=ICFUCO6|F;4cHGh>A}a>JJQMd#6ahvO=wN}Jx)I^D~h z^X|i#=$&_;lfAk(LGNTm{_p^_ey&;6&ZVOeCX$Whas8=V=C zKcEeKjFPXE`+*%QI2pK)NPz`qePrN-DD+b6M4KpL>>pwsN=wR`-%0CI=#=L6h|t@& zD4A<OZ*Q zzGEWoxG;=C?w7ugbb$q<0@X$%j6Cv8+8#`zu`g&om-fup!*t}6$*VmN>%1!d1PV14s0+>j^bb^SLt<#m1X zrs^=y4KeWS_~06zjjtR^EBnp%2aTwRGOUF=_$ONX`lN;Z*MkfDh3Nm>@gvjcW6ku| z$s^O};yL#N@W^;(!(|>kGCdQ|o%r@4o_Ss1>G7P8=V3hiF}}WwCnJE*#`6Z=yYN&2 zCXI;~X#JKyOXK;MxMdwJ-0%$C`8;=1e*a3`@=pr?|M)~=55XH;04_w{neVD?DSuMi z&cTHrRohNvARb67l3bMN7UgOUvpKv-EG-@;&?$ zrT_)(cb z&_zsoH{1od6L44ImLSeFxJj@h1?ZaA)ZV;h<>T zFFH&0!rKUmaF&h~4v5A#5E3oCg^);R=_p~pXxxL47~xHX40o1}7TPc(J4<7QHxLlz zEWJzEi|=;}ujBt7>E9qw3SRn;6L!NtM*24ie)!|1|FJ?V{0Y*3qR;~WIO%`9fcy4J zlcfI%!Y=rKVedv41Ragb#Rj(WjKG1RaV8{zrniR&W_U8gEJ9nkF^!vc7H-Ta4Yj!k z*!)wBr}9wBu&WXF zR=J5tzYgGiO`>V782HEC*tdiji!WyEPl?HK1}culqsaOZag-?k+vq2_X@1%(aEImc z`yQ7HSP*9Z=YZt{g+=-G-nc0H6DWk@xM8rAdh((~1n~Fx6=ytP|E)v^=r$7_0uL*UZot6Kgvzpvv9A)%uiR18 zodxD@6p~4e@vTGQy6b#yF;FZ9I_~C%+s})sDtjpNiGTg(7srtnI@JS&y|`o<-TlPIABpR`r!(ibmNlH z0szDKPZH$ixfOYtjHy7(JrB)Fc8v|1a@s|a*c7nqgoj|#Xj2`lu`oin2v63%&{s&& z#8z113IL?5@#H7SX-*hdS{4ubE7s&NmK<$fsUlFQ)7Pm}G+d)IZmcCP!CA;4z6;j7 zK{xY+ZOImI@*k}&DG`o|2OSgLxTADR^$7EOA_k#@2Iji*GRq|qSW}9^;<-Ady5i(P zs&hIFg%N790&3r=3AxnzGzG_ljHrf1G4=w`QbW8owX!`T?utIR)8Ly(bjV_WJ+7(( zw_(X-0y&Z#Ghwt;$M!j*arnT_USsM2e@fwGzebzdn_x7C|E$_B8n2?-sFma}zInlV zTPef9rC3ImU?dE}_%8{D*#Fb2AB3R-4EsYt7|a+64_Hh$BwsaN>DOOW3i<)#A4KPA zg;V?W-h_LNm-|uXJK9kFzN9>WKo8=ye5t~jqjlzlI&*ZXX@OZ1RE$Bs-wg$WZ(2Xp z{mQ}7X{HTB^&O$EylVHl%Is=icU8?&SCQXSQkm`db+@!Fby?yew>2=G2BxaqUFE83 z*-_Q#0uxxZ(dp^wKU~nY;c(7dD?5@>--_*u>|V^LEa89f;A zhIh!IH<@=DT#lAkB6#=V;JT*+x}W9c?jJ#?U<{A@58q5n+%5ZC}a{ly=%&Wp*L zovg1r-b{V?xqJ=QNw6m1_WY^txXjbY2IOs=Qj zT0Kx?0^11X{t@Ri%$9G(B8a#abes7H1E}4fY?vRa3Qy-{d8Iq2c74mQ9^?ojk`&OwKeJqsq_~^ zY;LtJhq0c*7!hhKRd7+Z94$uL#I(c$-@UpAFk0jvRd-F~v{@s-5eDc|g}^7np1OGX zHGL%dg4E$)MKvXF(<3aik z=HDZoD~Jiw>uymf=+~zy1xG(xRkikO;j5c_6UTs4)Sp-QMu_(6q24^kK1w_v$(;P2 z;Q8Iy5yOq&GXEZqdRP}L_m@$kFtcBOMkB21uRg8thJO7Qt?e4 z=s!?76YuI?DEyL|?wteYTWMI;2rCD?39;vNw3OXB4(AV?ZqStx{)Ne1=>)mZpa3m6uC@t`e4-O=-)%>K&MYy*l6x-7BJkoWy zOm9{czUkAihgn5dAE^{B_Qh5ZCo(t>UF2#AFAPovn2T=H4O~i^DKv=5;W)Xx12?gh zHGa~kuV8r(du<|b{21V{HziL`kWa)66Mhgel$XcErT}h0CGFou1=HWSN&W5+(yPHG zUE}jMtWD1dk6$}He$t5es1ZQ1c;hd*OcG0>^G?F}VRN&>fxe%$f%-b`Fd=p)vLAn}VxvXC08a#*eXX>NBtFcc~hbvam*I=!5q(%V&>B zQ1QGT@MP>5=5&yLft$j*K7D}_0`6=4y~0z3;+>VbFSbFhOkhF%rO$_BCrQAo$YuEX*tOx6WL@k8?e>8JW=YkRa7^*S~87qBwF=lJoBrXd=BrY?!)XNDz3@-sPt5&672 z8PbTEp(z?c-*1T<(V)ekxMrD=+ih9E*gjmm|)V_3_%*&a1?4Sk4TxrppvN%47I;^(|EmCUoe#ZpHL>S)cc4t>`K#& zMC0V$W;9FVx1z07i8e@FA}SAa2<8qbLKFm!L3D7}l@yjcDl_zpbvtvot^+C|bOBYCH*zK5(XBscz-D zC@r$jI@1dQOn^;wvu< z%17mPI6qz*i->d^mG(V1g+0CcUKam99oLRd!&oMbYiU0j*MQIdTYX{L@36)fXszpa zE5K?x+qv?HCQKiRurP{SY5pZ?<$;IzAMwJSg$jzuTf@tZc3I0D`VBH}rX4qhKLINx zunNwXG3a2)!sgzejb!?LifP$|=Xdg;d3#o{w+69H9*go8GQx&l4o4+;XjmpJj;#QB z>i5k#O-ZlQlrmVwq@^oW|)5mTs;F~QYFlt^GkVtX&k?F48PRGFOBDyjuoDi+S5`C zzjUHBu9!=EvojLrbxrEGm1!{OIhW-N*L#~(=oNl#y)SDBuA>KM3AO~9Kd%^^^TK_d z)8R2y4Y@Qai=%Le3-g6jFZ~>;uIBDwt}kO zVm;SObM%MUwdgILM5c004wdPONML)1nA9Up$q?f4OYh>B8UsBdj+yvQ!g}#-9hCOz z0cL36?jgKernBKqi9Y5jy?mVd8D+j-n>(!6>z&4%g0tiOI%#0if|o0uXFb?kbViY_ zs`ezwIBZ{F%PO&*VXNogm96%^(japW-lbC?(Cxg~da##U39qtA8v+qY9rUT0$3fY#h>? zlBeIOvV8!~x1@Ly!Dv%i&)+EgpclHQnmp5pb85zg`;)O#fY%~5+DD1X8#h%3oQ(vw zF-hQX$c&7A&qzNn<-kN&Vd{Q0)V85k>ZiuyBX7t zk)F>*-_DV+$50p1Nn(IID0BSAtPF`VDlek`Ffv$?M4QeVDK`Co-$@(at19cuHwr8N z6KxjRf}}|sBn=voOq;-#DBs&rP)$_Zy`T`~WdHX_Pzcv|nYd{$!Mz8U0f+N3@samc z_QdmZywKLNuvs%0NUE{0!D#sdIvVtOd|2~e6OE^OX)%|q=Z9hB*Gz>j&)1OE6h{s> zb_KuW(Jb3JoboXr>IFPW9qC?u9EXVv>$+k-(f*xith{mZ3y~%&c1RB!dY*+HWu=ql z%8gDotUQ8eb%_cM2ed-sEm)ERnDN;gCqI-^4DVrwoo9ziDR9*^U_@X{jRt2?tsT;%9d=$jw029imOJ^5SSwU(!;Z7HtE;uRmwn>N zKCyMrUwYhsZsGz{7>$UN$-~2(!*vzqu{Sm)N*DLKhSx+pLscOq)10T+oLaATvoln= zpk#D>#b`&FwE~)LQ!uHr7*xbf)pwWCkqV}`Ld}OV5SB|T*89q^Mv{D!gsC?iyqo9e z>iJ$LpMYcS3(*+#1vjLzw;T8!hCBWDBhx*2K0)~69=+jV2KU-n3+{LIs1`uybOw_! z@^mDBJh~vS=)Hg2G)$tEch4F7LJY4_6#Jw68l_FcI`&7T?>E?v^{1y_-8)8m@@r9} zqYAdX*K?{cB+waYDO69;jZbUHEVij_^)2sCrTIUePe_Vyh#XCgdY(C_u2zO>;){Y#%O&F*L z(2kK|ZPt#i>)v;xgc}rKJ|v!1SmH+F@Pul!7j5i*-DMrBxI2fd^uKq#Ng-UK?uw%; z<4*ovysQHU;CCBHz;YMU$ue!Z4TaoF;VS}_+i|@~MWaL#ny|d(!W4sUd2r+528cKM zl4!U}8M_;F){cFj zSP3aW!=A!$iG)4IaQ(iad)!n8vh}fuQv$W=#^K7#)k6NcwVVph38ma8B!R%aLA3N||EinT zivF0rG;EstS7Fm$PL=8i9H%hnJa3JilgC@{dUOqMy?ahIZ@tGn>-rp9QDOL+!U*1K zc(jPO8WWVbVyK9pukTbBZX76zXJ$^G=j9*HSzW}>TN4so>Knfj+rI*4<%$9JYzR&f zs4&C1aADzGXTtpR?a8sjaQ+japIX1py-|p=q$p2zi7+!^;Yeh|Vd?H&m zmZ?6i^N(82jVgWYjmc~^_&cp>{nIU558|GmuGyAsQ_IVSQ?JI%z2BLB*_nRU3+qZ} zJ_x^0;Xg7bJ;UJG-;(p-=3{ent=v8Tn#A_FnOWlt7TesF1;5UjwJ6pxzs2@y%;S@s zOFnci$@IbuR8B_t6vdLIhSF7=Uw?eHm5cpkvV!B*uU)@kh3V$#6!oWwOrh;KoVqz;Bh9A%fCtgO<6eE}I%wUqV&J2Z9ttn1W zON$nU(-&p1Q(y6`HQSB(ePKn-?{jYn?@)@O3?=tsQHH8WsoQY6wLx^0g*!8J)nTyO zR8^&dQUT7HR=Erm_L_NMme<+L?H7ky7JuPuidb z97J+vXi6UA>XtYr;n=WOsX6G1aJwftD(HhZw2Ld_+T3Lu&(%aYm0_I5;fRv53UiLu z6=j4tGdN)7a9N!hI7#RzBP3Fc`zNLvonsVUBU=+4yRrI6bXDC!%Y0q+{4o6s5fGx6 zXwy=+JX4>Xyr=3#1Ky?61SQ%n`(Hkw{%OeHpiW-(BB`7QW``#BC>6HG4|yB(J=rTsL1{v&Xt~oL5^?9x`8BvuCvor`{0$8S+e9`M*;69@2dI z;%byx-r+4I?-iEssXX}F;(6tJsw<+lJX5}h-)o=2Tf-K`RoktL`sRgN=V;t*w^32! z^E)gnfr+k1>LuD!nf)NH8&t7YL%s+o*FJ2~^cCp=M<` zHk==ozu?`@8;}`qZQG}MUEMgnm&w2U_$;rZ`-)f9q4dTyc6X<&z6}Ixf|v}h&rNOq z6jcKPOclan`u3;{5Z_3@dQ0ptf$0IN@tAT@&5!5P#QrK=!n_HL7I`pwHVtR)k0pz; zC~TZ1x+pBfV&YSZ!Zd|))S?R36_d@n8vHy5_+Jbco*-DI){y>HQa(Bjjyp7{ONd`I z{)4rC^n0c(nkD7X9ATyyoL9l15fiBFuU6^!tXa@GbI^birh(!<6&$_w1* zMtl>(um-)#CU0MJ`|I`l(>!4sev9Ym#rpkE_}E99ToI1wcs?5*d}PlzV9!4SOL%Xu z{^8f#l)4mMiqgQw-U~Buq9H%d!Duk5LoURNII?p8O>45(B5?K-k(uJX$I&v-tD*giFMdWf$%(JqkQCLR!N7hWt_YmA=V_^s%vHjtz)4!K0j)hLkb!7x*cBD-;@2U+= z^Y*1>`%f$k?fVD3tKK64dD6d?7I@R1bAexJ&4R{_T00BD>D>P$M1_BIA0N3ni?RBt zU3>bROiIc0KI{yN$y@5`K^nQU$|Vcz$CPn~9kOYe4&lNBnIf8$b`ka{pqmkQ z)KJO65(h%QK&MQ*C@cN(TPAZ&KCBYB3VRGU)`!3YE3mWH-yY&CV0;QgKgTu!;;WiELNHst0=B!g zsVTD$NFsRUQ<#Po`Mv@!`Qb1hr!W-AcJ0?03go>7in=)L$=Dy3kzCQz_~YP=&S`ci zp4JdQ)_+Q`t2@8_?zpBKaR|3m>bCUVUbZ=hJ~Xc4UfJghf!wFa$1{T1wuG+U-}D-3F+U;L zP(}C3rlufnBh6O$6p!H@AE#_;iU>Ye`V_dY@+tJb0<}f|R)JW_h>8gZPgPvdS@g%= z;A7G3q2ck9)K6_v-8r`zM-Nbu1X5<0=6>Qt_;e0%+W-nxAM zkj%f4pt9gg-Ksn>RKMVL{wa{|c?MEAMP=G;VJbF;SYp<_;xBIT?L0x+s3L7t`R3BI zHoEt=V<>}|Y@>TOIq$LF0wy~W;@(BB+IE+oy5FrTS)iM*D&O5LGtD^td`v*`7`knI z?I(^2V=4@~lo_Vo@v=g!cglTey~_|nZt#DyihTKQ18`rY&J>ao>D|4WDUa683*ntQ zqptMr`zg_;-DqREY`(pGx2e=SAmh5q~YRBxs{>MskJZRmk~=ttY%&5-z7w}knz)oioy_T zb8B@`^x)s5VEk>WYg%+a6D|5*>^~+^@p;sdPBYU1NlD(Z0K5ZBhID-cW)f>l*Y$kZ zc6HaB=&t~Rb%y5H;Pj4hF7>N~L1@^JzFQ z=XwrHRwJZ<#~ECXO5(D4zPIxx5yyqg*HcLeJc6Xxy-3+MWXe}lRORKH%XUlU@*RrN z&y~xOW7BFRUn?EFC(e*tF;m9LKG_Z`Hji4<+0;_d;Bhs0Jk^s9uDu}J<&o_imThre z4Q>8&hwG~D+RaSOPLKb^raySd``*s_{h@=eV|XBA;Hq^}4zd5FE?EWH7K}++W~T6t zM1|*3nVo`N0p*T%jzTa_wn4&{MD_d@mb253Bg!xuZ;|hFiix?$5snmB*`8sB(_|pv zIitM5usP`hqZnKx<>&=Q-cu{(gbIv|PX+E-V2qJqYeEUKU7~EKD7LuY=a!)X){ zCMWAn&C-m%LNeXZgQr{M%br1vIS=aFp%4?Np)T_mZ9Tu1vi4*R{*QEptC*^>tuHk@d z?Mc&ur=^J!?!r!9PSzT%8_0NJ;WE{IQjRE}hUw&z5RyNOGHRpE)|;i{C3_-4*8lBW zq5tJuIb!t~N^YVk0tv&S1%__S$7%JLuVOU{B6o|E5C@-7=F0cp|0#G+sp1ck^c<(o^#TUk@Y@ZZ zIugGbKjCwKVSv3QNN&ifcqaFj`G)j5lhT>uPHA3zSU7d3hCLeIH&v(Ipb}2qyU2bT ze&Q!O?MF)C)JQqIFMQV&ot9N*N37!bhu_OkWOXo$vS@xBAA5Sb;?38Yucja;gMX(E zN}E7s2vV@7)$90&U3Xf3xZXH|6j1)S@iAe(;lr@)Hm&?d@L!i?#t*avGtL0 ziz)*Rtk{*&w{&^mCWY3aQ1%>`-u%;esNKJeQk*U7nbB7E*0|31q#h*vt!nD6YK9vP zwa;y>YI0UJADG8$-$WZAg;H57mDg6fo_Ca9zPq@&^3vS}IZHBNo9*qSG7_eF8#?0* z-G}4aHCwkFURPk7d9CvNUFRoiQE}o?`4ft>5#Ypk!I^jmUwv+bpDsrolzY3Bx6yB~ zQ0~4P;F1Y*6|_`vSChoAh@18S3T+G)8v1O6pCuNZ*dZTkl@Jy{E-$30bMixSp3fb5 z_P5X2C&v0F#x)_(N0!_vJF#;66V5S7*AL(A+wRZXx(gOLcV2WVsOvt-Ig`G#Zk5s- z?NBHjN={CU5BPDe?{~jEZt;Y6Xb(LT*ZU^E)-cnvv8H{&+9$p8Htq1HnuaCq&)*4N z#2sr`+rGZ{L*>ci^jRMte?fL)@I`3{&rj_7E7}O75EG2CZF7)JfuQ5>~iYT zV2asYL*BeNxIcZcO6tiQ*`mJ`dS7uxPT*ECU40Zm)QAqE`VnItg82f){4Qv#r~$sb z1{tdKJw@e>VM)v%{p!1~%b@K@(D8ENNW_bBtN$-#D^eF&dYGjqzOi{<2*&)nl&T@} z&;Aj40{qS%j{km&Ds@%;!%ORk&y~l2uKwX_A~1hAp%4l;532~ARJ%MJ7!A*a$rG1r zS8*Peb-6MI)A4@Y|949m=RS28 zdJR}E&jcXux(i}#J7}ulvqkC0@1_aeA(!WI#egyO&S^9uW9pnB z&mHJbA*dis5?L94J5S_9F<)fqyr#^&yv+J`nokr(gUA@b+5#~acNSYk7VNmfRS=4C z&nvDKts!{dO?N~^_{Nv z{-$Pg{LQ@fh3!tCo%7h)04o#yw>NV?L0vvl7pbVd(_dRK`ej7yay9fnsPV{6)QDb= zK0@~%)GFFJ2owE*xnyiVdK5XdW8(x?&MUNb&$1GNmUjN2mG#bIS;Dz37}^rj{rmd6B6+X`H$T8-(zur3paw4sQ!nvGIp#9 z(RQI2;*+sX>N1JUmpnQ?0(n5=nI9i!;bGw-e=_n0h|?LMC53lO@D3qgiBg=9M-~(YS=%EJgM#4ozA1#TMbKkh)nQ|j zAw3nrvuK#XuoZ1HREP0Vqwz^g0H>$m0B>d7Kf)`fSNZ&ZS-JjKg--x3%jscx5H3dp zI(|0_myL@@+!ycg@@*~fA)=_(ha*yJz9+s~fXfB~m;b0F^1F%8LW_Y?m;MD)WnI-H zKE2E%CqUIv@lxL{)9ZDYrE+D*GdomZWn~TK_EPtKG7}y>hc~qgG;t0{l&1bo4siR9 z8k8^ztBFV-#dL9K*99qHmO~`=0gu8g_`vJ0qA1J@5d6!mI7gEK1Z*BU5>Yq-4YfQ| zJOlO+GbUK(b-dCW8De4M-rDt+AO8Uyk1IZZiv*IyfQbpv(UoANM=vd??!h>-J!eJ; z|3K1Z;l*rr-xMK%z!D;sEDVJ)QP<9q!f_OJuN4sRU=eaIrEj@zc()4DPUc6sC(1sU zbGB!G5HcZ_EZQvsooxIL0q{pfvP3rK6&)s}$-_@?w1Zx9L@Rhr=e8eU`uafrmj2=o zVC2$1(h=29_1*~VDo276IG8a;;hk)cdF^#ghG18MXa-Z4ZoLQUl&lfL*Wq`RZUu|? zb37mrlEssKRtq;eEEo|(B6z*HxA1@fOA0m`rMdUe6A#9@#IxM%E%==>H|3efA0Gx{PsuMqj~H`UFNhc8Qj_MMk8hC}U< z#n=;7x*S8IBL55OL6}gTvK&udBCZl7@5@Ov#L zAH()CFgW0VsPQ^*b+E9E4;w|)@X2a|+_b-T5&9+tc^;=E_d*+S+ABUvun2lfJ! z5S$ybwy5;tVc^5v_o^#HvOBecJec}2;TtEEdwL9** zx4h#hvFmD6dc(S-lb@s4ZH29neHcw`oOXyA$_{?38~r{w+IRhZ5?kg`X1hrbB_-3i zcJyW9nA9DHt6!3$xC(;-R6bc_d>)^>4`o^W=1~l_3+H!!%I9jJ)4XgQ<#S?3BmEpZ z0~ehh_3uH6#tuCUx)i$DyCe1y(yyV%LSTl- z71LrVj5|d$J=ApHq6nB7VjC5)nuyd=C*WE2g8Tg^8Ig}jiU<);(pK&(evBg$HbWOr zM=Z2hwR?1>pGffbX>BPq#!QKdTUr=z_G8cqBS+MxQm|3_rf4ps(~D37;wbvsAfLLfK%!V>bu? zEcG0OcP3Cw=*U1hMb%h)lctY;j(m(>SI}D-hRRP?hIW}kd-Y-9+2Ar3h+A~wBUX4M z7E)gLx(4QI0*3MSBhk~cr%go*PwGP(V{I=|cr9*g;PFMhe6%AD0f}P$e$KxwEigM4 z46&y&^g?CW&>!QS$;24Nd73>LwR%}X+tPCZgdjd~&X50j=R!{$*PQ^X@%TN==`!!D z_`*_zBwJdsYx4#^-oL$%@R9k9lX;`3QF7*U!y3w`p@;Kjy&A@sFYkpGQ6Jiqft2rI zXVpuWa*bvEvnZkqcg`XqlOQEt8G>TKW3wnhAF|frWGVD(nnUAAZ@=vsx z5-?~Ppc=DxKev!)pR3AQ;XZI3ZOV$Bv@8k%l||6N1vvq8q*#b}zH)?T z3e3gr{{+|+FGBC0b4@>10!9GT21WqT?|M2C_DyC5RQ=goIH3Gd=s*5iDrASJ!IPiB z@p+F-?CzC`H?VHK_(fj(^nY9{w^~>}8Ou!IZrY90g?C1>vYv7mUdt};_{f=%60d4% z)$v)*{BCE$bl<|1Kr-uto^QR*w}^F`p0>5ZDuzD)r8M}Z)YJOPaA!U-m$-JH*d0O7 zHdpHnKL5J}1Du}U;dudhel_^KdnML?VBy@}!GObY42Uk7zsKebrT^-hxoT53Yt5s<(tY zXHeyA;|yy8ZF`p*?3a2%gG;sg%lm*c-gFT0CN{g*6qd~&r>0m73 z7sAKxi|7y6Joz`Qb<_c0_#!e$@UHu({u;s`JjQ6-Moi(RFu*d!#SkDFNb3<{;N&ZK zOZx$ZvWW6VlnT`qDsQ-n>kZd*M<4$YZT6FOfwjO zcB9PZ3{K#$B?8nQdSy5e0%^s`;QW!Ql%zp$og2I=^<0&77jy&?RG&Z?2B3s9-;0L1 zSpC_o`!~PO%kOg)1U$y^5Dj*iDi2=k-h}DjwHAf|eg1BL9ZT6iVLaOLyShVNTl-vF z{g8fa&YaWl=X(8q&UvCWq0bEjYYJTX@zqFHVnz9*h!tOY-iLD&V< z!fVlQMr|*cYOeX7e+tuRK{RX8K6h5Yol8o?>XoV9p$>F{w%ZyozN9usd@FS3)^O7V zA}nPgEQJsh1jCO~)Sg3-e2&WOgK|_b3faivw)q4Am_h^ealms<*S@}p>m4bL58w?w@*K{Jkq1j zvf8(nhhNZ7sfmP{opT_1!RA0^deRp^2e(!M|7l!-@n zXdsD<>7|sV5Y29XX$jI70aH@vFXs@!Ddh z`O|Nq4Cze?$5?z0?5CgFcqcohcR-E1;353UwGe6He5g#)PJkh8vUUS|K|v4eAGz?C z+n;~FV%zB#-rKg9kHx!kY%ePKZmFc5N&6B6?IjAG3M6ElQ*0aH=_gP+o6wwODvLa60K;1N|vM)SmP+f=^KSfA=UM~wW~A~G?QHkoNQ;(j3I%;qSj z?p4~OpM>Bd#}xf;a5xd^k6-kWk3zCEPKZV57Fw%8|n_-~;X9SCtR3O3;Ddt`kelGolVEE2pW%-h<#a!|K#KAD5@Wf=bO5rk5yPDX8X4SXCR_AgQ!mx5|fhV zFm(&SifrVcihT!Z{yM82MA#w83L+euMYcs zw8B8r^rheH^1jejcT0w(={yUVDTuI1UKaH&KU+{F3|<(+sALA^SO(6Du?Mma+{@-8~>>5P+2# zFleq6gvvCba*}{;wiuRpbX-S$3RCz!1#%_zv)1TY^*Q6Vn?(I2SX|Di6)Pvne@W%0 zv_?mf-5M?As#>F&!uf=>v{p`*m%-1xGOchPHSySN<$ESZ9zxth#I(T!BXU@`zbLEw z8FgzmgFWt(vAr_JBl}ODPzK2t5#2WL9Km<%2B^Azs$WclT}q%o!~H=s`X>Jg25+!? zq@Q?+a~P58x%cHc6xG~CiWI(uhRbcM9;wS9)As*UOZ!M`8zbLMnfG47rqsUA0g6D9 zg=u#0Bz1XcqxsO)IB#2`$4q7$jJXQA6&1e3@Zf3V2cDA*2~P5ZwK8KMit;4J26=ZV zq7c(;hpfMHO4m5BFjG3tcoLHjUc7MZHoy>pK_9>HE?^K5Ta6lpLkiHPl90H^mndi# z(*y=3W}K4&22DqmLH@>CDR^W=>@&}&PcLcc+hk%2TO@P+73|`JH%nWtZY%V@6wqfJ zA~M>i)&383whul%g#I9Dtj@iL^v4&tx!wnJ{=j$ZedX2dohJ|L;Nb^d3}hArW-WgR zqF2E`j=;5Nkxnj6`zOjOBXAANrqD^*0qLg3_sap--UD3wHPvx1{mvE8@?yf7qIixp$|a0^ z7KN*|xxD5W(s}|N8P782JPeJZtV${RCue0cg(5&ns*o~0D=bb(NhudjFXRQi(72FK z%$q`3$kiXEO$o^ndi9U&KvaUEys%zc(AMLrXkv&wD&1SE=q326%cgVeIM;BBerv1af<#)pC=A4ssgyi>L>0L&Dlz^v;@;?wC&pxx^4 zoXIrkF0axVMOU7`?6J>Xy7W}JZ6&^m1#*&JRdqHhJJXf{3&f!e6_G$ze}}@RMC-@2 zcSvJMNrVz%=$JiaMM@P}@ZQZ0Ro*qYx5oP>L?uxk6HR0#UTrVsQ^HPD2lG3Fg?2|? z#ma76vO{597y&S3NLsRGLk`USOoC&HDKpNHl4<%5f4@UJNu`9p*a(1l5j5FTRqy}w zr{Fo?BI9rvpo_OAa4=aj{i(Rey#EReky0v6b6=FB_*|1&YnZ3scG+w07I`g*exC6s z0Ye_5-3)1gX}5_fvqzi4Enz#jp#`}Hj`kshyDs9Ky1lgUL&TBehl}7PluwgaJRH^F zb~Q!VF8v%Y?vUfXlxDsk20)(v+paAw9bb1hdp@}DTHMl6Xy-VEBhFNLw5PZK-VV-K zt**(xbrS%I3SzcxP*LXy=_Y#Y0K)HST$|xK=M4_qPhO|zRu8BS`S3*>)}&D#C0jd+{=S6 zD1D3O!BI)2d##07wqNp=26mQg?e@Rc(p#wj{pC9irFo!uDj(CntoxbrQr~IDzf%?{ z-sgK4rU$&NX+cX#$%OejHY}5Kl*2Q|>-O#JLr~Lzzi)F)=7P;W z|E=}w11ie;ZcXD_G8hywj;7j{b*OwCowZoA^wlO`485qG8Q4HAo!4ZaM|;hPz(%Iq z-|cb_{|@y#M5eBVO7b8Cd(yzKsmkv!!xqDugv{PnhrD`HZd;|l;->ZedlmEg>~qwx z#y+RsyoHhfjw)|XiewPJ)fyFk>|K5};e!=jDJppbY=b%4kjz^BA-aiaWei{PRtTo{ zv%#UyRi6%md%(rX$}#S#WQ0E*w4HgF53_UhY*~pd1Q|lSW~(WsXox9%mO5Jmo@wRX z!3e1*xoLRAQ8-AQx(A*KGPBXod^TuYfA0zPr-RxLV$o&$&bKQThu8~fi2mEc?cwU~ zm?cXV$F!C5a>u1H4nbk(9t!RuI~U!rmxKGu+oP*CGmomZYokoDN5f26iVHt6C=wlE zE3Et&1J9dR`k+MC&C|iDi}w^of+D5No4yefpVl4;KN+j>nk`389v}YZSa0&|fN3r< zk`VbZKAzVv_CdwjRG>qWisV<4+RTBJxrpc8`i*FKn)hgLjyoByu^d6EU}^A}#7~eq z_O9M=8JzV=*1+7f!OhE&@X03H?7Q4;HbjIUV!>$54ewGja1k{4mD)37y3L2!uAvcx za>c(=FPS?-S#J(qD4ZFmSKL&l}5wb5ZXCZ?mmjX5iF19cSxD z_&<74)zr39acA1hVm>m@*s|Se@b5})7rbTzX2L!J$pc~cR9$YrjjcwIW!P>Q{8AwdI6ROS zy>%H20Hmo05d_WqI)kwR{aak_*<@Xkj28gmR6gK!5e`KfMcskz4xL9)|G;km2SpJ< z9?L*}VK#q*h)p$fwZETWNSX(0YCBW68Gdv>SXrumo|-qn5Yv!P)di^1_rDQN(H8u_BVw36anes8o_1F^ z#6e*+W=I_&d_rsM^@il6y#_}90wu4-7Ec`SRNm8GQaYl>X*>X%B$c(+nO|0@pd86# z_FBXEPm{BiqdA!OS~=c8h9k_7UshcqH=KgnB(j&a1-g&DAWDqr`X#wi$D3p%ei~UT_b#wDjaC0AX_zf5F|`ctm3sC{aP%Y~wYu{jkq7ez zN0b`P7yzop6*LEBR@TN}NPaawc)^G9afbYw9de)n{JBmfD4Oa{*1(o&eRP1eRez|m zsp-K^sppSUAis=H1ufq7+C4+3P3Ld{3mEr~L%BbXlTW6cin7Ks(&bd}^B}ZOt1zq> z6-QLazlJ1=v_%2e&3}&xNx?=@!7%{?Y4Fo%W1!RK-{cx9rK^8NMh8RVWFpDvXoI7( zbO58%WetvprfZShj?tM-t|q8WvSrTy7@ensqm$e+xJ-g6AsHRC_jmU|L;S}yU<(=B zM)Z&W0M)4|!gOFBhKAQAV)@JiNBu$d`*)EgJcM$dhN-y?>`s1}S7yk!1xzE&zmT9i z>U><<+`do#XNg)R^PFZ4${TpYX!kbIK2Vl(T>`&;5E?$`4*{hvI=sHF!qYkzUye%%ky8c&}kO znb2F|0fQCFE@vX;Oiuk=fxJmhyER~#gL`5k{s&#kInh1`dtq31qGE_c=0=`BG-l|t zaEtY+@9c>wfyB80<<_bcI|O_(Ye6?Jn?JQI)@U$1UFOpQq%(Jjw7d7W!=2?2FRmmY zoiZR(14CJ><_hq5XXhDqj_49YAVe4)^)trayQ4~8(68zGuV)$ltsjQfAjCezYLLK- zV}FGnVN`aVp8GX?F8f)_S>7G-bq_K&m?(tRgw7Qm=cB2xJj&GRVJ$8L=GO8+hrAMW z@+`oc%b1s+LZ1fM5+soCNaycJgLkB!mwt`ZQ}~-rFI{DJ=F_X_i7TJOl{1nFK45-)kWEiq{v2+zPAG^X+5fzK zcCIs$3qIg}d@%Z<_*%iH&epC9_q-RqigvB+bJ{eHh=;6^Oo=s9H_tB^*Eh0@mkUW6 zv+t%gL6$c|CoYshfIOCDaOGFK3%|ke_4`kd%jzTr@dH^!$JIALR)OZnp-E!?gA->Y zg(fFOFm->G>W)f-+rh0cy&J*X(L-hTu)}}!mz)Uy$Jyh^{9%5fL*0h!SwCL8Ke&Y8 zkRkv?##(+dp*lpI(s0)4w6;x)78H!SU;jY5pSH7{nAwuabj15YZ$~*e92q+ zCoY(+{&>F>QgSr}DFXZ+{Z~6>B`=3SMwqJhZ_#bOCkyQShkr}Ro2!*Re;(9NQD4h^ zJKqK@_#EnQug)h@)2+loL!><-6TceRL+0*y8N=Ak@+#pB&5y5>1N1f3>YH{Yu6>CZ zG#&5kRZ?;7yAJAlEil+~z;38M<(y}Nt$tOAXF~a+Rcap$m!n+`I*+Kpjk4Pz9(Iwy zI_krTyoHY*NG8Q@nN;L&_GFmSQXWPDTY&NW;%6H|cJ(C^(cSS*SECMCD()?OrVc)> z?4nziWSE$wuHql!zj=0Dw5vt$>PuYzDtY_wo&72*&f9lf*I!H(-js67FA$4@=>OYm zb)QKZFUEn-5hj#F)`QAcF6ZLeGffq%obyy=Pxp%Uv=nhR`NfMdRj z4I6~^DHRJ3>`vOfQEJIm7vgg3;VJ_$5?t;*91G(hj%%P1+uaMJz);s3oz0@GaF}Ov zd3E^}mKJf2(YP_@^0cmvF`Je=B!(M@HpKY2&z=e7vvS{#5piq7V~G@AyJB;qVlK}oua%dZ>7#zSY;BjoLWV7 zxEQWA_+6k_aT3oE6of*saXv}L#!RLCQ@6J^eDL1C$DqqVM70QloZ%M}nzc6>_uY#~ z8R@m46#zSEMK1@r@6c=E@B`Q;>Hm5WzWo1IgXH=;Ul(O?+Ag9kG>4}7(2SIz=bs%0;P%hFk6U3 z)?hK80gTJ0OUljGd>T-&Or0H7Lzjn7bXebwUQCxb`s`&Iz_+nqKY(w@pjG{;oWQpS zA^U=WtC}ALTo~qDg*rDH$9HmdZZnQwcS|3#pLgjJiJ5pjMKZcYTgA$g2 zNwGOBf3(O2rLpnvuvB|j3iiQx_wM*>27{(osWUL)7its+OQK=teO*DwY!S=$LbNeB$QaG@p4^1fel@ z1W|Q++8T%bRnWIxy9+`i{R>I3+tR08EYC0VsY?egmdmWN*By~z<#Wn>y3&4f{V{8m z-A#%NgV{*Ds&T8`U@JW>mDqGvyKU%~F-UIytC*+NUB8NXLfz5oGuSX$+4%$z+iko$ zD~ulXlv^oj_fjaDKIY}LsXElz*k#4liI>x+kShV@{_wlfe#uo!3T7>5O8X?My&Qkb zSdGHZ#0eh~$RcD4D?uGe1 zKh0A(8w>kEd?PbORi+wij>3Z`mD%j~U<9K0_9D*X#4cYTI+ z0dO?OFB;ctH!6Agi*kg$@m9I&{N2i9)8ZV3Af#(3U#!NH2t`0&Lxg)OS%aPk4hjfw zf@dyg^WI;@*>~=*SQHZAbc?y_;&A*TgnD;X+`DOP>kej>fhW)1@ zuXvw)!@d41lO;K3On3R};mz%7{94{HfYm;MXL>t70p>s8Ti@nWQO7P#@cKGGS?~QZ z{y7zANM3)QST!Ri24L)w0(Uy z*H+p;;fbbQD?Z;A+R(Yb=X1!nI`{Q#sX2QF4ob>V9s)4titGUhG`@LvAOdTP>Ml+D zy?NLEe1G1Grdcg*%^t^kv0ULQ4sCNk=Y=q3l`^luESAf~av7xfMY>|8#mq;EmL%~c zLq+CMWu4B5f~E6x*0}{dhe@JftyW5l=xc#B6!DOaR=ola(j|E-(&jEqo6%y|%kwgI z_*9|Jk73Hx38lV2Cf2vO+0mjDVfp_-peq&%YSuD+s6gd)~B>+x6rFE=u$!q7R#~Wu7;pJDsDICtUg&s zGIU@K>8E@Pbupx)@RttTyO0iYB)Ff%%=Gx{ld}ul%CElkjP+U$wtysJ%l}2ni1m4(D7<`SF93z zbG6n|Do{#e)&iw$)=C8*wKRjwp%99!j7!^fm}@U*El9(g!d&CvfTUeTT`*B+9~wOJ zFZBDnyLD+~?WOPfa#jAyKY-kvJn%5$j?BsWgF*0Lo^~}}!Lw~+VZhL)?B^!;W$cni zQ?ssWR@&-E@&LPwVzXS@3;aTa&ynsb7T$VXkALu46h+oV@Rdxjr6gqWfH~J;G+GJ^@MI9#o3@ zwhK_85*?P&ERmnM48RRX?PZ?z3WD_wVdvXOD@AOQiqO71#D7DXAlY-`E`d!8(4-Tf zFDprtyil}Sn0i(QWLJF$p^totwzO%mXexRxT1cF8Y=isy5JE}{%8{gS1wtnUpDlV$ z1=A;H-2>AnDi8H6fNX?v(PS<}KU7|n4w-hK94e>M*3@cO5j1*!*Ig1jcG}wEO-9r| zKL{saaSau4QQKIH;PJbFOdEt*8FCrr{)p5oqTUE4w86^Y6F8%Z>;t3=Q0iehC=@*f zzl5ty&*xG+4~9YTyWIT^n4qEzg#aEZ+LJ#F!2LWSS0|*c?W2@=d9G%ci$m7Zj+$fJ zd5!CijcgK$)D{UY$e2+!P_FxTBN)Lot&I~yiH z{i<7hG{3HnxKR2{exG6b_Qi&n@a1%N+W}*mT#Be~HdAr#$~fw&Mc4bhRyX z0M?%WqeSk9uL;AE%FhbGfFkd%Akx`o(2^46KET^m}ZfYGx@amV=)-Ei2WunjB9>Y?+(EMZEfv71s@CXBi~pE2&UZrz7un_(#hIRsieEH7iM{4~{M5DCc{3 z$$75QF-hiR%bdmAsYJbGE}K zoOVPh3=E}#rhl{yHM(qqWt?Wcf!4#(w6{h7@fSN-6XW#52)G6;`~_M!OzVAeDj=s{ zRy3|aczq{l^KfNQ&OF96J_8NCL1q(WW$FJrH0XfYHib0@dwBK+KP#s)#k?i7xU?bt zIX&F_Vyols3bf83M_N^-K1IywB8fx|lEl(G7M3>lb0g?-$h4GK?w9jwu!gZuz_ zoI7^>HJ_qvutaJP#iaM-Hnh`MlvtCwp31RC?cD%m>zBOeTVt6zq|bCoysGtKX26Lg zilcfVz&Z;uD1${uZka?##m0KV-iWGEqAwy>CW_lf?MMDX2(5|OFJLPv=^tO=Gc%v{nicIc(1mTi6%x5SvX!!|>)Bz;`~lxQPoK z04Yqy1Y^0+&u6J73t?|~g|wWI#&;a~F<>&0O|Xt=W7GdBH6Qi@P&Z}h3TX=IV@WPR zJ3u(n#0J9N2!il7rfkVrPl|2xDy91eFDM+muxQCRA;tFQauwd}Xu#)k1P@0E{&QJv zakC1}R-tLOJA;3bdOr6KZ^{R|!i+WzKDM5dv|#zwFvniI7BOt803QJNRl!mzaPqXY zD#2oJ17_(%V#6Fkrc`JIi2-TnDzz*C-Jl!;1gwJRaAd}XZ*E9GDj3eV_qADa>h1~jQvA*2s7609o`&3 z2XEd1U=cs2J=`LnvHV0$D9^2YZft7R-fWfU=>6G8a*JKXwSmrs{eBlQ`gP}ffgZFA zWk5Xv|2bH4&w^#`Z$O~9a%#a&^UC;psbh9%o?V;iYSFK68CJ<$O^L0)O7t-c-;B}K zr!tGWqa8#i zH^DafR7zJ~V$pp086QtVe8qXcZyQ1^5~NhjtEzuR_Bk@=6;p9<#kIdT@lr6c`U{Zr zjzgO;j##9&g+aa+!nqHBAVWFN3xxbRX_Uu5$s>YciS>wdpr0`|j*JF$LOIXQbHPUs z;Su3zM8>nT$s@8J|7?%QoGN5*T=x?wD~Bw69hN&m~60ZXk5(WlHg_{EI&mDlx2?CSfA3y zuNlRWnJGSoCJ6&Yx==x2<%Hc9s|1O~z+wuO0P)_EIZ5J(L&5YN%u9;;>&?7Xt!M>5q-m4{m*zbPn=6vH)E zze_waEY8uu9EUaDh6Of%l;&Ru_$l|6FYqN2sZF!$K#iirp`uKk2t6>*$%?Yh|=RYzR>md&|Qqd53%O+<|rh)r=RyyxdP zh@lT88ty3s5O~dv2V8wO5rBgDoEUTD-iR7SZzu(y6Cx%$8g8NY#RtMQc}+KGzSXq0 zZ+|>;DthK<;%)VN{0!pk3rdMPF*hV5Jh%!->zhJv zg1QOVw)?MP+7ZB7Mht-Dj*H=TmaeFXa7wt;xPm9RORvu_N4LJPh@CCeBGk< zd1!~-KeDKOwofU`EnL)I;8QA4xbiIt>j`1v;3l-Fee!?#cIEQ=|JUce&Wak{Vkeo4 z;QPbv@!ycPcxdElib#qc)0mxPdI(I-Q?q)Y;zF9w!y!9V?#IaZy{_h;+SXGCXLvls zP0Nor@y-WZ+y&&4>>W3fg7ca{|MFGL=sUdfWapAIr=L*1u6$Z~MG0g6;V~49ysrIK z2lBr`-yVwK!}Jg7UHv&;eOu_K=8N?0^o}#V%<{Nx#M4q-exP@F){3oxfO&Bf6oTdZn#hXoUfML8KUbc%@l8TAfya>~J%{)HhW!66MO39kqip<)y@{nWI z6?0pjmT6@SdF8rXPWk;+aqa%A%Ymc{zmekR`5nu#@9Q39qNS+9aq!p0LE!*zJ zpOXyW;B7tZRgt}Nf&JE#|83Deg_!>>iK-!p>}3y&8%o>|{WR`)P$*g^veyQ{514Rr z9e7k^hkcAX`4WTq_Oai#{y`RCZxqEYDo$??ZP0tz8wj7>0Mn8neg4ZksBDeSSM!IMZeKE5#SEzMRNvWv^9mznjx)@Z(8xq{cglZBsIcuV4=F-zYvRtF6xa9zuKt-X#kB`mI_>?w&8g==!g+nC zXrF3p>o(czto(-`b8|g2s$cLJQf)}KZ9gACcoKxLUVvHx_Eg+HwhtmYw0#zM?=BHh z?o;^~o^hqLjKB74l!ZSUi{l*CG&=)FCeEo3#$t_iQj60|npQL#%9)^V+Ur3l%HuB$ z2gYK&e?rC^Hj4b8P8;F4PiCt;oY#~`_WRddcsF_n*Y5so*`-SNl3;fuLoQZ&=E^*g zq1&GE%w?bBF0HV?n^?a4(`8nf^331c+|~lv0dW;i^v$2k%lHY@{)CrhyVkZeb=58| z_)Onm-cmZBo*z1JoN%OZ2U@o^0*9x5T-MGM?x$L(WH{zCuqC^-IMe|Us;9K`6^pQb&IJHwWtQ?eT>#;lf_^P+@KGH>}W2 zE&f983SQ{IR^9p;36(nW6J!MF$2Z0>z#mk5Xx{-H!Y}y7u&#C*;JfqV>*#smX-fVU z)p?4X%MVaKxjZR`v8g88t3nVdk9=Va_5b;Wq5Gp_SN8@Q%aXd}+<(+1mp!QK&~-AJ zTdC$VpTmS&TdXSm^86viQDem>9W+9=naW;cI62o)jWg|4k&R)S#-qoaWF z&qh4Sr#6sybn-7RW=p(WAEROElb6H(EaLY8TW;{=Z?G;M08kN7keLJBxjac}6_gZU{ z6rd#`P+|N_(f?y?LJ738bS8ZpwkECWP3>F!BE!<4bhw^7; zF2&p|kO)d9G%wjEV$$O|b<#T>{;dK8>lVaG68DLiQu6FgYbhS|O#oLQ`OSpo=izI^ zdQ%tSZ{6m3EC$;Z#dHzM>P=vw>IjB(E^kqDXkF;fdC8j+8;PfZ#Y$a--xsC-mA^D} z+rRVuEP~&_b@nsc&p|zR=Ax1(o98@=c47D;0xJvp8y(?KG=fkutCyoHy=x<)tKlsx&d>Cl9i^^rYlde&1i{ z)@eLmnT&Qo>u!GIM666!b@UX;naQ`#1+rzE`a)(sUrF1d4M$$3%dC9n7M|J|je=2z z%je6s@wNyh`GO<-4AGZbH}W;%=WQ`ML;1zlYQAW%q3m*I9l!7IaMaJY#t|6|&NW7u zbwZa$bwm&f0g4%5!<2AJb-23F@3#d1^UR}AJZpj}fefZ0P*^O;7&N9s#TKHSQDzZC zfGCE-h86oo-DQ;YXOG9%mgz?&`GsfDum zr#GcmmL{BiGfM{zzxb)X({&|P;YUJ360Y!xArsfWKV7$r-=Ih=PuqCxO^B7J>#*87 zJ~OqwSwe0$__X>^H5YhzfJU|bRUs1Kur=pK?vGR{O#dPU!Td5qM;Z8;zCY}GZj54b zBhJ-r_}TDV1Kg$r>GSyg6W0roPQ`B*u60N`{FL||#63TXnS2qyE?l2P8i(=x0oN_K z{t4-Bq*sxCfV2;(73oW$Q?`QlAC9yH_r4JnwFv3s_>IF4=lkolr5~slR$dUur~Fkv zF*+!$=X^(V8BnVoW~OrKgN#j|KE!26uwn`W{4RXWLoQFyxCW8C=}sni4;;`btRt+~ zXmtq;y!`?Z8_whR!1K9%euojaj>jjpvG*q^^?EFvbA0k_KKb*rtMVfwp0ynw>PsGd z;R_xXM;^d*>m}aNCW1uHobO|NfVX}8lV#&(`TmXC+g+dX;AS}v4}6hQ7(D8Ni-%~H z)XD7TsTP?JBQL4OT7(m{+Z7$^t@37DX{v+x>Bga29oMF=kODjGU@8<*!BKv&b3oCf&wHw~@i)~bW+9)uIqpz&Su zK_=#OkfzILkddFR48fkWkr`yxZ4UAyY5V3N`9|p=;x%s$f(e$Q{)AlthSSa|GE)v; zQeR=-LgC|>`8nB>e!u8PXKtpB{EN=~5wQ};)~hPe%of96j^F4D)A+7w-l3qeK5r-l zB_Au{-^LC+zhzi;#iuZM^o3NuE7em5A{h*pLg-M?5T7=;qs*ZF;RHL^+}xqWmc@h} ziJ?!}spFh)wcmw_eAgqMYM@p&-YEm})Lh@JIv`Ti_oa-!@G#%?@JMc120wFGdwr9j zeB+ILsqxmZt^KBDD}`;S-(Ogo(K=6-QXih<~P34@0c#pW~=n# z99E)oQ?LA?=n8|3BTgYn!G8G4^ZFj)F!%K>$Zz&>5s{CvT`DY27%;0b#pN6Ctk78Q zY+O!0&iOQH4f3SyrTwr1d6F)}7%h38E{o=OJ`Z*qvNKnVFKZ;l#v>=?6vFfd>$f?XX zTz1d)8S-I*GAns){t&%^R+-(IHhw%g>Ie_cl<*^i_^RO24cpr8U_(QiGl-!&u^k?dk||D#}lQO3qmJM+onz6el&+~t=3wCte0Y^b_!CV`h)V|99`#uumxn78n3bl9W<7T2fVEXV zh)j&xEY<1P5G@GwwTagHo8c_SkCO}Z?Lq6_TvEWW)BjBJ({k}sOVpO;)THJ$H~PY9 ziI&O%4<*$0l?iWQEro`Lp;5+tHK?|h8niO&U9eX^-&JqAW=40{+%N=7BVW=*zLit> zNFh*>NIyTTK9UQRs3_(w2AJ2(OroRtX5!vB3HUY8Py#~JDkb9}5?JRlbY#G&u#Pwi;dl;!0 z=Qze?=i#HU%Y^PV(Qyo}TS!+SOsBn3TPUg;G_F98c}b%TIf51hMp6#L?b*dV3IffzD^3 zc_n}lZ|o(%w3i4+_rC26zDNrEufnA8TD`TM{rfqhf9-o7J~2fUwul~@_&sa>f@~D- zj62={YtI26xa|*~eLkfbrWeZLZsJv@tX zwW=F@ssyH0C8`|(>F}RTx!9?Euz3iL9@BE6UVyH zh%W>dW)Brh7OOUkitU?LzrUqel zbKfS<)aq{uSP6&Z>m zTjjeqZ$dtF5lQ|+X2AW2Cm)p6O5}qN@m+`z1#J4ib;{r)Ld_kb(%0rN@cDL?Qj)2_KpN}nBv$Hn?Q z&*LU(XpH+;Py3|*F3Ibj$1Z(-Xo~S7d45IZihK}RyLXF`<F<#F{@touh~R8(>8TvIu8MVbdrPDshBJ%ru}?3}f0~bpgp`595D_!H=~e31(zq>}v!tN| z^7G-!l;Rg&xqki?rNdrPzq3(U?~G$e1oiJquxuRm-Il=movYuicQy*lOSQ8|Ruyad zhGbT<4zbN#BU)Epeb!;WUca;6>9TJs$O0qyz^9J(K+{*G6t*8QXtJmR5nZSXPSM3r zxQ@d1ZFbTS`sip6F&!oa(LA zebN;BKi$=BObEoKBuu7p)Eyi5Pp_d(!9R_mVV`yHM_=LW2+jR8 zTw6@^#jXR_eC`9ZB{)0#R)}SvHqWIx0X<=_&y8rqtIw;)YpAJXMh35!YB;XxrS<(8pGm~gYZQBWi- zmTcX>nTCfJSLRkMIY!kSS=veC19I3E?)|i3b%5$Cf31+Ct4R#iS;PS>J(M|viu9vf zv#1q4-IW51H0cT0F*;;-Us$5vyoGS=Y*}{==>oFgpqq=C)JkCY>Z0#v>Oi0j6p|3T zn&CmM!2GSCkemF`TMnq&$mbRM%Q(3~!DRx|DGU$5^@x7VTTzclbXoH_2;-LAvVI(N~DTWF9Zk_X`>m_$QSXj*C2Y6*PX z3YY_~bkA%1H;3c3-L)10F`ZSTTn$F(ToyPwFD`uAf zAV``$X^lalx(`2$>hi_%NX*M>oYS(0oW6^Z~822}nb+*u{ctP`&>h z;--DKdL@@$4f6&4cLHu|QQi)jK2=zxT36u4H9xjJ~RZ?NV)Sz zo&$UlCg|`cm#~_tFB=QKCgv5^RS5aaQU$Vs{i>G$9m+&p`haRLRd7D4XhjdvdK&3b0)K}=rtmPT+{gs$`f9r76rZ|H#WZTh1a3B<^*gD}|AXzJ+d6pSI% z)3NAKN=RmBfyN5GY%U|fSw^FCRHFbqrA9&KF}~2RRWdXpug5p-E~T@#{(!OPW>n;y zg3}|n1?DivD7sYTiLkkiE=V&0BOw zu=^h)^DGCm@`^7rxlS&hd;%%l{u7xATl#}V-_ZttUw1tIzRJ)sj5fF{a|?sVV3X>U z!)=o0;B7Ykdf0V$i_*-AfQO?*k8CdNjhHdSOPJ=%+ZH_o<6*=UpKB6y!~e=SvSD!W z{aEeCzR}Im6yB$!z~NKD^z3wj?lZnPs*LD2QSRePw2la##M{_`+w6A`j9?dv3YI&7 zu6(0(u%11mg7g8K_KkA9d=!F!#kj; zlQ824mb#vmt~x1o?^9}KOKQ`(m`LjlSRKR1uV%c=*{1aDx@jRl%&aH&2+7#{Euz|V zcv_xWfB!le9y;#dr2OX%a*B|Xg(~n{O>RU~RysFzjpmRN98e}8K4Oyxjisnnc#mgB z-evy}Y2iO;X&v6iKBS)D+i{4C2xEfPDVyRw*sa4(IccH$CKS{}IEV0#Ody7xnKfRB zp&t_3HJk7TV&l%+mZ@D=$r$Le<}z<%6pl5B-Ok(0I%kg@cqf`p4ab4e*md-A>%YAI zk;OkW(3M$D@Of{&3}?v)aa#Gr8ATJfkU6i?{8RNTL?Za5KN&v{K_7uR=oa$=@zdgnudMQ=UXmtKS^`j zmdC`*md7zf1CPu1HFf_3<7ZeWcWjrDhxOY{-+P`c^WhGu9ouZPm?jzzPk0^%%N*Nr z?t7kmO`ZU6I1BTBVGiAE3m>;Ra2%t8?;5w*b`-?KzGQliMy-BfC8I*Lcn5vcCd@~^ zZ1a}K9CV*exODTDM;!Dmo3Lo}mN=W=8qWU_MjBPc_Q5C9M1f;_k$roKIX}>y0{!p6 z=cKO$OUoVt{i1qZ;tC(v_tYpwECgmUbvYrAs#kS2@w>08J;XYR$g|X557H37Z`Rqf2}fXGCbd zdop>ofl2Ocx@PHF%K~eg>HQxpGKW^DTJ}4T0fU4|NVt%D2>xr?{Ij(=hIXIK@6bWr zc3H|q2kRThKxfJHB$?uZlcMfruI*Z^g%*42?!6_L71Liyj%qbs9C6k~N*5OjAf`9X zdIOJL+iadyGE#IJdDiv!H`-J^+l!FVB#aw#<(K;HaAn5P9<;!SPO3Fvkr z1ZvQWjwP2kuTS%>O%(eq*?oY`s8;u&``}15Qf7ZDf5RI~Dhu6tbbia(7Q4DpQm}3L zvd__fQ#5I{0PtaY>DpG5*9Q2ZtBnMxVO>nYHrukr1^LQZj0>|aVI-z7Px*`!_o}c; zR0&;Y)S|*i3o^H2Y#hS8j`lbb|HZVPIDLlNL7c>1xTs+9S@PwSUEm9ZMf)Npj*MHa zC|;0p3$x{Q;-QiuHkvUWpI7pvDG4=%vUj~s7q9oVO3%&kMu$xD`&=H;!l z8ko#hut2DS@DM!iOQJJ@y%-8Ii2a-+15VCI-iyb3#m&&DNM~XKTZjjg7tAL`I0<3< z4nk$MIp0qcKyqjJI|A2a=a}aFw|5rzwE!LZ5WWO!!~=Q!NoCym`Lp&=;*a+hq&pdKMjQ@pr$D0=n(ml!-1>V94C;B9B@k&e~YN z9(=#YUq3o7ZVVg~BJaF}>Q2GO(P|_BGcS0T1eS358_sAAs$voQa3Jc>Es2Y7W>?L!k zOz*kXHO%+mE5|8=4es&Vj9jxut~m*B3*)`{?vrC+$9cbqaSAE28hHQQ-~W94&3IuR zImc7a9NhN01-7JSZK1;+fv#8(uVD&OEgd(5{=37j-)@)X7cW1PS7&T&fMhlZQd1o}))IF+N( zVdKda=4c*6n*Gq(95H3hFdZk=a)odP1~}GbNkU!tBj<&b=_q;LQS9zlW=iorP!3Us zVTQncMVavX_nvkaqdY_ln#)SYpZFySuhEGzfN{m^)Viq|I#DJhp4`+-jG3WjDDxV4 zu$Y6ipT1A{=8RTUE9AI947c&fm@LtFAPlNnArP3b>&7S#&PAxzA%6ERoN7X+noQ=a z(9dzNqH*OC8(Tai>vYV49>2NXIY_wnJw%Z%VMf#sWRTzGiIGz#1{p?9#?X;LZowRM z2O|4zo>&GxhV1u{9Za&pzalTPrDo0T3JaoYuPk}rx5VmeOV2Q;XBvFO#vxNhcyToj!AazEVe?KaP})?F5Xi@zXKj0ps6buCXUM zVG97%29eQR=bc&)j8+efEblxJ$o~nULrP=k!cHL* zVk{cN#30jo#bYoG*c>h6(x`oUS~7=9i#(%?pi<|m!H7(>(UNp3?d|VD6%|M>_eIi~ ziBQHKDp}-PsFJQV!*Qp3aF{SvOIG%2rG1=;iNQ7p`>=iek$w+Z9g*DRNZsEuIWinf z3VEwzd}R2j$O+;?|CfL|z*wS(XM(-9wvpjJkrT2Sgw-z+2Ctkjrio|f7b7PGKy8jd z2-Y0onG~om!G@-lXXGA!JPJKvO^*zJuneK;x2ZK>k(qWLh97cgH(y3}dlV<)}kP4Ew~kz9f;>jPN3e|`Mn2fz1`Pp-hP2*1bi z8;jrXGWp~)_|@b04t^&5Fb@bD(qhkpGZvvKiOlf^VaE;VQe6WnL+o3x^-CmB@!%#B zsqq8;h{^-X6ZjeOG{5zO9|E^!un=8dtAo5GGI$*GAl#eRDXRoM_T`;pn#NrJSna&r zFS!NTB^MTYS=TXqjp;heCnl%6xCeCGscQswv| z1^`@`e9IS(8{5ONP*Fn80e={Kbo#)e$tUwC9>-#nJo6my;2sKX(6tXFHu*qC)y8Lj zhrQth7g-(F!^MXFq5FC`@k?pfAwKgsp7(HZ`yLW^HS?_Iyp(l zu~pjSEFH(eAx%-Bjtf7tN>`-PaS=>da;}ckd=`{kq~oGoTuib>$7$x%rR_%u)1 zlw7If#_v8;>fqv&FJ1orOs$Jca1{CK=r)d<;3`sbMKC?dA}Zn33VdI4$=Ih+)w&wT zk7^gTq~)gLuVL`LY%`27+3ouY2b}1$o$`(|mJ{l%cNhmfMB(*kBzo$wgv2}382aro zYHX&?9J*$JcSnzbhbMRJ7~^02HJcO_I0(?@-|$-on#QtHF?lBTL@j>)_}%gH{EQia z`u5=*`&jVzr+umf>+cqSP0oA_2Vn|lXFs#eITdV;Xqlw$mKlS)H*nwHpZyEp!$s%+%x@5%FDpgvMgE$sL4mE;-PYg3#o$Q~f8J2qptvR` z`|*)}R`WfMPVl?ncP``~|I6>3bKw{`>=ioyGG@Ho8tlIBBgviJubDMQL(}f+J0|n# z5!xMok_fRkhtO|naIV`eLUY*0;;{L~&1qsY-ueNMpymKaOT+wq((&F?7F3&zX42O31!>u|BmV{Xuad(AJn?^p(F{PfG{AShzeO){s(o1Pi*){uB! zMq@u(my_M;e_*15cEfcUmz}?Q6iX&}*B|zGL%29$!fCep42HlgybMhmNj!EffqPis zunRRiP*pR(-}KD)MBg&rN>~T(|M4{w?$cFs`sK(FS`tAKENKcn19svVaQgspt%YJm)(|dO8X$NdsY3=!`XuB?vXH&#=*%go&)r z1p9HuRVJl+0wa&~A5)0PiQr03fcgUfV^c2UokNc@G}bnS&jr#Vc--(xyd3>r-6ucb zaXir@27meO(>D7M3Dha0)|UH&(}Fi7g4oyGANJQ!gv*9I@QLuj0UP|FAKoNA41`}$ z|7$M{_Vv>aJ9y8O4Jh5O-_7q|v74f`A&de`$x_q&MB z8{g?~5;F3R07pxJ@sDBl(04ixLCLM940NsS;rIA7+W`NcBmJL!+H4;}|G~kCviM;P z0MBKIv8VH&{p^=8NhI)ZdHZR6yWeYw2aDzB&X_0g)o2q5f5$6a)FqL2lumLT>xc^UeGg_I$x`VM&FR!x@EkyX2W*}-@4m64r54nohNlML(Vq=lX!Pu zB`#hzt7B}-ve{e5Fj8_(HTuk~I-wZv5|5|Og;Sr0?;o!UfwgMyE4E)=r?0_q^5=Bu2$@j$2z7( z)rwv6xX*Svq@F{`eBceBV9 zding3^fQOEJji+bcFmqLMD= z51hiepA!iRAr6#*nH9z7EXtWnb-ln}qss+DLNfjMh!M5KvsIHlFe?Yhgi7R>zwNsg zd!tsuD8NyK3u1rL33lvVNb<{4;ppzWq#X8NS5z;KK`CtAB9np?U;>x-XHKNzHFg)m zl!YkFA^xgHxg%mce3ZjalIxf#wcc*(e-%YrBF3A!NzK(2X4&@CD!X~Q1h>$pbI9(P zeWWDxl*PCC0EzSJm@Xw*GF_xr*_R}a4Du}tpdypPBeSL=+6EJX0V`PtQxYl1P4eJk z>%QO^v)ZDDRSslj>(opv{N6D$@yA<;S?4X*k&{V;7>vbRJ*1;$@pvr}=9uX|E@FHX z9O7YILY{Ss4uA5HzVv}5LPZ)WbL2+b@qJ$xHuun?@1VQso92OiB3t>h=?zZC3M z_z4}IQxMOle^+21($YED=RO;l2R45l1uDoa3}kZ34;m$c0{I6vs3%1(itd8b*Q!4k z%KP;4{Gi)0k=R96V@?s61_x(JaswYn*aTMhDjX z_2X^p1Y}Gc2Suhdval;J=)@mlU^e^vbka?dR_I`RBtmdaYGee4EyJ#Z5s|(_yOj}^NvHx1F|I9WMOd5FPxeJ3Tr_Z;i$g^B+tL-Ew{q>H-AgL-5)3@lo7cXJ zSnfILluJl;(gaiad59CjvlEw25s4FpZ@(~bN5XO)4(yZ1)jv9I&FosBq(PZL11NF2I9x2}3mqnMc zlxf|mN*P>D75YmG$IT+jF@@0?W#~4ZlF&2*67qF+EFy)fH@}k%)JGN zo&A(y0Sy@$WMTbOcRh6SngpQ(k^XMU0AeG`rwh0=QKA2rtyg&u^Tcl$vq7-eM~0`! zA?FB$XAFdeca(ZxhwOb0TE@V?i6>klxd&c`_?(6pDPyiQK3ok^5{Ad0=sQE7hrD@~ zSFZlaS0~LibkWf$RqI^^x z-KvJdPp{!;A>-c)RUC>+4Yxb6an=*8h`85XD8#LY|KIF8KxKxf*`yxdj8t8~GU3gKN)ald%!~XGcwh(Jhd0jy?+nApJRG?> z-ghw#^PI+XL7(HQ1fMAZsZm)en9qTm9@u~(*zGJ$)s({7%342J-V%FxBxxe03^GV-9qvDi z)Ia6f^Aj{DW49*>@kkfG0C0poX-=4PK)ldn@0TW^|7`kWN0e}YuT znmR%IwD?I}oEVGE*vo@n1n&Gh^v32O?&xWx`|%@dkrVe|?a5(%1`L#m1v~LtA6#!x z6y#rw4=}{O$Hr-8Q6I<~gRD=%PNfx?KuFbkOJVXQpt*1w7K^bpUg^WUcxC}GD|1Z2 zC$&0T(feG{7P5XxJ2>qNBnbg0bDvPT#qh8*6CaN(T4?=+b)1Bf#|dEp-N*Q4Du4ty zV{YL*t&fM4z;4fLRZw%xa3?g$dd!1uiA=-fU9IgQ9FB~r9T(ONl!4z5WYOJti?dVQ zBCoy%=zu%lt{E_E!>Y9=(J8Yw(prmNSaTY4Y6)|6WX%Ln5`DFouC5t^Mn2`7FuG=t zcPET?t{QEc*Y_GP@rB&J>52~5oQ&j z?74J-@tYw-J!}yLjGr2QOw$D@FxfBa9WlIGMskRDE`W=6PX;FxAUZ~&E{5ItZ5Hi_ zP=c9Pm)>2kr9sefTpY+v%zKYl-u!K!2OmatDZFMsM7b~a5;_@I&YycDQywzINFaDC zU9yD)k*L0GZaZ7zD78b*mt?%u%l`9Q;uggVlU?^M7^W9`5tD~h3bC~fGxCaEr5N~* zEw;9c8x&9jRmr4HLTEml4ikp%M=?xXkB|aCg~-0$?R}b`d)d|h39}J&F19Y`h2F8h zjL;Nm#oyMZTSTsp;fHz*fD{HRwG$*ld~5C(hsMjtE z%@vbNOX5n^;l5a5Xn0>%4nUvy575VtsbOXY&?A7)0x%O2wMDycm4*O~qhM2S-?*{p zKHOua&44?pv?P|mg&8Nli!g-6GpW)aD0lXw0o}JT$^#EWSf9K;%D=K~BEq16m5!F$s^_w1j1>>zX>1L>- z4`?^)XRy)jISZ6aw-hf3@SLes;#6kxb+{<1JX4} zKSf%N^j}D~B5gzZ0n!slKSbJ%bO+L_NNbVyBCSI@jI=kC6HyEjoVmaSrKH zq&lR_kUoa=S)?2pZ`hV*%)MM(dSv>53NNY@};iS$jRA0T}j=}$=CMam=H zij;y5K^;;7(mhCNq+cQxA?-jaL3$FY6zMsnGNhM~`XK!isT}D5QUy}UYQ^MH=>I6B z@8CWVDTDL@q}!3kAax>r6zLA6Q<3_Td+=V%crKj8Rwlv;2Vthgx!>z!J{*ZhD=!O%M#s%&)-WMZFEVVI z)+(V+*ah}0YJ0o7Nj_b927vHse#fh}43?<`>>GTY;F`QsUh^6%Gatg4cC6+UZ;(%A zS0iUgeobEUGIC~xG0T}r#&3DUGClj!y_{m?#En8@Gu>Uhp*@vdaSz&a1lk{f_LSy0 zZ-{xET@DUpw6asq_|+64R>2T`j%J=Uehqk25|-SnwU|#AkFv0cdmvg_lLNmC=!EdxXoR@^T+_-M1|}x_^;-WW0n+)e0T9Giqn9$=3khfGIkh~^945FE=+{PzIHj|^aFtnmQ) z7oG6%y;^Z20~k8V-3WM52{HF_qVDC))a(bmNH&s0_kFM?9PJzxTw~6S34IC6NntLSVGr3cVqoM$OhcY zQIa}XETQjfKE`;5v12exn2S;HkOA9-Kgc|V?lLw2UTA_GwLIe>B@_Q4az1zN0=(b^ z$-Nxmy*E0hsRz8E1OebBD{J^+W*g=m>`HNQ-eHni`C+FAPjZ-oQQVUDR>0P8eye9-Z*>y`1y+awfUA0v?m_ z)4iN-&ZDsKGf5mT5(S~x1fJM3Ev^dGbi7YI-Sg#xMww07!PSe z*F9)o-)rYJ<7T|CB%$+OPW!!__3n4EUPLUA1g55Sn5+qWISdmP_rB&GelVCle6Pwu zWZtmzOqk0aW3=(z%h)gP_23I+ov^=&E`08;7-54GeAUFB&;q;i@WeF-{6DHUt?Ods z;M168fjcY1Oyf?h8L-Hzlg!^k)lFk<+_ze}=hzF_YXwLzLbvNn z{NBe8u+@1h<6OXQm$-<{yJoLHL+2SP?E2C?jXj$k&K1wevG<7CeQ(nUNW}iZs~@sM zWf>u)ztZU`)i1uRzAK5wO?Y|>m?ZsOR7W_$SVfuAks^CXv&mgqUnSA(A6>fe3=ZVJ zh(ZAer-7(INI`6f>N%7uf?*%-if``CqFHyBK4O}dZXy~?D2FY7iae90x}WI!2K-bw zNu~)ubs{<2fw_aN9H^}Qs!3>i_a$ulNnde+%9wBk2a6&-1;Pu`F4c>J_sS%bGOD>^ z=y03}RH9q$=+-PP-S*XgbSm>_p1L7cBW}*Wn;YN9#&)4OputhWL0!9f`+z#{q8h3- zH6UU&aec%#!;3dyT%9H%=nBp2LX)eDaNEC!)#PB|xIjdIakLNZbcmR$sL#g2@3Tij zS{TjPWH%>qn<2vWvIxfqo9_{>39GslZ&~%*5QW(k`$D za-cavM|98D?n)9d3 zO1i|{4DkY85(BP2kk9;W#~lGmt)x!CaSK5QEX47sY8SCjc3C7^x)Y(N@j=YS3OXl% z&lk+Q^+21zbvO5+{(A%pu6#U}`wv2FRfdUv84ew%B}1!u{p z>-ml!?jp4B*$_*g)?-Tq9Zevo$U8_uWz1vSw1vQAHC?k93zRBtFJL2({^o0>7UW%5NIh_xOr@j{m;St`F7_2g7hEdJ zux&VG>vtvx+GJMdhY`=CGSvdgrj6Ja&CN=-Wm#rjwq=#a`Pj17fm4>Xo;hR7vW8B# zWo>|xX_3c{os3WaagX3qC%dVcBTPKNx^sl4XO3efd-p1=~muEl1(k!DGWXo z)J&7e04+`1`19bLw?Hn;0l83Qzadf2d(gk@67f$?K6dOqnrpWtD=7F{VE2{NtuJ|v z!{Oi9?nAks@duONaOpw&+ERn%!Hurtj&O)`s1q?$ZD&M|Xw9<+n{r~)?E(`DgQ>ob(*DSy=h*~S_Iqa{+|0h10e*k$2#+Bo0e zY^TspiG8IZWt@8FCd1jFjqlxWFw@)MYu*M2+ev#;PkYkZ_jX2)CoQE!Nir$tTz}OS z!N#@sYi6`a;=GkTeYFOcJwjEsTK1U{ivpY-dZz9nmp@J z(r9%)WmD3m$%i~mYBQrL$~fE8=Cb=$r=e<#N`(=+*JhBn&G{7gk~Wpn2KDh7o`!Q3 zq~Y80jZcxt;sI-ydcTqhzt zqn!1MR@fl&Qv6<7X^To(+u9^Def%PpLe5tLvpsHYAYHba*Bs^Dr+H@wZ#=}i5A*Kh zJk)-SHM~33ADUVa#r_)-S(U02zm@%yR~7%(x|3Js+{`=#eG5ZNU*@k~T`f}_)-e|} z``+G=pheuR3NxnJZslYjy6lviv#Yu5=Q(A)qrTSB?rYjadPIz%xLRQx6H|{hOAMRd zCXd;-zF~rJ5}5Tw4~A%`HVV~kn>f2Y=5yw((ZqKILl#bO0rH*34(d2cR#b{uN**ax(lYPkjaP%{A2B4WrheUTT`NO;A<2y^j_jVL?G2m$; z2oKG$TBM1`auu>uzA^QFgM+?J#&Ofl+uy0_<=1sEA=sd?83lt*hjQ38FzdYElCkj? z*d2sN1!@6sR52db{sEQaFqt*)KA_DY5N# zL~FOoHps2(8iO||RI!v46a(th}#LO7IbGS@kxlfLwvLhf07B3 zh5NKo-y{ruV~&9SA9Mi&$ytDXe*8CC7!HK+QQJqzKE%X!UX^0kgQqi%Y_}XWPi&6OIAs#II7ae z^dzaDiScA)<@FY2oy^g$ty9(tw+amdgBC%xHffx;wyw5ZXl~P%kJ~^SP5_yQ%AlCz zRD_d;+4bzdm=5;(*H}?Hda7z0WqO>z#laeOo$dW5x!&T}B=o}eV-}diMDvuT?%Ih_ zf_MLhQozRZUz=qUwK{NIT-rIXGYa0|10PQ%obP5oUoKW_p(~={LS(Qc1}4FTl|;tA z1*YkJ(lA7@&7YJwzPfGNOm0iwR6>-T znw3Ue=cY3bKLxJ!>7$sLUd>TQj^f^ULPo*N1pmFAtg-m|W}Tk`oJjQAKA~Bcqp(Lb zBe3Ym7y=FnjovOpCDXr1h5Bvw_+Iml(O}HO7V5rFm8Y@(gZ}E#n=<|5o0`Ml;QZx^ zD07j(d79aHhd15ktH?KT9N)VOytrNXVO3mM>iQsDmCrb`!E+^<*dcGi^!!CF^}%=~SU*t~urjYgS~C7EbAi z>vjQx^hz9uM5ftfoNy!BwaqlcdkqsS=p!;(Sc{T!Gq;(hjueuhP};o;P(EQ~|1LA> z-gA^mx>w$D$k+6U=SHM|LZH@EnEmF3Wa)F}HCvQ$^_e69x+*Ogo3x+dvV>qqM+=ko zbK5D;HF)VGjfPT?j zNBA7;m9&eUS%|1H5_56U97lMzN-jvd(0SjS95KWeW$QkUPkB=Fg`(q>^?=q@t9Q}< z_HANk6Io68#(=~VZ?~e7EH!jfgUos=)~weHIz=IS8f2zFSD?WeF(dD0TqQHxg(zaS zBUT;sl!*IZt_7ry_dC}EmStLF@MJB#u>2d+`vlUD_aLFo#y!=OT0)?4>N-Z7z9$(; zd+u%Dq)(08{v-dmCx6@heAR1;$pc;-{F5P`sm9KEgcM=Yb$S^*?Fq3^cRAn^^@cq! zbo4p6-h0{l7`z?reqVXL$~QiKB3xU;{xRfpsH&WYdzejX+YGn{6g4L+;pnoWVxs2> zf*puY%n<5k(63o$)_a1_dEYTE7ca&n;q}R2(Wd%NVi+g;#ioaf(j~ZWeF}%rR0t3M zNxl!nTzCTRjtBV*Iv&EgPli^2Jw3y-=3DIeFv&@0vXkzc?f)QvD*@Q~?k(#UxMf7V zgK~HumVZu;H8L(A-RN61KJD$!)Cs$p2y?i$I=3;XIs{H{Fa~8FAmLiGOty|wOa*g% z<^;n#LtdGT39EHm#iZ@dihMt9wxmqmpCWSC1bLkjByQof)~*S1Mv%aW|)lfwiP#= zY6$2ikb%zDclI3gt<7MhdFwzhmh3ET8hzPhsw7RDBcE91f626JY{#mZH|_{LWQaL( z(##ucZK50Y>Vdu<>~4qEp`(Z;o8}WfYfqL)x>#Kx6(H&}U`#={Kq@5iLxX``Sm*-?Z*0# z>Kp2})lW)!BsV*Beaa&#kEbN2B&Q^$JceVpQ@Fmy>BB6PcL#Uk~!v41*+LIcNWYO6s@RZ3tLCwXmjm)#kAoCEP_|r zRcuiJtpBD_1=9qvYpoX>WStV%VVUVIl5Jn7E^c{oS#6WFj&;}YY37ctau^MmB|~*A zb9QaYc!*EdE{L-H@{!gN;oE(-MJ9}GZ19QgC^wm2A^*`nsOE*b1 z4&IVh#};c1iLZ=Zr8C=`Uy+S5zJ1ra1=-Nlb_WmCv1p;VZ5V+s_C?W!MNz9xeiUU! z1K(TN7O4<2ww4asb%ZuOP1@KXcSwb?_4Ty?DjAG?MNuFbDrmh0y1Oqjpdeza^BbiN6|soY45Pa0_X|x~1m03lzTwErzD2rXd-D>h ztd?z}-2v#9D`K4LS*n(GPv+}bJQcY&^R5WrI@WrX{hKFTEDet~nn}ooh3*%620RmD zw@Pu@wCU|m0LNU{va9-<1m~sHipM(Z*j3V~v7-ti{hH*RD2gtURy5YDZ6Vf=PAJfH z$mhnXXd3;8pa%i;CSfYJ@mYafpgb#}j*W>>!|9m4*YTuc=bi+t`6>K``k}7L{3k14 zzz2G%7i&KE7sE@ZTA$?exlTq_DT#ZzqNsG%D_bR!T%IEO47c(9yom3o6W`Se=pig8 z-;#-f(i=Yf0*>l_8F2gg3-1gO`>4MGuCs^0#p-8fI6vU)vbpaB%nZ$V9-$%Wr3|zF z#6I(=&{5=DGK*VU$hVYz^BAWI-az9!_rr!0X3&RRgV0Q`iQFo(zH>WZ>lo|fHj!*s z)eTl`|0_JW&z~`C#_SncGxRh5GGpe9=|D+4MWO4l{LcUrN1T~c{526-{*Z{D0fuU` z2iX?L5xvU27qM4p@CJ1A1s&1@&(?vmV4W`^!Vw1WRnHpK$%=G`h9Ls( z3!kWyCfvK$#%B*kN=6ceV4j6S_+bW-BjMLuNi6wX(%Dv}Zl5D?>0-zA&;NCoE~ zcV5`9CN*RO`0LUHTc53EHDj?q(K=0->L%P{LLn+RnS>rQ4R>GxC4Agb%NE_l0+PVq z5lauIE4a+x)ltV5Nvz-L!aYmLVfQ8KE38Ws`Cn_v`QXS}n(2glxS4gV<{x~npUzL7 z8(7DtJOT=Utfd^q&b=|1^Vj(cV~4ud@Sj|G9{cAZGUx2Zj<|m`(u`<#Z|SV!&^MaK zWWG(P=tEdLz9l%*YsiY>@2qFp`Fs(EiQZL2AUZM-Na70^-4`{$E-CD zXq=8A>M`GJYCpWbrrcDOeI~yrU+Ubgm@T)MDziyfu`d4wejr(u#@)T+(a+Fzj=Az$ z*7^v1h{2OZ5h%a~6uMwuKFjUl7SPB$v4A#A@sVDpGsCIu zUA!wYE@y$T#a^<$9{cKp&tnzj2I%Y--CRXdU~QW25R5!QIGJ7{>mxV7VoD0s9lDep zn6&qcvz0<7aL#=7d|}eO+`Yhw65o^05(4SbW%!~YTZXSLMq9KOG)846@Y#|llnGmg zfNRZ}pia(|h^eCV?DvKcWPN@*Yy-%+EkdK!Hfhn=q=Z_w6B6-f>rDfHN4qTniuOY- za}~O%u<@cVN?ApR12ZVXfc>8nO>T~2-0-`P- z?rpnnr0paBKW!T;{{Px`zWsk~dqD>5h491PyEF{~2}wad6~C+$Cm9$S*Z>XCeOgwk ziWERa?EZVW%X`WCE!Yt{ZC@ZvR8xOZd-IHzUXPLrVpcrgp8w1-i?mh(wiD*cRC%nU zxUw;%s;V(lKvmY_qPCOBEa>uV)45|PVae`j*P^_ad!q&7UFfH&OqsS+-F<3fNqgmd zSM*_w^l%gg$`V#SA0MGckJ+l{ z;h~UQCUwJFFN(sFLO*LI*fQ?(ndZ3FC+t*eb1(QaYWqqzF%%+bKB0Yj?9jcS@8@YQ z|99T`AJzBsjKAK?d)q=$G$kYYuSKpT$*+q>^sKOM^*ctdpO855_awh8e;pj@KB0t1 zUBp7ozRAz?|GqX8Ln{KEY$_uoJ4LRVF_@c@e~r(3(s#+bpXJJPG*1);A_y@h{bg5? zi|Nf(%_*7KAe&Y>wuuI3W;Lm`hEAQ~tJe6Ya%!$nkTrqv+rL;DUvJmNY*anbuk%;E z@-r>@f$YQRU)CPW)GQYmcUe6CT?7|yrbPtmLPSCtE({n$ zm?AhiO=7aOfS>1)UOi1pXkCoI)=#aT==CNQ?&YXqeHy=0s^lsy;$6EDOnO-$cth~W zGZW0A=>oref$32){TsWE<{xZN^0Ck9)rAMvN~+TEp(hNqKPOrxgwH?P2zRqlLUQZ|-CP5oGf@yKw& zScNT0p7&%)kq}B;OE$}^gxyP%{mVqub8<(deCOtVTcO-AohoS6F7;<5OLrD2UzHRb z(JoaMxoRc(iGN?RPT4?Lr8!%MYh9ITjqTH zwp7p|8K;B*jV42zB`ESU^bTUzseV~^C?;OL7j#;dBq4PL@u@X|&~(WX_+lAFQGWwI zHqVQbEu`5A?aBE?v{jQOCc(M>yv<*D=q{vCpWJ>9_}+9cUgogV8ZqH|GcnFh!^PMx zBhMB~mrjwFRP-_p7he&?!c=MJx^l1y%d^4KP1~_n+H&MB@U)|8CtzywPA?b=rm~~O zc15Sceq>WI+~!Ds{eFewL*|)^yPIsk(c3CR3*Vn{Er|7h4DN|B(DU(6WJeD#kCCNj|xa>jh$=Enm!9k%G^8%cb zjFnNCo+qPb8aP_XT4W7kLI(z|G0zw6Do0q26~K)uR~*A7UdS?RbqeU3)F7j~GKRJrTj_A|w6_r!$)GkbX0< z#$Sg+-(+He-(DCd=bB#~n-!ox97Nm_d$GDbh@<%Di+{gRGtjl1|77`d&?9zhJWGab zj$6HFpXc}p0&m;RDRAll+inNJ@5O2~;W)W0g3xk(E(LtMGPrUaf{mHmu7HE?*s>OSoo`C%c}B! ze6FHmZ{8B}AuN@79_EKWms+R%??+TuRuDpV-z!up%m4cec~!plDpIdXwrWqo$73vG z^NM_zjUBCAqRS7YRuCzVOJ=gUB1fM13hD>kz+RC>-geO*$r&|*q4hnRDSHy|3lcjz%^O# zf8fuaJ-aX-Y+RfOSla^|1GQjU46+WJ+`Lq>aH3s)G96CK(q)O}IY0L3!KrwDGCK)o zov~aZO2NG0g+frY!cJ(XtYfF5mgY%CH1h`gzrWA0%lZHQ|DP|v?74sMpZmpAKYzof z$`0>%;=WZ^oMsv;HcDM}&yrR~vel6By-JRc!ziKSjM$d6!e(LKU%bPn{RVXO{J1jr zagUkxStJcFwM86X{BO^`n-U-UQ+a}-DI#Ew0+Po+U-B2&aXxcYNAedZPl*vtsCifA z=ZjrS$(K(r2H!t6p>ca_vHgv~pm%V+V*7Ys>f_}J_p>vR!3;rzi$v^bs5*-1K#@40 z8&#d?D-wH(m?s*L!ZC8qV%KxzJMP6SvVM6=*fk}dPes0nv*ihqo``eRGn?eW9r!3j z0kik2?S&%lGf;#Mu1v}w4~H#C-M(Z@0}nPXo}VmgeaTg@kYy1Xg4H8F?TnWhb`qpv zJYnNni?wxwp41W$kMDcPcCX$4bp*AAZ1y$eeD;G*WauJWT}By$VbWNu6n--e?~hd&#nq<_+94jV5{PoF zBB0}bfd|Omd(dvWfs2Ab*#6ca8>y`yG?wU189D8SlBgoW>=ni3j0dZCM-TKma7zTC zJL@Tnm$H;eXT(UH95rJTiMg@q2F$ox%AW1_ZvlS!_6m5wGU~Y5EAr`>ha`&tTE=f| z5(f?|T~^Ymq;)4G2kPx#DUb~IX3zOhlfb0=zq-=IRNY7M>7q7CEx>(K#0hy_4cM8h z&yZ<$Ssst%A6K|z>%VyxJF->o-9h&6cCznV<@I+z`}=e8$Hd3-*Za=ry$6-`SHHRQ z6uhM3BF%@D5WHT1w@?#(^XH_fsnu;_|4g_B!`@;&y8<%fdr5<9Yq)nVnIlAmqMRgh zjUd;EY}B6|De!~3Uu2`@cVweK5!iN^np9&J&7N65>99pK*L3h0n=rp+rpuA%Uq6Vv zQVDfEmQO{ey%A9AeP9?S6*Hgc~!(&8P1xzV}l!10@WO8l%R+T|NPJ-I%zI+0JL0x6?rx!I_xFPc-5oBy$~E$Rg1C8cpEB-QtKKwi?_ICJgv z1Jge^nzYV$SQ$K~ctlZLd(3)X#A)N9=~f{KUJQj04`H4Fd{40J3b8&CFfLy|e{H$j zM7AO2EE0{btv=R@JXj}Wu0NA$d0nx5JGPgvjM3T%<}}&M2iqh#wJ#V%N|f!U>Id!Y z;C55%Pedwa@<-y>rX5xNY1yBTH<%pPKzlSex-ojYADv3hG84%d#2Vu;J-(jHwmNIFMfw#3V71h&Wh?rL~ zMoc&y(8e1EVUWbyVFrO`wMPc4ZV_j0du)hYesO0BI}G--;l3^+Jp>eET>njuz{X<) zw2K6Xd{`IE7KQ?A7ugdBFJOY%6`q#}DFINmWu9j}{UlLFzl4;*$9~P5cKJ%^8|8I4#k72}0P$trm@*!F%_ut+ILL&~+@`OPi0~th85ZKe_@$1Lk7A1f#e3*{%h1J&*=Q zoNbb?mz2lJED`6}l4+|pdQwL%STJ=7t5{IFVe-m_NT(H1_WZGiy>Qi9sF;;!Ngc&1 zwhfOgUT_S6&7x&t!@{SInrvkwl3qe=xLJ)C$VZthq z8F_bZA(^{LQsLZGzJN{m{go*n@~6LB{JXrhgBD_0JBA8IaIbu?B@r1)+1~ZB1x?P!Qxs>9)it>#8pX- z^m~%T*_NSuva?=^LJ$m=hWsftHm=J6 z&>^55`)uqeI5a~1^b4M`&%QFM&z3HNQp%w^0e`k8sq4mk)aZ_JS`=V=N$=$~!37&O zkzUn>N&K4R|2UU_v|*F5@5%iJdCk&=8yb~0H@-sdGT|*KG5@NVa`l4Z209EB4-aYk zKL^MUv|~W?uKR`lzJ^VaM~=2g%u-R3IAgrpP#`h}#xPyT0~MIcOnO|<0@H zV(a<*#xZg}f5(`3K7VJ2_mlG2yIRhLTn@54zErR`KzOwTL$0~xyynZ$mkOR+$R<{9 zSHi&(%F6utD{gl|Pqm?P_6E1I@_KHsWPW8O;t|{fYk_G1>SVPr&+r^i(=7cOK8f7iG|hWQIf%Gvk;+E}BbC}k;f3W_DE-S2QWtF* z1E~m5xhstf2ivelu;F+<9CNS38wYBV+~vq!zgO8kSzAwJ`}TUJw_fA3iF!E_M4p|H z1f_&F6`PXCKsCOjtfH3IL>-0Sy4&K3xHeW%S?7g}JfNRYy{==l`BM^uG}T-`LdEA&f2nVs z85sTGmWdyO(V9x7zNYtlF5)BJuKbI-KuS?7u2inf_}Hx!Zbji17x6Zt+w)3((%S?( zqmAp~v2moewAP|8Vio&5((R?&iwZg8Tvr6E~jzZFuYi(Tnm7$6UqAK=W1bsk&3=ak5hJdNijCXO@ASFSzS z{UFfx<&fs%qlTCST(cw$eXID$(6`K5iT@f&C3fh5D~fz=OAcZ$l_`pk95oCdiC3nN ztb&*uyg%U?vvetp^xV-@9z z44~&Xf>A8$;ZBN7e?H={m6lkoimr5EvVAo#%y&y2BR0vUZq+(>0Zc?xj%^6w?jzZy z)Fhu|k;s}Xb{!>Et2PrMt@bem;iLGJ1=-t{$(_ga&bbRJ42esZ8Jyb35|=vdkAbR1 ze27?vkgfoe#-UmNDCHX_6(n*I0P%v_v;=pXO>MAFtpdmoDR++D9Yt ziAO8=`8J&M6*lYYpO!h7@t}=u*v-OPaFraXHMmQCdc>d+ZCRNVi7e`M2K{ji!11?Y zvh_L50rmbIJluNx+1u@P$~r^j!Tra(UvuZQA6IrP5M!f<_n80hA)Yg<91x;_9KUd9 zoH8dI!3M-a1FA45KWP6kq&F1WMh23nBH|Z5)!F@_e*CjnG7QS>@fhFpciw|7Ypt<~ zersfU%jhGKb*)d-89k#Pi>&iJ^O$G!gPUSvJ;@Mu#Fmd#)D(4NNZ@*9D+%Zk7)@Mx zu(CTb*o7~xcv!Hr1b#v?Xx=7vMb59k6@sOgeIU@EIuiw zJH&`CM!*KwA@cv1;^lGW=Ah;p!UIs%9dI`Hf6YEb+@FI_33Rd|8u{Q5>r45m)a(Ub zz_*G+oj>M?Nto;mWfc-*VlUM^@WZXwzXZ2Ai8Uc0ddbeM0c7{tj<5JG#3BQJRme zWFG`gZZD-tD6D^n9ooO_&+O`)qALQ`Vou3kNMee@64HlUHaWs!OD}wMA7ojWM=+FN ztply`Fr%1d4avq9BuD_sN}08z4yBkb96=%?#Ps>`MWZ^ z-i%ky66{Yrvwj3#oJV9Q@XcZ=-tX z6&P6nT_lML30)+Ep88MpOA#uZELgH%@hwfx5jg5>b*JkE@4J*Ur(M2IchMGyN3ScN z8I92M)vcb>@Z)+1=I6eh>3Y&`yyvRx#3JYAKkU^H18i_D>J*GUA3(!F&TN3M!1U?U zLEk}HGkhVFVPB~(**L90T`l<9*umgegG`@k^&u7lIx6huo1knb{MjTnZveO}D zyTM_J=9?Uat|~IyyqG>I67$6JQ;akQya^!<7@tVOo9GkFdgcNnjlf`vCF~DNv*UZk zQufC3!JpHHvGG2!tZmqrvdZIX*oyXgMKc~gtxBOzn#3vu_c1Zm@jjEpAQG!qUNdVL z!zYY6ImH@Bn-NFpodNTaff=iY*P47=-Dr=fal31VFf9q%z7V;9d3u~&fJcA=kdy6( zEX`F+nyayNI!_XuUl_+Fr|6(Y*6CK9;yMPhcp95F3}7*aK<~beY5ogC5j=!(_4#(< zGVqD^M{;>opZt#k4Q3C*5|d0Mu!8G;Qm^VJ%~tTRzjP40Rxz<_<;AdZ6=Squ(}|(t z%@T|gZmy+Vw*-@n=d}L{ zg|Djhm0U*%*DK8x#C3!!|Hpb1)x5sT~tkxi#Q$LIm9%B6aBxjaGq^BnwZzrdy;D?<@;cCNDL zJ4`1)Inbm?`wA$qqI8Ro%FS0=#}DEweyBSe2lQgId2+3D!nIrIwj3)(;T z0MX->SE1eDs0+0`Y}RsRY}5#b;6Tn4#u<&#X!@{SCE zlku=!;L|o!vAI-ZePyGvd$Z}q&LYMP=iRyYn>mp)Ceo53GN&yBA!193D>CZ?(0;>- zhRRE}$3|~Q2p{y8+3}APvH((miy=b{%WFz)@BiW1XQbvQQ9@V~sMqvgorZTpz7)z* z?*Of9nEaZ-Tao<%{9C@5c|Pf!plSdj0QoPbNx zgF@zxQb_~&nFm{b2%UhnVm$a7H)F8PH`H&rVi%?A&yR5vXBad28O}DaHXCN+fQ#o zKaPZHd2D1;S_g7ZfZ@paRvli2K%md`^C|P)4gB2s-9@gx_xZUCcrV4zQLK&;Vvp{R zkb<735cm-*w!NI{ zxn-n{SFBfVc8L(L)!2_*A%e{T`kDE22j~Rg>Sx<%@0*pbOLnvgI+=++3>F!OOJt48 zJw@%;TGSvr!7OMbfd2!= z)kUskzTaUKrzcy?wRgUK&5-Eo?5Ks`LT&o?wQJQ}Gqle|)CSIrp|ZuHf!hF^UPgI> zz$lailp9&>gwIeO_=d_195y5~eut!ZVh4n>W0KfNi0n%xu3e}GGJXuZ4k`3pKa%IFZ?Zz%YrwNIZ$}qL=8oBcKt+)D-&r#LqJAzJZfQHv zsEikyks%Et<=n!`-0PLg-MqZ3=d;ihVm%BgwUm6cNId0=`G{z5N?hN2XgD$pDEm?QJaVsJqcn@xL6AmT zcmV-aCyDG2_TvfHtKqf*CZTZ+~F*h?%3@F>kJk=BWac1!!Nit|Gr-d+nk1fvca7cJ*0V|z% znwCzYlCG1GQzq(ybiQeMN>3E!^Lo7pD--Jw66(Mq z&m%~pNCKDEE&jabhq4AEDf|mzMr3Wb=xw56iD*Ct|8#)r{c? zruJ)wcQ|Mpl59zpi5^dZQq4Di?TAME+>*w(8XF+8mSHEzNrT)Hi`T!vJ-KLB9>NLc zK`(LCP}DWKNMDQ-1TNr;j)ATp8fPun&#GW1*wYd+lu_z_b?$fSp&3y3kA?LLMQw{= zC)A;k{VFZTn8M)S)qnVqlkLqiXd{yaK@`FOrp2b$MQ(?YWh4hzVr*qAKd6VJCo*49 zdO3g_4ttTkGJ<-YX3xL0Hr6Qjye|qQ$6|EV2P-{OD6lekFgWyJ7?Nq@hs1OOtoh_{ zBuAjjYiJm0&&P+|auNYU3L2x2$bG&+5P?NIUI4I?P4V$%(XP(97?K@)6Os)f14UVk z>o~d4@QU_@Oso=K6H~nzz|4FfGQ2?6<1^pa`bv|$BT_W_;LvEk!plMXr2|5k$M7jJ zJB=RX>hOL!rJ=d5eZ$(7yj+~>P?9{I@iaJqaw8}j@+8Ap%wu`;XiHpn!zVUNyu~td zA;i&#HW$~oTeJ(e7|ySUWE+m!Ay z$70?V-Ej5L^0JzCn{MG2o#R^d_u;ok9|U?O3_8;B^t=}W>A(gVyy*zPIeWS~{PK4@ zb6k8ouYY|@t2=_s8xQFs5S zf)vzXixU@qPj{(dep_`92^@JhYQ3Mh<_WPODOb(h)vrp}KNrL!&-csr4X-Mz=Z~Ru8MvXi4S9W?8kst6|snxGWlxx#jJG5mmZGAh0 z_xT-zYuCIU39(`K8vfH;3nMG%Z>%ma*D~yj7W2KJn}}Vm#az_7)shl@e7B`cc`{Oj zAW`yOFVEh`4u_=5qoB{$Nv64p=x)P?xqS|`=l!c2s#Yd8HZN%ZO!rmw1Xhc?7L5|$ z%EwXbD&7{`MB5)k!kp;h``dZ*lQ!)_n=WqWTc!Dye~2uS65Vw5@seJ8tZCBR;z@J+ z>Cux!Goz`C$zRGo><)Csv$HTKWR1EgV!E*?W69#d_W+9~6&J~PQLZ`VDW7+CcG;T6 z;D=#Ps9;%U(ifA?n-bc6z{arE;D)OE~&dU#QVC7-A)zzy&4KJ1c_1@7Y#wyYg`IEp;{ZT02=m%yHJg-Ff}UL*6-i};C8 z@fxXb;sm}7_M)w(3z8G`fRs9&4C3>{P;qCwUe+MF6G}8q%%wb%O&jJuVmkL7F2|9` zq$QMI7Pc@AXuwxA$jW__mH{O2Gid}8CLo1c1b8wLdv*o* z?jq!-G~hT11^d_Z`Fj>79uq8m8xJ~@BZ?jE5JDl4YA`GF%-Z8BUM5z>IWZvip?E%~)qJ6%yxXVyA4bb(`3k@|?9$yrlpp4FCFBwLAs_Cmc$yvjs@9 zM%jM|Ine~Kl20YIK!t9bm!>`QGpmytDeJ3uRkenk$-`7jMZxdMYcsA_NeaXa!tEGBsLcJf|$AYW@dE0i`RQO(#)X0QG0bqu;Uxg}6u{MYWPd_vn`b-j@;60eX4LC!@UH zx%)OJ`XcI}{q~7hlrOleXAYP|#FS$IEn6;w_*pu8Tl=<#lfGp2p__)3g*$f{uk3u` zSNJTZer(i}utOYOw*>mwVy&x9U){a z_(tsx)YWh@^VYl7%lTv~?8Mv9TfKn9Rv3fM8zH9pWf-aA@Hdc$iL4hN_}OijGk<(n zbLPDwFeK)b;ea2esP}+6+zNi~RBI{aqQB$kJ_3n=FJ-EA?I6h+#8EKG1);xzqORk6 zU!jZv&k}=j9rW!!$QGxbkec_9y6P#@t@j{5;8G|jvdLb1x74h+q`A6}A~M6!lKmTc zu%CQGm2krzyF~1uVx+&eL?fYnxnHOV3w$(g>RoLDkX?42}tF zh7*Q;wC7);6T{};Ai1T2b(h)6_D{u0Q+)N5eOahw>w9<{QB8*9S-j@045a77p1`vp zzvcCzTPobDxzgpXAzH&Dwv zr$VZ4uS)wua{zNU00!^D8fXH4Iqkot=34OPM7~IKrf5yYd%QPP3TGjM9qB~N!-m|zD#5^ zZmp2fJljo|wb|VAdG07TT`T{2er(}JEg@lo9tMu&S+(b|$0^wj)-2D5N+i!*lwSZl zZS5xeg;Lre$v1S04Egyy7PdU!A}^Y;i$<1RAEKU^lm>4IN&H}&o>}-Vcs1)O3b}5= zaLWDyR<4mQ5}*BUnEj(cBVE0q=#;S&R2x-=*^1AQOMstTu=u%movD<71D~{>6*(0Jsq^BUib89u z2#`Y9tyAqYF1N{=WCaJ(NsX*0)VV{IE5&U*_I5%?6UFDl8yy%P)AsrHh~B;PP0 zU5$#>bDlNQQyLA)+32K^_9c@!v)*Kk*6tTne`X9Y@=a!pOj6JxKKs4Q*ja#e0fk%g zu&h0!wSQv>X)Um(!ACDQFnZeXg@|lkaXgE(C*hQ7qwt>%TC~$rO)AW+^5#XBN zg`h-UAah7uuM9eo{(3l%aqWg>0Hi-{gRD{q8uX&2{gRg{K)kiU#SjmtK$@14^ z)|s$adX5qC$kb(;nM52a7V6KrtViJAb9DQ?%4#Rl%$*b>pTP$&JX~7iWo!?^>t#OP zhH!jc@JNRs3ZXqF+kh>r*BZsv&OxHa5%ezI;r&sZ+-8b>*C>Hh5pFA>rhCTH%pS+(ntMzJb&VHh7`T;!ey};YO z`28E&UX8K@r3~d!lm}7tC?Y&l4;`Q#WeoDxO~&t7l+!54-%B`?%SD0Wn}^Jd++m({ zxG2Bju(>S8ETtST&3;~DD^ogR9ID_SnC;)3nFZLcxCser%ha;|x9$i1G)jQ&PB9aF zVQ|~N=w30?^UpifLegdF2@!5-pBr47^Igq%ZO$}`m%VAN6?>8+d=aAR98U!Oa*Ze*fp$V^?r#(rkG}BARC|5V|ud<4&016qG0tWbk}o~ zIihxT99{J+wd4DyVMy5Ql|NBDFj=&lJ=>&A|>QMx!< zs-~QgqV-))SdT-LwbuXGt9fa()CBuuk|u( zPMrIjlODD=UIaPa@g8E?WhtYy!$UpK3qTIhR3nKYmfc{(1VMju2INCS2cHDG;|^=4F&P)FSF>VOGsPieRgnf+#;Yj#_a z+|7_W?xsAzOIC z%R@c6!Y6XKs)JYhTm@|va`eTay3>)K>=61QRKf%=_bq;?&5ZS!iWsD#fQJ22;`^Y& zesHjh(1XAGKCF*1T6P+mHq*U_oGH)mEUb|2lr4VrQiZHy#FnK*t|>;+RsUld=9K4w z>x&qWddRP8N?Q~Z2gv+bCj*=ws>pMK7nYScy|)NgH-auWMcKcjoeHYE5Dcj5Eai+9 z7oL?P3`S4ipepNuUzz8n&eUwx&*Nb!YG$C#{R!Y>Z;Gwqiup5f_i~;wS&iemkS&Zc zsdgFyZ<-J6bA2V$`$1;IsSa62_hjpsOz=#q?u@{(ajw(CJw=8yS>3(O%#5{`+$WIX z2(>X*ReP^fMw^*?3(>xPl(S0-=kaXG@ym}W$TVmN;6ZX|L4Ef@Px`_Wia?rb=WBdg z+!lv>G6WD{^mV0d>*Xf8qbxR9JlLU$<;N_9EP>anFbwMzmx~Y8-UTKRd|`P)UT?Dc z_C=3)h3Ea7kVsxjW093X+j;pP!qC3PZT6>z1gI)rI=~>CHtp{6+f)|;S;1}hsJ^UlrERS4A&OpfOhhsGd z|0#i?#|~_|Z^!{G*tMA%C#2{gWB)1iM#ETH;F9BHyaJS~q2l+?V@@b>CoO8W;2!V8v67-BQkUYHl zK`D_Lo`_7k=@<1?0kD!fQjz6*m`9`R^|1Z%qSHO~OW-z8CpsavHx6nfhrmyeoD)x4 z3ly5g4ea$VjgUSi}YJ{pBVWs8$VXGzZ}iZS5a0 zj$aXg1!3Cwa6BcdCutC?3`-2C-rvwi=&~PA3U(~vIAXk*$}LisuPSo~Ov;1qqN2RY zl@hC7!j8;cv(nmt@L#&}l00O%nD-rg5W!A?r`}7e(nIESy_o@S3d%DkH?C`Jd=q&} z12K_Cap1$Nxd+>;ujm6EG@{%_Rt)0S zhOWFAjlnQ*?dIWs%6T|D$GPvt(^{(amGSPiigNbCe7?cdU8I()#^ImV4_v>;Or zow*qC@JSAi9B2@*C{SpTki#N}C1yjtTsx@Kuy35bw$G4%i`ap+lc7XA5IsB+_6601 z+#@t0cFVtUzV0{Yw{X7p&bg}#=_b@9#%w&yl|(8xHTn~w zp?V^tfjmfzFn9i58Ez)QcA*l*lFm+OPhclBplm>?K*>OfN4YwZop1)_2+B^BN|cvS zo<_+-Nkh2{<@BQc7*OY<$Y0im^94G_Ql@||YK7RnHso-DKwL*D$DqT<{s(M4Dn<3FHr35YUK-mK7<(bPi;xw<-O*I8TVwWi7eXU68dY_pD%_qL{x0(S zvG6EtR_7s1xj?pfSs44?Z=RICn zYcjuc98ESSGXO_Jjm{_U{sC4Yz@$%;Mn4elEU$^hmmkL63qU#cr~25bRgd619r5M* zhit+Z0N{J2B|Q&dw2*rRpD<+wXZ1NIvJTwg3rfOm|4sMMi-LcKD37;iQ|)BiJ3Thc z)Nj9}C2e^gwON1nmH%Zh*s17ZlO*W7Ej<>ZgqbBV7UM{aZLK4MUG@8!xhupzPg)q9 zMtV54hYg{V>^$}_ZLBk|1QkBkS0SBZ2K9qUlED+CiE*Y1lXT}K`?r@JznuK$GE3fM z6=tl@C`FO<&J1Z;1zRmNR$F@;RbK+(feMTIh^uPw4BX`Ht7L3c8%*_^qWl@dq$kMc zDVx8aO{kDMQ|FPYCW4^rm6ho3jmN_MYB0QTwRwFN6L~K~h`quRp3Bay(5NaR2|05b zb@(71GWFF;r|^)?Hi55-CDK_brqz>{(Wfk7-UR62#bb%Stnk#)tAYcmw#YkwU?y7Mth@ZIpa zXOM)5KnU?sTT0Bwwy=8&@0a2ZKnlM5ft0YP;qUjj2YbCA{)*2>_Z7^>gblR*e@)mQ z?@Un z;BmncHlCF(a;(I(beB7i+{9t^HQOnXQ#!!DI&YH@n%;V|JVikUkm<1uLamG0+~E=Cw5yjq2{f+dmRZ{PH^z4DTt&j!Ny&3^k5uBPEC()3{+pBzB-7Z?K9AX@U@yyO-GO^^l6kd+H= zhhTg2FUQ%e^p|5oFv5IIqYSTSXwHOVa+y`wfbCi%9O65;-o7uw|LA>NzHo?JCf~5> zh}}8CsYb{&Ar3SWJ8=dyF7fu)t_Hso^Xnw@rYu+1f6Vi^-_En0VKB)(-T^3&fLv0v z7cU{|8J3jZ3=U_^7BDRAtYF$DX(^RSx0*5A$-lwU=p(Q?+5%ygd4*owvIfbfAH)nW zc$rPN?~KrkeVl-^bFg-PS?4hTfomYLUI|s1F!Z3YpbBQ$F_B&W6T(dd zS*&`H%2c!pNFV^2@sxP>o}Q%x7ZClW>8{|nmk1INg^_^yNFOU>lX8!3B*+CG4nero zi3fCX3|Q7L-Y(R)N7m$TCw~T zPWO7Xlj9^Rj>3yLQ=OzZPO5TJ!Oul(B>5Pa0*u87(7_^>7e=KD~(zB*vcVma9yx*(U5{(;hG$$FzW zaf)E(y_X~Dam(J?$DVY4D)Pt%E=V^Sp4MniQxUo94K;AMVq;S49&zfPQ>C&MGSz31 zXu_0nUy9L)fA8@fl>(oebsKak65V(edTG`zRJ{@b6{Tu?1k7oL0P_aC{J7jWQ6(nP zmhccNFXvJ;?XV}mnp*n|ckk;e3$mT4XzB0z2bfJknZ-Uy;L{HxZO0`}Ho%@bCrTX` znR(;0)W;>D5O}ug*5D7czhbk_-qHe1XJh~LO`@#8uEE?MSViD zJyO)@D8UEMRlZN{y~^tss!l`I9j&UMuwknhOd0?Rj8$Re1j%-A|D_c-#|p~H^D5}L zP`0ADP@YCngZ}}qH~VXn-cJ>X=8&Pl5^aF#e=_o0GhjId8r;9<(x|@7$@_Ho!U~3g zxmlQz*6zN#LA1g$G1zj6P{Rav+Ns;f&v2H|y~v~< zzO8M(RCz2K8XK`TEu@XOPleP_k^82Qu-`uQkdF6vzAj_0|LW@^5Ezs|(TMQ@Efy8@ z)C2_j8!wq|?481aA*P3rv~VOqI;N!)Ngp3PA%jB;%rlZlJ;ZFuTAbS2A*0&cmvmPX zOADivFl$3TG{5zIQDe*oh6{WL?w^f02_>FtTz zXQY#`=5W~2?V$_CQb^`Jt)v~HGvcPbvht~jHU&~+ZrHtY-=9fp=h>4tT2mBV$sn$N zm}g1W>9oYHUZ>aVkcASeyyP(mSgzT#7t?#kmy??_#(GV9LdpVoNR<>(FBku$`a%L3 zEgm1e+todbAb_boM@U~D#!}S&^%6S)6WK)jzr97aM6#}&8(V*5Z^v73`O-#(HFFju z<4Ty3LwB7C;XgSW$ucwc(^a2`x=0ir_%?#y9!1u08OZ?ws05=w;vr{(h6`3aVxI}e zG;b2`HOG>r{1Y)t#ZrcU>3S?fDqRDeJbtlmA&*IVamDq*Ja|=yuyv4lQY*zAS?T&FQlOzEj;m02nXxP!739!xf<>%Q^xU=*O%uVWaGm!r4;ld1Em#moLI;dI?|xaMVXB90oKTA z$d{gl?Z-%*!}gNy#L zj$%Mk*}Jjjz$_4S3HFyRA!`-ogKYEWmQQ`3iux7SrGFD%>O@LB&Ee1u5uWZjN@f*` z%e=EbG2_cShf17z-?1`hUT|^GUZQlCQRP{Y0p@$v6V$aF+KES)a~J}wtbL)xrSOB3 zEQ66X>6iBTOB#0X;y|c@4x|$~MI_>C2hwwfj#`>RC({YRAwwb`G5{yjM!hamjg@L% z4}Al>4zU;w5(dB`B?EwjmMyDirA5QC03Gs_DYWx1am;rBMu(MSayXMRxb0$Q!(Pp$ zJw`DsA5Mum=3F3lMIu*QZ_pZ>13apV0Q4uQ(7>E4a+T6tN_OJ7KujEm`@8rPQ4pM* zEQ1i+fY;V}?x&}|ZsRSL5ui#8it)$mb{aMbeMWw>OZjl{n7 zKR^{^cMl@0#VMjPr#55mbFfZT*mG_$V25CrqWqYmzJ?64d!5w09p}`dT_4s$PyXL1 zBz8~P%*FFvh-qR$Oq1y(rpcwyH!gI={!zV&)WW8@s6=Zlo0E8LwPrj77Y|oT@hd>l&TaK21E)1U#3b6=7dk4Fa zqajDiHex{31Q9rBfB?;yX*iNbJH)vUsspry(@mfbMrRyUv+V4H(SlKt^xP1#eMe}D zcH*VVyyzshzH(3d5j7hbJUFP`a&LDI!thf_;<9JTbR#U6?wClBqa3U*Bi&MQ&5%qc zk|gH4W48o4T0%%OFF!yV&#oUUI9r>S=^BFL;VtD3d-(QOlyMW>JsG3TEQ@Ymd@Z z`(VT;?vT+Q8B^I9P1Qvsk8tyLwMQnYtP>$pT>Y4?Z6oQfCaP-E|Meu-wXN2|6u zs)@G$^J-Nts-Qet>#=1Ip(f~9|BsqlPi?ECrC!P%ZLMJIp9nS9ZJ4(b_LNl3=slja zQHOr0$=bVP*YPy?3G{r;Qnfuleh^5D72rc{kM#q2AT17C5^w`&lVLTVfLjri+k`MG zYfMr&B48-@KDzM#XvM2&#mK5ks`sPo*In?M+ws}U$9C*uC*xC6*rC>2p77Qw>;LBz zMn$@uDv(j}ZZTnkkbk(|$ zTcj%LMdk+f`X;RYst^JoH~JJJICO7opbPlWaZRge%m7Frd*fAii&HzAw+we5<}I<^ zn-)j4wUd~9PWx!5Htn-{T3#boT^F?>Gez((p;u>#+y*vvQ3dej>Scxl0ZfD8Hi#Lw z0j59|%4LR~Rqp7WMHa0!>&1Iefp*W80lpsAmuHGmhi9mt9s z4t<7f&Rw7#0PW8W`fLZ<*r4)KYcZ;6f|g{sVJnq!P}wB$KXb2vHr$CY9&3nEjhGWv z^kSxWCL)mtf^!OIhjFINe$gmt;2}fc=42eVop$;I(w>tL2*9A`InbMmOY8!Fq3HNx zsQM(qm4cqA=C$2ylqXH|8P?X5p}ko0kA%8rhCV)_4jcm&_<2w8;8nEf>3dK2s1!lz zG&t?ep0~1~2ER4ycYD%Ot=V67tq;|&-R(&?SR=maS{nlVCXSos-_gNWNz90U0G7@p zuoJd4mAi`{cpqtJO5~Q5>Aa5uk-B=>rpr*45|BRo}Eau~B-!Uhm*u)lja zEQJz3BA#8ddOzKzq&)TivwG%3J}+foD{{F@S0`k;M(>86$s?EY?_Yt6^_reVCIrMI z>bY1Y!4~7a5v%LSCGyUpi)7ca2t6`{XGvr)sC$ta*|lsBuxW?)V{)q9g)o5R5o$f~ z-*U_!=ushFNPLsXJe<-7B@ZExJ`m0I(9}p6xCAc}M$D5=GD^53z!`awCnW-b;t-e* z#wa9gBLc;Va%)YVBC)xMW{Z%^2rm9m+4;Y@C{+2g5H5K!>gyqPOuw)1U;k0xzy8nq z!d3Z-ME_CS7ea0JzM@q;DC~1PpbT%U35b^^j7A+wPFw~vqTTrV%LWqqEu(7*^ zzWoTg;ta||m6A&IQ*&^anPWgbS!u$Ww*1&B)T#ZL)M?||t#3sb=!kw=0;8<#OamM&@y&HHe8jZQzNsnHVtr3A0F(!Xqk8`liy`Ik{c$G1R?A%#FAUw@(tndnj|N} zBCZoqkQxJ9Dd1B=R9TLULC=Ox?oe2G=@Wq|M95D(1!^jZ2(!G$aVn;ujJ^GMrhS*s zcN=#`8sTm63xIYoI+mb(>^xgjWWF__x>~rW~iUd_7CW@_{ zOwDbKXCBJOZ%NI$+XTM-Fr3mO*|q-BlEVl=o5-#Wrb`ZAhj||e0l79oau{J`an(PP zIE`Z;x@PWzz`&!AX^K$&0I5MNcFeYTiQUM0y^NPQsbfE2zSh&TXJ4JBzN&HzuaK)o z3|}MC!Xy@98{#af0(1Eo#Y(Ub5HZWswR`q~jT>fdoDnIpbn(^~mBqQcJpVv6(%T;I zz=q9A-6Xnd0*HWxZ7@-fZ4$2$eJMRb15Zx<w!p(G7Hf8#8X86Fm92sY5E>EV5 zVRYQ{qxzA-G-wfx~=6F=(Z*q=*8dBZA2DF29iq!f2^v%9SHNsiDZ8u&abpPq~l)2@on&j zS5f*<)`4GKf_?fNeji0KqTGYh54im~$~y#pi)cex2IQ%R!^-u{fnBWP5vh%XY;e5m z54#1xwn}iBb)B{1%%jQRHRFvVJtHHHvChu;&D(rsnYwe2`Mi~@(?=l2Vl7>eD0-49 zjL*`w($ZMEYlMi8gv=>hwsn*Nca`o)&dQ(q=?n#P$0`MCpS!H;AgF@aG}E zmNHlOE_1vThUBGEYoP>c1J%D71U@;4ly9vw&=TuSam~??>uAV~z_D{1Dst|r>app->%^~t+Q@KbK}lz#fSHj z{Bb_=Vp9^+^2zbN*ef(ajg{^KKVuI<&b|br8=+#!)ejU zjr)zU)5f)9=i7#lDn4qRX63W)r==f?jCsA{SFDdZd;S6`)R;(sQ~qM+%mCLB{~J+O z*!_eo9=ixz4GmM(%KgoV(4WD|S0_w3$z|7?rmP1o7r{P@Y4wziJ6UFM%_D8WTzWL) zPHGHiCfC2TE1P#g%F;cWgnsWTPnp)E zN$mB$;wjU4G)Wi;Pg$x*GwSATPno_pvek6{N5niJ%18@lPFrq)>~A_<@T$m?GHOc@ zF-S4st^#2=X3cdyMbd=<1$P%==u|~>c+CjaImWt=OgNbrvKSv6YVYk?JC9}-?+UAc zcE&o(v`$m4K@RhXRR+^0i=1JvWZeIP0oM*I59}gXn^3X#3xfvOO^6k#-_l)6Ft7&k zE2?qymaOCDr(bHQ^2bWaL2DIq+G@wkjl>Oi;wwadxn73NP`of;P{kpdyrFXoqNr!> zkr=#l4c55KYb}N zOs{Bt7s$P#&c6xN#i3WRqqvy3=3QVT+tr*m;ety5cUS+gEKB-`IN`2PHHljMW=}dmx77a4U}WnL2RKe=Rt$rn%{+OS&OjI3rqIN$n*ilZ zPv_tTxu7-2!bjV*kCL`^XfnT)Ul8iHUmG-SZ^1P#Wg#{L$Hl3*2hAy)E4OO4eL`3L zK-n}IhYw9J+S_w*dOstlg30fTK94>TvEx+R9hh8PA23Ulr22fN88$sf2KQSPlr&e-h znFYY~P!TY)UhVg3wDcIo#j;$-P3Q`UvZ9l+eM)oETUK+1_W7`sl+$D`a<+e;N=pxs zXG!l@4PuxM^2Q|;GB<-C^3^qXn1jm7h`?WvrJN>bc{s`0Q&kXm++ui}O2+Rx{ku4Fi%pMMy z<v@=~g&+%WrPKCc2@qi$Rjr*~6=Vo4<=uzyitTL^xyBjkR z{5xZvm54U-BO}+Y=F2kik32XR5BC>6EMNh&z7m z>M9jA4-t2s5>BLu*@fDI+evNS&1zI=t)N?Wwg9HR83w~HDH%nqJT~!!-2351tiI~0 z8`f>K>SNc-AiieTZpxAS>i=gG5)|cA9xTtaxbDx%8h)9_$VHZ;Urn35vipJRI2)H!E$v>S z>VjXiuH5nfx`3^%ednsRkp=(>hcVTz1#av4WZ;4vt0cTc{LzHYulKy~NuX6X2w(&) zs+rWnZ*}PDwOshz6=>$*01SIVIh0HyWUP1PwyJ44O{f&1tEn zId2~y@wZC1x6<8*AYyCNxQ;~f0J(2y;otntw>9^+z+DIx5gK@Oi;zE&lOsC-IZwTF zj;%?~H#CFw<>bN|CFj*Rr#bnup>Mkqh36SgUN@>QHj$msmmu){n&BVt|_XvtLvU@@Too{p{HS1}vEN8oItZS{QlHt_9UM7$p_ex+YG~8H$#*nx3aQk19E28W`HhF z%ed1Pm#LXWcrEg6{`gOz6ZYzVO?*uHm@V{C#L)wM$IqmjE5lw>5E0-j#Vd>S`a*e(jbIs|e3GV?B@9a!kl{#MU;+oAJ(<)QE4fIoTHfCFFeD!9{ z^>;8*+!)}+X?fmksD;taUt|(4RWn|97r{OS458<=2*Kp)vka5&U&|q=2A+d+OE4LV;6(cF2-&PmbM`njR+Oc`i@2HvTCsvwePyC zw5bT#D!yVX^ZT9&XnQ|W%$zxM=A7$X=Q`KR_X5<)>?W{>@rv?gp05G2 zG*NgF>3a;mBYWsx4(_pv3B&Df5TB*BH+JaF9V}^*hfO1H;*Q5jY4!d=&(}1o2+;eS zfUOL#d6e9IOmX&Q%#RCS@?`g(F}J@ah~Qa#BeX?(VF&!5%grnvrw5!VtSJZs1Vu&4 zjGfH0B>l$cb5I8t9?p~_-$D;(lKDqnCSXQ!_r~z#j^tK!3EXp9JeecklHx0$UgT0R_ky9>V?Q!u>aYb$@x_zW*-o_+STHw48jt z$RYS*YY~e*2G%_#cb_)2^*fx_@dWvw)z2Mzw_)oJGIkk&`%@<(KRgHy;Rp{Yoq~4A z;A%tsD2RF88X~H$a<%dJsjd_WDxy}Pmu9fF9JikC5T zVT4?}K5!u|K&#FjLo|NrCdkW81!fCHT?f68F)vxk)PhzKH~S=|1~z9B_5-8h8} zxBV$;)4C5KOXf?N5hq3v4WIp)p`T2o3`l&vq!=3O&y%*$@J^gB&5EH;xhZHYTSh=b zXkQ=|q8#(3u&OkjC#7}sq^F3BHFaV{gR=c$iiv@SZY;txuk+fccNke1wc`utBy%tZ z6DAwdtWTbCz@IDi0I(WGTSk;GBgJJHXZu_!3G6JrNES??9;K`c5y0fe{dbh!;0@Du zFbX}?k0H%$GImY=nNm;eP<02_k?Mq{246Z`3baA|r=9sBcpkn?mrcGb(w{92yr5wO z6y|~6z}{)5lu#M^5;~BmyF+}dMGW+5$6p&kz98wkWVdFeMhG86IVhhApN+iCx&@X& z=np*vzjU7?mJoc+x&2LN_pfK8^)EQ3+vt6&0IVRjYw@usFzu*U@wWp9?6k#VDrkwS z_#=Xige8&~#y{k<&@rp`JXaiA_(0J5mAC`HEpq2DkAU0`t|7=CEuBS|8mK~6A6-ro z932jI{A?v{EeA@Q=di|cd-yEWb677UTV6zONJ+VVa;F8xT|2TPj%MWUzixz+;5jj% zw#&5rheIz10cWs5`c&9bk^ij3{({`l_P)pMLtL@B1|Yo)u%QIbNu9)dg)d}BX+P#o z02sMrWf$qqi{h<|5)kuPXq0N%wstASyfNJsr2UA8kpf?|E1eB3R7qjYMd9K(XhcJh zw~l8d?|g7Ue>mFe!4WS>>f7wY2t%jJfe_pRD6GvYYhK6BiHWN!&{ zEdcJ~YD9T;QiQb;cw4_h>Nxm56HU~&5eU#}kXO{Nd56Ro$;%3=OviF{J9uZo(cDUt z`{){vHmoWPL38ekmB+LV@+#4D=L5Zf&$pkDI1AKXdHZ=@*To~!`#Geodyhv8nzfMsa!IwNJn}xQ-U+PP!tfyNtGw(s zXuQy@(f)%sx+_rWxyKgJtab!Y^A0D9?A?O)7WKaqwCZq+nC!pq%6wABGXG-u+WDc% zo|_L#UkfQk-Cb^x`}H-`m#B-8L!dGgE4r(Ix(vgH_mE^ff-Q($dR)AqFZ6Hh)hq;V z*LBE(+DG13E5!KMHF+VXjH2e9|QrVFO=M6;|QWlvKrffP++W z9p=0ey~*{}&k%o5QK4mo_9{QFoU>9jSNJRmEc=D|qqKc|O<&;Z@j8c8pn+i-3p`)@ zjJ3m}{gwxu1lA4R>A-s{Rx&_JyPi-Lrai~UXDA^&8ISx_k*G=5L-Gl%qt_b*`EudV zxpvL_%oq&FDKSSc8kPioL*w^iuj6n7>D9Id44VL6JKV3wpSEy0l;cLAU+?#7+5-Py zhI&6A+tR&Ak{M@0ybM{$8P`3Bo6$Fqp zly4X+zgZ~H4wr8pDxZV$x)p-~dF4TrKRHys^4Ilm87e=W)UW$v;DHd7A3Zi8*O&jg z{HCGunL>FTP-^fT51LFJDqr&J@~;n-Hwo>3Fi^f1<^LQgZ&>;3e&UA8KmN;p^3Nm) zGZgg&o^ubqr|{SBSs8fFKbgG8JM^A*ls}L1M~0qxssW>+Hv>Ym+yezbo>g?=77q(o zlbt+`wb}pZW%43u!-el^!tGpx%#2+94YkeJ(`LcdY=w$)%_G`9yJx^uMN?<81IqZ zr}eO*^6W3))9U2e6<{lE+B;<4Y~Qzg}2=S#)ZKk?%lSqs0Y3exuT zi4igpb+Jk`5V77#dYwu?A~VZzkh(7Nhwqa84fD1O^^~cgso1kg`c(v;rV8ss$A+Kr z!%lGq2c97&#Gm1wmJ3S^h({MLw40`)i<(aJTT|tY>l&diUK{zyIe^ARuYRGN0r2D2 zF`B4KxJRu&2f;*e!gRb|-#`OEMHPT)LdzPRjn0TX`Cutopn*Bu)H3u6KXci!rXIo` zio>^)pgNHcX~7<86OO;&C?NYK;5|Rsi*1{GfksV|G7+m|eJ)bJFn;-EViCv3`^WUg zPab2}^tQqDeg(itxyj#Zu$S`6E^CYGW3Chw)idkzWmeq+*ggR-s6gKH>_{Q)-Ig!$ zV&ueIV2|$A!)O@TWnLD;v$d4C;QAv;RtU(_k-Y$|M72%j_y7^J%uih&@)J7&U>U?y z-VhXmq`e}U_mhXwWa@M*%urEKh5H#t^l7|E7fz7fcEMz^6Gzoeo?lW}SeT5zcy7ZT zp6PyZ(Zsh@k2!Ah38@d@-wn6&JS@VdYVuMyw){0#Pg(Y{xcXQTp6;2O)8h>&M zBH&x9AV)8su=m*N+e7QFVQjs2PD3Et7?`|qcz-G!=i%;&GZJb{J`C@5q~Mhiiy7uNK%Do1GjhO`E! z98M7Wp}1xk^?xEAg8H?_2H*|(Xig!S6!DNjT9396ATMI>9fH^;?GYmq7cQMnTED8? z>T|*ahqREik8Oag!|JJ$e2t5qsoq9$^eo1>Bvqxm!#lCVU@bFkmRV`5vYq#I0*L*_ z%G+iopCYs&OgM+K0q3t`P53rITW77nzxbIav~Qc;U-Ngk2YlN{bOENb+YKG{PKgf< z6>>@6-Oh|5!mnkNW3b)@M;y5ic^qdWp$Tir)x!*|9&Rr-v`Vf70>jq~{nq3W+Wz_y zX9d9X^lr9I*!S{ycMp!JDa=#}jvt8sy@@J6<)JC~UmK6}Af8FPg5Z5zO12je%<_^% z&@>UkTTk>AUjb(uH;j=YOd#F!5)Ul`9Jp!6_QE8V`}8M>J^+SUJ7u!*$V~_>o!K!O9rFt zP%u&0FL^t8E{-y+sYma4Vu{1LWAFW5u?oF1=YFriDhPU?`Va6Y2W5J{p7}OkQt%UR zoc0?g$oP-BF87)?_Xy4%QWa~PBl_oC(wa%UXWmO5F?Dv?-dIn#h-@+13Z&i>yeESq zn+#{cZwmoNxA6MMDQn+4is)yP$dFK(ie z$M11Ko;R$n@4Umau}}j_okokb$Q3tt4{4&Ct%7fNE=$jWaDaHNlL0;LhWPF9>ivr5 zf+I`TC6|;)xa>%QywRq2Xzl#Zn6c1Ic<2^)iKKe(9z{4Cyp;>y3_eCVV@udhflqV6 zTiZp{c=;ZMl-$$DCM;8`IjT9>%eB#s`i+y8$z8#9SGceW{YFwb>Nc1@lffQR>qCBl zWY*0c6|pR^H$U{y+r3A4XVP0mBV4CLxZn@j;6n+ip9gqL^0&f%0QxP5;WL&+{4G(! zxGW#qf8=%B`SM~twkHCTT+Ee!KEzCI!U0_bkV{S3xw>teH>f)@Qi^cC2XAKsM-9XeT4!0V`TZ;4P;!}X(P#}vfFLgX` zl-(^14?!GgYU~W+RP*_9=zyU-ZzV2ik-FFWjB)RjKcAXWo+8`^ZBLtMoHofgE!8+J z%{Xl`LZ)o8!NM1waQvgE*ex};Nti4Efc~xPaOgQq%ofQ~?Mq=$@m(8qwDi1$-}i0` zc^$?6tAn;_6SG38!qUebyLy)7;%O7T&)zZYKLgLi=2(MzJ7pd#%3CaIj*^^JGSS%Jw?U-`uKVsl1!2MBlT4I) z&S3ihKfIojNE*lkD~II)$N>L^^XG@pAumk9d9jd*19XV{GjP6Q_&&~mi}UA%bN2~> zXBw_x1nn*)TZHceaxi?b9sB`F);I~Zc8r<{!BXLIZ@){Q2m$zyM|VVcwqGpa+*Dhe zgewr0iPnF6j={sw#CdMVTq#2Cms$-obv=$^9M>LZrap$hAK|YBG(G{xln}+#*RWPj zz+XA;E&Nz!?hnwjA+0WV#Ti?y)cRS7tw4^*bF(ga=?1%#N2KeY71f0W)3yg9m-BX0 z4nGg!5z9?U)<&C~e@#|ywxQ{CJ^Y}F3FTkBtAH+pQVd3G!Je)AufBvg(he?0DRU4t z{Y`^em%`ShYvMGlJ>?hflbt;Cdx5%&`fznlz?j2%u>tNJgnhNrty@>oG~GlHq-US z)A%``{M;`5%uC%+J_q2E8{kq5!D}hsxs-2`JlVfF0pk1&B6ykI+lw4I<_3xSQ7Nq1 z93GVAt=%{s)RGq=vB~B$Wrb>ml7R3}+NVYsFSIb^cUr8meUMQn7j0CR!hJ*1ITsT` z1goVzt1s2zsmZIW%|galiH^Q%*wNIgK?hMryAeHW45B8Do@B`8;iD@Cj9)%qorD3* z8VZfGoBIMTM(!Ss`?$uvUvrl}hiC+UIiCgLtXW_N{u*Ragbpd7qxceW0-+a$y_nW_ z`tZ+i?Sswd`B_9t`mb#@1AL;K(U;K(_&gQD+y>yfxyAn)R4MSThFk+JX8t8!YZRjnTxhfX8E;#aWfUnsHpifk9l3*f!Js>c} z@mSYR59mG}fXmIN1I_MLfHFKe(D~3OhaprbJvAe}T?lc6%V_m7yy7z8IEbKRPwFE( zEihX|Pz9QVX81|28H)N%G&3~`hXlvM@xpr@D)N1>d#R6#y7c&(5PYA|UU+2a@D#Ql z9k&H>ntUGW(#7E_aohe8<-`q2U2LEKn!rLS%Od5}H^Bqn!9l%BQLtCkd%~*0x!;_>S8CnyrqvwHvErImKKBsc#Zn~; zHacznwA@7pmzkVa0@H19nx6{G{eUmip7*b*8iIfIhxB#P(Et`CgGd@QqA#DW)m5|Py-@G5A`;gclA2R0AntLkrXQfa?b)Fq|L44hPgAlfo5CqN}44pt{Y0|TrmM7hTx zQnJAAE0P7G;MI|`B4gk8Mdq`+omM9D9T@=hp>wvzFmiSbw1 zXW}RN8vpsHg9Lu4d3sh0t@~`?oCJWoKy*ToMcq(*aX=E7l+SJ z(9uyi|9<#f8|pJm@L6bI2Ap<1$BW2@h7Q2C)F-88n8mBryF_}p>HfEwor;ZQQ^;=^ zrg1;#IYNqh-xu^avw-DD zWq8I%Y!b0l=f8R6I_r{e!Und>oG!%Mokk1LF2kKpg6p{h+|>3_o=C(;_@#79Z@Qsce&(mNGN54Q61o-r}>h2*GP%vct?Y*jTnmccPb( znqwc%8bRttM7P6D>UPHUDaxmn>Dtjx+@goIf7;pr2;qQ6jujtNvTVk|m@CgnX36sw z!>x&0gm;euuYGc;4JUrp26F)d|D@pifmYo3AFVJKP~MMvb8TCh;}%2?E&v(MQgj># zt^wu|Xvp7&o?v7IN7u2MjKtD7TkOLfY78r+e`LkXyF=L)(`c7@(Zf-Jrr~AKq6~S{ z9qQF1^?ac9F;^ZZZG@~p6INnCWopAyQx$$k`rt zEFtej>^pQ&DN+MBi1&rrrTpHoJ#`O3 zZ{!qJufkK1q3{&AeG+-7Bd0W-LMEpf>=IY?EHMP-R4IE}?4qC1p5gNnr7V32tw?>I?r=7UAQI2E0HovoEi@VOFS9ruOL6eg%x9cL%p_70;so ztcWUfiWa(8uASW89)V148_jG|F@1=UEtr!PPC0h-sG%g@?a1g%((CD8)Fl$?x+BF$ zH!IeAPLWUFIz_$p?a`?H)*wDnvM)?`Yp_`>+EXW*MU$LFnw2jawN^P{01rEEOiyw9Vi~35gB*MbBadRsVxbx}o(bW^ht?XuVQ4nvkige} z1AaIg$D3NnsyGU9Jc(l_`2AxzQt_L?aUHxr6?6_PX6?AGrp}Xkx=3Ppa|>HADw@{B zYZ4{-8M>2X2RdGHF#e#zTXq*X`Bs?)Mp4!AEaHOb^)~l^v+h5n$FGqQ&ll}yyqTuT7n^sS04KotDdBuG zbKPv3ZZlJ*=q+}~G1fGl-BH3kOX4_?guvohNjibmj=~ykZjl_XaV#0Spa(Egy1kdz zLAnx6C-~_zvHg$qzub#ZJdbGlJ;)plcCWj8^cmRYvh>`Jw$2ZRirQC!Qx&qR&Sv2P zp|=-%vxEsmhNpqRi9ZqW-~D4R_Q{W;2Ag@V|C6NhD460Xaf!Kfwm&zZ01v8R$9jV_9+qZ zU$OV*XFw3|f*}6*Xt3esJls40L116po0@^|D$bY-y_Z$IE}+D<7D!NW=VDYwYGopy z(vmu#4%9j0DUp9+FA6ir?U0;%iiG$Yr&0DF20Q9fCC~N~z*33q3+#UiBSXw*LacEq z#+vlxi5lbLl$a|suvfrZ;OO>p`WW?I82`2e^ds2U*yr_*tA@+q0{@1dm@9)Zz>&fFfi=T89JpsJ5F>IAuP_$S2;IX1F2wG&FeG-* zp4)3!^^-+B-6e)nQ*65ODI>L=3315|VM|jwg8H*GYM%SA?scy^|bYs|E)eQGygaGu<_s6V?z(0xJC2aO4z=EQzOise6enNuc^YjT+AXe zhYiwtwy;>Wk*vrj)0%$@{8oFU+VxvFWFEc52Nlt!c1k~5ZIkHA$)>eOoQgcQu6oOi z{Q}^5U9DpknXhLjbK(jky?s)p5=)+a56Th_Mg)RhUfb{~bgpDMwC(EajK`C&_}19kWU?p zbDHvgbKB@0S-u45Q`!N;Ge2uLeCksn*suKM6;tm%mqLQ8+oTe>^{xNFd+zqFZ!?SI z0ohUEFTcHYl4_Lg7(u5Y^V0c)J3-%U2Km^UCLkD!Xs8G7?GnFnm2iHfu4B6!`lkpWKDJh<#5`=YLcy?%cjOjgmhp*|~ z3_yuD@AzLxDv$IBaCCmaoTfU2@>+QVGZWgWZW`MfJcs!f*{l86W{#~WjDTA=XRF}Y zKL8@iX@MUtDDrSW2@-{zvf++@MH05U$mdKxU?JAirO?%;;0VIeh4uQCBgaJG9e<{C zpZ02hs82JcC$Gf2EVN$gG+|3%qkjij=_Nif%=r{c|2`ol`sShswTCuiA!O;7)vTac z>j|{WABxo;Zz8;PDh~%VHb~kZ1ZYhPxnqMQ%*;eB8$`_Lz^(~`Ru{hMk=_x#TInD| zavdVAj`rg(9OpmL)6M$&qpIqs^=zA4q^Vy6IVEo$g>=>Ut)48|@a|?d@eg?(F;}2y zE(o?WcRjE}s^6jAZ>~XlJZH_`4b@*a(8*gi;=$_4=x~B_1bwQk*U)>{mO-|T+SfMQ z*gvptk@^KuUIyWb2T2*)E9o_K-DNYV(hlr|LO6Z$g$7Gmu2Y)Z$Tw!TG7KicsaoHL z+Im)3ho15UZ`k#TCaLvgJ-1w1x9#@ij^B79<;X!-nLB^Xv<*H!A#JAZOR-J%OrN?z zT2`}f!?6`#IW860=L~iikd_`@@ulNpUDPc4vI3%)&9i=)*Y*v>O_)B`FWC^M6aLJ zJapkK$I4yme!sxF;Q8wIl{hSf@8<3Qu0aOZPhAZ^9*ziy!TSSzU*PqG>0|K=Fnuhk z0@EkVUqsdq|J5EwoV)T9s2+%PmoYoj?Tg#0Qi7*HWs_*YrN<18ySe&Rb&-VH8{7DHMPrpaIi+H9n(N5O`s@zwq!eB|Sa8~?OSyEP8^6E;?>@}J$;RIE_yl8b)F)4hq`iM5 zlLz$}StZAcT(OidBH<;``Z~|?P;Vw*ox_U53srK)%I0iqhor|uul@!;3%V(T<;yS) z?RMxxS`_}r1hojMr}-m9bV&rqXR6NwC%!3q)z6D_2$q z3_B8?^wy+EseM4)&htLAQSk;+?jYzqXSKmsYZ;2BhQoy!VTRCE)Lai_I z94lb^p^j420?>t-&<1=rKCCrV)%wh4p|ZS{5~vLVo*VEhF$2|z)Ip;0$;p(a%lrwL zrO6eaIaSOirJaxfWC>SV$A&dFx8lWD4ulDQ5;x}p8!jN<5vLSM(|K6Z1Ic(unjrfR z0vEeq#Aj~$oEOk_Nd02U-y`lkGbEtuj`kcrMc5$LVV&uY;qRM`;;iIkNxc|-I1{p6 zmv}D06M90Bmj2PBH!~>#TT#pvpcyd3nA1o2y8%IWhPc6-Tver}U%a>Yp&p;ebpX2t}ry#qbdz}U% z1$>?T=LZqc_NR?5%LI>Z3Us>9BHhGdLK{p38#jZb5>KtkmL(e@D9(6IhJ{g>Qj@&Z zjNqj#$Z3YlwWg_y>ZwLIgyE$o`M?V`VJGg?rz`7oA0Okaf*DBg?jf(#?@VDv&QIP<7dDZTQ4;h&KUND3A7xLn_miZs({iUnQ4!x zIXs+;mAPVrPKA%^2HH_o*o-5Mjk{nGSVLr2UA3@92Bv_Z$5>9MBU||V6HphO2{Fed zGY>4xY)zlPW_J3@+=yC)%jsec?lNtwb%jeUK;agfN3DKq)L6^3`o+b-`Jb=FcA|Qh z>A}sO+U`{nbIrKta@I^wRGJ>xjHqLC9Knbz4wt&rqZrAHVK{-mH&iXw{=^_b~pyI(1j4SsOXtUkHC|&%4@}+i|Z3z!J?@>ZzV;It#VB_ z?Y-uyH4i`2M@qqCAHj*W=9vuW)V-y|afMukZgaY+^0cQtrTQ4Dp!cn@qftRt^4`;U z8t*$NJPTiznG6{1w?oflB{##Qpd564PJ6Q7^(gN$4|=lS@q9+NMbf7fFSR`d??dY3 zCmY+QbH&P^i?geducSEJU96B5XK(4~*Dq2xQ^ho0ob?Dxi`lG)*>K6;=h-|pQ|@Vx zaI})Qu;^H;{m9P;6#!*h@o{Z4oIw&|&}%0o%m}lah^}TcrTu3uG%2S%32|g!wx9(d ziS`S?6BD!Q(VDpAt+tVRk=jE6w4-qsK?k~&$l_k!XuXVO6Th&HZ;lPE|C8lWhJ|T2 z@zDmEoxkIL#NhU{e!hU#i|V0rU%P)d%&4AQ(5O(XY%Sj9UK@QD*2pc6_eeXn2kY8m zl~|kMi?O$sEv#ckpqlV2Y;2eZkQgdj_rO)y?BHYLfA~ssY*_Vg+hSGK3~4~#q+-}z zy?^|%6ygpludntn61~ivRbR}ez4&Gb7yxYQX7hXum;dn}n`7mu#e3owWQ)ivEbx(R z!OcKNp3)%G#Sj=ps+jDhz(Wb&T;U<1I*51=7eSK4`bu>3-aliHnAqpCnvA=MBRI57?lKnnFZjL!Te8$G~^j*v4&&bPD(-*E}MmLD7 z96I*apB2Q(uR-Lmlfmp>lam33?r8#oYct7>;@{aMKbj@i8bqv# zautZICQ-3TVKV_ze6-P|_Fonijy9MWkKt!eK0K4Gy3yc;j=vl$x3Ti?qgbq)_^UP} z12${c50bH{IE%u8glhiBz(6(fLs7?2W^1Py~ay8n( z$ZYlso9>mku7L~ zQ!=~I?TMcqPzd7!6J%?jjcYH-Ji6I0Mk;M_lr4T7sw}q8_ULlU_ZPvnjhvI$<`zgg zm;_F&cuDr6JVF2xE6xB|e)En2w4nI&5dVMi#wCUoHONM6)x~hrse_|)PkF3Tk}JN? zp8Nc*z|ZOra%XywOGA1ur_vFL+gsw;L5K&rmQIVig3>!*8&ZJ69`j_6H7LFOAIf>w z>FJ&$694K!nF#TE-rIw=&qDzGP?%hlvf4j8FZ8Jt!;e>SA%n_&){`pn0b6f0R(r^9 zwlt86G(al;S8kErq4Y;6d$h#^%B-3(Am%XYg(=B-wNjFgDj~=>N&PheK!+2~epK8>HaarhqdgYs3CLRlt~Y4} z>`YK09Uyx-kg{$yrL~&k zn-6Ocym!ZHO7Q;v63l1FWj%#>{>tw}RkdAIN`RXDd}kQ$R-Qt9BrKPLfXb3PY*XUK=E(@bc*|DyMFR4?+h1{u_Ng5`FCg9q&h9Q>{Gz7yQ*MI`+hJLfaSM zZ)i3d%_f5|28+oUT;ngicC)jeZ_;B7bfd`_5Np(rpbsYUuJ|(T5AT^_Z7Qf?o`u!s zAnB0uo&sx4kxWSIeUni`|Ba<8x`Ea$yh;2~p&Tat9oj>-Y3Zd9KBufzMH1u4!s|=6 zUT$-T5woHFnVWdx-$Vvn@j36 z8pnDva)L!*fO%F3{nWTj30y&_<4@!cX_7Y7WkOuWbxwYx&8-Bbc-%UdDUmZJd7rrC z2&xeP2{`%noBHvnyWA2W4j_00s^d%vjw0bLDj*`u38f)&J6@M|39fLA9;8rSgrS>G zweKJ@LuYwF*McVa2QJ~|h)a~Si@{;Qn*Gyn8YMobF+L@TdEmKJ4LItv-zKMmZj5nr z4{kKZFZa|Wr*1q&C%3hw;Prs5#k{Up5cNwOxy#N>J>7QL(0tg4>un-Kv&cvknZU@6 zkEw<;9drr<9jDm1Dfnrm?+A8BfK^~$eM)BjBno>((3}r{!ArgS@2c+vYg62&5Vzj! zgxNFA=;>e>(8Oe8({;Yhwzti;PnhQ%3?r3)C}h1l-~Gq)-I@j^Gu7XXyNHr+EOGu3at=blOIVFXavx2H zI5-nL9il!Zc5DheM)c~CgUolAi2iB}bjm-%k7pl_KVVKh3wsmx<*b|XPrl6-NVNQW z*b1^~_WHZf#@F}YAClC7S?@o8$;0k7v->&})pMB)z*`z^`xxc$_qggyrIM zNKqz|Sa*@gG`q%~8M<7VzI5ZVlqjciUfy2^*Uvd2jb4x7Vei+lKcB^7EadTpJKWco zknIuwIZaIV%ysaA@BoT17@pD=k;j59^S*IPk3}+blgA>Bs`Xf8fPSz9y`y+x{m3?U z?^{pH>W;c*%A@LB$3%cOoU5?TRl*r^_Lb|=uVX%-O=%+UnM+Ivvzi#-&xJVRYCz+_ zRKzwFWZ-@0y0?qSkdS2$p$h=zRK1h+bR@+Z{{9~BJMRT>AUT47qZTm7c)_FUaJ-M> z100*5P)z*+F-AWk9&1#Ia_Xn}`!~?c3pjp9Xs1AA*z%q^R=^bOqR_p>Lkd4X6m+Fk6gCR z{bkf7kK)DW|+LLj4rfD0IBu zT3~)}#v48vfyoYb6%fQ*pUkDz5U}jvz{y8N|JzMagTj@$+Wsf8Q#O=3I@14=*x`S# zNRCu~XCs>7i{svih3NpBl8y!>&Kof zO1JY8^RoZPpHP-_Q(RV@_n$P3s^xhFGu%b8j#A>P1udeV=EpHDG;Em@NX@`7Dtci5 zF0!DSY^HR_!{nkFud7{SlWYfvH%%u(t!(``#=W`3R-kgklM=9U&hYAu%v?;;&X+M? z$a;f^`>i`~C)_@WEUdu#^7} zU!n%OGJ?c*&px?wb$>ZYD?j5(tY-E;Llf6-Ug;zTDs*<#g(M*dddUCeN zXbMRT^Zu)w@V|wKr+*qpK91Qqj$+-#Jc%z(`kqt78^hzx(}es;@tl!!zp_^K5WLb? z_uou;9Kpz%6nD0mOqJFBeW$fQ@Y!@b0@?ff!V#66O_P9Hl#Csv?cqzW^W!2`_fJX< z(suJ%S5b((`~3P~664nZUsV#K9{)W1{TfAlN_c#RF-r|@5x+*ux?f2M4{ujd+Rypj z6Jkq4DL7eFpD7KYw5R#%xupy-TJMfiWGSd|s_Di!?MYrU&fQIzi|V|K;GK8ub#rU- zim3A;LjRB+WmFd~@Oi|iAGqWA-n}^LaqPkohvPqgzfSo6r2qf#zZt7V2ejY9SDsy@ z?H%fh|M30Y_9 z`>oJV4bYe%UqYoE(c|}M@HyNoBKHfZ6$qvY>QDhgoq2ZqPVC8_Hj{ZmK4coAmVV+7 ze$6K74{|nf)aq07zo(HGf)$V9#1EF(kR~9S0svjS#wLzfXw$Ibm=ac`Da4P5mL_3^ z)F3JLGBMIpFyF84pTB#yO%lAiYkTS1iCL(B#$3a{`AN2sn1euybxa}C^I5jOj+Kfy ziG2a@$Y4fJlQ099RR&?+U)phX;vH*5iIf_b=Wt z{#}=waupT3ik^pWy~}bE{&0krFou=kj^$0wY`ibRIR;4-+|JgrF|KgsrcJIn$|-Mq z*aRkgPZ|xyM_sMU7VohoU@deZ|BbkUAoPmH1y^Khu+=bI;NU*XXptD{w~ZF5kxf8! z3d<&F^d{B<@El<+%MbUBPG3+0chdRY!ZdWe@*cjDlZuc>qNFm6lS(vojespj7H5}~ z+B+HIiAzfG(}f>FR;i3Y4*Xq{pHwKN|4zRZ^}+1?RwjCWst7)f`&wHMwBG9V zlqFS7wz7I>xPEkHL{er>c6NkCmlS6XS8C!~**VGpneJ=&1SK8Y4ZjpH_C! z3Ie_xG6cRmA}x%iVStlC3USeqc~4O#C7J4@5bA~s3kLrX?5G6W&j^?mgz`-EID(gK zy#!JO$8k&$x{4mtOg~7vijGE~4RsYAkvzGPbP*lni4_lZ7yS_FtG6|KV+)mZykf=L zx9PVcRgGV^G7o-%z6x<4Ydzo5dh6MIqe>a@x@az7JOy}vNLfu)6Pm|Fr)?nL@gbk) zFw!H&DbrRMX{LcmD=^Z^v|J;;(*(B#0clu(875yUOBCs*UlQ15S!`8GfF{w;p4fPE zY(msJI00=Oz!5LvzDcV%Z91El$53)up_Yr+mnnkv zHIsIUDS{HB;WEWs_$)3HP;+l{nNlt`fy=~xFhM99WZKtQ*^ESNzU=h$2_>(FnFc>i z&j~Hb)MRHnn^%8VDK~~w>Di&#a!a!@GZde{KE-HNl9mSM!;?S!KbsE+-u-_%A0CHq zJ$N9Qr;bX}l>G4g(K++R&MT6x8ttBaT{Exow$3@Ra3mcsAA9RfdhG+MD{rWtvZaBcDrzA<`Lvp!x^*Sbw$O)mXTFsD?SVPW-f=@ZJ2M2!WPGKv`Srj0T& zHFj*9TeQbW!?k)-HCq(#DoS8za}LGkh**7m(?y=mp^f@@raU>-WXut>5wqAF2`hh_ z&5^SD1U5%z)F%kHg4l@PusL#8{vMkXyuHrx6;V+Bue|I`wR6?h+t(a9fia~16HlZ6 zFL_$S|H;!f{a^AlER_=UA!0XoRHGaB7Ha{Xbs!Wg;@eAn;o84nc1G$|;}{JXeWWKG zfkmDccrGzvO43#%-8enPc4 z_PZqkk|(*P{Wk?!wF~P9-qkrsaxZ}>VT$p%Wx5;P0k%jT`Oo*S4d3_Ozkhz^{pYWC zV~0%M`yJ%ps=)V<98gww$vn4l9!WLX=BB_*8>Gq<|BR%wNzmHE4{;pc5AptTH(Mi$ zx#GW*n78PzfFgrA&(tQwzM?U@b9?#oDPap1K@pM{^k=bY;Oz~Us-X)GhAy~D1YJe$ zw|u4O<%y}{j-Aj62M?PMs8pFx;;dz)W!O%seZkg63hbZ?tuelxEz)r_9pPl|Ol1Vw zCY1ax?Iyrz7Wh{OOq`|P-HYL55ox~@J5GS_+HdlcT-Pd4awX(53GnSUCnN6~j%RSB z;)ulY6UOylI9iDQz(f_?#6GqH78RM}#uIo|E{XhU#ktgRm7M$M$S^q#jePk6&zvVq z;Ywh3r<>CF`$co`4Kh-=l*Go-|6G(#*@ey00=z41c58uOg^uJCwb zDG$8=RJSfi_Ors9GyHzla5>~V)U<6$Pw!|Iw3;H_c|@jOraN2s>aR-^D4k`v0jhR< z8bytx**R#Ts&hAaf3q8gHJQJo+fhOcEA=nir^{GzD=KVJXgXS%O}cMTUEVUOf10dw zJ0`(UdyaK8EIa4fHcvhv>FWGnqR`KgowYmzoiq8S**cB)pWV#7^hZmKHl@`Zi2*c! z6lZi-*v}-gZON0~_)egD{{>#~rk?nnAdh2@h<%@ib(gRe!c@ZeIR@@uV`;gtF2fZO zChW7~4gI`(of~c_MvL5N3HH|$F(32`aI^F4!_G-_d-(m83(v2FzYBoh?gp`b_Sdo1w~Vu%EX)q*@(&{4l5}u$Pi#jvZX7Q7A!T{aurOx&7{Ex zGFgJ}kNq`S`KGAC=*qQk{y+-PE?LPHQ0dn-U>&CphqfJ#Xg(YbZK(Yg@Z9n&cR7(J zq4E6v1Uo=k(xEsWvK+uJ8Z7zXMD(@B7b%Ox7~Qy3G}7`Wz#!{z^R%}=WR zbvf_%_pY6g5dE}wu5{>K;7d7yF)$(Rh&@~Qoup%?hT-UY_Sh4*p)341j^I#%r=m|W zE`Sg$j14s`=h2X4w!&(b+04Ld%i9>7XBBk`F@@*f*AUH$|DA3oG7GkeLRADZXQJs{ z?qG65SKZBQ@_%`!6mzA}NTcT+Pw`-cB`Fmx5*WImK6ZTbI6BkxFW#CVvtAwyejy(aBc)kQ4$M~BjffE>B=RM66Hipe^I4ygK z0=S@6Vn~=5LF!4VS$#A4(d6Wb2oN`B8iYd0c25t?Pkau?;=* zK3S89ljAa7Ia#bQ#(upp8Uk%#ve_RZcsaQK+?AC`Z3(r_2??edW|lpK+!4z8V=+=F zBj8HvRtUpJo&g;#A0E%Z!$VXEz`<-nm~r!|bWMJMUK?Qx_qeG>??yl|;fpcZ5g0jk z{wPt4)Nw07$rA$n!bnqXwA@3B)EV+qbnspke2DBF5+FBj3gT!{ZqHyzrF(W&g*7;6 zgX3)%awR-fz6uKfY^Uus4B>ANIeYsC+h}z&t!O9PxEwfjp^N_#zW?dz%Ga_NH*uFR5 zACGI^trC9*tgX5>F(7Bx!6Y+@WT?4gf0fqV0<2+q9Xu22zzU#R5Z2;1-kOw-=j=bo zw2_-q>C{;76`*^RslY%DJf(yRFbU83kK7yA4f|Rh4-h)`1(&QNbX*oJjPr(_ zt&R*riR3yV#G&i>FnV~r1zPa`<6d3DxxM4wIdm{8-5@%R^w~G!fN+N2pgFK4hQ65; zL^Pc5%7$cZP~LswIVoJdN26T{fxZvC^AtG?@V1>~(&?%sWYUrMB#Hv>`2_L;&LNWp z)-B!B-TsdRZA75l%KPo{&%6KI`{MoIj=>rx3ZL$jYxJ!-o&*T5$^{4Po4|R7+oh9| z6THX!n7~cpLp1Qo6LWDu=AoRFgle@zo&moO4e)G`eEIW;4oel6qQR-OZ)at=Q?YkU z!Ya}F1D!8C(H1E^Ss7ZM)^5b@7mKtmbF_1(Q@*5#Mq)Q-Z5BL)5EWWQF8lKIe0w6r z(IfIA6dXM=Z*MTCV$>GcZ=>_NgDP{NKdX3SVz)f13?7}W&aic_V^ZW+`wb@xGnq z-MCL6Jpn{%2`6?OoGbStF6!nK$qXl#!_<^-a=cpuuU%mPQg>EIrY_o>nCxsVS!XI) z-&%5D5tM{J@sgKimBBI*-tS6;jYPH(?yGBBH#wQ@Z|#**%Jh=P4Oo6sWrnx;270g9 zJAqJV{ts7&VqmenRLV6R1dT@QVc>uPlLhozW=pWuqJW8Y>jyHcMP7>fr|*aIOOExY zf^D=}nPK{KTqZKiFX~`|v5UgqMJXwE!}spPC7&|zRZWY~V1vocHw zSMz(QYGwlLB#E=C01e@V+f#^e=}*8}xWQkz#51!n1;%3Kg;{M}3)AI2&pZZDfa@rD zF60DBrRH%TO%pJ*K2g4R`<#IDw1EERYb&_ z>8v!$su(j)l`xtveU$p*I!T+!iNl)}ku&PzBSJ4oAG<1fjG+@nCqmARL{{ML@T(X3 z^KlcpcI-Oe#Q}cw&S28Rpf3ykbTIxL9a#~_wYDxk-d6F3Ct)gdQJb76tHWBy>rOhu z+P>-o_5&017Oszg(qHLJ`T@^WhO9e>KhBXAuFpzdH)-@?6&zus`Vjon9b&HK&tKfE z(Ja2WOKOUJ?EH7FE>B5?g*(ZHv8Ot6%&Yl|N)IzSVS~V&)tw{UaPDfpM|XF`(F+kIt^FZ3*tkZVv;ppR8zmUMXMj z@4;0&eX^qI3l~pZSXj|o7`AL#QR&j{XJyXm%^&MaUY$6^5!UUiEZWJ%OG<{zEnHUO zDwL~JWR*LM#w>hw!uR=4)qFbG7A$L4j9&3?-gY!}&(x966*d;jf}>t6&I$$=5vE>n z5T_WmHZ4TBAtyKFGVlcGw*|#KE|+gAmMO>$MZ1V9mMO_kWs$4c-5OQ6)K#>TQJS;D zi*^>t7A^^m{~q!Hp8W?QsiK`IlPXJ|6k7Q0NM9BqwN`TdF-2b84fIPd_on z9O?LhRBWN0yDX#b(Ki<%<>rcz;M7M0^AY#)PR>oiH-odGCKa|?&;%^A-@NGD1wMUy z1OUxQu=Tk>Ss)Wi9N!E*x7MMYx(m9mBam$;kaiY8eqw1=ye*pboVj|Nzi?ZCsjr%f zK#pj#eYf!>sA(TtmzJYnJz)icHJSMt5ZmJxd;Cxm1w{p8h* zI%_0#_8)A``Kvwrg;pLal%|4v-`6F4A65B*)7v5~&K^OWyviJo@5qx*M^qtX|7YY0 zj~=ovCL(d3U&PjQU;UE5@IK%972oumsVp=a(dhku1?r(}I%ToD#1>D1jPeR)#ggb3 zo1ZM!DVBQQ2|Bxnt+{;l7=PhCzVkTWlrA&^`7ggF&%Pro+D0Z$*x;4gup-&yb!5FP z-Rs=9tz(AknBG+sJb7|!O4#H_Us{v;fiIj9lf_Osh-MlyqX`4^q0dgBuhP+1deUDG z3JXK>$(6gU7~BP9a2-4XJ(}2|_v%gu+H81wut(ZJq)a zUpN0{uEy+MnAx~W;@V87?s1BZlHiKU(2i|27w47RK|!@6Ea}NE@T^- zIYzTeFJZ#<%}f-yGqfokLo{T8Tw}&}Zoqf0#X*VEDVkbH{TaXUy@3@J^KSkwQF%%c zQX5eP0LG4|^4dkLH}UFK{z5trc$TIMQ-;RP-zN0IiamEZTImWJ{nlPdl#??nU9^%5 z8ohSk2##Q&DBjwuh;l++;^<(`EU(YR=sx zcP4o~0(l&@WQ_AR-F5D6`uH)Yym4=6-p4<~+}j|7=HRaaotnyXj6q}3K4Z{0Vibe- zu#H-{TZ6q(Id)-8q0Ox7*od7@)cjq)M;HMny}cDT@=i+;>|xl!PjW`2s*A#j3|Lq! z6ESO&CuxjXqEZqIy=0YcL*i7%nYw$GI92^tkZe`>`WLe(YE`%~NK}%dF-{lR;uU${ z-i+HP!Do6n}FfQ3B zNN{9K&5Vm?IN7MR`$C4vK^f(2N8b$5!v|b&5TS>ljy09|EhWEyO$W!WUf?g(40c}N zo6N&>pnE$&2Op4ac!Rin6O3?XRrStxEL_eHY_d_DJZQ;Y_>?$UfdqQ)wo52D=0~Z& zleWi`jqhf38Cn6a(9{o##D?Siq|%7g(fQ#CFNoI9GfG8F>B2=C+cXhq3gh^Kre@C+ zri>I#9qkO?Dif`D+T@~&LXFKLvg#Ch69@A$B}_E->KMZx$va8)!WcftdM{pWQLE%wm6(_TeKKkr_c60gF%lrlFAgn!2*Cr3mA^TdPtQqt*Du&(s+STv) z3ps;;KyB*zMTLJQg;Mvo)xvlF89I4tXvZSQUh-WXJ)nwHZj$;IE;|R&-sB^ifJ>DB zUpRv-3^jrMW!Ym2W>o#R$?k>8b)oJ%>2CKz_V`1P=l>!F#rE?=r?RVNb$=s^SYS$% z66*hxENci;>MqvVdHC)bI867xAGB8Jiek3}6Mh^sNciy~;iEvVFALv9*!33X>i@^s zyTC+Xk}TVUh&W70C=Wzl-D=`Nh^uznST-spKUqG%{1*{NB%*!L}|43aqq?c8%4jPV8sj zm~#2F_<6(_5y9L(94R*OkD*L#ivonOWDK1{Tg*fo7z>$MGeT{Fz1(z)9Ji}lboPd^EP(0lld-4mSUA=3 z#@&RF^?ygf)V}SvIoY!Ca#17IOGW7hsl=SpfCSjqQ6SW}J(g1sfCt$4MN%H5 z%oqa#Jr#I@t62c2EGtDILm=guPYq9uBdAkixL<)!u&r=*t$0z3+VG`g9<`^(gS{bh zM_Ih%jf%@S1qDYTZEaf~Y9ql5zYNu7F>^IzvN2JSJb8i&(6cYl`^w~K(C|>DFHl5v z>Ht}_n0OTyV*Ar0=nAk7+B{v3fXd;DEPX)+sU_;l5JPesSwJz*v6@tV3hQIQV_gGm z`Yy`(DCXj;q^vx~HCVn>=GR^h&ER?o_R5?H z9AI+iK4eRB;O%q9I&ns^yasgll~_UMsdK#pb~ zoUKgIqJ2V`L#zQ#CD_^Wm8gH(8uW>vj;xA_tKz0$32d9CG;MTa2(kecOCaZg?x{mq zG44~AULV~rqbgmYzDi5NDhJ-GU{<$zd5%j?7^H}{1!?_}RXSAYBrHI$+q%l3c)ja} zv#YC|{U0nqxURZw?U7;av1F=1O(w0`$IkChX^G6vk4eX9tK&uBn;Y&SNse&r5s?6js zTvT}jIG*_W8ddm2SJxiXc-`_MU59fvAPvHaJP3W(scMZ3 zWUNgYZxg-)y1RU?klPb(2mh_@5RUgZFwM(Yb!-)DHNXEd(Bdb)&HeLws40MGa5O}y z&0Gq?cQT8Jf(h}RP*ne~r1`_YB(6H`Q%^DOR9O83oc|bZ4sdm#WYq-zuVTMqV+{Ud zUY}t0W(zTs(o((bVUzXBi69dXv~k zUwRq+JL14KV@?R!#6P?qm~9M{Y87 z37qI5oi%t=9K0^#c0y3okOootx)R!xIX%dxX}vL|f{%?q4afM2>xuk1Nk~r-iS?a`I8fLb!i(4siViv5n1@QX`JsU< zqy}xv1Spzh z0@W!61@*AEXz_Im(P3+awSXAQ#w9paRhI*)ZK$})x4lVpX zwZ3#|Sbo4F%DjIJeJ99cACzCU{uZQgrE*~P9fZ|w5^*i2Ac>B2oV{zEewzs#p+NvA z)><+apAv&I>abPSbLF7cvO)ni`$W<+Cw4g$F^y+fI(ondRO^Q!9Rr}s7`!F}fiEbj z^J8$h=MRq$T`CCFR3~IcKZkksN0^v!ea1)#AmI7e#>j#pYudjaw_Tf;hTagQd17bN zKi2y-8w&Z|>-P>pBr6Nii@|^l(MY=pY9kbW$65BH<`}0T^w7Hp`1){i-BNOeP2KB* z8W0ApIMLGdCP^-p-?V=3NhFuJ?Zl6TR7VfQRqiE?Nap(!>#X3BAoVprGnRX&IMCo( z)1Fj+n&AIE@}TuM_~JPnXcsz8w`Y2~*S19X`-CLL+Stilk|J24E9`4aP__5Dx{Es7 z9(Nic2# zsV^zloSWr(H_iNuF^qQOM%p9iXeLyqO;HJ?6T{_mcM6tB6+nz&A}Mf?laTv31DhXT zkM(HtVgB*;q1(h0{Y^_q+cek9n#$MnS!2G`Y29jF`>}(BRU0D>>mrfbuLiCb<=7m_ z|7h%`3yF{jEx^U55xE{hH( zE<>Rp-@v%g;9yZk4o(++uQr(Uo9A?r{V~}8OIk(*6@}I=?i6xzHf7LUfcky@N*bg- znhQ;}{&WjX)#C??{Q>J9ZJRy`A6 z{BZC53|ypjZSO6qnbuu%2U4L*HpeUkG1knxWP3Ea{G$8ug{!%PV=4O{@N{P5vH%N! zl`cu17G$}QaMQrs5FG=qqp*GyoDFpRf046+dB2a8YB_6t3dIf{kYN!Th_ieN;m1rI zUl~3=kZ}?Wkedj;oOtm&!5W2|J^`_?HN{y#Af!W*3&z8X;$;zW7H##l`bC$jBp^e< zkbCYsqb2!qWvZZM$zuv+XxydrDJE_Bod?_^()d4u@x{cXfp~5bqXtygekA0Qy{NHe zx!EPtSmh+gk~@qJ+FY2^el$yE>I{NkBhZ$Mg13DqT2%~iFID~DZPCF4z~i)+*~-ZP z)m(D(XQC;IT+XJ-S!I0VX{6NXW0m>=j-4HEk)|nDt%;QfGl(d;%L?9JoJN8qi0XONvJn~ z5PlKK3H_4Bi=(iG=R%WB^el+?)Gy%6$NVsD^@`WW$cF!Bc>TM7l;n!pXEi7Abf+&T=kDuYYxd*ohbeu5;o5`T0d;sYU{yCJF z**1#S6X@6-)~L4o#W&)BzbZF+$n}B1mA84M`R*m_{36O!j0@`y%=F+evl#O5^mV-sBVfe@kU6Qrwbg?E7yUk?02SIZ~k#hSwL+ zcRWvq3sNs#4h4mijFB>u{4T0(KGEI<3Q{&d-u_o8r$Zjr#qn(FV!)sS1^o9?G#MFa z=qG}o2j0NBqVY68?y@B(&Gz(J6*N#X<1TRBJM zV@P8|QHU{#YO_N*?+C(_mQnn_LnC?B0tptKd)$}%_Ypx!ZSLnkC3)b+`%(;l7~{u? z+3+-FNf#mC2C^Fle0>V2b}|!sDWse{`79$Bu##8>g?`}Jz+pk5VSZSftssa`XvhSi z%lx-AC5q@1|0L4@`l?92fGtWC6HAGRBy9Z(qp^lDzNd63WXY42d=?3D>9j);&E{*2 z?`a(hU>Kr}Z>iY)C^lclZzbRDbH}W!<5_mGn*Ud5-)Crt!pP=pjju8V2_4r8>+zO)HR5$K1EyS?3?@kJzZ;{n`OKsr*shM-U=K!0F=Lf@ctRgvi> zC|@OAqKI?k8;#K*w$eJ@lR5GsZq{;-G&f*JV17QyF}yYiEZKy*)ssQsL_jjqkCGn+ zzrpi2{Vc)I9JJ>(ZR@)WzzF7?^>L)*k?)}e>l4r(gGJvp!qzMv2m*?E{XEM)2?Q_7 zMKF4JvryI>3@m{+CIxc$$|&G(7R;TSGg6o1v7OLk^f00#Ge zPmSC#xmU0e6QRhexOljfHl9w=5G?ZDcLBe_)YkKZmM=U!6HDYx-?PbAFi9w&*#f;| z{zP7J7uvgR;SgI8%@!Eh0u5WBWve6E>dEH#yX^6IvDL}uV_rvf3Lu{O1`MO}c?MJw zuIePXx~nZ=mq~J&!!A~GCBhY~Hg;D}X}oK}Cs+aO>mg!ef&?y+hMN1~jSX@3r?T3Q z;##;f2yHrKca=jLBpPZmjwiv?B|<^GkRRhH&^Zd?>Uol2!GxeWs@ohLtd3*ix~tj59yI5hMeOBcCNx^AtS#~j{-Y5z4pU?~Dpn|BK`Ul-0Ie~5Vr3e7AOW!8 zaiL6*gUIH28B@>N<6rWDY<=|xe zL9*6mDNeJ=SJ)IZ`33&UA`RNe!6sF(7F$j)6Mde))-)3oQp)n(y~VqGyBEnmIxKG6 znhAvKET)!;wkW^CP?xzF4n?QA}0b=O4~H{KaeS6oUF-$ z)D9FD<>c5T2WxQ_^%msFo>Za^C2X>7cW?PUy~U5b?fi`a@-&B*ygm3|gWYV(bJ`%A z>`XN98_5b*ti6r}Ss~9=^8wG95rbg};0wIMT8AsvQL=6D9_8TMV;o18VRfiI?4MreCQZVnh|ZB%iHR?#z#R;ZH*)w8 ztd*gi=#Uq%!HM>9{J{n-k&&N3(r7z0$p3mKDrVnsy+l6I_)lfoy7`gOOmsz$lj#xB-uIKpS$Kq8zx8VJ*YVFIl| zf}g(hVS+`p>>J~|$mgj;sp};8w~4=wZAkbOTWu^THK*%+nu#gR+LSm#70$Y1CED~T z&;!K4kUfeVa~qHI)i04VR5FqRwF-?*5xy7sikGm_2-aVRy~#OQq`((~2jVu3ilAeM zq8&G@2QwphNlmFHvKPL4O-dWQ10ft^?@!!4KXGrr)r~w>uY~@XNU2{whqBgDaa&); zmDZ=E8=&2TxM>K%4*&w0n5&5l@B7Z9c4q|G#TS{7zH*!mUi^+p@T(EzaOzPaI60RVOH}ym^A9!Md&_$^Bw&+2jF>s;kJi|x5#1u)m&;2^;#8RQZ+!r|XiYJAi z_!9H`kUxR@s_LVKz%oRtI>zwomzWmmlhl?KfO9ztDn2I*MnU8R@E&~!uDww7S?32S7pCyeULu{QPUV=4c%RoDEnPLHkAvq$!%c8jKdX7iNDY6=vJ{we(_IXelD#E=6q zi|mhf`0}u2ZIOFs?w3>DDU|J1cTsbEMzBzZE?b*%DcGmCFcx4-`l8x&mKmU-Qca>< z5%n`0HnwRkfeah<3#aXM-jrrDWw=pO%O_%hwWVd~9}Ih^Wl$WkdS)U_TE_jJ>a2fY zM^Fx`Eqmzu+uGEYsY43{-K=65Y7&ODI7#}SAoN|ivKMJKQumyUt&YW#!B#VD^#t>c zVMj_V8wvXqQ)1r^PRRu+u6l~KCo2a0aEY>%6}00$wWB&Jm=Yya-vJU+)D`kHXPc~< zIyP7xgM&*+bu_aE2ZWS6QZgsoPl>sw#Iy`kk{gFd?1*f2bVW)O7J8M;mlA_(Eq0E~ z)dS|6GDXWe9My47PnmK!M%PjTM(2u@=(Ll?CwqO>#_|VyXQ&xX-4w_noZ?)%*k9E8 zKOzWYe4maD)WhqA_i6_UcFJnsW8P%6C|YHH%4=Gb!FYULPfUAUPkeja!rcc|woW%b z`l_e#b)=hoDbo<9If#qQj`IB8+IcfH?LA*ay=pCLeM<~*dNxRonTm+6dZ_V?q>(|SE>zHs_~q7W&X5KHdm?>`l_c!R>$#g z{(}stB4>VXZ@$5q+q-I1A4Qo?+B#2id-cGRr9|yf9IlS9{7?+07|}$1DN{Rl1oxWw zM@d1`mOR=SoI_nnNkDm@oU~^WN`5#cUL~i>H@ee0uA8 zRZC?k!z6;eJ+&OZPG)w6LP4ImyiZ*yA=}B$y{q)K+Ni(#_6W@?@ zoq;;AF;oMDY5P1WP(_Ag`5R<7rd4fGS^h2t4%?1bVKBn;&~aEX7-2;chth1Jc1^3g ze_;Nsx6x1_ISTH(1{`j%Iy(5iEtGQQzmv$l*!!7-CzDE2;*hgGn)JvV+=f)7#OaNc z>O93LUF}aQOR9}^+bH8(`kEMPRF4rOr# zE6K)3Q|=Cfk5*7vLt0EhnhrGMsdVp>b#<7!=?M3$hHhHr-ECUfvMF2kB;##gv8yV` zQRzCc!ghfbmV?4K+8g+;f>U|@&mhYnr|@w~a2Z*n&<-_`eJL^Cs@|oMUemCDdv{6{ zq*TA>K)YbUe&G$GkLnE*|37wVup8M&wDGAp9d>)2yT+7e zO4ay#tlNDWoYC~wp7!lluIZs(%x`zM!CK^H^*s+WkG-gG)51zU8XLpS1S_JI)?f%}0rM^|B^dQwcS-RTE5!Hl=S5bPexf z;YPJh+Yfc>ofrpcp-^m;&L5QqD%N2(O56R2E^T`23I~Grkf4D#NuUmrRJ#U|dJhoy z2gKc@X?^+Sz?%f~8UB9gxZZSL}A8 zOuI5I!@SU8vuzI-L@9ZiU$NirUiGpmO~S5*zT-?_znE#u(Lo z^X<30XT3bmf+l@;It=DFGeLxJ3-xiB-(^Ck#Ajnxj0r4d^bigAIM=3wuEb&)1kLo( z%a{}d76eSN9(=ukg`h}cj~oaD#&vaV%!ZlCGYw4E+L;rXm>t!ixd(!54iH>c^PS(W zl3VFz<(WjOH`@>D8-F(l@!MnferZQ9lxLW-V{5wYYKLFdJI`RtHu)p7Rg2CMeDpDm zqhDW}asTVR2TdKZ!AR2U(#Scc**nc12yBP{Enf7?4OP7dNmqvhIS&|Gmu0S68Q7u+ zqdfdOG(J@{^8~nLggMc{_tg9$8>trX9ACc9A0&3?S_1@>0Te8aEGY$f1F5HkUu5#H z3UM2_WCwUZ3LJVSYDWdRnE@0Dpot66=HV;+x1+#u+92L6iPW?=L*`?3PRM>Q1YCzS z>mkqr=VO=3(+$nRSi+j4;eS=piPo%w_R36Z3ewRIH=k`URQ3@e- zgH%6TEq${8+XmUR@V~{YZ#_i&XOrq@tDWl&c6%nQE~KJ|&=*(>)mhy=;NZyQQ-)E~ zsY6AeBTI!jV9?G|0RC^qTaWke@&40awX`%z>8|Y~G-0wzdJu~3K*W$t?o_@US)N20t7V`l1feQ)g~eA=gDdHF z#qyM2<9ca1i?fq@dCISXfqpoHOF&TwnQkYDn8n(*(X*-yv}_vn)Te|>oU5jck0>}6 z_YAHhz;Gk^ql6;3L$UedN_u%p(%^4t4}DM@w5ENKi_)DwFxH>D?L!en!BCbfXC@$r z=d{Ujk>J_h)@_sM@+-eTTc^i91Qegbb8QtlIeF+U5rQ=ZHY+@>r*TYV=q!1vVsIIK z<)35V+U}4oH9(I1S8|*UBQQO_Jv((Mw`-(b(b!o=msoxc;iimofg*Q{2(MZz&9W1v zL~yx7`1nI-mE5IQ7&#m0C)6K_?o+HG&C{-KY-t;@cob7#vuRL z6+&>4pbPe!e*1#%18qlKUy+{C1P|DlxS-1xbd?8PUM45#ieYMlIM+ecT^w_O`9shZ z&op$q(z{)cHmjRsnyt-Q&90!UvAJyO(6tijOoMY7s{8IPKL3gj$Gg;nm;BwS|2lC) z_`uX1(Es*@s6Mr3ApKa`iRb#&1D6h`el+lv&~F17#uma+#b`0jw=jfTj|rb4pBS;y z#y+L_aq{Mr_9HJaIxA=(tcTGM=Br`{J&*?Q9i5N6yGgvh2iY4JQkEEUz3u@c$9P{L zPN**+v>6cfM4Y)9Z3n#{ZD@%OVOBjM{5+s?9L!!Jh^~P&avmdxvrBVgDU-Y0 zDU0X9(!Q%g>c5y+H#wA({^xqNV3pDD%HAZ<~uPQU8vQt}|2Sfy=Nb3v?YYF7IFV*xTubPVncI?otJ#wcXLW zenaQ(b{R7Gbnb=Rwuh;lPUZ$=Djb`8KKGrdnI7}SUGG@$w9@5u5-(!+wHNx^u5JC{ zg@IqAIU9ufS8+UOTd*ZQ$>bb9sVsf>DwaoZ5Lw^JqODV`ktt8hUSNYIG9px<7Hf|v zig#dq_mMkXCAeMN{*Y{fG#Tx{MPQ^uWOSB2m|>72zuK`-XJ0htTobTmb!Eq4Mf1z~ z9~?w#P_sV413^RZs+aqBF)`U`!a`MhZ0{Naw{=oTnh>KajKxLphePPDp6(9cV3h?U z=FAm1v9kIzobDsJzq<%QLrtHF?tGIsU(cuuGq|m5ggm{FXW;UpK>9RwC{y_0K5Lb< z|Fw={i!?*jyz6I3q%~)(FA#t$Uvfcc?!iq( zW@me_=ELCYvlvZhyRYV;ujXIDnnS*tk9;*B2Vc+kz5YOFyVvgy)*SZL9078t^5Bgb z+L47Nfqpo+M5^tQSS+}mo?|;GLc7wtDEO8`xR)>dIdZn(s?+{0=nIJ z`5W=`4L3m0xNXtT2nNRT`dNa3L0fuBTe=eYnLi_4Lt>NuP-MMj?ao@9T`_gYDV?Rk z6WE?-PrVE#r}%~}$cC22(Q0grHL?(elEBg&t94Xpv#tJaK4_DO8S6D57VWgmP?Vkx z>o4JBE@NvBS}BvyORx?tZb6e;+AfP1!f7^uhH}^7H4&>6q30cZgYH=FUf0ZmH{OO^g^@bjGQAcm5q4P~oLwy^rBTN2iEveN2C9(!JMfF-< z1!EJVOs#KtVF5wqbdad?Cr`9??@>b46_}cK?|@H;R!DurswQIqecpQi_YXjLJ^$*R z(pSk(5~wB{>`3;aL+xNiZ=gMp0hS7#@^^xw~t{s_TteOap2b;vE;ky=1yGF@lb8+ja4f&oDD<_i&(VjV(Jj z&@miXCx9l$c8}nGLFjyaS*M|)6CK^q;1=vHo*jbf9?z;iveOVSKh%yuyxO{koz7t< zxT691R=sfG6F|i!ttJo>lmvQrESy*pDBsbbX0D~rhB%~z*-qAt^Cl?Ki%Z%lXy9t?r@O^7(T@rZp85p^V@N=4$`7m62W|NLgWggnr9Jn)_hh^>GhdxE1i-BEI% zRKJ+X(s~3&h6m0w+}den=d$idXlItMo%YbVEEOX#$IvP1E18P5)9z(TX1NG!j}Ul< zVb@NZaV|^EKyqje(1!36fYn|*Ee6vUyx?o6O_G$L%esRU;K?#cpv|Qa9vOM1yGULS zoXd(zu#%*~4bPOUopukiyz1}CQ->b#{FweMwF+0tNVEKw^q(NWoVuk8mBZHXUtgTVudfso2%YW?FM+($wF%B5O}HNo4D z_cfR)MbNi_aj>sZ!wtZHMk4d}%(jdK<;=FYgn8uAMjl6JwoOfseM~zuHJ-8TS5V9|bpRIU z%>np6TP3Z`%w2(bPw#kM=?FwPA6{qwhgiOFogj}X_6`?&7Q1aLyv<<^TfT~zu8crX zj)AHGH6p)9xt!+p&rkm9&a-L zT=q+;9vemGGg6P|oy0v}kJtnqGjHabu?fFq?wa{#WWr~Ra^`d$W2pmR-NrnPf})hg zwqYp#!hM9JB8G1e?G2(cYn`Jac7Ht8^pZIDB@wwbz7fa?2f^ejsfa;)LS;0LA|(JI zqe)A8M0=e`iePb6Bp@l20*TX;%2uZ#)0rz%)`sFRyzQ@{|+%?9f-8p z!KoPKqL>DUirSF3A?Y-&ghq)`B{}y60mrM_*t3qnpU_IrkoJIj3EVh4IJA)5{)9L% z#la$(IBC8E=98@?bIcx>iUB}=QpOvKWfx=B>e z(OU)>yvnwd%O&qorIvk&j*aDC=pvC!k5 zYwkH*!X@A?>I0J0b6R9i^Y0V;r33p7luw}v2Hco*oy}gK*=t+zY=+9gCDcJ+3KGCp z%$p%xWB;94;RJTdvpq(Kf+~Gl$I+{CTU<9CqieM)jeJy~#}ojO%FCll|5sh1K&fSW zR07tjEhIDK6KGu#Xzmrv)GKZG7|&BB?36`8asJ)Pp?QXqiYYyPD5FFaVE?Q5>|6J5 zddtNDMYr!049p#gbZL~5XaQI~Cz2Z2@1W&O8TD zPJlX!yTF>Qv)5xnO=0BO-PytHD}-$cGZ)3FyH0K^B^J2*BsW-)$av_$cuc_9{2SgB z_P5{Ty(~0FOM^7dLgV|4<$iTI6}oN@Jf&h4Q=1Q)iUYmP4lNIi&OPH| z#=JWC>%`XbG3K*MydN6_wx{h^i3r#oP+4GEJjP6Kdn_@W2DD9Zw2oSn_?K%cr&Ce7 zUgfFyzGg|*TUnEVQ_FEUgyR>)aWB-|>t{ZqBjT#y_rO>11EOX1c~S(b9QV#gu>YYxi)N0Y zg4piMBC*r$kr&KM1M2}Whikwz9XxOc|BrM;JyEpkJQd}1yLjh3{ennqsgu(emQEca zOtvqf$H3X5vV3&Li?m4_u_R45*sja`k=_Av7INZ}$Zzut)c4%lIugU{wfMX)rQBB&h z&(&iejg9tgb^B^!PS+&(YGQpgaffT-`&>HKmCU+Uvo79O8*{og!B-pWtBpHc8{b!? zV~dj6qSf6+{M4a4Lgm^4b=jjKP=(ZIAMya;-LzfgkNkZgD5#GFQw{F*3P2;0J<|QM zqH3|x{PigRYC_<{6&`fe2dw)i$Akjlo2{;2ph0Xyc_lks6lxJ=Bf+jD7umsvQP{$1t*aWu! zn|pPpS*u^unTJL9hSlEHCO_?Nv{7oO7Z=p2dY@MJPGmg9%9|ih6l*jj#};WAhpFBq z6ooGRSM?J_aglWYKM!s38OFwW1LXg~psY`=5HU8Etq7RPS6CweRA1uEu~HWT=SRA* zBVvZOG)@Wj<1n08*>#2tG(Ir8vaDeKba!1~WlX{RNz4;a+!gG*gHI!aYC{@fz!rf` zkcQ8eGk|7^3^)&A$_$~K_?Jd~0oih^5`&LwZuLHET3(}+QY+ysjMDpxiR0WA9ACmE zOTq~I?q~+BDp_v8OMFQ>lm>2|qqfkXVYL!sAyT#t@E64)(NDfF0y29TDovH;?%}GL zbX_Vn_nMfcG@j8c`cK^h+AMHeEUY(!MhwIAUaiO6V=(5ZnGql**S?Edy}6rzn`Ekz z2)l`bcd*jIz3tkU{YgN!x!svmy8%){+>LrrU(_cBbu+fHxcSWN?UZ$SLVkM`v%nv~ zLjVtSVV}Y7WR>aG0>csE{@rT!;c39pDwpToy+@ArwyL&Z@}$ECJPkS9^Mvv`+g^2q zuKNR^xoglAH22dlvt>0(7pTxyt~Ab!Duv7qN#;?&HjFKh2ys!4V? zZohc`)T!Z=zkCIs8XDN9@+Y?2s(Nd`hVKP)p@A`OIKo)DeIw4AbxREibDTA+QDI1D z$!6q4H%3K3?K)aeNAF)q@lAD1gGGh>)}U(o6F1bmaeR|J{_w ze$|TLeSvg-J_&7AcE6X7a`)4_-sv6K-ZyXrxA4Jz>HIIq*Exw4^`bWTUiwNU6WbvJ z2-`E|-4D_Sz%tDjkN_lRgkRP(1#yJ~>4@I4dy5)L@Y3Fe&;E!PwcZWeeIFdLHTTYm zph%Ry6vA|kDE!^wM0_d82f<&{^Ygr#J+sQL78C~L%qbyhPo>2%*As}had z9(j15jR>9$9!RhJM9eZ28XZ*-jT8(_;8%B}cvW$+{JnKs6jpVp*PC+ctQuIisH7o4 zJ&H3A=Be}4aPl;RbfE>%-Gnu4@YyqUNtC_xOP8EYlHqW|uOMZB$ZX@=5e~VnYVv$5 z{TDg{nkjms(g!f@#B-oeEkC46Pe0O`)9{H63b?Vgi!+kbW zp9$rLBEl@dJ&we5Op!XY&s;+tMw(T+iUdXc!ocS3Af`ci$x`vd&HdE=voF*0vp$}w zsIW%nD(Y&8B$oyRN5HHdGqkw#bi8LX7AeXgkhjFL5b)V!+JQLWv7jPR#UGP4zQk<0 zq(UZf(@x-Lh8k=#xbUGTj+BP*j$H026hSIa&L%0AOw@ZCqkLh+>AC=VHuJ^L8Vo`$#K@aFWr)9VHD5vT7EvhzxdwvF z*0@wsVI%R5ZTx=>N80sE(8KY_m(Eu68^#JlIJ!mDtTf(1u+%8RzgR&od!T-8{cmtm zt#U2($z#)F);2HdVie%__vB;~l-QvWvq=d%%T?z70F5c3qEfqMb$IWWH>_#mE|JLh>* z(3_<^)oE}fF2Lf9L2F-nu_IB^{4@mWOma_Xx#vt>A=qf9ZCSMb%auzSt5>RkmlLYL zUY<-rJoM?5uW^r!n@K>(S!Zfgkj~*t{<=Q*{$}s8;brdaWr>X9Nm>2ex@7FjY0tN- zyVI~GKo&MGAdce#f{7>VMdle*B6To=(RC~eu{dD?>ru8IPxs6P{ervGbbb+hI>@Ys z_d)SObeQZxn&;%vuf%n=bC;oYAU!@muvrgkm0M!`Aw5h65l-P3FOItxBtNPApLw3# zwHxUVz=8v|C=c7rZ1D%w5CS`jpZc_g)jw(ni zp$cu*GnHq=&<&(j{?wc4d`J4C;_1-fBCXjjY;4=f9#*^^#d6Wh^7X}`OJiDc0nJXl&xjFa3 zTz(5-2qAQfn5EdL?`dUop*si~RcU?``P66DpOg_f&Xs2bA;#kc!*2qB(0&jw(~dxDTBLlLeE6bs-aP8;lwz9mWci z1Vh0L)n6og)V1_C`xbHmPC|^_g5B#SW8M;~Bbo?TO+2Q^A1sv@0 z{z3$I7U6z^}Y zL+~Bm-(TlXkoQ;DA^Z;S%h&niq6w&Zr2k z3+b*$x*nK)2se}$!DT%F+Wlyx8yUepitpApG@MBl!M%&P9)z(vBDf9U@2i2kzbJyE zP);@Co!X+|%J6+NV9f_$PT~7za|EX_MR4g;BRDJkVh}cA8`67I!%;}{lrn-_f;bHP zdy!@o;-vRzxFC~&iv7DjNJQSL^#m;4HK(nN5L zFvYty+~!|KaM$2}38o+BU6@laM_@L>bio8r9tBK4>Uaoc7(o7~l2Gr>zzM@d0XICW zTf>dKrQs6Z)^LZc5nN3w+6-pj^a$?Q4h^>kerw*r_p}IZCE~Avc>-~U5&tag?Jx?| zg+YB|&|hWq!3*&-@IfHoRoM|-6v~=DIf5HNT@p~Yfi4YqYP*IDz^p)7jwMHM>kA^d zOxTN2PBI@Q`h-2&*2hH)k3A6*5>|J=u7}zvPzRJ@o#3m)4z;u?ERj^hss=wjEvkp8}aX$V={YC!g*XytIrJ_))=YM`ds5S68*M-_hekPui`Aj^c_(?Z}+GzeE zgfU9bDg1-5$M9PC$4bvQ@Y4yk@%(FF;#AC^$8#z_a#5(A#?OZT9lR4hCjJNbn0f#A zLM_WX;h)G)g*}PS!dDAF3qHxxGljngcB}ME<#S+9lb+N0Y}nKJjn{bJ^LYVsiJ_@x?J{ci%`F@1q_$1i#_(VMOrRQv(g?*0nEa1(syQF6! zZ-Tu@dfw090s8~e^Fe+Z><>xLU+`s5s_zu+ZVLk@-N2KRG9*l3b zrP6agBx(w^Wzus29}W9L>G=*H1^X|h=cD{&*dLRgzv3fd|F!fi=MAtglAaa39`?o3 za|u5Q_Dbnl#jivwRr3?zQ^PZOE|s3OybkuqrRNj8>by|XK?HgYJV#|pXF)TS4z*{@u@zc_V+vmpFi;8m{7Y) zdOpXG!Tv|-`6qr9cDM8ta6l1idFi=^zXAJd>G?c=9riluxt9L{c8~O|=hvXj4gB}; zd4a!*XCoi?jZpg{KMebydH>f!?MwU>*uB!TiN6fHkKfuM)UM|zb_%uY_%~sHnZE@8 zzevwl_%C7i^RtjnGk*d07U}sae;)P?((^U`9PEFUo`2)NfIT2RTlqoQgVOVLzHLaT z-N>JTPaA(4&o`v!CjN8S+ofj*e+u?aemBbbCg1&*P`jB|Vw`N@KZF0@rRP@uIP6{0 za~sGXYP)f25^CQP0t(#X0w2O3g>yUOF$LWd+ZT4;yz}Sh zcUoh{{U7PHTF32kJFQvc_J=yHu5tVQNFSswQu;2~jbZ!jPHPznL&Zqv#n#+T^V>lo zP4D(9a0l-dR$DuL$7oai>KSS&7%5*?r}Y?d5c^%7*7M_bN2m4Lxcz6yL+`K}#@)Cl zu|~l`;#fObH7VHpI2WvP$jev4xM5bn;2!BuYw-5K;0V_XUP@r$%FBnLXX1DK(x(Yt6K(>Mp?LQX!o^mMTx zgJdt0b%>zgSVMz=NbTyTw?uD?9_$)$HLF3VOvtdCwXIvlPLe@0Erl|_HWH?wCd8)X zbWc%hisj|Vi^vzVl0cdt)eKPU@y!k6g=q|hYrP>h*}g?U4-xj}1g*Gf*bC|iffOFw z@vvy#`WILUOV+R{Rc~{U57}z_H{vQ%2#*|yHcUMyXM%=HcofR<$fAXh8q4r%x8l6ztXM$=NIw;{vl|)dHdbM69 za_ULm+FsT`SOZKeZfRHP{L-`mDkRA+zhUqL`LV{)sp_5pVC=@PSOv$XXSv8!1Y@aS_sR3x0?`-1_{{Ub)ysrKUa_dMWwOfF)xe#UQYN>tiK+`EHg8L7(9f+m2 zhTE<(=`?yC!m`lw4T4|I|3>uo=Qd#f@^*dKZ1BwnCh8gAY?T|zMrW(t{%GH9%KcY7 zX^e-~i(q$Z>6@+So~?8XmQBqHrfk0`-jl!WSEQuEY#9o>i*PV*3Ky1)RZ31l zBv-i_hJk^^sB$L^`WKi$&~Bk37<}VzXif+Z$M3>lhQse~#RNaX-nw|nWpXfs%k{k= z7Q2t?6_IAsAem!0AVVy2{c7wRGt{ekF!^Z;}`A-;z!U;5XhOgq%MRWH;WDtTiIXq;fx;maoYJ zI8!*-@pd|@!VW!qpY{AvSjO^TB|7r;c<>bwV;C~v~HkjqKUqeo7E=k z8=ChgVct&p|Ae`8TNvF99@oUj2O_#|^LW-kp{YK<-NtN*dgY7G6Wx6C4RSVyC4fahm1PQC*V+JvEzmU=j z!D+{k=M$t10k@m7V_$$-0e2MMF)-LuQQa=tO8DuevI1{`cMQyWm`5Z(@lPJSvGxx2 zg!VGvx3M9R?9~r2C_ik{cpq0dBe^lny0`wv7f}4R-YNwRGwP7Z)2{C7BxUZ99PRge zfCDB@Ki1c#^{>L}(H`D9X}l3|!wIh*HMHpBEoIfCZL*#LzaZQ@rG@}!cIYEi&@&!* z|F{-3{X-Oj)K9(O_Gk`y!cF(C*i?F*1W|iK_*+u49(C(GVqmwPJVVTG>ejbKCX0O1 zx?dpq0pfwc?}t)g?NdjhQW z3?C$3{pzlF^*3|-n-ofW=$QTiwfm%r-$2}-k_w}2%{NO0Hwr%90P98mWfG3GK&X1y zUVZ}HH6^|wh2Slt*R>IA+JF=m5rum;aw(;FN#XS+XKbf_dNdsM zsMz#zII8qUypM_a$WXk)p-mYn91?wju;+(j)4z!I$F7qczQ7;HyOQr0oAwd8F+T~? z^d4C@q@<lIoY!>N;j z(hSye=>?gxt9}UCAmt)893+$>9Da4g((g&e>MCjd$5mFh92ZVBz$$Vmmv1m%LaR>e*J5W7p3PdHTp5%r>|bIl zH{>|Ll}vo)_VZ#68!8rU{sk#kDj8QT} zDa$0xm%${6jg_?}6XCIy))2D?8jC5$-UyQbgYnl`k{mf*6$3a3z>}3^jon)+hZZCL zHBy)!GH$He7j>&XQ|YMIA(3%O9yl+9KnYfCAktQ94TY8>N#S>Qb(6umQ#(gSVJ~eu zCC*Ua3-AQ_qN|(!BVOW2N|29*BewatGphRNJ4$|(@;{F zRi6T`gDr#)$VA=rFi|k5ak`q-nCPQ`19i4=WoE@s{ZJV*vpdSAWB9Dff=yvoY4BZ^p?Zn z*Cx>6gvkD9#U0f(Z|Lx=hQve3tGACNAZ@)nC&0FoM9k8r)8Pphis`SP!t`NCojLRR}x=ix(3_@ z;`Go7*FC!hHRJ0aTDwc*b+2GFlHL)J6EoLe$%$I^d#DNu9vFgn2xjMw939hK2a?R4 zS3R=V)P1xfxOIDAKxn-HX&;8p%_!9|?cQ@DGX}P3?*^$p+GD$e@j73;b}tUsEx`$l z*E<|aZ_|H6o?UQ)-s|1&cIOXu$^hdy{RVG?$v>gjV>-7~>whricgiN5TRkS6Fnn9L zdk+Y$ABj>%^;(uz5CPz-3@65iR5lLRTDZdpuR7`fa{Z53ll+6POK>RiF&jF zMO_mqyrudB_~zoZ{@p^n?wF>?<9d}a;cU?#R5T8A3V~tBCAD+K)g-zv;6C@{gky!d zH*Po|)E^GGod?$)rWM8<0~#TBvluuj_zR1OR=4vLa{ur=#OpgBBK=-CVW`m1a-{d9 z;rPd=XBf`;70k5L59T6j%X-B5yhZG7Hne2; zgl_tXWN}5F+d_i5CJYTlojR)zh_X}PkT4fWn9n~TVZs6M)%|DkseS@SD*_t@*Y1{< z!wLpI9!+cUjYEf$oF@r30P=@OD2;13^8=?~gng3s#htY3UUKGQw{ACi1T5W-M95fj zJKafcK##pEuvPj3Q*5(Le4AL1O+)V4_!143lN373zC`q*enByZ6gX=_?%$w0Tn{uQ zT-g)Qq4w8AFJumP!hKA8>MOnTA@ZqEi%%cDa`Tf*d+3NBP$oWoszpUUGT-p#3ptst z-SBCbpH?uC4sq&KlJjd>;O`_l~I=iB8>-N@I zaYqKd-$wQN1qYqb33M;z((wHwHXs7?fqs-ZXPbJ+!d0k2Blzg4y=w#vlg@S$kgV2D zWR$OW7KYl;wcDjwdp4njR^-kaDbDW&{~!W-6m#d@C@v$z5~|T$yTKLaQt+80&e?i| zEcWz5Py)V{sjJRvax+9^StkRn%Td^*p{y=@lZjHPP={`Ms&k*IxXpy^vtTmI?UR+Y zp@B;0jj5DWFUCvDh*Y#JEr1LpnW_!bH31gvf~K4l$wck@gti4*`Q8;36vkW6{8dLRxbnDZ}mk z&TV~0$fJCDG@B=PyPvT_Fr)#%D-c{*uj)e}4b>h#o3@UvdV#6`t+X?>tA+mUp`&K7 zW-{M0w(7Zb=7}5R^PR{-`iwCRD?XE_b#myGdeix1KElnVSbVDdANYZp-J#Jv#Jt=j~sA zcboIaLP>!ixLHQY=>v1dEr$nE&J$wO&n3JH%0OBpe41xs#{)_cnbg~zg||EFZ*%&# z{^z!|d|UtV6ZfCS-56@*JO|_?%7QV|=tz!)%D&tK1j_I$UR12w4(@H^YBiIONvAS_ zo+~uK)Y##q(r*4gjC~7O6z94BH?tRD8HGi>E{Qv{fPmV<>WOGfv%4TJY7;;)X!Mjt zA!N;lnBegWu`OmIV@3`Tp%eGMmMg zY#AZ~G|^;>^IG?vE4NEDf zGI3y3qq^cyr1$#$H}$4a-`fOK1)p?d=$;P`8A=X}2(BiBw-AcVfubVach#%7M>Rny z5@-{&Des2j=8IoSAh|nx@RzrbM(ztzf=Cs{w(^w$(6eZ<_c3_@BcGyiW%tF=QYcj` zHVgN7JcLm4yF7q&=p0t^{s1in13iFANB8ihteGku=zNq=HMR$kdhkL30}_bGmEf{G zbRht+L0PV%fy#*khy${?oc>N%`WzM$dLIueCL8!Do+W4p@_75@7@7*GcJQ*M0|N-( z_qqg1p6fAXChzvL^6>%J&VT(N)zJBHoA!P7zxBQHJK42`f+1RNa5yUeC9kHJ{(B)x z8OdmuQ5hp;kW{pd;E`SWEt0x*mN|nwrYeXJ0%-+P7^2%k_0n~W)(iS*QXksw z4C2QMQIq!bb2zM3PA94WeIEx3aW3sZ zWFF=uLmvs<$67yq9y0Y}(x`HFNUNikUHH|Vee6EU$t0u+dE%b=_|aHAQ_p<-s1eWC z2s?r27q~xfz|)WS(&e&54`Qsll7|Tlg-Ven5>6Sf4$h zHk;_p4|;O&@R~*nKloBSUg|L?Y`og8Ppd^VFKxb9oq+pb_KyH%4R%3;J!t$ zMhmd|zyLcJYfB?w_Cm!nZo1j7hvPA&l051`Zm@FJ`r+NmVMqw(Nfk4tU;w~^ zs*Thf3|9WW?0p}Vzu09aU870OSP-sBMl8mV#0xB5tf2Cbz+k`vP4*vTA)WB0&trJ( zEC;(hLv<_T!CLpw@jyc`9B@@SI2`QV;b4Iy4h_fsE!q)mQQ_}{Evk~?OrhPkqb8Bm z3@0^_q>v+Vqe)>DX%&=qnb{1Uq_|~R4NoH&7$~Chk3Fx9iJf)4j8VDQGU_D8&Styl z(AXUQ-b!c55|Lkzu^XUt}!853?mex!1ZV*n1cJ!-VI1&|3Q0PT|X#? zzaAWo&L4zgA99J*sR$%MtH(euoo?udcxJjjWjlXD0VJxU>_zo}X5MXIOALUA=rCmR zISy6LTha83H{froYO-$H7>#QSk68x&!b{8!rm-ctT~bG;7>`0hIFnzLveNoo_H^&N zm0vb=-Bb(!{jcc?um9iDb@lz<(iQgp-_mtm4yFS<&EwHOci6)97#Xaot~;`80pFRf z2P4sv=1g|`XKyuFvw5?`Af2$B@Q?FtBl*W@uIUtL*Q#bz;eB7&+$q#G9X%6k;LB<@ zUzw()p|C$O4A?$5hEL-c#uDB}!xd&C(o*I7zmJE30N5#Mc!WR7g246n@m%PHDvfIf z9qrC4{f@0wVQte5ZtbmL8^H?vWW3AFL-xLI7h1S?D+HRGg6()0?aNFjJPgSztoZ2<_`F}%WOt`04{99$O;!ofETXSaa0nwD<+=+5SJBuX}0kv=oj zI*#w0LYIuk8a~5pO!rUNN>4h03CE(P>&gQyLzeMaIng@I!?Gf)@>qw9)`+T z!-O(T9a>}#<8KTm37(~ZA6Jds9H-!4yII)mX$&KQFs9MUHHNXCFjc@htkF8$L)(V@ z)uD6Uv>pekJY(eMRtWw_y4Suu9x?8)9X{uYQm6k}2qw)TPWQ9zbC|#!R-B`nTTS~+ z+x9hWgFCD!0Tgw7d%rTeP8*e-qQrZ@FuK0D@4IUtIFE&Qg1_ElM%S_X;=OG~*U_Qx zKZ5W7^h@D0U&SxV#z?39c>lt7jrH_;pEMID%EW|)sLHpDe62eUqZN}Hy>Nk5W9n3d zYy;9v=oq;UQ3HWpuMO;9oS;4oHM~skc{u==RR$1CC+oJ{f-c5I-C)+P@tqHJQ+q{; zCfYxD5ay&r?N3ncx7BzC_`j~-nFK>%XSN)1!yNb=lSH^Lo zj0ccL2;Hg3gsKr7G}%QQ)mP+4VCPz0cuSrP6a)Pte=-@RzPN?ToiYJ<2EqXU#S^Sx z-!nMQQ0Rf#mKwW47_hwf@uQh|a`7z1)7GQpDg51d;59w)JbP1d^L|s7ju^4YY97V8 zv2Cy~kdqpppt=|Vhb$ct=(Yq_%PI^7?4XTD822OY5pKhMp*{&6n~MbKXax75m{2rX z*D<^qS!q64?MJZLG?=eBf(EM4b-Fg-h%M!m{8RY35QNpiDkf1fwSk)x{Q5PmaD2yb zs(Rs*o>l_y)(6*Z-x2?2y_Dd03VI9N1{(Y9v<<(u#TGS3&|EYR&SVvcZ?VB*)o72| z^h~e`%K8k*uD9f>Fu`60mwukaYF}tC7p`bTfgYaE<|q4^{Dhw?;|BvLDH6T@2x*sn zY284~;b+d;D31yZaSH`$`Am~!-YAuA#iFkbTWvOiqLiSAP#RcH2uh^H>#9)U z?QbADM!!P^4gB5+fmCBo1&Ohcm%N*Ejky)caqU9j#>^|pLRzZ21KMuOebQ5C^x(%) zxMcc40N19>$bCt;um5`zXtDk2%sub-U&%G?JS~eIf=+91L0VZy{~}7V`D|AuDJ(tN$%z!@q@G__vV$p%A(! z@((ll8}MMAfdC=yroFg#iBdjXpIwFq6khj*1;r!o#Vh*`l49apw8QtVM8SL$s+_@_ zYFMRW?(<&!1Kl5Wl6?C~LhGgIickUC2vj)n&V`)aK1Y~_`-SS3OR&4wO$=XrCy*p3S)AU9ddC)@s z;XRg@-2FL7i;$C@N<(tJSxf1dEedOmM%r9`J4+|{21jfB-rIR5`Pi|k-IpTi=TdLh ze=2D?7$>@yRQEq6)ftKmLVi~1!9TIBCm?Bg1CClYZ@79itRpNVhZr5!rw*~L7IJcz z+*u^8&`Mg;t0o01;*|Ak-jwryVeGki-G7Gm02*9qZ;_Y&z3lDmN;Y1K>^x1|b2dc# z6LRuZ8M1XJx5>y~qU10BjnQaS_nf`%SMIVahEv(Nbzb`S$6jMxaz1e12{eD=aP5!C z$xU(@jJuS{T9~&hlgr>M{Uup@f&N8tNVQ`4DK%E2+IflETrB>z@m_J0?-l2GPA(fu zPCoa)XPR_hre|c>w>r7{f6tUSn91Rg2ivYSxDDxsDNbYQL0HCqOLpt+)ZxbX4Oh9F z-k-_Gm&j#Vq~!^@Bvk1HM_nKM-Ru8La&f7ezOVD11mn6WGXE($6f1|H{Ha``4)*tM ziu~1gqm9d~r{E5%%xH{_AC;wz1HINJcm9%2&|#L0wvd(H{KDkU&Z+eMk~hCCa!`_m zZJ92gOr;w(feTDt>Pw>(v28kHMq0u;43>8|V7IQoB0d;Sh~>%D}^z@PP^XcS5}t5WXZaIqQT(dujG(ma@yrPk*B}{K_mXl>-;U9-SQAa2Ov+|vEtH$2idYJ;J|*s zVRAW$ORv1sv+|BpFmasuZF0&-+7HM}o?QE~13BGc)~j+dD7``P0p-|GKinp5Q9JE= z$yo|oagy75Nd{reQWxn22g+@7x_QukEpELO%oL%N(|NCidjez+biV|{Zuxj5Jh;22 z{O707i7Nyuxti)J)A&5~fDW0S(ZP4kBlohQtno@|6sJ@Kp=shZ9kj6KkoFc1<%vw; z)670s!YHoY>Cy^dL_A z817w$9Mb!kVbAf-Iz8y!jm&UpNbl3by_%-$H?q6(MuQ=Dvv9_H{Z+TkWCyE0)+j8q z3lb6Luc3H~6S(D=a^ROP@?6TA2M4&HSA~d7Iyc^vSft?5?_+$*t-wgDLb&%mi3K40 z&=FI5z_N7x%;+sBSMNG9vkVEmi49)F`aO7?<~?gn}`_%*F4WW z0{hlVd;#J;=_DqiRdp|b`*AOV#^0YsJK+cAiKuioxuI_zb1(Q5^87;EE8ne``x!0f z@r;Z2g4z-AGws=X0caZpwLK_{hbe@+P2xU4Ry0Ft^bMvrgMia0(hfa^2BW4^v&e>s z_Spn68y@eSLkw^MP-+kWG1CpXhIQD1b%wSZ*6Rkg^&qw&d5B?bE#u6RPH2hkZAm@V z@}u^a$Uuv&v1OjGWueW%PS+R+2q$K>R&D z{ycG{<;I}cvsg%+yG)k)(!vQ0M0``?uOfk%ND*ENg3Qd|#ehPgR86->FTPD62Xc>P zqT!}k70fwL{n{+qHDw^jOOO^)nB%e*Q9zipf_}rQch2wStVQ&ygb>&3-(V3#ahrZ7 zkk<<3>@Sq}IFglJ_(W8w`9nHj3r)B9hx&8aosYoZ^3GR%Qnc$8n$f3vy=D?KYBPZ` zJG6A6O)<)J;!EiE{#TgIav{W)d>*>H=R$0uZ%wl_eI8;{@115@T{F#6k0<^2A-1V| zrdjg;1l{H@LTnfCR3YzSJkswFUp>u|fM;d{bcT^{H_EWnXWul-8GLW+M%iu9D_%k@ zvCD$`!2?uY-NpAqit}GkiOg31iqK%Es)qFMX0R@)q9gtLgzKO9yF>X~?)r+V1>?^Q z{GsZnU*hk}%HNF`AgXjo|L%fBr?~1lUnrUoTT`q$|C41aejf0seh zh$@)Uzpp~Nld4YAzmK|(jl|ROuGV4%o78zsHodc+LX~|jUV%` zxAz&^fbhL%W~0Qf7_(iS0!B|@L}P%5%{XKTJ2i|LI!?_a2Dr-kafhn@V@k){5_T$_ z2LE)J;l0MdBNd+n1KY!3U9LUNV5sK8PI)!l*0mdlwTF{)A0Ii=juNVAH5nduHbi&8 z1T`<-VrK1T`#3Y3>0#$2Codlg|1UE;*KB`KVCM<;@eg!=_6voMVtxE}_VU}59I0uCtAPPMsz(7+cAT|9>!msQ6 zEtnx{46UotV>^Oq`uJev%lL&-k+@$e?=Ghp{-_VDY39{5^S@-z7~&hlc(RFS3@eUN zd9qnCTLrPyY(`A@|2ER28;C>N7c(s!yT`Sauo7LQ0E-8-sujY<2 z`QHrOk>SHEAUUz2;;^9}zkp0RRyGG4N|S>P_0qi79BW99H7o}ks<6PSDX?k_tf4vB zPaQgcSezW)8D$#VcZ9l*+ z6ew4%@IzVDbd^YCT~!RV+-V?%a~_nwG9G2AL(aIbJq@>K~~mkPHM z@6_uf>`~iU=&v~Iq~y8^bL=nY)&as&?;tZ!k+K)D9tsIxe2dfFS0+9ZijkkD9Q0sJ z`tm`S!t))TGk7lG>Bh4fYk32n7xC=IQ;$bxpfAZpL&hx~!*d1i>39|pdGu%^4M0uP?~Fu`Hu|0a7{vIGauABA+Z;26ud z$p;WIj)y;&_ff=y{1Lh5nJa>0JmlmBhn_!-zi`#d|5Z**nLI2bo`g^4woaJxT*^ms zGK~AM60BcI(vC_lAx77*UoLw9pH<_k>1XR$;%Qk8Gsrr~qxm3Aj5g9MH%W}8twOa(wb4hN8I@yID#<%X@Y_u(EK z%?9^c_hfsRF>4zVWYX1bS2=U_^^iiGMX^CehuRLN(xB*)3b=Yl`~g<`u7XxmIP5xf zQwW$nW1ayQKpgXPPPg5x0jtXCGx_Ed%+B^y7#j9F6HLN_;Q3&|n#t;Vv+$zArpzMgS3~ZLKu|^<9xZwoi;IBC;C=;$o;=9@4#aS$X#1>6QvqMQFu4p|S z3&?_>g)F#2=MTz?0A6z{qop*v=H_G-ZY+g2WE^pTl9@t)f=N-&;K!-F*!8ln!K7`P^wgrr7}9xamB$64dLCgQCgr3ou_tueVw9#zL~Coatt7M zV|zjXq|F%owrkJfB!rd+#J0xYxWCR(A8IRptl1$LdD;R|irAh2o6V;C+Mo?r+VDBO z1SoY?!j!6%PKK)MR)~4!Dnz4KXw?xJ_NkPVXk9(T%iVC2I@BuKny*?<%b?Qae}{89 z3%?kO?)2bTz{F3mniHYmnio6sEtdSO5~Fj9$ysDN)}g^c1mb>6x7^Sp?;njy)^nAf zYZcp-lbh~@_2Hh~1cp)ZS{>dRLf06Q7(X8jqDc;id)|^OK(eH}t5aa|)AgOX45oh6 z(hxXS-taLZYQyvW(U7l8u81B)0~B@4@C`F?XN2nVppbodvYD@(E(fg|G=*8xj4%q9 z@{h~X#K`3INMpJYunMJRhUFLV__ee7g+^zY5w|{RccfHHTe%M`=7xm9B5K3=!pHv2Ex=8{8}N2FGuV950NEH&3gK4k95%QPD#Z zYal*@j=M+JhS1;LAlA?!*hg=jRWN~}3?Um-L&5i;!Y==s>^lBgFiE*Gi7f9KJ}lEvl%BuX1os2}wT2()ozQ}oc9ac*o>icWqs#p&W9wqV{*AWP_x zZdC-O+;!eFvibLP@|$$Y4-W~5>ngvMQxrYG+A!F`hEqBuP`Goc;ogxwP@<8hL=Kh^ zj{CrlDq4UFYGQ+HKxLMebBUWwVdl4Y&iry(0uysHuC0t`ldt zaf8#Qa=HIG2(VSKX+fo}(~TK)9TCwtq-_N+Uanu?pA5xi(!l3vAZo+9;LSI<@otv; zIT+tT5+(TM1&Bsi1oxjx=+THEky8mAKNtuOITuWsKvVvl^4o_WjTofvQOrtND=Fh{ za4)!30SRdNa$3wXTDA)8F8U3tiT6Tqj(`Jue+U5ea&E~`NP;Tn1wmL2x~2pQBE=bL z9i70T&<~D{nd3^M{c;Ul=N@#2b_}P!!8bGy4>t1a#iF!qlSkw2le-!Q3<3sKl$LMu z9HwP70ivRedY|4{=Xa)?j7gHkR*y3PvO+QY;zxu}63rjpnWTGzP2RB)!p+V^u^tvq zw)u_a?cywu(o3Nj;;i;N%k@TkyKNQ&sAxQ~MQjD|9zY*pA9V$fv$kIv zg?gjN1K9zmmA-G}1KS{pV*2Zt6_sz?;0z#?!?vj4BfX%NFJ1e}Jj0Kr~7gPza6pr4Q>K1UBLzK7lr+ynP)< zD4i@i6~<$5(;XZ<$Ur;>83^MI>phA%6@s97eAl@pkgYH%hpTTUxJ)oddMI>|dQg;6pobAq4Ce9W=A`C;% zu^SF2Cv41#BgQ%>i1ZYnpJBqK#|Zaa&)+Fy0K-hid*3!euSM;Zn10cS6FYgTCS}(serE1g3bsc`J&w^Rm=(f z8NTh%j$FeO_&LF3XAsi1j4r#r3BTKGSO-H%@e|aOhn1a{C#$!NNKR}|)l#d#8wt)n z5P%lDDrngtwh!(jmmM7luIeIexA%k9AqB-%`mkm@G_csrChsy9ekvc(6dso20y5k* zqofyTNx-f!Uk=j2I6pGMJCYH?HdN${NYFLKT)2|26Q9Wq(rl@i@GQ!sJ89(g$+!jC zpExtDS$9;8vb%082&?ys3{+0?IW2!Qf=FMp50uDM&_o2L@oO+i5@?BpI|fzZGmKS#og3#?(>dGA)i-s`hpaccwHirop- z`j07EdLX2$PCg9NQdeMc7p63NWH$_!iwOoA$HrZi3oqX>t4~+yw|vNbTfEmeEsCw! zXDlpN`P7%UJm0A9h7Ys2wgFC8xJMS^ag|kJ-N5F#Mi3S$PzdAaG@JNuuGwG#BsLFi zo49eqhd?S)KDvgzZ2Oe)s2>#P7cbR~dKZ2%#*=E3v%DSwja!RBblfbvSvMtU_|(0D zt;OK(kUCS%Y25cxr(iJvPrX}H5T?F;4I_YL9EUlJ`%X50$_Q!jbwTIx&(|PpH8_u> z1&N?o@&^+w@Vqj2cHnpmD4vIB9-ifRWC}wa1kykbMGK}%lYM;I&_*+py|4I`5Tkmhk`qD3L@h`4Ea;{>f=&Th11MBuwJlE*VYvc@1&0scT+*CTT8Od47I8~?GxV*NUQ=euHOb>pE6$KbTp@K@MT z4&ULJ2@2@4utL(Sug zuNd$NG0@y-%9vi~Eza-bLRVUW26GdfD~!F8lekUJ6URBjt&U?SN{{XFqXJlnT~qGp zvdM0TlwS(Z0ikg=`Q)N&U}4q{Z?XoGW|lk7X2XptM~-Sh3}p+?P$lLQYut{yjx*WN zYF_n>{Ftj~Kvq$jF=0L; zIYX9^M)sKE9;Q=Qe9(xsQ8+^`fZTUFb`j2=%a~)=U;8V!UqpxDzK@F-qNw2;+9KEG z4umLZXK43jqs3q9{DTR%$(l!LiKyZL+o3%QrR+lZogEjKBMS^8%z}(45oReDsPN6o z-VdH_)3Z5Mq7|!Pvg!rZ;p`^sMRfm~}tC8frK(y}XZ2|9(c&<4S@_=!~ohGz{$#cN9Q9i& z{AN|4-waOuJ`iWD9ajP61xIv(8cRhGo~Cm%ksCMtc#cC~;D`dmkdjMhLgzV>)0DBq zn6F-yi zaEvM=B!?ftrMoN5K9cF3tonLI)p714p_-Vh8Qdm;<6{zl@@IP8l?A}$4m0HqmAkLP zp-Gs(1uhg;H8ljDz)jxo?*qhcc;l+!L)%1hFAC_IDj4VEN4mZ|fxYP3cD0d@wuj;n zu*Loa`$3IKjM|wQ*v>R=C*Hks;phRnXuNymu2BP4_f;RS7O6n&!^ph1M*H;_WooPl zySg^p7EAB>d*jY)mlEQ(R%Ms>cpQ=TO4qNhF5EV}Dzw&$#UMIH+xSsf4CCWS8 zaK(ny<>JI`$kGkN8etZvvjGyx7%JD&tF_rxp}}eg>x^StlYr|t@{zUKi1=Z>BCq-X zSFARq2diB!PfQP1oQ;YHt5sW^mWHw@Iu_{Z;8@g*v0L3IE$Naay&DRz0MHgogGFhZ z^&zy)4wLZ8pdn#))qM#e%#MUp{FrRpt~*?)1zTlGnp2qXoHD;E+if)z*}cl}Vo^U8 z&9=TkrN0UsI+53pvNIlDcLUyPPqZ9%H8psr%hgQW?S6re8kIx5)8xWi{izKle^r|u zVeuxmtU$K$`Yq|+6uIjLh&q5x*fu%Ra%4>-0<})CeWpUk>;OBVA}qW$CSq| zQs?OxMLYA1lWuThpQvTv`e@8G6ZLyu9vO0mo0R%QEgX_z^%reC7{r}a%9_u{?&PS- zYmOtc#4$g7zRi*If|iSZhRs^O5xpsJEa2QR+N>2DbNI{x$HJUfiQ{}Ow6NE00Cf@7B8um}#T;IKi9NOa5tDhesOtkQz`r<-A}9BMs2#}mc^qbOE{fY-5xW)J-RyY$orglgFi*pfR{Vc0@vc-$#%`8A13J>N^+b*f>W}Ox7heJkx#eO`E9&) zUx+i^pE+9OZ8m!>LhCPeb7^LPfG^Vn=uQntnks!+B zScF)Cf84__0%#T-Kh5EP3N+`5q;PQpA=nMrgbAjqu-ehsWy;?B>83ii>~m5z8nM7x zoQq8THlX86S=TT7e0&se?I*s|QxTf-HcT4TpR2Csi zdR17k?3rWtCPvkc=0-kIH<}BRUfo)el~4J89zOkUUGXEL8~Ir?XBBXc*XnKjyt)R@lf||t^P55_@+aaE-C;XYq12_!x2 zn?QhVc=(_0P0XlOBcOIPzD0Z|JmldYo2lEL^rJjAMj|-!F`w_PVasC43IwGW#6R4r z1-xak4(Pg!66~a`h*)F)t?Yf_BA*Kza{zyWW3F3WJ32V5wBCK;0zWUy<9Ak@JdSxb zEMBji6`RI|Vg=iHsBz3kmaIi-hFE9rd~AXo$2_e09Da_-FT`1&COBcVlr{xP5?B4_X2c?h&?eR5vZ@zCix3mEM}>b3(dZ9C5fM7 zZDwYq6gcchLu)lBdQT-id|;U9c(T#4B;Z)w?pTUZzo@I#^kRfUPGOxzkW>63k$8l+MgmQ?%MxU-01&LafV=V(PA_$PF%!% zIhq@@Wi@yg_}8|lcp5+HFKF$@C3(VFHsfPC zhtD*>O1}6dC?Y~G!2%Y%@-&|YP)x|f&}HIC|6(hRT*PExsSZXXmypY(ynRLF7uZ!u zW=Oz11jZ5^?##K+@oJ0UqOPc|dSPm>v2YN2DvP3tJE}cH9zQ%NUoIey!-? zU%s;c9Z&}-Jpp(2nYbkkisw_36GDr8jwl+@{ekgr#5l7ljG{tdftyWHcuPQQLFm^5 z@!~^nrJOhiY&VZh{tqL5r?bjOO6mZ-2j2JnH+krrUvP!HB1G*m?n&RfOXEAzi3>0A z#+WM;c4>rwAEN{k2N2G}4R?TWqlJQ()cooWPB9@nRLBm?JU)ZBv10`Gfkv-7u?5RA zDV@)~QrnUTM>cuS0+_jbc0#{~Gvo-XfGyU)Wkf@{N&X{7&H$u|eghm1_iAd<)BTMt zk%fz7PRLHbl6Z0wyF$Ym3gTBC2oHFBuu<>(yopaPqLJ5MjOd6k`b&YZZ@jZY)3^(Y zr!xV`wFGv_(c=}NA`dZRe$-(#yiplSCOpk8?hGO8H5YaCec~y{2&4`nt}XJK&GG=% zZC)ROKkm@t;uNPdfiwAjSIm`?1X3~qSC|pVjwy)y z?H!RHQAut%*BhVOhrx^1vZOL|&r^v>Lg2!R(At(2YmcfiN@$z=WP!9Epd=q2Jhl14 z{O}bxI9Hxr_(gXPA33U!$$|4zp7XAf7r~A+E*5OSt1c|4i#vSie3qB^dXS55yI#)`Sr~do?epd zZ1U8*%f!y6%#(+{>C&fibuX}-nmfI=Y)oE=rzK`ic~-?$Paet2dU1{`Cy&fsH!
rf5%W!mwF}H4mcuM3!=aCSw`5JrZC7jWJwFq|}Fhb&@(F zj88CbmbcUO4E#sA`fE%2i>S{;L;1Cr;B5qjHyyYMXRA7lc#an5g!bog8TD`BD+e4h zO9XRPX^;S|zb-G{aSYk&g#~5`t4`XoZZwk@Qp@=?rFa};CSc$J8zpq~UnqU}t6(?R za*&*m*%=7&AUG~A_Ld*gz{sQT#rp>cZHY7^mW<^Xz*iYXg9q0Aua$2-3FQ3@s}Dbu z?(AuZQOkJwjcXO;0bgXbnx}?0zeIQZ*5<_j~IEvVq%*^DpeP6)m=a2b<7WYFv|k zZan&ARh%?@@}+*u4wdY32aarHeNwJwA`-gwt_I&1HmX9O?uE;=LUtEMvu>kfMp{2y z5%27u)i-2%jSfxopbVrJ`OS0Cl zTKL|ydcQd9V{NRZM$~{vy*jmQ50go+`JXU9)2K2*J+tUmzxyxcnfl^!U!rYXH$4(; zHv;nhvOHe5Jl%esJfs3Is2%Yf!DlzB+!9f(Lw9ZpB~CMkFg_<+;34WVZcVlAJu;ax4EAEOwgt67k~cD2Pqy1%_q|!{DIc06GHI~sjQ-}naCSqzfcFv3n0#QS4GLOgl> z3v6Cpa18ZY(EP8+;Pt($PkrHoVyQx2bYic%+;Al(VG*hS zOq0e+vZl`P8D`NRQ)olsWo_qN<$ZQmG!JbNY_wBu`MQQeaGXrw`%LlZv!)c14j%m+ zGtpLhnW%}WoVX8<>JJ@K_9>1t3c+WEZ$IR*0sjYc?-kW#18(bL0hOX60!md-dO&(F z(tGb!IspL*y#&NUmEK$E2_1pZOMXSV)Bustq=XQv5PCiFUwf^+*Tp_(jB~R$*IY2d z#~6I`na_OjZVoj_vvQBeMvJ1KA+JcooQI214J6|n#Xjn`O_4JTN+o<76suijtZFus zjSf5&i_>6Di~XjtGFPa(?h>zgjpuDNuc&-%__1!o{J><1!6U!~`W1QM!U_{ijyB&F zRkT6C`6)0%y)S<%(2E~b!-Aw3=?-pFfY^-5tUp@XGI3T=t*P#%$|L45S8G@>lpuBXfqTbD6LR*!jhg_vnV&`)Q_c{IXqcM8BaW>Do}jnTy-e{8bhRH|GYOf%_s|H*ODsx6aX~N8ySukV)1u zSCV;QmF<-jJCqCY6DInHbWph4#4qXX`xmiSmyS$R?WK`uUiH{D0qkS#d5KVCEZ|3I zX>`@|Q4ZcXdTP5c|CW)$Dm8m;z@5(#*%P{JN)Wc)PTZI%26NF5Dr*zj{<6s`nCZ!H?)jvJC+9mwK>{qlo*z>+>(h0mXB;0#4@~AB zjccqJOC~t0AldY$pV?pE=OJmW528TwSKb2-?9jTk(e|AMVY}v@`^?*`{aE_orOoBS zK%*-4Xd;-LB5tsFxOVteotP{ z4X~cY7eBGaUQ_6M+*rb88&|f-ApH-ysiINE_b|*21zCr(`c%rs^$}vlk2GPb#nsN4 zZ3$z!>^bA5JqZ)t*W!GLZ;raK0nMd?mXdK0?YLFji-zQ8D%3Y@E;6u9b+)(9!0-?; z@vF85#)%VDwQi*ulQ}~V-4HI((%4=NJUTZxyIa2U>kj;5UscAT$c&(Km-fNL`J@er z0<&g5Y&#dHgOI1Ze&R8Bp#+yc0&5+`(#gATmiA$kX5$J^TRAau)@5`J*D(&eyund!nGzj<~UnkxGa{VVg@UAOTpn`czL0$@J63LAoRz~2~ zt>H#|4U>dpGLyt1jKjIJ)hFmI?e1A5{=~vB^<4q;`$V+QO84&gQdPbS_R|?0_buJf zzie|*L^po(pq*XC=1Xzr4U4k#h=$ZUKLLuj)E?gQgtqO!~jEhcU`L) zEkB&A1u7($mZvf1j|C-s9xO5|M|Qv=-#nM9G`tMB3^odxU3M)VAcs#y+SfJQD|$q$ z1e(Tanl)T9ex+9%<(J%Nf-Pn%5AoJOSE;o)%8MbGbXJQ+liwOkXlhZ zW8cKBVZ00e1O~*{1K~^Q{0(#dtt{Lp|04PMezt2Ln?(s|IYTMu^%o(_YrM_s*mNRC~%|U zHWI@*n9vu4=z1;4haBWTwrg(ayL2*V+N3Eag$d31av4k^iey2#SuLnnXMP2kH~NON ztf6;tu@VV$w8F7XZ&Ogy!*`Nri_VI@*abQ^YFaei&dzaoCT>sj^3xF6`CfM}-X}wij6_yB2mhVZcA7k2^73UnqbiB5 zj^+SV#py+>^v>jG-HQ^=)MvG#`yx7}YBCnHiQmLko@SgdrN2Hp5B5j8UjyZ(J;w3q z?fEl*?1R@dB>(KkWmr$TSuse@8+Z^oCpGr1r;E~|twRDCKB{LS0!2!& z0qQ_Z!5291r2T6i>2)5@K8cBH(Sa}D&(G{a*lZE35*zklbsr7bTJ=yE=Ztlb7v`S3hgP70rYeIW@8jwzV+7zc@U3OPY=Qu!K@ z4v^~_e(aySXXkRf`hn>*2;0`v#=uze$nN!#;EM&SIbQ}9o&KNRK|8l51GR5UA4Q!5 zVm|ad3$KR;^VifegG6hKv}Trs-h8^EC)WNbw?c`fTInml{G@SE&;&5_LL&+ChV9~n zTo^O8jg-b>zQ0E6@=L*xe?Zz$TpD%DG7{tg{AO^}W*bEIoKZ9F95fwmJB1OedF(Lx zvtOK(4G6GSjJDl0e92W)@_0h?1)V43ur{r|JtHKIZLUlm6mQB83XHIz*husJQcBtQ*b__-jWKEd<8#*0^4;=MWL_y>25jh`pbBZ|fy)HB ziRONZAB!jw)Iqj}-xYSvo7GP!C!CLKscBtxa4uwYVUgJYbJkv z74(FS6umBU3$;uFl$aHnhMJC62E{&HixKA9Rb-E*IM0av5viGW*L{^C_jw!0jcI9{ z$FtXpH(3(h`4shi#`RY8#>b4xU2a0{T%>vDIerD?ndX}tsGy_L=}6-l9~)gQ=5Bck z*z-J&u|^*kD@{af+a15Y>XJQA@KM%=VH(jzsJA8>#L>g@s~3cvCyTx(-Aa}Op5F>< z-`&Q=Hj}pO`)pf#4gPktL2}FOOKj85A|qm)9Z8PPTbxv`;`f~9zIrrhr9q@mM;ds= zbCg|&9y^u4sS+13f+M*uB@|t{M+5-vu_D3>O7lOoT+wgqJf4^ou#Vd%SC&RGv_j6j zoFWldl*Ivt>CC+gu&C(m>z+%6*q7~w(XhcgYsVH zHSN}?W76ASq?RR=+xL9}+8s=|UkbKu^nqJPhD1H6R2n^J-!bu7R-@PJd(hwJHsTe2 zlxFqLu3>NPm?-xswk}F+ib-blWg_yiQ(HYn&2-obK6k`@@jH--BNKe6Q_l5;xy%|? zd|rEmi(j#E;jw;0+&V7;)oNRIHw`O`+UZh}D(|_OOHy%|QKi#}FKNI?t{v~Uty8~N zQ*1?rOr27GeA0KP**?ADuv#o>jR6?O1Yg{Z^#of@io|Yl^?Dj&OTcsLDKBgG*zY&Y zvy(ndAUu%V_{Qx$RJ!#zLbJK87-iYymySkm{N6fhm3Y_I>!opn%RWn@+U^*_t~BH- ztk|dvUIE^Lb~PMASv&Rdu8?PEgL%5Ldxn%+XCu_+4EBlP>WkS7sr^(oLyCc8!_zv3 zA;0ro9GC)iLn>PD*4mkq7+_t5$Ei;ocBu&c9zB4|r>JbJuXC^3mmLl-#F zleI;n$2|y((k>Ul7g}!zcB_`^C=Rtf$Nuqd-&`4%I)2)2Ecz4Vr~TLpF10(DkEe1# zXdVms5*-F#CxNtiX&y9{m)ACv@3g(BrP_3ONQwR};^qn9%AWTUyE;hb7PIrYX&pK1Eq$j1KjBiN`a8DxBK~O;t~>DOkofo6Rt2ixq1652v#2W%Nxo6XMlFRAx6s|-g(ED{ z2+nWyRJc_GAT8^Hl^tCvnj8Ny_uSoZVFmx1%FCi+A-96903$5PqHkelF(o*_M{;jH z)6rXM#1p~Sbm%*eWVa{GA+8k zR$K}2d_~ML8uRWtJUI=bcnEo`94sO&N9j6=iLx<9f=KoDA;m&7&ir{+2Y(Lvki#~B z5{N7LZT?*3l7O)v_ewflC(q(C$tBO7+cfe z&qd1uxC?aiaPx$G>M6|R4e?zXrRHn7x)mDRnhMplV)R6{6&>>9jDd@?+1ri67Z^y< z=<%O+?$AP{>?+E~g9QAie-Sq`rv}HhF1yj*9(+Xjq4#MXEBPO&G_BF-6<>ygZ1(hA z!a0PqAYpg|37;JnOwvdB${zjM_TI5d1C?Z4rw9?oI%!u{_vewmE8g#5OO(3YcK0dV zb?IG9XVprG(Uv9!Q1>>)&v)99L#7YY#{V_0iEa)4e~)bbm+Sq%-{Jhf^jma8SDA_e z6=wAfF{Flt@=BzCK@8)d5py~%Ev{Y1x~LFPQ`E~DJk`Pi@{%16wi5s+mqt)zZ306Q zlnL$|vRXi?M^=4w-j`5>nTv+z=^`y7>WyU-im}@n7A52Z(mFQ0>V-_>Y310P!uLOz zB)RWeN?c=D)R!(hyhu-_>KVXEXpoH!Dd8cG&a;rK^XOAz=v|LEW% zO>o^s`+lC~Igt``SUyvzZOU0k$O%9Ask>!E?!xtkb6ZI-_2)SS!Ey`Ksn2Btr;qn1 zx&e^Co;o%Z-iTsPJq#Av*4UKKkO}_0#(b%u_3kvLMLG>qAs8EUkfQ<{>_1!%ryv93 z2aj>xyMwdDZ*3jN=RDHNmyM2-Xo8r);pQe)s&V;TCwsid7a4F4PDoMqIps-uGRy3O z*||&D^h=5QO^Y_u)H^hiIQw$zp211Ac8x@h%4_R1uD_J4TJ#A~fo99a%}e6l3A$?b zj|(;>%vZ;}K{og6$j^fEr|tRLIE76*UzYZ)7Qa|yFDv}Ma=NV+Z>$jdmGGIgPWV%F zRu-KR?{0X%Jc~Fw6`VcTOYLAwK3fRB^;!S!RF^y3_7J7dRrQYRhy6mQJ_NGU-lJIA zwMEr|w?HN@eQbM(&0zMSu=YUqv5vZ=4=V+enFZ9GLpV5q@scnw=;MMFKa+Omjw`QZ z>(HE9mq>y3Xcwk~Y5Fw=CQv0REOCsJ;om3Uy@{VV*?S4*E-K3{=vMLO+{Ge&((g38 zm-6BF*T*0i_xl&X2*3No<|mKh@((#LO`%LPE!=ZQBGE1iq6<==f|?DwyM9Qi=VLFl zWjpoWuQ>NM`>^n3gkFTRo_o3s9g!*0b`{NM>>*#juix4yJIE1z$@;UiCXj5~Gd3JN z)#Bp1@ntox{Un4o&jsXq^JR%Po47uL3ZRlc)vl=II%ZX3TL5}r<<|}FoU05y+C^rUVRAYKkMy$eagO}>@PR|9pW>@ zu$oVpS?fzPn21$%(5IbOcy`HS-_P=MI7#dKkgnmYpDvseDLj)X&G0KMKcFEfOqml<8(YFCq z=oGY*0PV#dr7b}p&u5TGeAHecQcmSBeQEThBVwAoMh*a5KZf!#e{^Yl_~M7_jEP)t z&j)dE99hk$_KR%K=~J?b2XiH|TJ{-{y1VTwQ+NHbd=8Z|f4E9SDY{nt&1H^&>L$g5 zjG+}sH*N8*C-DVNsDVFqUfzMLY36X~CZm+;*Br98yA|e2hw|e|*r8s5NkmPSwenZr zA=5x5KgROcL>%^mgpOQr%-UgBOa;M_BJ3B!(3B?n_>x27fVqVaB%qRAnm6}Zwn^~5 z8!w8<&4+LB`s4Hb8?kQP^^y`Z?&f$ZqRs|R$IkSn*9YQpv^%A@hDsf}JSG(6+Xj~= z;rT%64%5pG3xO6lrR{2e-l?V(6l{Wyt_r~F$!!Fz$qPAY(MJ@ z$3cPI2KYYF@f$T#b4^Xw0}0%U#|G5F1o}Sa*wNMM<6u!awE@#DNF*ZZyMeAteCJy|6AF}oc`1JC;QvkekMeW z=7mk*O0YF8@R|WH;=P*`$C&D@F!Oo21QL5H!%kE2SdDOd3#P$(~fr5&F&?XE?H-3t!aI9jO8oe}qIv$RmcH0VhG+T1& zyXrCp8SL(+dOws*Za(z6I$G}oEvnNy>I5}f&Uy2d>}y5#UAnXg11hBl#?hX8 z2g&E#8BNARtbw!S+GNq1yk7Rz22|cuzNbYO^^x5>g@zv$9UMN#G>H7X!mDUzY^16 z0hOp+o;pc@^`{}ncR$!-^FXO$DbJ5~^It48fLb>iA2rXVb0h~((cF)D6m4t&i($vMjqs(v8aY=FDp=Gsa1U>D>1VP5x z?SdiH!}1IuYq!r$QS7J++*~vAY3nu%CWr?Wrn_aZNx86TwIm(s`G~c02os~7-ueU5 zrKNeQ3pM+XwgmpfdwF%q-tP1jnAvS%M|)gUTn|o`fqNoqwB-x6`Eb(rH7p7qcTT9g z16iy?wd2M3_X`ox?R~WmXA|6M$?o(F!e5TNE_=C;h@0idyZ~JL(Fiuc^>Xjp`8B^1 zuW5VIk&Z(Bi-2*W$0b`S3Y8>-LU*a=oi@8?aQ2SbLypN$qa(rYv+pN-u%>!Cp~m1< zHsS9PmOE-5xL(kYa9Fk*4vO*2x}(E5TQJ@FQFVP*`GAp79JUX!8GrVvPuu}rruiZ- zxVbh;2wM#1vWh#c%QdnqV4$6LZ%U7JkdLa%jbs3c!#};*5VM^bAWg5`Iu)#CrLq_} z5N)!J?>{qKbb2hKTLt2+qy^EF=Pa35_y@m?YVGx}KaPf;-)F6^g!Ya)oi5p8X)v1$yFh@si0kvqXg5WD#j?E zPE*S{6=}*#);^^^!cS4c%VmM<1fC< zmy-|`RB^J0cLjglobDOEd-93;uC#9b?e27{NWpdAc+s2hSf9NDAaIIXKFO<|u>y*?71O z-S`Iep09X7wHZQ8KcS-FAV-etFg-pds?rMOvmP)F-ceGdL|+87{^j?}ldd{Sxat9``;^Ore)4^ffAcUOcn zxLEX7hD<*n_9-mV!- zDG#jRbOfcxT;*8;eRqaySbmIai-{K3F?gKkH@nln$)+Pv?v-fwpb8&BY#_MpzFjg+ z)+Wf|S>>g&FZGM3H$tvU1yF6P{il09$=;#Zg~wKyo+55p{IdD+owWNNlGZoyX@Ib` z(g?t*;;0*h_WFioGFSGHPaPDMG^t&5t=}BCTjGAF`QlMo!jFfoaYb|Y1(>6QSsB0U z9Y4Q(;-$^B+g*k1-);exguWj0s2EG`+ugowi^`xKgSs){tj1##0Ji%d)Z*%jt7{!? z`o$Bu%=i4`Li*x$9#^k=wkc$Z~AQ_*$75OQSJdtM^y(vWckE{;pWD%k)o zGNHmxd?_y62YTZwwLiE84%S1%T(G0$AA41%s6hMiDv|AJ~^a6i<}AlJs_8(j>c zWd^gshlqYUk;b5`;zvgMomFZpZkX%)q!hw0T~(t*v=&~P(1#JI{McC8zXq;EMQ>7W zg@>UpS`>5796I>eVK_Lio+J(gHTn_dn~Fcj9=K>tO$QLz{91L)7vEWuY-q2@>D3Er ztCiY??k+~q_zx83JBlVy=e0Kc(!pYyW5hp`veir}X_5-~pU%FPtmEGd@)rg<7b;-v2 z;r)x-ojdmE9e%--zUC-4@w%l(e#1h^4?j;}Goid+tiqD}W8v zgr7GNNAsXs!D4z_Uu0#d;GM<|RK9w1{WSrkCV1C;>(Xo+uX28d7tqS#|7`Bc{_)!g z^cCKnO^BmEHp{ca^0LugYxu-7{6mw$LFaUrjm#0aI-9JKv`_?ay1dN;4pYCH>5LS+ z4?2l?=b#%BUBnrfaP(<-;!=BvqzpG$9WQirBWw3~xG19$U6Un*_btj;*wxXSVIDgO zWFRg@B5M`LWFvC;%s{xw5WH~aiShitku1x)?GW6tYhxOVo&WBzpz?{6QWc#WX0TknT7TFbx8f=5V#>a$LXa0UG}g1F1R`JJ@%nlH7E zC))_{i0b@YmGccnb|ll#JzL-KoPOIY;Xt!CL+=py$EH8?-t+`uX2GR-UMw=T+_WN6 z3t9mB4O%Jlbp-q~(_z`$1lZ9b&$TOebURfcIk|s|&A?v?&1Sy7o^dgl;+0O*@9gZT z_h6hz{@wq2-#4We-t#3WahVZ@s|F6OFK;-QkH3hP@MF+;AT(M$ypj(c9)gZXSo9{I z_HK>Su5dX0L$lGf#N$|%r+LfbbGx+OAHaJdX|Lqa-w483#dX}R$T>vK%2r|Ra2Obq zP|T-q`I87e0lt+tY^srRr>W>2f~tto-Epm8v(#2F zapzvIBz*6W&OvwE9fv4Ov|+3?9g3`QPAumj6{g;9u|49%u7%$Z9Df6u)}M5G9pk#! zH);O4A&b;gQz`u7?S8$F(%9mi-XXa&9zdvl~DH056 zu%aabESG(}7}#F9R$gh&LOTpdEAeSbX`&WOBC1)kDVtSQ5mpB-13eiU)`_QV?V-$gTpeBv9kcbWVMqKAaW22q@+lf(-zW=Ox*pxG{%3-TXJq;HW@eScF6 zS%v=*H{D_mt}`irWYcC#x`uy2Mrgn=mf_TSJyA&|f#Rq_k$FIBd zpT5=$v~aP*F2P}o0UKrx_@?naw(i&}={1lwX%wPEWo>OxjTybQI=8_|WOR#9a$ z(DtJol#24858U{RFU-VeYt1$9$4?hPRFP70tJ)*yO@2MQUlR5qO{zK*%{D&zZPP4K z21j<|Imp_xH%qQB*e$9UEv>Y7A9?|6+>^b3b5?(U;hR~~{Lp@_`esdTbK?gV*!;z9 z`&kd#z9Y}az+<0>gep}>vu#CyFIjEOu{*tPw(Gfb=95etEfw6!4`Z~1cdX`lYg*wC zM09YlZ{mvByhzk``vcd_LX+Mp7_vbn!gK@x`vj*l$g8Yvrt3jY?Fd_J2|_jAvN>#V zeQz6nOy9iqNVZ}ydub0y5cw|c?9tLi3V282hYbt*3BX7mY`v`-ZSEpGyBWOdIo zIA4%aG#4=gYjAnEvG5^b+U5P6Y{a_owQ$gxN=(g)XhjID*?9HsXUzfY%I2>t1QNOf zQ^wdfgHbcfrn#!z_1kxdUfv52P4Xh%vHob){7;S~A3)FLbtEv`LHI4Lbns6kLs<^* zBhAIBn;`dJ^*^W6sprxac2C?qXh|z#!QQsh^u5}AR#Ss;#1p$H;rUHy9^j_nB zfAg#yNqn}uDj(kN-t_tTIG3FREI_ERI^EIUPzC5 z@;*8fd0aTR98DfyDYQg*UdjHGhT1&-6 zBdO)6q~BUtQBi`h3AUo>Oav3Y#Tgcx;Z}sR6TB7@V}Ul2C9PCO>3ZBWh-<`3N_+ba zl6C&!DpD41w8@k-z{5ZH0K|LHPIP(O{DVhtHWL`U-g(%JmylwWkbeH%NODSd_yt!O zUrCG$$f6?D%X1BNTYyAR+Q_>X@OhakXy9M=DinBI4LL z0j##ueg&x|B(s0_I3^O9?DuNM&)i~xFFAx!6~ zlKDr}yU&C29eQnw+eMMJVObtmuFnq*?QdpiwCj_M1z*x2SDSXk`HwG}50<){C+J0) z0S=eX34^bSr<}zPh(uUi{@Kc!^l*%+9C&?Kb*;nDd_0J4n78N|LTb?Rj}$mG81(!@ zh>=W~*R2ibd;4apQ$dE6L3cKMJ`zH8qL()b%eR3h_74H`DT?hXC5f$F&rZ_YxjzRX z&PCB*eRRj+q}{w8#iNZe+p@^xwZT+J{r?rYd>0uJnwdV>W{ z;f+oy`JJ4Pk)Rmze5<0A<1xMMxZrO)EZa7z>I0()wR|QC6U+cYulWT;p|7A`0|J6O^CCl3EGe}UjHLQ~-uFw^a zSvHt9fSy&EVjwU++EU$w^}$K+a5@uo%hM5dHYB@Q{i3N%mPnkz(MG--2zWSFS!RdP z7TW$qN1YgZFwf36KGvjX*!qy0soziXPp^ym<)2i`{d(4aGyl>%N+vsMj*fVn>5eu3 zqGG-#*dTRn(YcH<>rMUr)A^B}U75m+h^A7GuziaiSGpi0rHVNq9kmPXo%f)ne@Tdr zOq6ldbF3F+)~G4oyCFm0?!-IiBlZIXBozkr_vj12hLCwDi-ubG@^gYTZqJoQT>zT?vUeHgxf`JnvE+p9;ZO5W{8gr3U0iuqi~ z0kNx99gA>ig`5gs9L#%v7v4ozy#rie<=0R#4m#>k} z!RKkiIy-x=A8W+#m%neX+ZeC0{|UYY=Ou`lBCdo@4R6cuA?~R?grE)}o1mQzhID{s zuTMv4?;XiP0mbJkAah}6&!I$SEXZZicX()aG?=y@R3?Ok>Lf1m+|5~>P2nku^9rs0 zM|>}+qk9w>J3w%MGUEQ(L3^)$4<$_+=4=C-t6a~i zIkw5#=0~FagOy@_t>&?)#BsV;l0;CyW9C@iwki5@X;a;f9Orm%?}JKLz4YcKE(og^RXf_y|uKb2WGV5hm@pi7KkASqSDm?~XfPG_Ld^Qg#fPCDR<;?7a zuY=qD<=Doo(jB?X0WNQB(kcc<4Zej|hzt9=^N6dI_sHotl)9jL2m6fC^@2Kw8Oaij zIM*rqsJUSSm+AY$x-S2Xdr&LxIAz;4=qj~WRTazvK9F>XWy?>HjGqXA{Oh#0KfJwM zRz)ToW^7C*hgG18&{Adb?Ne?*g&S?j=!39C<*q;E6lFEaX_=bY_d^l8u>~DxaTu`P4I&a9J|+$fRSJ`4@PJVg{d? z4-dUCrhRpCWD`4}avH7o-1>UA%~bWBJ5QrkQF*PKJ!VD9s^7R1qqcT)1L<}@&-ml# z^edq_D<=8Q-FOpLQkE2&1Duj~6`9fGPzC1?zgU7}XcHF2CM~34y^w0qlMH>y`61hi zy?DR3mpI6+ZQoVg^cI@cYAu1OWTw8c=za2w?*g8QP36H+{8cw@5d{{AD6i9XJGqN zsq%HqxluM9(EThf_8hyOhK27D+)>~II>bU+af{|3dQRywJyWGD-YocA7kF-yfN+?r zNf+jwG}olojKXxVZ)@b59s;_w(af!j`2!X5$vWHae4q#8Qq|v|jTW=`yY!dcGMhnfX z2SwYr1xGYJNAg`ejWEMq70IQi$J{DC$P@QzG!f|h3kpv@8VSp)@3Y@T76x9H5irZ4 zMecD#a@{F@@oM^lT3hRUxLAR>ZTH&{e=1rTH>Ag3)ue?PjfIEB2b#^dua&Dz`amR_ zJe8(u-PpRlr*pHdEfUavzn>3z{bMOQ7@~P^{f(iR`RVl!UQoNR@ldi-Wn`~zBxmGm zSI%n#^NpcNZ@U}&?Fl|3$6`I_q$rItj=lk~bUo0*buDXYI}71f{mX3tHR8q1-oo|R zW=?r!<#5Hm=d1BS>g;PMYon)8UX2^n(<;=?+r6`?7FWC= z*yoRLWaj@ilR*|+c8qW=jvyt6+AP=(jA<4*npGbI;Q6zx*rn1hr%YErheB3!JJRDs zLltb8h4W^Gp!NG6D*$AL0NfAw+s^#{&g74gLwHVD=!zo@;NOMW!5nxjI)f~)$Xts; zM+LwmBMy|CcWL*4=ZZ}XBO|7~%Dob+0-uJ2e#>uRe++c@{n)n+>iVDbfqf||%qrA% zcdi|*R1(;AKB3rgaeuVC^{#m34>@t(s@b<*$z;jSu-Lkb+4$T1i)LRf-9?8$%vrzG z#T-J_)v>LF^%HL@IB4iJLv-PVvT+7}Usa{2_~-u9q`ELf=2&>fY-d$1_|)c{Q205| z&0`T4(T9$DB)volV^__k=Napk)tWyy%z9WT*%@X=<&?nO30~WBxv^~%L9+4?1q`n^ z1<1;*{`DdQcouK$8G56Funu{T`b1vfP8$?Htj|JgpIrCS+l4HWun-W1d33bOL9OfI ztutj(mA}jCFn4erpH>>TXsOvqML5VZb6w=T#5m!ZB)IBltbh%{bDHA*s)G~+$;N$Q z5g;>#6ou3%TVM??7jUn}= zTRmK;@90@^V{*aL>-Nq^KP(R!v3{^10lxN84reuH%CuJ|i=uM@L(Vt$`*+W}t`>f8 zfC4A$&;^XNfR&pAyP?pA=9S0!$KRLP?g1n*4$(UKt}nlbR3W+&{2tx|&qmEA+dT68 z!pu^WunJE+I09^wS1s}-^vNh&E=-|r$Ow?AbNp6f*XJKpcDqDrWTkuIW#Ep)bTgT3W!nKj>n z%WBA9Xbm~HhtgnIgZ6|Lj;Is;&L3V6(Mpy_N8sg>?vz!@d_O6W$P|4-gwHGj5xnO4d z7~pO?+`eF1XFxxVJN=$(LdHhm9@WV~ctdPjaw=>3(e8^mW%0njztrT#grQivgu{vb zBwwq`(cP<3K|F-E@_~i31mvWq!2R}_M`YDjX%WJ&`&$Rken8KSj#Y%p?aM?ulc&XQ{i@N)`N2y>7D0YdSDWh&JUhPGg6u( zC)r2k*3O-`SL%dhD;VL~#r|4mQ*OF z9le2j#6{ZkR2+(NcFzUC8|9%|Kkk}m;3dIVE?S*YnhDC2{KJSgW=0zsZA5-pY@5wR zyi8zp?5!{FXpZ+c{)-iK$vlQX0)|e5J3j9>eJOG^-@83E-)PB=PQC2$lPJ(xQ@Tm9 zbAG3DzJdQR7$+#v^YWfu@3>gIROSBa%&rcuTj_Z{&6J zr#+Kbmwf~ByXg>9)Zz|qJw@q_?B*kjh2uo#C6jG(uKzJdzdPd@ZXHPVqt2edN1k-o zy7Y;-b6r=RXEv?67wS#@#J9z!Z_3zbli-7hG@#;O~ZC(BssKJR4L z2YDBP`QnPFl*a594C=x?h%gGhY|yyu;?mYk@v;AcQb1uG2dBf6{BF=pl-A?a`YlSulA zd1wAIjpnN{DgT|IBajZDurQ9LY&tI_yfIUT!QGPU6qM8ymjEl<2rI3#vC=V^aBgX` z6@M39`-^IA^U$(t{u{O|I(x+WHbubh?d?;O2NIHp*JUGVmOCd3(!cpAD2>WVuRDFB zdB?5MmuXFh1IKHlr3M-i`o&S^C7xFg25N_EFSQ9HpLJ@Idy)Y90w_K**96i!l4RkZ z2tkS{S_L_bq)cv2*aaOdT|aCNOAOvn$Y!feX7o}MlDLk;L_VQAd?O{!%LC=~KA2d+ z@zoX}WX|Q}XiFTkZE#ywKi(g&|A2()B{Ue|t~V^z^!7LpY;OpL%uWxlUI%ul61{uM zDVpZhJ~|_(BO;jpn(^_BFI(qz0O#Bpr}ta!tI+vul*R9?-Iycb*U=cqQ+(~fkiKy{ zjTblg>1$o7TP_;M6h0#M04=6pG2buSBnLe! ztN**pL~)W{`JjSRN&+<2$B2Jazd~icOIhhKUNs5y@Q>gAtltnW2*l%f22DZimlx^} zVxohS;wyCWv>`=;`#RsUy~)3Jr;aJ=DYYVU0_-p8eaV!(|nmm zpbP8PR>mn1_3|5tao;=O<<{pMZpK_%2J|c?+hS)XcJzan0Yp|-tv8M0TVo0Cd`8zo z;;LU?a{&ue0E8)Ouoyr^U=3Lf>oa-&W2OkMFB$AzNO9T1VUmfT!!S;6`+M`JcXoPpDUtE-g;AVlM;v6;Xv{SBsg`6fCfSF7C;S}*d_sr{E z?qH_dKSsoXI{B4=`l)sB%)hmB(-{$oF{OY|&v?xVMU{%Q5O+6pwdI%G>R4n3h3H>codoZaVq zL`L*YL9Q^1dpw&vpp|Q7%|y+&OI4)HzGAXs*g3Sm;y}X2iXxp#!IE_o@r&Fn`naX>|rD zji!JTd(=CXD%Ay`A$W)er*_s9r>%}M4;ol}nnid|4~i_CcWYYESN4neJ96k&_>ypd zpw4TEcdxi2m!2t~t%-S-PXs#8DlxfC8b8(Z`&cBZyw)Sh#&8z!KW&y@u3u7y#6YK^ zgc%0;J>7gZmSj|I_w73H4!7MB(EYX4IyFbTxP>(TkhP>~|0dBAAt*Ky8U8EvRPX$i zdMw>)u!pb4-3eEHq}|H`jNKx-Je{b%y|mgNu8RI@ zwI$O}R=Q6p97d!+_+p$bkJh3d2`au=v#t5FUIqvn_G?^A{l%kLI-^|Yh)NJh_;cnx zq&eGDcS)~=mS$80)I2r=e+}^LHY{`x@(E$ zG??Xn7$95*n1yfHrFjcz)$;Hmfz^UKw?{+}L)DGOZoYlbmnROU6NNTfp8TgH6qJG# zdVjIwEDK4Oh!}?B3EEz3jIB|E@mup=Q_yQwe{P!oL zq{jC?5<3!0>P;rgls{CUc+>)b1$K=%3N?#;yOALC)=C!Z=}LGcNi*kqRf2i#DT&WL zfIuH8v2+8k(cJW_$-qZ7Q%+Ht)k@f(CT+pbbz)+f-?F+!{PA~qHT}}8$|8fu&QUkm zVqaeR1WWdzOStj(A|+b?MU6CPy$JBB&tN|gmcOz{@j&To;eC`LR^Gj<2uK1>U)oO~!;@$UD}mBZbd!@FXm(NDbu-0?t? zsl*2FqILZgEY>{!^j^Kq+GluNknSj=vQ!0sdmPoz_jjnV%loq*;~T<&Xa)D_>wydA-cctraPJnMLw6eU>n4Y(o{w$l16q!;WBNqT^+tQi_y4{g?}`Z{YBkU4 z2(kVi+8?v`{ntXD4UR*WjFQoY-yL{=%fORL>x(-SW8}N??M=r0rp?6BI>NcP&!iXm zbAdOWl{tCbw&_Xk1whfy(hhLnCe6XnQITr$d1MnrREJkz4Iv(68 zc=(@LK4~d+0gKJ)^N+}TwPUzQnz0s;u2o4uzfeFBuaR0{fi55!h{4l}7KElh%l~Xx ztX`_85hhWOrZf<&_PGuab-Iu_OY9BS3 z_!P>B%aT}GrxXezA<~5wuHXL$v$hpT>Puae>~F!Hqq~NqlwPY zvgPxu6=Igt<)bYVDs{RW|6p$a|IGCN=KudEPvq$mKQF(!+3tgTv^~=h3DwEh&Jp#5 zZf(6iyf<#2Agz2n@1IWxQq$IkK9D()qiF3oUCZ%7YKfLV&yWG1jV*KrPbCh)@cvn^0M~~zeL-28QpwqoR=5hm=wrhW>S_&K7b&5-Pwcc@vhEBq# zj1JVg5-<7xF!$b3O+8(sFp7eLf`|xGZS@s`yp4l^dIX?+(q$;xW zt3MB$1p{AflqW9d;bUP(s=&|VeN4@h?#mL3_X2mHH7$L#8jaINhLD{0HfCMEJ779v zOpqbL5{tuC4%)<}B-_!r4Sg7qG5B!3-sSt!+GO=;jw-dp;$=2AZP)Q~Q&o}aI=4KP z*Hi!knF@i!HO?cWalO+Jq;7F>F;F2Vj5F6T?*Z_y2c~oAYbY&%UP(o(R{dN+3DDWY zjb`VrcN{>&vrRr%7#Z{4T2IxvX#+WYfx62>d2m!{AqH3G*D4Pa1k-^z1;Os%5J9j$ z*j7-FS&vIlPeV^%P!FmH6P(j)L6rJ6Z|~MF1bsPZi`==0C$`7f2C%f`W5oKc$)lQsSPK;2hFlseS=-ii^2)f4{AZK~Qn-w?|iW ztaeaP)dGfv3st(`(AA|RsHnHSH5?gR5)@RsfVsmJTgIcCss?GzNVtxD zDzmDG-$3;-jxD~%*?C-K@dK&dRkQ4omDvc{SBzR|NAF?F1Wan`!eT3&2CnkSREIOKeFE;jx?!1oANn5Z@> z9q+$pm3&t8)*23~(3`G@fK)}o=s0pY6#|NGB(b?Syu2!8p8eL^tOBfS-x+PFDzZ1@ zMX#{aCkCLG2mvRo4#OSDbwZ?mN!812=$!bWBIo zCow@lbFF6S^wLV^9<4u*>^l#lc2bzI8UsmY=`a~CD~}Lt5TxUfdXv|zMQJO)>U6;-&w6A zlBss<;QaEg9Q8p~@b0x3b{V5Uoh?8Ol;}US3jhB7;hBR!P(r_KiFctN#G#}&e(k4W zp1{0#pvV@J|H;>;EiidZ!C0D=7HfBZGXrhfIiz z%Qp3E{*~%F`rnkUH{+?>ZjM$=VwCmY{v5e)`*PUjsKBZdwArp&!CD`_p%u;a^ob*t5LSH)3poyOK?Q0twVD z8B_mOXp$De?$2y2IZ`1{2ZvyUk(E)?-G5Hyy%_HBkm#g`e~$0=LKK9%!An2Sdjh9f$23VK|yW8pOmi37MB?qa)>j) zRC!WT`4W(J_T-}egTEZ_v_{3>7(08jRlgUBJ0i%+3tt;^JWo<^(-?C*E) zj`nsZ7%D@qkW_pB|GdQ=hq2Hk1QGxpYzPeKqTs__;N0fPlTbjzB;a&6ASkGX6*Ev_ z2Cb^9GU1}Ql7vVJu!QJ$F3OSK_iRX`BlUdtyD_=SfOHw7Pu111(l8zm5faLw~aA*Fcmp0}mv^9WW-?i?8ASiFhgjZ1LKr zCMJaUh&JChfaFWyoYSPyQIqveU>>PO0z)iO!)*9_tVXV#+qG{@epv4;)q2~&W>!FG z4MKFX6m1R<`@~RAu(Bf>e2HmE0ZAbMU$o0iakMuewP~GmO9)(2h5~i(Vcad|ZB3TarLi z=dJ>|eTGUgHwqMsXLBs(4@dyyOY?kMYsZKV9M?c*S}=LW>o|1HpibHcKp9}-mj!`#jT^gChc&6?6l@R(uZX?>uE}5{%xkbL#Ac@G)di6 ziZ9IwNA2Ye$hghzOoqDI@^Tm8o?-4fUEQ`lwY~3F!~~vg$4ss>?_gN8!stDZ^THNi zXBlrhhFF{_J=b^YItws{485td!-LWYWh;W$Y(IgrX^P7?I!4soWN%K0YH=fUCo}gV zH$`>}t@gf?t&9pqh<5;9Nddf4UmS($7q<>?p-@RfvD=U`7gaoiffG>0YIjXj;vQ&3 zPW}1LTnTG!arugaP38}|Lor+?l?xz`XD3@2xT8L12HHAvle+JCLInd@DL80O9`ot7T9vL+~iyt z|KNlw8LQgKR76M4BsT>`?Jhga*)FjWx9{In59Yr`B2vBX7&d z&3Cv2iG~SGLsH%%ld5EmLozi>jH6LH&d+Sj(wa_<;q|6-0FLo}1C-H~_cJZlXb|h$ z!C|WaV_o8_t3t>1tX)%sd8iq+G?T5&y4582&0TLGUrUNnAg%E~C$Y#_5)&7~2G4m$mt${W?koxmr0v*>|-YH6#~NbK}J`;UUs zR-PC0ALLg~7j2TBThnJF1Jgbbky=Xl^tGcPPyG)n4SSE~_dI`xs?wD4j1z?|?xh4A ze@@h!5i=e@zD+NnBBVbH+CAfOo@agGCz?wKvjknVHqvQRB-cqGD!LbGsVQDm^;amv z`jYQjQd7)@O5ZoJ?f$X7U*3#?7;XxNd~)cW9#%qQn?#p8Om?LGRh|d4<=Q3<*%#2h zf{BJieE8sWztlI}>dR4f`ck-=_ z=TX7OnrqF1joN@;4t9Pld7 z@7n$^W*D^&)!NaOFUR$BJFE#X=n?e+>>}JvT; zHB2IL%c9#TCZFPzHCvAr4K%7?Sl3W#xDMqwD0wY;e$YHiiJFXGJ@0RE9U1&Q?ZS@3 zpa{$s`TNIB)wfSgq>%IC8^k0PV31urm__ed=G3{n9dP7wy$g(JrB$Y%K#Vm}FtAg z0%Lm=5$VUHRBq{~5Wc6rbU>R{^r>pL4|vGwn1HX@XGw;yor(ooxAKaETCED1Hu=-E zywaLBR=7a+K?bM1FYayMQ~zQuIg1GV+GXR8Q0GF7yb2L{`i+(!eJ2qGU7d-XOJ)j^ z*>)7E-<&%z4gJTiB}FaK zIxmCJg{eK);fv$VJ=p4E%7$l1Inw!;-uFpQ8^D)}|Ou|`a-Z+Ir@ zS+?s~mDLk#?fBHjM+KXHn2^i3_y&iSvs%|DU36!s;5vsA9;14@RNISfd!F7m6fEij zf=>@C3o4g5wQ7jL|5ENR`+XrNKla?^*kqBLCXAT}W2$`j^AWmiGb6FmecmHW?h66g8@kU! zv=|Ajd7&-MZaJi@%x;x+Z@&qX7f0BC@wiZFHi56 znU)~_8X{|B(1Xf!5&gJoe;urNRbV3DkT1uPIb9j}g-_yt3;Do;MCM7&C(N#Hi-=w3;Bx~khz?8LWE0^lLG58ON24g@+J%! zwK86z{rEAcNdJdtTn0(d;R$*8_wlr`(f+aGDK5sbX(O8{uPppIq0Su8dyOWoZAO!_HX3chqoC5~)dbK%H` z*V?1WUuI#lnj15sB{wK?hw{3HX+@(!s(|OUW@l$tgTxMGj2v46ZkaOw59WLQE0R&Z zGu5ii-{!MjC<%bQOQAe;!sW>FvH;8^dJ69?c-tb>m}dU}dD&*m^kaK`x1@3uw7 zY8Wgv;7mSg`aF}p7?QBuq1vMG%Lpn1KTInR0mSLmU&}cbSgTkC9HwF=mm7!)cWPma z(1%F8c)`LX{z&d~g+ErTroJwrrxCg+k1|8s z9YVq9oh^xAm89Mukbo->9w!0&2O?~(cHUQJPc@`>e<2pPS?jb1QIlBu8^&ioMDs6? zPQ8&zym8?7CeV;gvjo379;EvRz4}Az@{tQWkj)9f*9S+7@to>yBwW_5{tL_&g!KmlmQYFEuJP{5V<%H!84D8A?cYwIBuz_QUluDRAcCN?$?H)3n1 zN^!K1P>KIai#bsiXPW;*@izd6CV=PC8-M{_(bw_-ZR2YKzj7aovNceB-q_nCtAFFh z4HqC!mk2dL3S{KQ69n<{O)s^=~j~0RHZ$042vOqe=smF zSX}-0T92{}I=?rj#v4aNB#hgRvNTw4#b+$_f{d^OQt-P#^MFM4lmTiDKaQ*`U0xazDSwNK**A zg1rAF($S20YV9U-Q`G^bxc$I$mz(jy{mFIsJ~L8DlU_oTD=U-5J@(88_4Mibzdp4- z{e9C?rrs>BXVs!_vQQ=4?nmDB3hb8VUx z^9W=jZ|wo*j$0~O{2~!)g=>Sxriq-^x_^tc+LJk0vXYjh9&?@sJ2seXwV4~?NzC$_up`LDA{R58eYmC1bT6Mf~t9$D$D? z&)>1MHJFYN@kdiHFVpQVeHg9K-rY0o$s-RR#^w)?*%ERF!(B{@Eh!`Ek_sc0LXCH> zz9&+19_yehXiP~Rc}6iSCecZk0ej&?1q(-_$EobTM(;>n8e)& z-MsDPWVj9xhXroDn<)B*s`90JNET1mHe-~(w{1OJky|sH^N(ko_6=$&R@lMiR-0w2 zbvY@6q|Db&GsCy=X{q-el{VKsGl;(Lca|Q@Y|iCjM?f3ET_}R1b#8|jQdUQg@4&Lz zp>nzyb!)Lsf)(6Y6PKl(QsXn=gx#)f-jd|PVZ*$pou`6l@ihzUcTrYD!rDzeHI3_v zDPVUc@QZa*_SG{K1}AH?$|)bVCb9VHmGRM14T5O}V#>Q84F~N|ug^8<5_kt|+{B6t z^PlhKljT){Ka3A|&mYH8Vx49eY8M4Zu?uRHMBVIKYLrn2TV_Em*+MgK8=M<9S0&zz zX;1VSA5tA-{ltqMip!}OO2R&QNbG`m?<4k>Y1dzCdq0|@Sdn>3m!IYQfNb3y;~OlQ zwsB5I$CY4WcdY` zzVzh+4{of<%BQ#2>Yr7+j%y9G)@vp|$kJ^td$hDD%8~7Vi#U=jgYzz!RB~RgLoN4D z3l1gi#23X*2b{gWsv6KR@03bjl2K?#n%?Ri=lpafHcB$3eCL4y%(y9J_t&}!uJ<5e z)bT>2R>0W?M5KKN1w~}C?aSljyY@A}IBy8{+JCcREMWF25L0 zVI&cGXPl7~EQqMeQ4qE`6tCTDZcP7%pR#}8k?UFnUEnau8_vQJ!$y`nZ?Vm$27B|u zriJevzr5PAMXOM^<;Ln4!Yqdb2#s{$DofK$?o=YiR!&dslz$^$ZWiO=?K6F{4j#bn5;5ng46%$z4L-|+w%q&**|JdD)&IiMtfW)4N{>BB{=60 zNxxNPm)eycj@wSY+0boD`ARm4$UP+FNp4p?=~!bL;JwBGYVNsd>cXN*9OUx$8=79( zK*pr%;sUT*Cb@NG>9m?k6<{5*AqhCNDd@CIW8a@@q@BlN)?ZTGG`6EOh2Nmb(l;rE z@3LqV&$<}oYq=FU*h_rKkbh7P>S`=>H6D*0K@(#~$dU+c!4^_O{ko~nzi^ZVY+vOR zPlx0s$>s|)ed@cU;UAXk1Mo7qaFH$tqJD^^?KJ%^Mm*oD^|2zyu_yAv>=kXpTmj&S zOK<-{ka(_{8^NW+v`IH7My_t?!=bjM?9D)3!W&Canadr4#X!j(ODTDL6{re=CIHgFQ$QKLsep?>j1C-TRX zn;JI!+?&Ti(XN|FeAEzKp;0xM6784=z93!X&2Pt?&4iFl5dx#nT$I zvEDO2&9bnIdAiKk9dU=o@cK6#igw^1W%y{rA++_@9&8dHwIXa37Mr}W(Q6CqG#wfv z@XV79>*~4ost{T}z~DIj0@z3DNl2BdgCbRrF&?r70qYsLTmwa5UDKu;KKWG|5Ogrh*_ zshRQ`n~G$(Guzc1fihgc2quT@W}zb}v<}V;-185R_`)TM_Bl;)2XJQo*({*YQp-~d z*hv|CDgwa5n2CI=tKcnI2j(FjL;|43Ggz1K4^_kJ;pCzO) z6E4Fis8e%D&ZU$DLSTJF$ZsNV;9Hf0F^LVHQw?w}~9A zhnhc>cN>GHx6U8QD^wj;h)WOt#pU^5Tqr3LYFy5X4h4=-8+*OmB}=R9aIve5NM>c* z);!vk>Y1uf;N(ouvTD5Pp3k>(*W)}Q0HlEWXfBeGC**becy>GlDLhs@+@G**&dU@| zj+Jc5MZQ_7zqL7xuXV(sru=5hBcx>KTYI6t)dMBqk3Pw}j#El$MWZr_U^Z)z9AB0# zS+eX0F61zB&bmtf@F;SuTU_C8*5F-7F$E=0y7iCez4^WP`wt(Ie=3E}>%<;?L-Ht# zI%IP1EP#)2rPFNwFuB9{Y4!et>Xa4DUWoTDZ@vCQ6oqYVVb*VhRCXLFcVm_R~mhr zY@J~_BbM^C9g-WkVWkTbZ+W@kzM>{IGZaa#*wG zFIDV_Nm0WbTCe)%JX!rM-W2>J2*n)l!>aQdnFfjP%-};?Uo!(=ct#rc!f$`g@YW%F zz`3N}^2BWBAs*>4@9zqX%ATUQ^73Df(e5VZ0kAP`pl3lzQWxyXZa7Er#i1RP9h-aN z|58w>0GreQ+7mf>b$DIDGFy==XJIkWIScH~3N<8XLT0C5KMg5+TQc`x#7sY1kB^;> z;`!u1YgH5pa&v<3j0#PrmH=xC@V}w~BkKg#nH!LoiUOFw36F08%Ty=NpZ5N)VV=C6 zw8tUZ_oobB7_hf`^6^FQrPINGeq4X^?+yBY4tT$FebJU{f08z5vwutT05(kl_vsd(P)~lm`meAL{?UTK|Jl!e1BwT|qW2$y_k~;! z<^5rJ#h?5IIr;MU{Pj1NWNz}s35BuWf5XiHzRvmLQqWRDBGdVm+~yyl?zttWBLq)h zHhE%uD%5AmT!q;{UwByVL~`UNNu+T$A+<|7r?S|MIjN2MRwOifVF3)YNJtB3;&YNA~d}s!sE*7pH2Ri*0Yv z$d@xf3-mHv*`dI*c z;FymtBgL)2jp(^kni3Dq2o3+PBS}l*q)AGpO)Odbj-eDa(Mf2nvC z!9Wjz2hGpN_dR?Fv#@}_3}M&EIw5t@RY5oQU`e3y|33pLgHJx>QnV}oDaJi7}x3b^l8DC&fgaPXWaca@F!_JIGj<2Lg3ZP%F6JFh#~F2 z&}Gk_J$vnLfVFixc%gr|NY1y}M*jpe21DJ;f60%d`}6wW;7;`~@_aYSR)TI=V^sch zOyV_?Wfp)G!zLw}DJCWs<9LGJO*q)l(6G}hB&2ogS!aRVpQ8V_g#GwnwsLwsAo33? zE+0OBzN4@dqYM4>div78QU8ao|HwWdz`$ZZEB)V&T|4Sn1|T`lG*oV-r=&Q`|6#2m zAu=-ZUgpluPK=}2|A1+e1)2d(CXT2~^=Hq7fR$wY<-gGNR8&+RL~}AT7bf{ti?6(! zbssnWgS<)no6t}kKjc}H>dE_m$ke~-`j6OEU0DXgddB_!$f;rf577tp;wySU3}y_r zuTKY)@&{o!$iyUJD?KXM#C?+}zw~?9ovWW-|JJz_dE&51At9$!bRiHnu2WdP2PUi?*?WK^#L~ zS4Rhqdrl~($-aJ?aOIB#qi;Wc{5YV8E$#34K70MY(Dk3;EOeF#gy%?b{_U99e$ndE4fXVY!Zh7@ym#?Hrb5)jgn4^=`!e@m=oznG zy^3a05E0Q6-FoUg%F#SqUib%-BA)ZKv?IoI{~KNXZ;^h&KbR#4E#JESmoaU&(eBQp z8J#g~Y^ao!luoff_?uP~74czYl$7TEB>!O=P?Ovb33)v@XrQ3+r$cDn4y=HlXiUB` zxgJRpJ_&ida`o!Uzj|w9VqL!A#Z;KoX_Q?NVg_CO{(k=en??ASbc+axfVLVVN3`vW z#WsMb0R@bjfOmfxoEC}Frrl=Je%YFvy0*Bq^cXjszrWGEi8ngI z{w7>NfDW|P5ZOzTD6VH;F3Ab}C}i;ph>Emtdv*3f;2#p*gtH19n|U-uc9~j(#|Bd) zmz0&HrCI7{ec6EE)^OmWY5U!^$(HIr+TG-=$xPV-gek1V&$?qtKv6VmN!hmcR1}@@Xf_Ht_%*CCD z+s(`0?i=3PDN;F9y?;O2;jcU>u5^7Kl|FD$;{eG8p?Pd2#tos{rg64~dK~ut7Cjux zf-AP{bYWYr)&gHZJ3w%$9}rjjd&~;rUpo(5tL*w+s6^64HFERtEC3gn#AbR063`$I z5@CE*rvB%*FwB>ilmP?lujv()dp`;j(yai>Yae|{A_l11d`b=`llGQpo9i>ZRwpWN zjg{M!U}x$e^Y7lgIirB}T`jA(8%UQ47;xKK=17-$ASr2vYOICK za{|%FgI?}((2aHu=u8fF=Iw=RuYgcxLsTQ+Yp*`k(;M@0r&qvV+CY*+6tI#GJy(RR z>U2n_!W1oN;_*{~ER&Fw;vmxl*Mgpx z0`yPeGs)BE;M@53K?UJ6#6oF0nOj{0@XO{Gj8Lz z5W$O2Pa{Si0U`SUeOL5SWrtE{12+*9E&kPj=M2KGln8i&w${3zu#@tKo2vZa}mJd z0{{*)*zc4m0{*F0sUS0t)>mO(l3K zH%2SNi7dTUP&qkPF)A zS>Py!*v3rB3)>6`+P`DPDfwgChMy-QMeBmkm*SF(K6a@7eqqDpKdS9JE0b}FpJfNy z#j;~kV9v~=J`!0*b)Xfx@z=aG>^IAi`)g#GRR5P*_J3pG{Cgt)f3k>49%XPBPQOy# z`OeMNi)}`T=$?fyIQ^ts&d=pWH{;$=2H^6|thV`~#xzv*C9$$9D(B$LFB?ATen(fJ z6+@-#eCRF*sAYpq(2G7D!xb)j)r*Sm?Nx7j4GY1Lmi-uTw+CyMi2l8?)i@JV4bA=! zGICUe73+6Oh(hLs+}{&seVWUWy`oKuLrdAKOH0~2UscVI>Qg3M#&?ni^UJ%ery(FD zWy^;zm2~Hm##In=qF+tbdh?BXswuBUF%I6!<$SeRXg5}*!d?qQYwlaehvV-MKvgY9BS z*XbrvKTCpbbAP9UeY?@`f;`u>@6Q`@*%w8#3^22+xgfpv`3RaV;2bg6i*bpeGfzXm z?=K!2>2)TSe8qu64{?E9g?6ql!79~Ua#Udo99$t5;R13xE_s{KM}&_A^PMqAX`lD! z38Z)EiK>u`bHufTC2{DJzHcv%wV(Ib+@84v>@waXW)EaXw4T6v{6>SvuX>Tj$$!mq^pURloPIcu?iGY=o!S6@(j z5B7aO_O4iTUJNWWwDXav>~i(u=6&?6xt)AwJ!YHQ_=gexg z_OLw$(WT`B0ng&qN__i647*F&L@@V<_~5mO3a@GPL~LwFdpOMzR0arG_=m9`7GJteh;u9jxM-|tlv@0 z^cS3x^rWI|izr|2r=&(6J=(BJ0l&u&99d z(G|bBrZ|;td*wch7K?IDaY|16gT>|_PAy0CzmSoczXo}e)?L+-=(DQ7lrZCbTx(Dbw&hXMdG&>I9e-~Q|GpB z@<308Si`tBEG^G+NSs#w(V^sBDdOg6J*(;TX(e3h2|xR_sJmLwgc4?ku!PI&hn|)E z>ON+0`gNq3=9)AM@SV=9*TSTg}x4D1C&e7q=>8d*=X*)0lMI)T2 z)D3>U0)+KE6f59GUHb8=oVjH;Z}NK(=W99-U@d&zzMbXLLt^M0H4MC6|iI#(;Qe*j3s2dGqp~;YXkR zo*DZ*00N$rK^g`AB4)g5JwB^Bz43=S&bV$^6RSgkaZ!kE%G;cbxE})TA_Alj3r{9T za6;q!wXf5WH9H-)#}AK(QT=u@WyA7g86zjxt*C?wlcaz`d_@K19io zrnw)j^OI6}VYu7H_rO8Vu4O@mgWP(a%_@Q8Pq~ZR1>nwt*}u;vziIgBF#7XqPi;Ip zp!CRL_}+-j@bgEKc2nKkrMVE0|Hp&ISyW?x;k_NrJ>;sSHfJU0^r!-x}kBgHDSNPKOy zY{7in$b*m~`=(2B#xq?e-Vp(lTK@dD#{mQlAw6|dDAOi%O=skF8y_3DXRMkDY1Tf`AW&0aj^KF;&|Hu!hQ9M(+f(7Kdr zls-{i>SyJnh;Wb_o>YT2al2&G|0HT{a$*0M=7VXK_RgsHvl@{JATR^}t?BSbwS8BG z3t&supxVk{`zH;bLpvizU&HB@UrHyl^4>=WaJuD2j=g)|yApMZR_`48S;eESZ@Gwz zUwP(7XQNS#6~83IWV9F5{SMFD>3-bXRSj#sF+ZDh`hDP0T=GyC&t6VqIC;l!3p*;j zFt}xNQ$oZQ3Mbhkh3GQ&IPzxpk`;fVCw$ym7R^T3SS`=4!{l)<>yoTVOODv|rpHQ$1+0%Pd&;NoGUeM{MiS2e zLq%T-UqEO{OM(w7xwfi(U(nHP8~!kT;!c|dxhD0tuiujGdvh82`|YAeJ_S37Iriyx zM@8LuhXImed6jDqFD>r&aACwiTUDR_#G{0*g%W zB(H6;!ri;OsFZh+Xiv9F&gPY^PxDq@!ysYQ*|?6W*u5D9YJ zdvo9_e4jFc;sWhjnJWBi^vf;!X0hqVv9}C{y2jE~A*%FN>kaTThs4Z3niER~NW?{WpF= zVbR;dDC`5o6=8Q)al_kmZu~o6QuLQ%yCdHyOxu7u-vwDm!|YNfxO>}KZ9wYLArEJ~ zdethQ_ulZ&)b#jxK(ueS9KR}*|ClgJpVY&B5ubr4cMSYq&K9Kq5@s4oJ@n-3;Q+Vn z7Q|{v&!u+mgP)J*bKYmiB_#FFWrcnNOq$ICY89ky(?Qd)Jf8c=jNL}RTtttw{}(1m z#cju>87=u1kf?zo?0!ws=8(%UUF_E!L4~~g!$H0u1R7BS(vW+K{q|M`@64Aj;?ts; zojElvyrnJFN?wQr$nPoKY}ik9W14oz7K`fzA9UovbO*?!14ddh`SAxg}k zQ2iNPUcDJ{ceDnPgn3hj(w?Ixv#G{VHn*16^cM7HgW9}XArUVWu-uOMS7h~CN|ac5P8b`jEcFr6e61)@uLxvZ;J25aWivPeH{%DA7I(|RHw~X^@ zruxWt7^>$x_3A<1z82DGU#M_ww|aYLqT7*LfU7H?X2FK=z89-slSHQVf4QbZkL)U@ zA82vA+39Y;dqLRoJxjKTw>;%1qn;E(i@@a)cjK(usD~nS*FWW&F4WC+d+N`&W^xLT z?+g@HEZ$2yfZ6zC4M7L`g%$01C;ubgtq}crpB&9r`Qt^|J4uvFt5`MtG~IT8t}D5q zJ8I9#pcq0~LE=)tT5iu1P43gduUmeD9NIKUCROr>vh|uwLMSTR13RH8rcCw9nzrld zU6ltjE3t~;rEieh+&z?-Z8~ll0V@yUpUX_#Hu`x)u$fON4I~8A-Z6t!rD>MCsE&nVhJFoHbWqi=hMEu%c}ihY zk9NA{g<8PDh=l;}uVU9$I#8b-3oFg8Ju9*T=?iek{eW(FU%h;DZ&>~9W0wqCY|pHs zNXXj5qiy0PZx#t^u50sHjmJF+a_ORGYOi^mR^BJHft22AO7%pv(0{hz_L~Zv2ze~G z0a;7ogneNp^qaKtPUMemwOK04d_InW8oqF4SS1hw?<^eSscXKTUGv9s_|-OCfY6}K zQaPslv4K;8(;klxNlw-+728{35}w*FQ0O&>B5OLKE~~1Unic;HXjXgyV_fzyuIr+C zZGYo;?v~=v%$H`v5^L_Cm@>LHG}4&K8!Dm|_n&piZLYP2nkjf>bPd=Lesu9&R)5$$ z?HcQFqm&j{OVexqAP}dN*oF!*3*dAelQrPzeB00azW#SWY!K~zUdOX9QQl__w=gxQ zZkGRMY0t*l_#MUSF1FJ>)ASPY+!AqI>MnV7AuTgpL^@o?c&wZ3-J_Fn>X2hEP$ISg zu0}uoZ6AwNOmx`bxv=Y^F~nq3J{C_!i}T*k+8-}u9$YDCo7275SD~7( zDwopbe%z~Vx>RApwvgqy*r?sO+XcvAkPg-8#od`l)GUPw^KZX+H6%CrI7|rNs`8TM ztFA&xl!P?+Ez#qV$EP(2pG0>@9PtTUk7Iz<+}=^8Fv{CTaLU=ZN-5K6o)Z<$8$sbyo@CW-e;&|iwHr>9>TIEScrZ?i7vL$%QJ9G zJ7}#Wh@B)CwBc?+7vf%{E!>@`x1`|Ve02o<9&!2n`q#m)7U7jcC6{2Y86VUb-yMi7 z6H3fdVtPD>(n4W8Jvf=)N8l9a$t~sA1$Lw_x{fLzBObLta6Ks?!##|shvZR%-PNnC z{c6x32lcR6=e?9~Ppe4}LA>T89p;)>hn-=v>1hIi+Dn9(`PW}vEG?AgO&}>PEMq;u z-3fLpyeSjA^@4G`h3-y8L_c9~Dp+D3GZF92N!FnIIC+kJ7sI>QktU>%+`OeKx_Xq9 z>j#>C=^VcKOT%;Ia~l8rxs__n;QLE8tJ#jipKZ!E zw!gK!)5B+E3@=RSaRed+iAK6QuDK3A2Lo-FV1^P8}utmfLo2US_N;iWV; zGP)kk)d4Sd!>DRn5c=9e;PwE&3I^@iE_lwunv3Lasi^6WgTFbPdyO z_9n}_KRrg#Ft0NB$J0dzw=aEnht3fM+GR;qk_+pX53(X0vutt-1y?RF={pql$(O$< zrLXt%N%ft2`6QMm*E4qHq4!EIZ?FIa9Qw1>aYMB|bUpcaRB~}3>n);QQRtfhY_YOz zpsgxS{#aScj0BTR#30;Rq~R0N#{|S$5w2IJhZWaWC&N}*+HUSf|L?&4I)`9 z6-FO7QVJ#=d}rydgi!qozWQ2meEF=yWF_t45-uAR=jZ<&+WsT$=IB?Wx^=DJkk6h6 zoYI!FkskRmOSY-y!IuP~Fi?;19AcJZGFgm7Ywd-|taw({UUbh(WfsiOQ}G0SHZLqc zexpOu#{B-&Or$?oL2j%)eVxTVu?M)k(0OCc}HTtUMlx}*!U)EY+@$#Hm%gp z_dY5t!`7(4%2fTw`a~ehF>UbpySDvE>vvWU$024SU7Q+%GqM}96+mdl z*E`EROeBu>bZX|nATag4U-IGh96d?C+@>MNCU3P+_Wcd33m_2Q!|YN6w?koLIJlSsqOjPAM?iQkXw1JVp6)W)vAU(qo*$Oz}9wqqga^l9l;_p4~)t+9P(RZ5H!2 zLuK}BUcY}-(p7=0Q_aj)E44v9h(viVYk5~dI;b<-5j);fXQb}q7sXqAp!VrgtsENZ z+EE#jowe&PL^ziwJY?w$gBKBnKOT!d4hbsi8@v(?MI>o%3|IO+n?C!d6V;{t?PzXe zcLCXAt#iCWi}9k1_2en+J@Oq;)s2SryvOP>S1M`Mwm&i3Z7e|3Cn_g|t#nib+?lqF z%%Sy-u%5-V?K3rxzR%Tt*udfvZML8ZeNb5o-8&x7OFOd{G%#z?>pHz*Xu#u3cXitZ zd?C4B{n`8;V&g*21FcE!cU)69jW(m1giSOGc#|%CAUu|7=SX528S1ARQkWL9fr5lz~8tIfqK)SmZ5$RZzMtW(Gr5jcnM9L+X zSURP_g@uK?eBX`dKKK4R&pYQ#oM+C=FXq+m+i!{^!!jDOKF5plZ}W)`JK3Mw!avM? zuD|GY< z(|R6@#1oB&ZW#>0)(oi2B3YHo!oOUm5*Bf_`6q9YN=+f}8FUHqG5n zC_ubVnfgn1!W9mE8dQt@m5e8Rn->tB&T>l8B$jVq-RRjGGDw6>irvQNEBI@&cr^#0 zZMhP6xQENdALI8XD32!Sd`a@5%58GGfi);?76q8bmx2`tX1@lML^=~Sq%Ah|*HnfZv4W=>@(komj0)L~P5siRCcl7lY{K#&e&7Cm0}bV&xGUjzz4H(xw>0}Ph&jEUR_?w2FA!)t;aK4Aa1{I zGgkd()mAmClmGK8nFFQ9j^rbT3rt9&MpV%9%_hYsb|b0TGR{9KZMSyrcURUZO(KJ6=vl0zbU~V1w&R@pLB0qA=g_L-kj1{0{nz z%6@e2k^Vuf1T<|b_qV2(Zu_M9hJER_J<57PD7xeArS$^c@mF`?kpz32v|{gtVDPoU z3m)<+&odqcTK5qq_n;YmzT?!uUQKY-mw}&~rk2ne7y#-Zdu=5)W@?hHGrW{r@RT46 zi)gU6J+Jwrm>wq^2We#PALd&;W}gV?+8`a&aDMA&CF2Q#YKL>6BPU5w<@|+t9%L}j zy{q}4z+3h_R1bGZ(M@)u^-8qn2UWzNDrGD8v1K8lZWf-Y?1cMz0jad3sh4H=g$iz%8a>=Nr>doSHknqnWcDu)wz?{TGJz}P+l>X;H`2ism>Lcv7EhauXi8V8~ePE1*@5{R!V5S zl7<}~G1#5=Kzhw{c)u|X7Dn2~Z#B!4@s@=)rdW*@Al(*(Epk|*(U{4p>Ch~1MZ2bd zDbYeHuA(}KCy6Bzo>3sm1tLCPfQ7zT_t2BORW51NO zua}3CnXm1yiyRYvx!R)UWLl%6`Ev=T6JRM_UdTsm>}srk9hqcwkMvA@&Kx(?#Lcfa zKjN~l<|T$eYt2RE+|wif5wtu&az8aeL{bH6k)IQbJ6-%n^V9GZdW(vx45 zl=G%)QFx{jrp$Y%*6j49d;)nIhY#0*JlGi({FOpvyc`#F1%-dlokIz-dVU7*D=5FR z{_xF==Yr_$@osw-r-GO3cbI{cm+Fe)W>!#|d9J-)y@wPiT;NQ5v??)%D)Qb@`Q5ljB!@k^lpkpFy^cKmr zdnT5^5~S0$gWDcPzx8f92b@yDr*nBOM1rQ-hU}kbg_^IVX{<1DVwmgt8%I)?iz)00 zp+yVM+}1~GH)pnIzPI>f8N4_7nFaXp?WbDa0F*3=l;ovA!aDiNg*~1J5C9TLFn2rX zhHQ;P90_E?mJxY#*52c-TD(;*+C&p1Q2EH=y&WM8efoe1E4bj^Sp(trj7?ToYGrOB z+Por6{1#R3WI!pRU>0Ev7kSO$fB!W`Tcw22>)|9qPGXM&Q1(vOCm5NY{$q#vd%ix7 zpAkZvra+7$k`P0hKIm;5lbX zB|r1wN4MWybsAULnG1~KL#4F1is8%B4MJP*rfM3?OP{oBAjW^$^K^Lh*;)0twxyw-uqoBg&%4Y4QII%#v}Ix2pJskUcwV&4C} z;Aathy#ZA5;f2vAmaAdUqAG$gTA1?3chyOEDP7A@e2f@*VdMKe4PAnuV0dV}dCr8V zZnmye>GfUU&RyUGM**t!8P;DIsocLcA2=)om5Ze_-u25wF3&v5%6RKpz9ROn{PUHM zW^Wr(uu7y&6AHWbHKACir`m2=H)O*}xGc3x#GF&Zm6~f!wztTv9B}6A4ZMLV%-CgC zt%d^iDLlTBGp{8bT1WXkqEnL5)g=COEme@XPM!it?BILk?kuCF(P361Wz_$CW#1}% zP*r*2{`7rT`HUtoBZkeXguO3x`o(?R;vq7$uP`YvU7-VgzN+o>bC_P>@`i)GAaWo; zlC4V4N6q!OqjZ$)U(X8%MB0lFs{W7Y`ZulTk4BAY-7B-KS71rfdoRnC(|Iu6gQakc z`&O!V)m7yIY6UC-i=R5b1a#JVXGg1QeSTUN{9fnuSmoL0qX4owq?JOhrj>UZhBKY; zKYD<@2z@Lj@6plidVG^?V-R3P1r+fHnN@Ju*x-}>NypwgAo{`FP!%M7RaLf~nG{hL zX1tbJ7~I#@X=P4|t1c0h-W#%&-OUSKD3F^%3QDIZ zUTwl_wB$!`v?ErxtT3VUmO5fM#%jMO{R(-0;r@fJT||r~{?kUmvVvV;&xdyZGfCAb zx2g?N4^u+S{&tty;5!EbqILG6GH}mDZutSfjoRy75eI*^O(=bML!|Kg%^u`(PKjAh zzQ)%H92|q&{(y#0KKBGRBUcazx*v`jL+1=R(T^3S6)Xd3fhN+_8qTnaiK3O+2*XoT zO3#!ck70BQ*SDRQdkOE3AMz+Z2?|H>xJjz7J=|SM2%KMT@d?1;*Y)ni$Qx7A`d%YK zgO!7i$N$pI7PB0@lprrDaeuNk$({47|IyZire#sqCEI-;Z#NT06Gv5RrnRVmgl;Si zjn^t`mak)Ddi73T7BmdJqd_>yEKZbdGZt*6EHn5yY4eDEiFB>Yj@{NC?EAeUTXR&H z^PNlxys&lcWu~DQ$>_Fq-+K!l!Tp_9l=4e{z-c&Ub(bax?WEr$Eg%->z@q%HEMD=g zj_Zop^C}9%2S&&8!uw{*7~|QI83#UM{MDlQc+9(Hw~#Ot*IHuz&MHma?AC~Y5UiOd z*itq$OlLxp?Gdfo<27)xY8QP#t7?WJugf|M>3(+8wThe5d;&WEI9*5{=NPUBT0KOD z?@%{YD?HvnU=H=GSv)E$`QcPkLv10Uw)|rJ6m8~dpa2YKSPgIZw5xgbBuDtSq4x!6 z?`X29ZRW7Ey!4Js%=)_nHdgX2+dC121#*ZykN$pY{QYNTN0$93U+{5D)FQ)IWy46yL z3>dIDn*@tY0uy9PG`E0&rf(ltSve#kzC&0os^L|flPwibiC@Ah%+eH(ud}U_G1!k7 z)u;)l+#!fD+mp6$MV{OtIcdsP8C?ESmI#qsR7YP|szDW{@-p{8Ah7 z_$)-BkiG6TybX)y{oqGohP_sXoRNq#VZEf>RU3uxH4aV!y=Ad`iKz5l%&k(66uPYr zW0V-_pEM^n%7W?y{wT|wl&6H{OZtHmBXm^2QmPJ5fYCG>F|rVjHZIP4rwPcRMkfcZ zQw&J4n!;#Rs{C;jM<0@98^nrpUp=hDJ?~2*ce?r`ie}2mX_6mn!L}Nd_FXHw!Tb}) zI0;>Xaa!3pd-K0a+Nc2)^36()X6Ktlq;T*yytc<#7fM{_1GPg}vMi!#YRQ|Rb@;x5 zQ^Uj6&Q}bx1&3Xqs{-D<^hkTsqqn$~cYNkGYfVGxL>F%^l^*^^L^@U#vp1pdA6STw z=gO5W-qq2p#(K)ae+1F;wJ0MY${lFw*LwK#0vjQ&Zm|YNT5KHJSk6$k@nUXQ`JH=d zuDX;zeYz$CC|>+4>fsVZBS4C`QhsW_`n^q@>93&`536^{70sEM$73@w58Q%n?CB+U zYN<@2hruet<1im=MI06mbf74=Q}>!qXW#8v^gZ?c{wm1&UW*D};Trnl?-DZ6*9`bK zY-|UE5|#%`QK@6r1xiIb3DHr57nBN%4U^4oHN($@%k3F{LpgiHUCp}9Q$>1~ zUGm-NF0rA(HOU^XWfJo>mb!|BnWq8=^Jw!+D9*(?oN~7Ky=Nv; z8#gmTKJEC^)^4V5@WtM(*CbU3yZC?gUUQo(-v1i=?Ot&By4CBte(7a(uvlX9*3IoV zSk}ChH)n}oMgZrC&Par6F!m%)H@WnlcY@W$S2`!10oVM2tM4AEaVll-f0hc6hj%}h zo{}KjE#j2wDGWc`MX4WqDw%E1lf^*)PwZ{XWHGGzZPPa*7qN!~UtL4@WpK^X8`L2i z1E?BQLglFzPbFi{*wI8GbF(B{QE=oME$KwXgniIoNFZh<-Pq2quZjEOD%EWc@mhLW z#VAGw$#w$08KQNw>$CIESX0IYpR>5Hu{b809+7XJ9WJ|zbdV)X`T5IRNz@*Z$nD5? zc0~3nI}%@)^clE0(o%`MxB@8zHLEE|w+w1kkgY#Ew50`{L+9q=UaSM++8R;_e}R_s zNW{YPlq=Mm<&lX8ITwH&#;0NW-z^gbYowh-qt^`kRjfIjLZOBG0w`(;2~VtqVs;j| z&;48}X#xcitSifFp@x^m-!tRvY$GhkKYdtWo*DA9pa|Nug9&d>?%0@=08GeyWLGo& z(_4&if3J%F+I!w}ku3Lznh$}V6W8dMW^2F$nuKMXP9ZrAMS=X-(6+|x;{Z2srypuW z?!qxnFEJ@H*ExIqb~E*aU-S8#sA$rkJQAmf2B%p4rln=4a=j42o5PWebaoz^-b>`l z+Awuk%E=!Ckw?f*Zwi*1O9nPyVJQ-T29W4oLJ&!604}UeeK3!%x%K-lu`$ueq$0QImLA1)5k1FJ1kbMbyB~)qtG?xPn)LN& ze;D()iOZiQB1??^`8q!Re01jQt#!v8!To)6)dF3`xt*Yl@sbbD^Q0?79eOp7hPdf` zFzJO)OU31VoTv~+P&!-2FKhXX zt2RDqA)Y?ms;QW&`$^q-&*!qK^|@B1GTuH<;fV@j@s@~uW~ zarF^Se9=jJzy$tv)Djid;pLmKiK3(plNZy^nmwNAR7_@Ht`-Z-+)C30Ax@w8W7X?o z;ccMfD6fgJ9kr3fbCzH5D zHoo*DIM4|wO4?lgcuxXv={T{?OEBk0#f6&<(VB}#MlCcvCjQ#}14K@yjklhvM%T=@ z@<%_O1OUsNziWF*tpjxMhx69tg$uJ4kgg9goSi?qufSG(HJkI}atgcwu%JS3iYVP~ zyMLsK#jk4l zda*AwkLK(zo-YP~o-LiH!~265pv`Y@Zm)3qoUeN31(WuYB|XN%dZv76RvbWVlB2<; z8RYA;e8lGQ zFqYOcDiP8z{KVyZm);SnZT6(3!ieZ80+ey9N?a<}xSW);F(ykwZw<=umwr>UfGt>K z($0yoncClwme*5NP$sw?S>(nnuGk+mvkYGa;s#bXe2D12qfzvEW$UCCR>1;?PgvQZ zB_$i-^XdMxKED$xQ&`+~!@Ht(1?J~M^2svluAVuZ`4BE-MOHo*;9~WS`4x|J@tc2 ztb$6$6^eb3MqO{YOEkj3NVfyF z1!TV%;oifbIuEhGqDCvz?laW;lNE7Rw9`Ldb9tRD@=7@%mZB6G%%F}E`T@2t7z`;t zGi{8HS8aoVN)GSvU5-%=7XKpuGbTJGX_mh`=Fj9cAHo*^M2y9ML#@VyOCuyZgTqpW zkv}GO_2b&APPBi8_#Z@diIg!qW29HZK@H>rruWD;nILgb*4~Mfb#8L({RZ*^TFb>x zMiUxa4b{=DMYp0Ugz(%`novd5YSw_f35mzs%k~mr`5^tZz##W{l3pp`p9L<)0~clJ zw>QOgalNnX=l6v^;Sbkr87_&KCn+~b;8|1>5{uFDFliSXlAdbzrHlk5h@o1i=UlDI>S3TT5hy+n$9eTJHNgy-%Re(7J zvp1T)gvNe@N}fUmFb>*D(91sq@D1bR3HDd;5TZx#1m$5Ir0dD9802S4XKID;rnOh> z?dX(gwwkM%gziTjhZicRJQL*!Z2^X9RmDD+HBh05Nr{u}R$~a3=U8kw53u`9hSJIi zwKKVYmhSw8{>hP$>HT~$UGdU(KPNB{$pj=Gv*>mP4 zM{2oiQuHkjUf-uL^zuFL8zeB?Oxlaz9%qi7$+JLv5NfqiPrfRIRD+e9L>|Y`Qe{_k zqN8QOydhy>i(+vg8V8RcW=17W&{a{2`RN9S)Zmy@hq)(HkD8UFv69t;qQ_# zAy>~BHy2+3YTo<<4LhB9>y0XKlPbm{e50x6s>|%k5o3B0?I+J{qp4_Io<4NpMyVk< zbYLw`g<4OXm<#9ST7aPj;@l>)PAdgzCbq7YAC7NC8O!NtjXn9(0y}2k`f~$nWUh(8pF|}SAI;*tld5LLgF3Ej1~JUN7Q>)ehEr&JJ?N~iKagG>c?_S< z?Ir53*y_77o~k|UyQ=CbOWzZ^9G~|%^&9bpAU>?XzR(c_yk?YKRP2xY$A863=%J8&luDe7X9 z)GqBFCF_#^i(~O(@HreKA^iTlMnZ1K2Ps6ar0+rjcko$n_9D&B%X0N!co+nX>mTe4 z>S!`%N2@-iSOoovDJ_71yo3J=03SWB|L*4h&Zc*Zjw%(lz`$z5*BFR!vH@pDtQ`MU zhh8hjSUSaahxcVXNt@|`i3q|xb;@J;w&(7QkmB9 zb8vhzYPxJ;GNEAuC1uRWwfr@Mq{ravjZM;vL-c|2gknju>rg3qY z#(hlxXE&oj3D{6h+=&-mQRfQ~&E1Fz!z z1FxRj<4xOS@xhPoa67ZNHe9sRLhxn8@6P*}0h_+F_&vb73oL;jg#?_~f*;?y``=GI zh52W>U!PR${-^*<3qQPhjM+foeYbuMfiEj6UZ*-AMMu5J`)@^h~B#qZS>Ay zFoQ83`IPTkzu)uE^ZfmsweFg8&g^y1+57Ih&)KinJ&`(ED#V1ega815SWQ(?4*)>Cm+S99Zc%E>!|w!A zz3u#cf~;k~tECM*b6tKmmfnU)f3CI7b~mrPqEpg-i*>(}J*W9s^SLYBU0rxu<{Z?!h;Cb-wQqNBuBaI1 zv|iwFi@m9n@xWmas|NF0VfcD_dL}Xhv5F&U!ulQnIZkSsk73@JBALwG!bB!umQ9bJ zRm)n`-X6JuQC~bb8wI!4unuaD*n?GR_$;TY9?sVrf$$_TU#xL&b+&k|+KL(4{`}!6 zc3zkjP{p=iY5d_-KXxU#5+AR$hGvOmHu6Ye2x&awC6xEohd96=3z|K)2BTM8Hta15@fEK+SJind$>KGwQl1oaD z)_U?Ak^t+7p08_aDs*qMdeUDB>m8ztW3U3c(C zO%#{0=g{=G#LtI26?kKlN!Q3&kMunfA#XxKBz17>6UY-2b3`T5vI1pXp2z{ip`fHJ zpu8W1YUQ1WM`10q$Q`MQOo=OdmVU*;RuL^d812S;&jn5 zE+G5ynjGBvz3Edik1Y;$O|=3M=Xv|}Rv$#6S^|fz=b7-luWq99@$HoMc3tZzu$JQy zQ1Gj(x24C(jHCJ=u}>@6-pJU{AO(v>uU{{Z+uNS(Ep)@mWdi*NoYXj++uG0@35P;H zE?>Ckwtd%1ih3P=uqrXs9r@ki~VCBuRNFN1Do)FMdUJFu;k8kUuo zY!VfnBwGg+B&}R(39d_y5z|g5aHP_04D?!VhHYQ&7S+DL@*So}lQPMe;CE%dV;uM< zzk4CT1Hh%lfKGan&0dbk%YvVYkGsGj6<1|NA%qW<488V%?g#VQ$tufAmrL`G`@%k> zaEH@GE~}Z?CcyDzG~a;jNt(Oq-Mb!TH`zU~m(U(f3A1_R>+ z_2LDuwOm_IcFJ0-aA*fv7QTNI3=Jj%aB;;u!FGPV!5uTUa0|cazVU&!{$Z6_cM^hY zTG!abdaJaTyAGS4_*Ymo8@Y6)J4Owo60D}MnwkuGH|DcVpV$;Bg}0zTfb)8eolx=q zvf_B#Q7}|4;Nl=j-T=4Wf!z9t5CDAiajts6>+xFaPRvxsiCUpby!pbE$;p+%yFny{l^BR75Y!gzpJc;L_{kk)GSsI0tt&@)qfD6yp|bJO_gl(y_d zqA?L~8#*sP^j35^g>5Xy+Rw5J^A>j*DBAwwNRJLW=5xsucqSj89_l^V6)RqL%7~$J zppSWTng~^AL-TR=m)QqlP7;kcDy1j^Vu2rqQ!1o?t%s)I0QZ+LmaP}#SLaNFUb6W~ z?BZw`U67SAJHc03zuXtvLU)Q`>@&fY`LP=M`j`)kf!kL}g3B2>g-^a~-s?!FQuguqg%HH?=VQ81lAGjaJ5{X}>p; zpT}>S6RfRsV>sCf(#0A1MQCv8a+a?)RY&*P!C|blBwRGhvii+K=eg=s>{*f3bdh#$ znwfu0Q^z1J#-rS}*KO>06_w;)K_2H7V@4gk)|6X)-?Ul`%{Ngg!>zwEwm02ugg*=$ z%H}ctd4ZbT97>+C9vnT}t*z?-m+7B8?5?CG)n11#`8$P(xK_t=Mu$TELJ(oc4Bsov zWfKT6(CO*Z$T9?OXxW|tS!)ljx43J0#V-2o5doaYy_d!!LisPc%eznCwK zPLn_;(O|`uvt|YQ3lIC{)Bv!8lcLd-aY?k#9HDWx&=6SIJE)xPm&-x{X!2zIIFQ9K z2a+>auF=E$Jf@Y&d4B)1xaxWM?L|jF!A`3JL4B~S`8H@{RkspKhs4yNz=S*i>U-A< zwQwW-HX1x4q{Q(Xh~VH*^HGZl3?6iQ9d^Gll4fX=S@OCm%}mUy!B5)v<+{+#kabqB zVVSWbXdUEt4Wuk@8te`yqAK>-PM@K8@Lt;gVj3&l!eEEs^)+L2Op0=M#a2uafu z5qq-I6l{y{@p-`O`p`u&@HFznVuugLPX@x9%OtF^GMv=B;jMx#EAQVBK~B^d8rH@0Tx)4!x{S{N}<{>w~Y#rn_KyibG%j}2$Rwf zv*x{J!+TS9LYa6DwcZ5@KJljjh^fvTVM-F1Q4P8lv*o7igr$ea4EtB-bUNyT`i@a; z<3nRoBlN>TO%9<~`GQ+qJ1!!w1M zqU&CQ_x-GlLG)LnOI=_1rtYH9EQ~Yb7%DY7bx1Gu=^L%EW4`bf{)*%^y&RKlR>cf* zdnL=& zwSDKAF;AuVFHc=#j9X|3%F&8Hd%@Nn`6k;lS&P z8*xa!8=s;-G5#%B>Qj2(uix5QzJWLl>Si?rx1IQYl$xegfEYaq*|q1debKHvv*sYg z31)}ppGH}SZr5IIXvalYhwy|n0+a^xRRW#n}MDXL#3>iUx5;X z-t#(Nucq{o10cTc!hm%x`mrH67YY5dYr;|*f>xl)QdE&!XU_f5wlCgiJE;CkAgHWeCSBnd^oYL%mFG9xyw;;%JoTy3?H8ohOA z{vt~k%CLUj`^FN|DHZP}Q<-*#0DW)-$3oK)qnEXeE8qZvH9n2JZe45D=_;TD+&wI z%>AE9PmX>guMr>}+s;+V#}#I~H)YBAd8@I_IU0Vwmmy4|r)sPzy_#JS98U2^O&xlZ z9Ak2{8296zV9%GaQ|PrppZTcTs=&M&df4XX1#bCZk9EnatW0))hDE&9=NPKyk4f;= z5IHxYYQgnAzAA`>(mvk6%hTZR^Cx!gwFsvc8snbko45tS7Z%j$hg}5w*u#1$>#2(J z{q?b{)XUb&@R1J&mo-lu#o%tjoK9EnpV|YjKM1}1+WK9JV6<^*R{zO^MQ43q8Im@p zfCF9UjL;)IqUD1Ae$~C{U3OR-{bwd=|7l^S1$HDM)l2-r&y&6_9l=t@!UCe`&CA@% zNOztgv?dZ&zW;V*liyY{23|KxfE9$Q4jq|@(gKihbX&JJKDsi%ZBCJ3Qx$vRh8>-#)h zuzn&6S-XGiK8c+%g4A0n#XoSack7WI55(etBp2x>?v}IqNnYc_)mI@55JBBpAIVQk!~8Q3ZsPuq>aN@5+a*d!=X5?z7D0PfwEs-&dH^m|&m1&x~pMrm*i5aPHwO%%@^vQcnv1G_zYPC}~GNIM7hG+=3 zVnX?AL*}Vc%8rh&D8C?%+_GIQ?JCUbE%xtWS22U_8=pW}i_8eb9#UVv@>cf!={sY< zsP?T%GWtyTkGDOZ4i$5u8h5)%o-naw zqBM?dV4Rnz5Szy{AakS?F(zxF^o@wX;xHaO ztJ3d*yUmjqoUhBtsij;!TUR29xSGtCS2%>9(ypA4gDM-k4Bs4NnRsYEXq7-pRbd&^ zwdMuX^^sdw>9);FPuGurHwPP0MXW-NJ;U5il&)tuQ>t78s3AC;j!AU&wvgw-YCl_4`L+OdV&#K3zP!B62^yb zA%rYa+71S>0>(IkApzFf^uw1Cq3b`Io^Ee!j0*=nA^ZnJ5dw?YN2Qlx(a}Ww%E6sd ziHZt`gE2_bMy|DG_u-pM4?YR0OjPWNPy`N_3$(i{X4l`+1=3}nK@6fXW?S17ak4@; zS2?#f%(wNJ-V`^v%}`g~?{bTi)h_pAtlS!euO{0kiO8rN6N{2$|7l0$7Lk^of{db7M6mGf);;fF5p7wgUB(Fs zm+IL*(bC5>Gr=(&ILaONFXz)Jx=oDUqHW?@F+HK7KxS6o)<)Vd{#oo#AqWzzS<+8B4YPgWB1<-` zGkIbfiGN!zKnWPsSQ%@=9lg0mO3-)_+;k%6+gEU+zA$Ncv>UiIb`Vb=WC4zauykWx z$e+7;AJgy4IaI_qt4B|y$2QBz!Z)`7bBkjTh9H5C_G8=-B5Hb}bZFmx2j2a4)B!CU z9rDu>sCT~~YHe~(FSzgHyV!!Y*jYUnqzY@2#x)KUIKm>KQrO1E#lRYn&(1fChMS)B zA`eze9D>y?H^D3yk_}k7>Ar69%AO3+czGvxszVwtm1ZVnIQ*g2&m`THm$&S9ZOa3kOCc-`=HQeu`7wgmGdG%$DP*!k57Gu?#r5yP{7$r9>ny1{4km^adec< zl#KjvQ_?}>ob|({ZOgcx%Mj3ieJEUT#s0>_)_Q%Ue(Mda!_t%%nM%O=faIrc)v`&} zL{gz!MB?qH8wgg**(CQCgHMV%>zprp)F{1j|MPdoyOY{UPD^q8n`5Bfi@(`}wXQOix@tz5SgM(fVlf{G}ivd9N#NXZuNd zuaDmwC5JJov1O^BKKA+`FMG4^3|qw^JF!C7Xz?jB zYOS(`iSMMN?rCQ?PG>hml4T7FY5mA$Lyu7)usZ0HExEEy;8koADe&p1PzFdky!_Ak ze6Z7@^yhuF$NAGB^^Wjx6{f)I*CbYXu5YYlS(&KM=bJv;uOzU!5+w)yskCgZ$5#eY z$tV&`$wuis(4_{ik8h5rk9b49|vMMLquZ#ur!~Sf-ou_7@L3L21g z@7tooP7=TYNA0k(F%ZP`cl$lJ8H!%#A5D%n+3GFe+uzi|j;8oS;j8-sRDdR(-s{kX zm6Q79bzvDkYKG`f^1%pn@=U|0@;7agIhL&=j*hFol1(^dR-pCpoE(?kNwxwyjKTmv zQ%4M`jmslj`X*qAsQTIa$bITl@~75Sj~lzZNc`GI@WOr=Os~K@;wxbv_GVboB(yLn z4>8bf)A4z4cziYG>tKJfs+ z^9M54NWb(kIADeD>XP8^7!U64Y%~3>JPmt_{&x@A-F*J`yu!Vk^IiG;Nca8rXr}!! z(chNN0E5`4Z>0~^V*drd%dyqJ>1-{&_*di6^Zy-`|3AR(|6deYrT&j`byls9WvfID zOZ3=|a~yG%ECYDz4}wJ5;;q%dna#yds3o?}8`P+5?SH5TOatX5+q3k@ZHguMs{H?9 zi@8ozXGucki`2lsy!x_?JVQ{Wkl_8(i|X81BfC02G@zYqXS1ZEls9ajtdLN$T1b&t z%zC|U9Hbmf@7;OC0g8}AeONxd=lMQ~&VKQ&a84L*GBhB;%L8Mk6>u^n-EKL&r>`w06*MI!rnuNox%MXRMrL%xj9E`Yc^)`M z&7#zoNXf!&9C&Zh9TjD&28;gG+Ga4m@_-7)G4eRzMs+GiF&#m*{w*aZsGo1U+x($F zHcoB1v@q$JUpb=%v$8*fej&4fe7>tJ58-R|4{>5Ofe`9eAlb{ms*0zZ79oqLaf(z_gSSk72)HW-lSKS7`-F=7r#Glv8rTHr<%Io zbG)oI&+nekSI`Z;UE*SyZF$W&ul1;J`URV90yT{@vQ?`4S@f%}I*%zO<5tA!xL3>Y zTdZXAcTSf3(VUkWEc`6GxabOj4T?Wb2})%zo&0yhch6$4yB%0*N2GZ!oHKZSNUHcz zjf(*+(|MA&(p8R`{^z&X{viE4dHPJ zzlf}+0yd?HvVql!>C>MC_Pw)AJH_X7LklXd7miAn)_=H?&A#?44vIC!FZ-=MaojcY zefpcLWs~Qu`?Txq{0tZ2A(wM%e;Fpqr4g%p6mZtNNH47S+fgN<>R?FOOZgB4 z*#z}ncPsIFMpaa=>C~*e^YwWVzGL~jx@<3wnecHabKXT^1%P$l@TBGO8mFe`>-o?Fal>^HLa4!iX z>S!Cu6IA*A51eC5-;111J5`3?K2vU?U`|>L`I$QYWeBCQ?vOVsfns+PoR>>_z z&Ni_v^oL&=y97v5j9 z%E_r0l#Q8Hr0LgP9XL=0wipJ6txO-uCy0Qb6+QA4N8vj29=m+nToL5tRbAFnKNLxv zHArc)A#H#8Mu4Fjze5j0zFyo2t?@ItUm!(MrTqGoROc$&t)X4h3li}8*02gQb9s;U z0D+@0uUs{%|K^Y`U`T z!b@&tH#~r{w#})X%ml}77F4@vaa}1^VfW!)6F%T`cB0lX*dr^*h)G&Wf60H3RgPJ! z3iR?AuzKe2Umb#nRg7@Bf{P>^(u%&$W(*eDvHv8vWKZX_JsIFSd~WX}Xn!7bQ`|6b)~nX;npI*FR&Qb^x|RGor!RPo?Yg*w@}#Q6B1Yv2nH#&eOXS})GC z&9`i=JUs}OK#3!E$9a#2z|Ylo)!P?k$$XX=Px__lI*yTs+sDKg=s4{C(hzI+g6*7W zaG`$HPgo0*15s7{bDG=h-XV8fm?R{{Xl};;ZNOuS zrbo6hDyz1nqWfYL82emd=5}(}$!{B)|7T#Sq+q|`)0O*`=1rIuMFFGXvfq{arms4r z9Y+`BI%n3%f?RhX(B6=zbdhhPFi0irn89@qR7r)^NF7B+&9Cv zXD7!ion^eep5**6DqmxhQp59|g*h< z0-4I+U?h9u9ztdDdkHR6g=<^X)J(lu=0h4I}eavVGAl4a>xZ4^E(3 zs#^v+s7P^OTexUWwmssjJ5Pl5??$6{LsjA)-jQcvs${dzP4invL$`9WgRqA5CMDsq zaX(&lHWlur@1LJlD^QaLCF8VCXnK5h`8Rl-H7vyX^gXzb;WsnV`duGI%`3j7sB%9X zX~Kf}H_44SEBoj*<~cxX_WOp+NB!KZ>MzkgyyQ2m@7}iG3f!0&tb0Q@Reia$lRNPW z&q6>(Km103f=YDde5C{=*|U!wp@AfyYIdE zS$o)#YCxx7yIZRHi#|DLHfjJs06ivIryd;bxwTK~;0U#weivqG9M0hI8{bKG4WkE! zX~u(mCQ3K60>4Lir6@Y}HrAh+yFADvAupPc^DC&85WM9%b%Ygfe7sEj#hH~w4O=LT zJ8?+!fmIYFXRV*G!K( zt-Cvn99+sQN9aU5&Nt>a=>A@?IrcYj6|Kt|Xoxi(Y6T9uihU9Jn5yU_vL%!ByW|9X zD^D{E5#lS)$x#!07e&Bv-OY7AV7_$KbBiBAv^}KK4xXyD&O^D|+VAW@u4<@fla)#9 z5l@9cNO-Zk@eSXXHg7a#%hF_k*1-yal=W|_*sr-UkU2g~KZQp$}Vk(7fJO|C4Et z(huz@c5SVhE1p~LD$RSZC`nZi%st{G#s8>1=Q?I!{jICDX?(#})C>dSllLW7H< z?-V_vP+!|!vW+xnRFe4|_WSm1-=0?GFTgLq2!QQ1gx|Y|HLBKm>(ut_2pVm!bj zLiOs-s?gt6m74WVqPZ81ODbHoqLZB&U@oUalfAdsTjO1x-Tv?UWi|G}9~FpOV)C?O zAAhb28cJfstayQo|Ft(p& zS60&6#1T^(KViipPV+i}0t4-GO8aE>6b(J!QaTL>2x+|aqu_y%WDb~Ko(Ur3RYj{F zN0pS%kJ)x6i|n`Q1c@4cNwp`r%k4!9_>J*DR35-Kbp5+k-uRzpI>>-`41r3+PFAj} zb}&8Zu~4fLB~hQV%#;rJ1*>$m03IJ&%eb*3HtW(cJp0qL z$7u*RfVf*~r4%xB8BtAfmehZZ}xwA{%)6d1q!pjqqoX4on9wgI=V zT%5A6vQpTdze0Oo%d4I|JoNXzchSS>-h>+aBh#Hw?LcEo?w0jB@4#>8_@oDgmV2xK zx#H(Zl`=O|pBZUWSMp?_i9TUJ>y+A?sv z4wb3gRRo?g-m?wZuk;IE)SLo8~C{GF_K?WnyH2%Hmg0(YH?jhicYG=#TMDBjePj zf#S0n1E%OvXj{Vvm~r8A{vMtT7sl39tqrSL*VBx-a>MoMmW2aYqNAk;YwXQ|v9PNe zI;FvXX}CXkz`5Kxx^#R+cyEjs{=+g>7w}m}FD?X>YeveG>+{&z?4V_1>d$$+-Vi&8 zUBmp1c0FTmGSjcfCl^`;HD%|V60Uyu?YH}fLcjO5@_4snTA5QdW?>s;*XKXHzDBus zH3FSA9z1MlF-orV%!}7Mt_i)KDU#b5Ppy@C)%lgzQ)7jXgwtrATr8?6;v-XM&8SeX zu_3fwumodtGnOaCTb87pM6F>pB<^}^*7MlSjcH^n8>(yU?|v!1cX|b@J9oP+Bg1>K zEp?+3=owv*xYVn^9rC4XV?T%g?wN5KcgpHI)7BjbrY*A$qR0VUx83*me6G3imQVJj zwsal$>%S4zac08pON|AyTUB&GOpOikA3#+ysfT%%k~4jT`2x-ws>Q;e@jcJoa1R!g zf)NeUbug7wW;=UA&z^|vrfQqZz8H;Dya#w*bxMVC`du+>0QDthJ5x~_^Rx1 z>6UMI$>)CAW)S+u>m5u=z}?vhaU_u`;kCJQnY}N`==+<)g;yJ{g-}YT zqT7>qbdnmfxR0fo;2QA35KYg#xgV~QwgXPYdoV-rFt1Kc+a$9qM}c>1=O0g zKOtJ)C%4O~2@bW;bBj}>skssnLpv6N$u`hj`&{MOhI7_y!OhYhS)pEJkG>4r1wG_o zGnM2zZl-UHzzPX-#9hIMhF5)-laz!BybjDVw2d)CPW3IzAN7xibC;|s+C!!rII!>h zO|vVJOE|@6%W@N`E#7tFF9{{VZaD?poVW1|fY0isz@(CobM-GVABxIt<;R?lbNT3)M)iH+#Vtf();`m<!2p>G7adllA-!% z)nIBkYPQMVJe*-IKB8`=S)@Pac=6N z?MhYqgEso*!a1Br&&K&DSvt$@nuCveISNR@tyeDSh_QA#aVEMplcM@iOV5ZI#F6Yy z`62>O`d5-7O>f%@g*iBQQ z@1abzgbOp0qL!ZZ!}~iDmKk~=-co&RGNV z&L9o}x43VAMmRoPu<%zig3DNxz~S4|25|M~RhdJo)01;jeBv!pyFXJiCG`|DjInE- zM{TTAa3>jYMfNCDQvvINbq84avk9?bJI1IdT>^)Xc~jQ?8>I+Rs|X4^b+ym#@H$^F zv{=7*)2n>XxW{kZ#{9rk_W-YkF_%PjEp_+wFAYlKWlG5{UOk zCTJm46R=K8IIN(RN^pQ90DJUD#bljlyEJ%NzPfLPA^#7-JCeKXPRV3QA!2e(dOP6~ zW;}_wxd6()(eL_~3(F4=mqCr_as*3|>TAIU~1;?*RL)>dmTEu<6J`~ugyUwicRkK;VKDuA~ z+Skn=uFNmlTx+G{p%^I>WEb*d$ms%)T4j;?gzN(~j}C%$FT5wRJIi~UAvLSZ=xb=@ za8E(;Vp{fz@!?)g7f`8GYp#6_F4yY96{U*CF%aEL=O* zJHPwVuU=Q+eMS5p{g{JyR z57+n9-246ua3o{Xo0m>ix7!_$N1R4)Z2U7yEwAzza)@dKH zo3T~RfTQTsp3ct~q*mDq6|bj3MRLZGsjQ_DDOS_3`0f&`RnRf`yM2Hj&&LOKPaeT< zcGI$WGU%TTYm=e8oR}*`?lY(~HMR6Gu)N1wEo3*mEGqO#qLPcaOPU6wWO3s`}PXxzO?VH{Un;GtwU1jVxD56U6+`$Nlejq>4?RzCUAxZe2)eJ zpbTqllCZ!tY%9N5I})NqCyJR;FQY4|QT@*DOtaohtd4h9<#7~sFIs8Qb0tf!JX`schK+;$pQ_7L zxh-px`wV$iy`^aT?fCVHPkv;*BXjHfRWZ!oyg#IQpZBD{sj+9l70TDZB>h~=moh)Y z<~HNm#483+2He+_`$YjnKlRkNtZX(te&RxbFzA$G+KtCyyH-W9@k#yC0tXR`@J7Qw zTq}!r+odQmbk%RBje%d2L#dYmQCJOUGj|uVbEQx|&hD&f_G!Cc^&hB$qB)lz5%v)I z5d#3~Lm_yGpRgcobow$F?fMvzqlkHv!B{I_1 zn%&|s6kckeu5O;_{uTQ?So#C^Helz*E6X)=cnPAUmDJF#EC^N0R$)9x@%%mgbT^r-XQ$NjGt=^f|dCp&ZZsgM9aX#J<1^_q%lL|*cCWuSVmxAFf zFk+4fksSPb)ikA6AI)5nub574q`z^IndbGl?7kn7ZD2hFAo~L@^7kC@xN5*J@C!v|MWehzkVnAUrulQ8}ENb z@cK6XD}pPOi1XiDeU`laJIdWzDxd$bt=*b8uKy)||8IEI&$k#1(|^uDd_|D&D3f7SWl$>IMoO5*Eduum4m;+uMVnzGK! zGICnc`qch>+^N{{oLA_uDGk^2q(4H)%bp;rWI5e}%U&I3R}wI-`1x0o4_3qM+kCXm zh+qAy6C^#wxL<|M%L~h?ucZd)5`SUh_QL0e2!Ys7rM#ulo*P{yLUrfDSnv>%WZdE~ zO((q6phTTa^LnCtI?kYdPx!0@IwSStus%;f4$DpV{`*l6{l3AxZSDgM5tiz_pwti@ zrjORCN3aZvyNCV$tu;A{;MIee;o`DH|I#9o__axN#jmaTL(CHC?mg8fV98hFprd@M zHO`?Qp%>Bn-C(KznoM{47fj6mwqHp&;Vs#D#a>|f9EsueU~~JXbI>=5*!jDI0tv46 zU6_$N_gx7Io58Xq-S4ALNdEG!u?syZ>^(z1n+D=`B1kA|I?(VBx9oML|7G(^7OEryp6cPhu>p z1p~fQn~X8vtz1fjQK2k-C7P7t1fo%H(z#(>WNiMg;e18Z8#RwoIqjNBpElFkZbH~r zWu!JxJ_XUZ;$WYsOmbH?F{nx3$>sd?kML(oX} zcRVIJgW5y#GwoI!`!U6|U*cd+kl>B;)mz~jV2L%t?40XmP*H$s|HA8pXhEEanY{G* zo!yOMBhBmN-*i)%f+BkU#sm}L!e`?tGgoKoY#A<)l4fkprKV>s?Cm41ilde!+EbNF zl*`qPOf>yD%g^rH z)j!O!p&K%_(I+8EDw6uG6YJ(t(BJyYqXv5y=qIv_d|MNH{CV~xc)4hf!ins8$6pci zLiY3I6xvkwcbtTS6^S;TVjf2Fb8)BOSby`#U$)kC`sw=^3-*cSfw9R`_0yfnTTebB zi;-N?5#FS)Wg=efrbK~UsPUjH-D?V)cW+gnfL;LUkv%~!T(|C{LkT2u51+%gZ8^30 z+RyZM&n^;tZ1{;cmEHfnOl?4z(^R&h7IGzO%f$RzRoVq%DSKu&V|(9YOBD7Q&)TXS zvrFAn^&+TCmjf6#8B#QEETGA9>oUT|g(G=59@n}UNEI;XAx-Jt&S+#$pU;MyW)_~8JT&0TErqm|k{R%=QqG$uX2-!`^^6&I zJSDKE^V4L{0KN2LukD0_Fsb{ir4IKF4$E9^Y=RkKO(gOBO_NicCZux-onb-NuDZeZKh($LYr3y&hF;}~G>K*6O zf)O^o^^4sYEcbSWAxo9dg+`7q4iwC&_HFfFM7qW8&)pi3<$_SWKiyPmu`O@*dc?pP zeUa$n&CO$}1RwML7%E@}WyVqXp*A;Cgl4GbwZ5NKdTXO!+LE|EX}iC^K5hAjH?$uw zA)P2c9v#0#ud~DpuC*3B1C{Fy$+0Ub7q&5HeRAp1_js9kr#sziIgU`HqLd)yqJzr- zYt{y8b^xg$EzMJE5TF0^W@|E-g;Fn&9!Y*M#shW>g=`$TFQpG0iWFo@HHt8_q-f#R z@)p7L9bZXD=^T310nvk6&Giy4is*(}lQdB6=Q@3KlY1tr?%Yi2!wej#x4kBkP`mL} zlA%d6Z1(F!{m9h;9?@~rS|G%|4Kms`#uuc%{`l;?igw~>7OLqDrMqR)&{Z6rn9sM} zKd-Hb&@F$eO0pVeY#oY4@*w{h)23BLdSCKopC@423l|0?<@aC1K0+WD=>Z){R6emUO--Wh3YK5B@Nrv{L7k{P&I_Q#hg}!k+ zMp$Qp_(Z(0?ZquMC+*!`q(#xO*KtvYNCNxw8HPxjQkgTZ@Aa-=YWTpfau3%?T$OU_ z1`+dgK2fKv9j5hGNS@#wQ`qu0vEk!hBIl2Jjz~9GC{Hh&@a+Di#C+7li0QU6{3z9^ zN75fk-n-JVuF-FK*+g`QHIP)09A$hIpGjqvp(W>dx5LIlQ0YFleMi@=lA9LhxG$1B@LQKk>TNS!XGCv(v z;I4jUV4roQOW^*sbxy@!!{ts)95l%Wryt&Et}j11o0hUGAG8bEQtkX zggy&oGDX$PZVSU>Zk9hyiv(KbRZ#Fvc`kFl9BY{jQ{?em^v4!1Hhd|x-8J~KLr%%Q zrd&C`@Nv^F>McLUyfCnI57US8H<8MK=pIP6SPnCF?C*XX($$bnV!Jq`Lu4lVf}~;U zVQxWlNvwSv;%x0@K}Y;x?ip9~NA8?aCo52+^j5|8PYbuJ9T|QFC+!KHvx6y1PwlMd z%UNkJBvekCdw%f->B0bcu_3RK;*sAAz1*TQ$W?lNbWU8kF3Xw}EXmw)Vvq{Pu~ z{Ge-HH_vOuiKKYpDZk?JbxZS~n5+zCV)CA@F2*Ojbf|&%Zjv10BNK)(KurAFvKu1$ zFyj_~_p4~i=@AZ}M zHhFg!d}WYIhwpR8=hU;-pXGfKxb-W~(0>8pR(>`*G1Xj6tti>ZM^kRuNPE#{{g2Uh zc_8Gjy=(sqWsKMoe*nLbg@EGn9$3-@clDf~A z-WxI>o}Hj7+7l8kdS(EHIpi`{J8nF5jR~rWWo%YUcRxZ%E2gOAE|gEf#v;W=J(csM zMxOJ36D)U-IlRB*)v~+t{mI3I7AM(h*xJx+A6@^7!&kIiZ0qcj`zlft4deaCDpp=Q za(53`xEsy9Eihs261sg4YWU3K$om%?cfF*Z#F)k_v`U|Jm1PTiZz>&`HThAhcbyA z%{VI#1ab)av!)GPaSkX-U`B~ZbqD(V-PwQb!|c^CEagL68i&U3={LW^R(K@P^z~F8 z-g@4^yWq(a^g@xKBH2&2F^9fL>;agb!mw>uq9>8nTZ}f^BUgf-F@sf7lIQboHA2a* z$@#6!)voNYMUNIs@ZqOD?i0kOgHsbtPI>|Ly}tC4d$oqr3qM#`EjYE+oP&Et94GR- zzH8_s%eKR$(34#YD-7H-#nm#P*_Pl78ww;+>sYcaSQcsk@LyGG8@%h$0&yVi>18dS zMT~Uyy%(V@7}SO8?~0{~W{YWmILOJ_MqxzehpV|5*Y=nFKr%-_JKC?I5lle{&g_KK zC%@cxmu$QzdTE-N$i~K!UIaqk#H@L@V+C@Z{U-8gd=rP-Nvhe$2)9L&j-^!JTeo2 zYmd+QTY54G@=)C{>TVt5)6GE@`VIP1*UE=!t1TC_mqoEx;t=DB;lyEWgCS`26PJXe z2`&%x>{(%@EzGeneM@(}y+P+HS!L64VkPJKam8r!&B04+_>sT2WV?4XUrtb%%ZiHo zoW;9AB0#~_<3^dfH)V65LK!#L0gmy-qA?6-gr}Ih0QiSmS*{BH7$!P}(~N87b?<^H zx=tP3LLaU|H%%I!x=RuYXA*I&r+gojiZ+!$D__vahDgTa3@y~V%g=bRi*-+fNMwL8 z2WEq&Fry8r&-DXYqrg9)Nk`**unob;$M%<8p}LFJBRaVd?ZYj-x-*m19g1=~m3Nxx z#Wx@P-!d@L;CM*gpBhmBadCItb@+G#-B^=UCl+7D=B#- z{#ms$7|VPCt=FX~=nW?5d@`6bhHMUjlsx!$3?$7)sI#C0o z4o7l=dd2U5UePU{hRuowD~!eBOxrXET@Av1}46MGRlITmo>@FKmw-`T@9 zc29cP)H<|nX!@vx#>mtE%9#imG<*walO*Y2ctbk$!Y(LXQBIGBNp_0_l6jy(VcbX-8km`nG@;ZPh$qq`H0&)H`d0tVFvo z^JVuwqse`DLr>=k3CLZCzFlgk=9JE(v-PHhx04r}x}8g3+%)frl98}85pPSShjy<{ zthZ~5S@=M7gH!Fn-o6qZiwD2uWJ9J3?ZL66udT`^Pkj=oH5OIU)SQ6(Q5|)sGmCob zx1CFs74^5o%*Z8g8$mdx{sgV5Dayr^HmAY1#^gttt zK!9an@OGV60&Ukz3l$j-a{BoqbwU5r=2Y}G#7WkbdUL{!n34j-7IV5qklx}f+4soh z+tD~eR;%VMde)wLQfM(}lv6dUN4hXe$F{Bbm^KL5ZKkN7q8CK_r1iMl9Q|qj=-0Nv z_R>=E?flfEo`tHuK*Nqw5WTZzA3p9FcP?U8oXs;dyAyVaGhQIBv?}B$b1k+D%1|S( zE6bar&1BRl*~)_JsSpO8Bp*FC=@AW!wMF#@F?!k{p1|At(^M!HB-A(;ca~-I`j)oB z#GE)qkWvX8uJx1tJj6^rBTUFcs%s!V$w&PQjDOe5;otRuj?HQ~+cSLPQq}2kTtk}W z$qoln*ywTUJD`K9tR06;LnU=g^_@1Q6^tM6jlk6zK(J-;>hA#L!37Oq$n@o7dMR_M0?G+GTpfE8s#y4FS@*d0klGMwiVh}=M4O2w`l?2 z{B+SX)uC_ttlw=dJJ&KkN2fI%;71X9Lo7#w^TxGV=OTR%(y>ors@-6UyuD z*t_(~BuvAY6J}ABUuLyUkdQf;Qi<5Ps{I!-;W3{*qL8)lrTo6C9UWGu#e05Ap=6>k zOzV=Xdy34JzI6}C3;gYM#k=@g;)Gn@MCNRsLVe@Suy&nI02>mgO7Hk$(&83bVnI`2 z%Gw^pqO9)|ujg#2P$JCx5$Okd2ah$TM0ZL}YXxVh*D-xCsj@YX?0Wremwl#1>w zFH}?NP)(0BF#)4dutsgH-+#_)Z(t{5gK}k3;s0Q%H6c3py5Jm;4`2Sxyfy4MU1BDU zx!2ek=Vy3%ECJeRrFfT_qC7FFMFcsP0$TUI>_2rNjB%}tU~-uF6zcd;|HiMg2_jl^ z_YSxhhWg{8jNQZ59?tM^_-6GJ?K@#5+QsBZ%@Tb}PGL?wRANJkt9gv$egPat!8wir z2^weQS^TSEmr}z4TmeByl7hB^FumxlM~5HJ25sijo(+T_X(fhTy8&AMc=S8@a+2=ven7H^Jtl2TY76`*2Lg?k-L)7dv6_fq za&xD3nQyL33h4+M+k2bcs%CC}&0Yx|H#30=CWK$(99`& z*a5S%-0K6L^AGF`e@t!KF^dY7^A=O;9p4fXiz!hDOPAe!1&yA!qXCJHx_{wJe{|L!N?y>FE%iCu+#&=xko6sPO4!$W=DZm( zSR}l>Ra^I7J-11XAZ5Z%&zgN~!L7r50Oy&$P8yglic-;A9^4GcCgPi6lox#A@9oYf zBLPb6DbTZ^d8(1A5m9qLzTNMhCQn@oIqi3kRksL?7wxKD5atVzL5{Z+Q3ZV(8oXB1#}Lay_JTLD2+bNurN_EXnls z^dsYi&t<}AszO_kZjz$;G(KVp+$O3g+UM;(;9^mc9ky)XQ(m#-$Cq1v^!rW> z!xg}k9j0ljBAst2%xCk-m2!C{@rB#VS=Se+MUi_wby zW{1;Q4xzi(pite0^8jh}klDL%hL=8zsN=XlWmprSk!^N@2UEUFt<%_qHgKVyh>w6c zQDk2IqyOc%20tq2{+Pm>Y`8@tNzz*}D(2eBIe8h~R{1J*puFjTEzrkhcyIv09q@dD$3jBcJ-;H zhjn<1?tOVEVD7>5iX>H0{4Nu&86Dx_D>k^7OSdes+}TTI%Ea5yA*rQH`XV|sVy@XA zF5ZmSU)rk@N(W+0^9iG#(iew|HS7hP)iml_792H1z;~O~Nsl8EXUsHa3yy2pCiI5= zTQ|C~o=!Z!Vy1WN4Q|so%QQ6vpDypi0pZVhE4!o69+uQt?f#jaaix9>NW+|ur>0L; zPg6)fc-rQR->h9krz#%e&b+M+3J^Jnc|bX4KIgtj^5z1-I?VZ4{IwFk(6%u z4&HkBrqCqMM~=0|_GzSQ(=JyVg=PueTvsge$x)^1W3W?9CC-wk(T!Xzv!#t5Q{s~E z*68myXTn!uSYzU*GXeo>Q?7SQJ;!v$KB7OWe9#BZpnHz|xSPWV{K(H5$H?n?Vu5$1 zW?PpKzoyLYL~Q)zo;*&P;}MARr;0mu3#lS_32n(MDOg~O=I_DJYcfT5FOn~M&Xt(a z=hfDep=fJ79Bh;80eik4E?I*C^GA4m$cg(Zf2|&{Dvr?7cO_@Z5qM_$75gup*S=_@ zJMC9*v|Hf`2HR-5zU!N<)Bd2%{M<60_BRHb&Uh--O&`bx92OL*I5P6h_VRF{E34H1 zTJ*4XeUx%c*U4n(!4kqJ!ND)X-ip&)SaqRreTXWpqcM&tLW^Qv#z1_{dkdAe2@M58 zvkR<4iG2hmy2@DuN3gi>lKGd;0c0(fU&A=V8_XSaPv`?{yM5d*=T}JE>w8)*o0Jx< z-fbl*9m=zUdsv{BrUtHt?f?|mU16+2j2jG_I9)KYvJ8E!3zUpmHZ$pxu3MP|<%qOQaNTwxq(^4`_^f#|MpLcRw;nom z`Y@OVMF{pth9Ih-tn(scSCaNYb18F2&1Im+Qwq_urBF5hCZA{O<}2C^w*DG-9)eFC zSGln;^eG~FAx%DH8?E9h;LONVSwBTQyMozzw}qF$DN6@!>FNm9;5ih|P(1n|L->?> z*StC$SNM^9w^6-UodZaJe z?{&|KCzNM}9}<x>IV)ijc0{dTh@=OQQN*`f6`r4EUomL z5mVS;?@O#KfB$XGLE3T4HJLW1x(7am`1@(cTL3FOIjpRNiOD5=vHtdH*i;m?@+J2C z(X3^|Tx)ITx1*TJ`mz;MFNxi}AL13cHeY<4pX`TUw*5{INGL)Td@1$iuK}BEIOQb2 zAiIL>5vI{3-Dsb*dr`R*$_$A*NnUEnN|by*>S zj6OwH(14krSydqqb`y}2-4UX3{pi~YjK$jh-j9Z73=YTP`tRekY z5)E|o2A_H|4_6Oq;6Svc*hk%!z~qEGBn9cD+j^cQe4e%Gw{GZoIBxZc;qVa%MUsBKj~y|3?05)N96<-i^P zg+-$SLD%7AnQ+z9)!aSEN@<5o!R8PbTOW>(>C?Tc-}nW3>;H_mjja{uY}nj}@dStj ziFbu~?o^dmlnIcQSAC}ayCml5l1Eej(!_6A>HKd^{HEPVw$guz>i6>V^Z%`W|9^=c z{6Dq2{=W||{Qv$b1`K|L_Pe8Gl=`jr@IrPHYx+?^6XC%mGk4^FVh58vUf^^i4Zhui zrV*`ta*xSIyZ}gtfN&`pta(DT1c-z3+7~=X4J#KyiW!ZuUvo2)_{VKLj{9WvVpp?D z_^Au^s(x=x`e&yTBpFwuG18LPYqwsnt+bdvrvlnaTs)sQXnk&h#YY(K+#q_&LUS=v zuwW=o!w>*E)#bowcyQN0$veo*;ZPp#xXnU9Ua*M}8Zz57* z;WO=ItqQ^Xp5F6$J$21@eo|a8GdUy@rO@A$sqQ}kpD5C{EQ#*8)))E;1q8h=Ky8l{ zTH?7~gu7nog>6Rwvc;XNQc7>^9!x1o!2@ViQjU<82Z#ets2q)9&qe%YPp}pXr~D2- zOYcNR9JA5zv31qAdmN3oWvYAHqkKH8#kG07>tpgl4I%o}=;4=4l~*2`<$AUJyfq7+ zH$J&@W*KE>5GTxwo2KKT9)KuF>}SX zr8HZfS3>i0XJn4$-BDeeexAlDUAU69gi?jn^y1Y)P~WP26Ce|(CTG#6 zLFk=XFfd#x-26FRKyhkUAh@_ovslz;D4}bh29a+p7(+W5!8+;thHRqItqA0w-^frB z9EN>%y`v`CQIO+O#E->p{@DGZ)!PPY_ZA(>luPO!x(E;CF$Q}}Q#l7^Ogd_}!`~Af zqD#9;CRX`zX;{Haoc9CNNCjrYH<0P_na`5NgS?M&}K&bNbOag;;=mlpp-_(k(S9UBE5_7l0 zr78|*azHO!UYo9yeYdH7K%=#plJ~mIEC<6-$8m|XOsT`yY(02xz8ADVC?M&*Ljwwu z?d>V7B-D$4yvaE(6I{o|kk$XdAW}2`GgN+3yzJq7O&0}2f~q``V2Dme-mbfWzA2($0hB!S{D)&BE#mPFWAiRQ3X?4{P}O8J@%3OI zI6AJ^D{*HbXk=%f9>7Dd;D*jkgUmm2m^(vgf6}Sa`EY#un&P3Oh0=b(G~s?}n+WOZ z#v>#X**qb_%Q9_K@=!BT4L$1`20SnLcWocLTYm(N{B&CE| zlR$;_D6bWuE<}0Fb#9|M$5Gl`COMQ3h5jeTQ3m=!8flf6 zg#?<#5~Cf`4#lfSjL(EYO}uOn$Yy4@g4($ndL#s67Bo?ZVs3tr6%e*yUddtN$B?k= znSq#1UAMFY;|^7;XkODCml@x-8SzTQYAm1+>q4i(8R8R=xj zBIdG|LMB$s6A6894wCpo__tZn8O{rK*$M$j7LlBWVKG>ez&mc*np$5+KMRGe(TJV^ z6fO8t?~At0sfkZfyL9OG)53UEUbYJ7blP|*YV*Zpy~3@aqlWBbe7KB8hVir|`5WDj z!(ANkwdUxEgdtnHUV9Va>Z7D6m5q+FzDWycFl6Fp_K|may`?lSrqi0Pe7YK_Avfaj zzEW#4@2wDmV)pU=P~X@QM>!F>=?(d~&mTg*i%2N%Z2`&1;`XK`L{x4FK2rM7?;?`l z>kuJW(}2u+1i5@X;u^qFMJx0*+8NgF5EMT!j^Nk5QkuzPeOKX>ZhIR0QzSJ|(x7NQuho2)T2QH2oX^0n&EYHQUE;IbKv#UmJ`v5G; z-4d&k%Kd2RqLEJTc6LCB#A?m-J!h&~MER-o@dQ^BCV$ntVl+r+$8h%Hj9uPdR)<(b z+?ac-wf;N4>JkivzHFjH^1reZ(wCXNvc3itG{wv|`;3^!YFvH!Hia5JQ2kCO_{!~V&0iFY#dF|+MA6NjMOnhDx$OSh}B_kb{Xxa zmhVQR?a^DOo2Dw+@Ip;74LfF;1noXZ^+=+5R)Uw?i`H1u9$4Q;v4okE^RlE$YMYSN z{5eRH2}<^|e`S_-)h+88nF+>$l;du9Jx4U3JvsjQeRCHE_|2(1DA1MwH!kELH+3Po5 zj$U}r!0g7VB=vCf0=al_E~U4uXz;2TpG;`y#?B+-0?hlcy&iX6BkEg8{Uc@PXQNmI z7d17TI=C0J24tVb^3n3-ues8pp4bf&BkDqBFx0`!TGaH%j8x}F_a~qvv0ocBPL-HS zI+Xis!3T#PtZsv3BDZd=&EqqFF!*n(Pj)wU1U$2t_LO)Y0QcSzf8(X5Fn+R=?s8{M z`P&;n&_ve^6;DPoD&3as#cGIB&N}T2AZQWt>FtPYu2^M(184^Y%h`2WSkC326*`gQ zoOt?+JKQHylT9MrD?C;ocViOfJl3IT`}Sry?&kyAcb)7cSZldPx!2+hq@62BMUY=M zwc_Tar_6&G23FZo4u(e*$o?n1vO`qt_LG6`n1M=G2@Zg7ca~KYYFV83{pXI~_aPn! z$KtId;siM91KH-YB${v zyH7-lm&w)wYbYlhIGk=81f+Al&m2&^=G(24JU(V(9URkb^e(7oV(Q7M0@1%or^8ck z6vZsgVAFOOdlrz@smKdJNWA|Dxm2#Zml>Vgb}g9ZG@t42Xa32Z7c;l0L~i=Y6J=*< zvAv%={&VFAiJ;ZYft{)2T)Z}}WgVpO?e=)l=DDlHfUThVoX(*I50p$);uUG{ykQYaOV+kSEFz~g)6t&^l*gNABf4gQf?wDH{PZdAL^gisX7qdSR*F9 zrYb~^JZ)ZuOfB=vO^j^WsEtCd7&s~`own6He?$65m;m1=kz_ygv;#+O9i%Hox^gMp zOok=mVm*&W7hGXn2LMx(kypijq{_2*gZxML|G2FDvCKK(0Z;Q>d~z>i^g8feVE7k# zD@}1Qfn6`Z4i0E4pIC6HqR!re%EKJeJ?B00;3igv(+&_S39-kbr$yvwY|19v>FXf#vR2M8q%d6-sJ(a_d zg*u_4b4PcZ`G(KMW!2hkBgM z8En2S?Jmb5Eq~n0n09^o8qmkd8g%Tl-tM4&^%#hA^Z+a>aG%hnp5Jvwwdg+>sD3bg z;Xv1gddjv~eM^$D@p*H-!zJLGcj|5VBE6SiuE{*jUKO5wSkJA$Jky`?7W|&lcC;e- z;d|AJi>ClxZ;b}l7JpNZyled(Vm5pW&QnSG?k38N`ROk*bSc#}y}=WoFKvi3I-Va3 zRfdQnqqmc7q|_V3qsN@9a&T{o9_83y`AoFWtny7>&s54p)ZR~VIK3fa6*869EzOR- zTu8&EPv;*A5BWyfZ;fA3CY&?13Z$?VIOBe8!#0+d)~z*|vP`5C)2GYTeMSbZRz(?K|XbrXg$~xgF<&}WapPp-~D_Vi>yny?#*3f zxjsE@k+V>>9qXZYPRBZ{7YJMNFMZI-sbd$f%ee=qHJMJAJ|lW6t~D+-kv!2g#f1xu z(w%I=(g2^y=7@s*Ka29`DAT(zYOd2ahFPIsYZ83On-$H>tAHigznN79bCQRUJkbQ( zO^{!>&YwLE68gyku3FEO!Bw7Wzdc>qgG|EuZXn%f2_zF>$sD8~)Z+p<6(5}Tm;6eh zntsc3$<`#ZcIkh^!VEuGaCIjKO}mhRTHjKh`esw6{?q3WtTjxlUaLKDYb9e0m#Xr) zIPGU51(HYo&FbnAJ+wGd>XXlMc9=98D}WxT~6oOZu<`Loim}@$-cQ+x<7^sT_{s!Q-0euf=;XXz z9Qz@#e%{dIu>WO*t7<7rc-0oF{wP;|LKr3OW+$<`V7p@o>F#P$Pqahk`1xHeXw56n z`t;FMIJv$~49R@l>hhGAo;TZdHY(~NJCjw*C)p7vRUfh?>bbj5eh0ko8F|0PLnq%5 zc=-f7UV}~}({v?X+8AlL_=7wmd_c6UnYn1bhsN|y7+2$Ww zAhKn?1TmpN!v8Rb%<E89N(UIV)E!RVvpafr_elqkz=_nB~vRbD(C$UNs z@meLI@h(fxJT}Uu!B^|>d&0Lib$dpe)qujouaCHlf7sV=4Q4+DD)$~n;<;BE-J3H{ z`^b$_U@uX7TSntHbXM_Paw-4i0SCf0$LzPt_U2cAIzgaZUwYB;$w_W*cxK*K+UmL*RFX!M4zAkP7*-B2pp%^Kk__|w6?|R zDOOu2HgQuLSBVH%n-mMk7=NHI!nFU^ovq7OHFu|uXv+*73l4#MWSduef2_+~#G@Pd zaRfF41yfF&bjV*2ib<wA=jWdB6XNrt;b{1g(`=sD$rNg$Rwdcw zr%e4i za?TrJ;m!nV zGg$H1A_Ya%Zpl70cDwJE!%Y~B`f;Bv{IKP1igqhWDzxQIhMn_zO2k$czcfiaU=+`z z-pt~m1gHU0BB-dB^`eO(&BZzX{SudP1zrBFDPpNQOFikJk!IIR?Fmh`_bV0Vwf0YrtC(7#QT;FRUqNpf=m$k8XjCD%9)VPA8 z5KdppZU8>gG2k&c0MsVmP}r)-o_l>yYZazA6Vn}=C*uDqC(V} zXt9bDZytq*zb}sU))i8@AaFLSEeK7-%d8`P*mJsO6^!^;FvQwI3pYR)a=Q4&xSTr5 z9vbQ2O2O+#gro6+QTBR-Bbli_S5koEPBiX_H!SKm6EXheEbG4br&OiGayNdLD&4jn z%8ljLpPy%bC9gH2S#Uf%lxx~Wd19!6PCp;f zJXgVi*E`c*?vd+^O))7Bc!TpT*!245I3C_hX%I>}m>&Dt` z`g;TqxpPG=_#cem^cED6I7GhJU986uIc}g@Y~yHJ!QrBRWaD?W&Dq|}Yr4fMp;nV1 z9tU*G%^SHIc}Q~k_kT-XF1P6%aR$%lA2o|rUnXOh41kwm*@=>+hHqLoDBt0S`wl{; zp_4AvT=32BN6A0>cUjS9-;}xdud`$&KgRN{Q%L-(DsCh@9^DWt8SORdO72&(sRi%s z`;$(RFyV_zh@nTp|K)4m$;B@b{!5++|7-qz@ISkR|9*$%*8fFTSe9^SZj%s)JBF2d zc%2fQ_jU5n6L49nxb{Dgh0%1Q`lub}SeV2?ZFQiab9&M4*fq4TU`nAXuY3wVTDRh; zC(kz?M_w{MN+w{?N<`1p8+g8s?s?`sPu#S=rt)|A|I(2^vT`A#e_)r~K^7}Dwq)#@ zzhzjJM|f4qai3k^^`~rOE#FBEhD%Qnu7DWxym5%%1;8mxq{&ve=4F~-P@l(u}tKAy4;f+yf1%j z7DZ@T@MgA(O9@AM6`+@P0B_(q+aem%hPUN1t4p@84d!J}LB>VO3q4PgybWNELeGEg z-u*P*d3h0*7_Ass^;xvDcxRdetrg*=sXjQ$VfxqmJit_Q9VCcZE%N&|;adT(dg{Dz z7YUihygdj>%1#&)(r13);1Jt(w}$EYP~9oTnpp5#tUp{|u|i;|id39);q}K830GoM z5go6LR4j_H#s?N+9cKzk``J3ZeiR$m# ziTLmgOQz#%Q!|klti3k5smDQOg0Vrc`EEb4(w05uYLBrD>*)z%R>6(;sdrNG1D^5D zJ5{Cw>Ve0_IhbG>)LEZ6IplCibrr-oC_#4P^Nu4eo$zIpR{5%RG9{#)S-?d1N@C3il ziZphjOD)tBS$ltPw`94j$ltE->FY|ucmPjVvyaiu`tfAO(=89$GAl6Dkok7pzvWO< zGG5?EN>JdjIGv%H<~`*F+w>2$dR=fR&+kj4D1w$Vnvf#$e7Ch#zIT_vpF5ZFsuSar zpSBQ5*NTWDpmg~#e9Q}0eZxWteJ=Nk&B`y1KoO!=v@Sdp=zKHF*e3KKVR1kef7BJm z5){Q29&+9Isp1=QaaF?e~a-5pKsOay_N@k~H+pE=tM-QJLPg7QD zviGOYg_iez3K;tHeA3o^i9f{0K_-HXfDuEd0IGYD15W@$+w4Y! zwBOFF4huUj-bIiRUIqsE++-bVt(^>vC}KYze*VL+`5Q#mh`j_gW&yIDP!j>Xo3+dp z;Qm#LX(e`i^#b2S3O^hDu>G3qxY;rC`(pb2B7awLc?#wnt|)0LgNQ;0hjOLL^a0h@ zG+ldF6)|yN;olxNAxa`udd%udOlS?XGT61+;K6g((^j5IN2RmfsbjlsZ?SK|>AG>= z{5KNQBPlLWS-DN1sysRvy>5YWOsQohix^Qu`7~cJ*9oKIF?)YdG<={xcZz`> z`;8^)9#5QLog3!ULcign1$}h=9bl`U@t=0v?^ixhimItTAHpm1(Le2{`W6c-4TIY3 zb)tmMy@r;3=^D<11D-Iok5vltQ`jLik#zRg6UUN2^Db|)RV4iyVn%baePxSi3NaP0 z_=SP~HZ zFShxYgsN)ldzgXgeN)6cKWVD@Oou7!+Wlq`M;blZZl1BL<8v&uUQgJ>%r&hlg|ZGR zyULi`1Sn9nR5pw(O_47v_T;y3ci1c*uXj|Wgzbz*h62}S2qxL`Sk_##HHAQvDM#YA z=4lI=a#P+Af_$}dmXgzBNDupHqjqH_$?6qt35wQ|Y(&Vf0kKf>k5k*bwbdYwA$@9d zOx$UucH%euo1Su;a{UfS;YO|2hUhPi}Sqb3*p-?})+T%IUQmzHH zpleK<4ki?-b&^6STa9{3*l%0BkqMM&?>SD(G}?%u zpU2yCZG7UC#qR`@fV)@Ab?FDq_*&Si2I% zxJg;HX#aC(0lu{;*d5qhbhft610rN{0s*;n}q{ zFmK|f+3wz!XV@fjav!O?=fW~i!4sv8an?%7r~|fh9$XqHTB0j9QT$=jH$Lx8saF+@ zw4B&vh0ZkZui1;M0s>)(DK2-}aYPa;VWi=160E@{n#T6^bqMHD7sDYVBCrJSp7`_i zHl6T2Ob}O*6O}&cx4U`Jk2$Mud-t)>zVc+7HaVi}&tD0GtQW=;#{+&3>mRqLBtFShMxpUUAImmD>=|CjtI zg4bTvl2vkNV+A_C`XBRAf>vd*$oDsNc0Xoa-+OccN#M;%tWZE~-n7KsDj=*^-h+NI zP=B!Ivo~^&$n!SYmEn(F%QmU3xka&}Ec|(Lhc7BKz_-4cz1@C$Wxu4gbZaM=; zzhB5`wtP9!$Jf>Key3%X)k%nU-cEFJe(Kagv~|vpZ6uOh6J&6wg&K+<`;4&-vPf!4 z6m!s=FEG6OO-N(L?D`~Fed27!C8tZG$wC@jWz)q?Gd?3?!0Wgd^jEh14(<6R3pi8k z{n0t(0`~>f(Ih*(o>y@ARj*lrbRA<8Dfe;Yr~S1YxWzT4*!(2oadrj{=F1n0Fn;`MZl>%bYT*%aEVTGf zOy7-IY|z*HJ1{rcPN+>MnX`Ebip0Z`G}gm3sFEchARU5Zm*}dwviRD=PHlC{BQJY! zE?_#kkL2TJHf@-?zSy;r6k7BX0GbqU2I%>+ePla17Jt8n@7sc9JXrUNLvfRB$pq>< zo0WLJy*m3Po9#z)<2m%+GktSTcYnB-nxIjiX|#ZC(zza9}+9Zl`lDy?2YLp_+~( zxG*p1+qI>M`he(m!hK!XrTtTQ02yzy{N!4_;ywKYlT?kI(5>3yX`+75zkxJ8l`6{# z2$z@EaTTu`3K2WDhQ*9JQ-ept^qZdlBr-M>$oUU7m^BN3Ltp-w9lL1uENGy>6J`W^ zNs3qQ%9YjvtFzk1?653x1e}-Pt0_3`Va6G8)*k?oX7{)WTpDt)nugKdSm|OZ=bQ3C z|9-Kx_lqVz|Xzy zX)Gjq8v@1$2v5LPW2hWQk5kV7sCRy*$D{W^Dx==cD&}erm+)Ss))-j2eB(5q&Ja=z zCgiTb+<6_riIno6eF+g0*%nKZ z9TSp{feq~$>8`r<-@~YDlcE9|V?=GY5>~U>bHaQLoCSkgP4!2l@86&@yGwi}HR?

}XojHd=1lu86l`t_0+GypE<296j2WL699u_7d}tkd@nFH%tdR15vu2%uq6ay`f}ReDQh9s_QTnu^IBIM&}=) zjNc+(y)F#@L}<|SfMe8HM3+LDLuGRz=E@7d&WJ$HWIgZM!mLJ(0m9PL3zn25SC@`e zft`|eN2&b(g#%3f3kTHxg9H9#t%Y&mHrR+)8^4Ehj7&QfxcZ|(&HF`Rx1JX!I|UZi zy21!}@>by2FI$H-^5iDlBds6lIaqw7ZBu|79bBKbyuR@Oe`e`_I6zhTe{lePf`;&Z z#*9xts?1J+BXYF6k8Y6i)$Id*{v`m>QxNqzn>TpJC;&%5q2i1z1K%tyb_uyc1+ahy z83L1DTxR9%YNp2vKRa`cSHVj!rsw~cFJ4+26_3D3+d1NE za;mm-xL(3dvyFOEyJt>lcDmo9dV#Z6rnr6O5ss#GXNSbcdl?RLCFHF>ch&|q3LG3& zzIxH9U#q%k`&~21BaEtEO=Zj}g_4GLZP)foK4<9@ipy`O-yqFu&&K^*-{RY`K1;mIQ7H>)y-fB$(`}WuH_V8B z^hsBwcWOJds(~q^akub%_R^MCo#xf0&1(DX`nU!-%Kq=D-bL*~p6+p{#n9hzxN~(B z$_%J6MjP^FC5n0!w>w^&xcL4CsTV?yD-*xqfvq&78LlumuB*Z< zrC!nQN~t4^_o&e($n7$ruKBfVc|kWmxuZ+&lC1RPu~rNw^CfeMPf^Q=Sb}j(3YI$D zUyG;FNsacq)T&&{!K7f?#SSKG%z4&I-q)pXcfrf}ECj2}DilsW^*h3-l{onX})#ymJY8yxrF=pA~*9dG6!)1ipVWD#|l z;R-Y${lrEw-gQ)P-r0>iS1f6s3l{|3x2$=o;pY)`k&kqJDAN@UnpHX*P3bhUNjL9_ z<35Tgm_sTUea|IhsAo|veCs)rMx9X$sr(gRK3}boS$?FT>`W4_FOcCGodAP7eXDa=;O`*u8C~2KeqHA1}PO zv!eHvd*t`ZmeF) zC>r_QVh1_GU35?)YL%Q!9=(wFOH%70w7%?u;Ju5%W+~^{p!+dv(%f^6q8a7t_b9L+ zc<;yqtTWp+2BKFw17#^%xcJh_nDETJP_l_|rU%c%UdX5$@-t&DS2E!Ozvrzv+S| zZn=5oxef6X!YyBF1o==256Y1EU0M)4KfsUf(^IwqDjwR@40kqHrqLdk#oY)=$di5> zV8yZIWBr>y*mz@p*5#K7PINLN!r**^UbEy6YwNRt?wG5pmKRVFF)TSN9w0x&!5zcH-FrzuC}QZ769Z*ti5NwbtX=Cb z=^JzVnf4UJ)YeTqmYeQ=VV#Mw!0%%3OmE6Oa@I`#83HSz>Eph355z5KL*FIbbYb2V zeSyaYvTwt0nA|ItC4_=Ox^!^~>nr=G?UU zyPkcHAOC6~h^4hA1infwFxig#W-$0%y||ceeuXhp-wfOt*+*C1il;1x3IO% z6(;<;k1|d0grxn7EKbmh|G`H(4u|&52TRWJYiFoPLxudW3XeZXQYRME(cXY+KAhj) z+WvXBv{2&QVsVM7B((RE!l)pW$VuwuW7m7}_(A;ZN4y5W159D6&)3zt{g!ZW1&gCq zW&NzQucrqZDJ!-Jf|IN>5w!{fvRSSo|Y%&EWr`?mfezYL-S( z41j`yNRq6e(CtTe@bG$netzwdr@m)^pDj?W-Mb;ot0bIEr}gpW8LP-&bLx1zmN_ z{Y45=*mf~fe?c&&O&LpA&ewFHyWi7dr-^YeGgu_OJ>-=G$=O~NtK1R(*jX5 zPPt_+4}RG9m%FWW3EPc47FnyeK`=s4t4ZU@CX*Fro5RXXdX~-(6J=kZ*yAh!-h%&r zetY0Wi9kb&kj+RqQ&3UYoVK=hk$gA_Q2LJAJo^SANs&N<-DrW<%adtX=k8M>Z^DeqQuNopJ4hxdsW-naac$U9kcymz}8osAHD(7hHqzbXd7K+@rl= zc_kD^&IQUmZ>(Nkfx+atK<22%y5*HLmAShak7H_N9V2ot}~#sz{M z0&o!i0oa=5#%v>jqpP(qq(={X(l>t^2Af(1{)vnC7!*A+#Q8;%>~lGj%Uw zn$(i{-&VBjNs6o`rl-FI;>K!OI@8e5=(a(;L<(9wH}7Lmh^xM8X=x!ROk99LVNs6N zw6`yz^>CPP7Lhrd^RVxC>PzGi4n%tbK$4T6%fG(^0NHQ9;N=0g1H&zY9Fq6!<7oh< z?^hh#=wOBvFpB4gD;&JM>M>0*j54X}nbO5wa|?d!3`C@)29}cTx}*>IQGc4cr4kZRKMJIXQ~Qlb++CKiQcX(6=;}sfoHrYWvb|!J5O))+|(9X z^TuJ$bP$;`0Kq{Zl7Rr_+V}JD@WiBwxd64gKG4W7LUC=tNC?>g&Irc!?E44y(tC~% zFZEkZ&D$v}2`gcgm131}N*6YlO3K3Md6Cf3PotwzqZy-%QD@})d|V5at$7#paf?EHLXWhe?}0CEO$*H-4D z5QG8dUzd*wvy{uu1AeSC2!Z$xQNN-PmH_~qW5z68*}3#DfG|LnN2u{A#E$@wb%5IH ziS1~?-TAaua2mfnl%G=JQUZL#OI(F=!`cN6Pg zq!0Iy9(0rFUVnSkGAN7i{{0<)_tT9e>3jd+5MPlMgpO=KUnVYZkgK45v+Gv*&~%*% z^Do!D!NGKw*RP<-b*>s5Aova>efH8PGT5%N@Zx!5`y@c?;xhMw@Nt)G3wovccEJBV zS4DEu%Z=$P9Bubtotv6VWJY-k_1sO+E7ls|w`VlV znCUWoI#BVH&k6wPxYnNnM3wCw5uo`~DS05|)Q^6re!2cRm8i4WJjNawKE<^Ob2HGA zH)02B-_pCP+w;Xk9X+{sT)WqjHBP~P*T)n}IX_vzYUU%732Z2G8&?1wf?PIYg{V zKr~=YqDFPx0VP{)SF0xIpnWOu`vm|zo0+o;xJeIbKrbPXNHo*1gNDSDcF#dV82)3>v(cje+&+o`b$Hv zJ-d5G23?j-8hfDY*~-Ti1jnTy9bt@PE7LDU7}Moz2y|eKh~9hv4Zr&@Kg0rqkR0H5 z1mDuRy{((&E$y4Jdr+xv&&W=dU$4P+tu1&cG|F%g=n-DLAz*~?j%zmv1E2SZa86%k zT+4W5zd(y0yd35OIPR9Ss@??R;RaiH)PC>}PpRiiIzQ2^b1==<2xjKm4mAE`diIK- z91PE7Me33RLpoPnXDNBrG54*czAYWpP>YS_%;Bp*TD8d`c-eSJYS$29!PYY=M#q;r zsU`4YdqqZ#dt;YONMUMj%)qM*8&?89xbxtJ&t+vv?##Uhp{VE-1cbY9UOhW_A3(p@J`Tx# zh0l@40&c9Z+LcnRi6bPZM`c@Zz)V-a@F}VJQ*cqq(=(@}IPvGWz?;LLbSgOI?UW<# z2UUg~?&9)YR%O@NZne>2lz7wFw`RDC8$c>lw@S2}P>4-j&*F+zeT8#zlxJppOi=-% zl9JJ0+X)OeQcqX_t>U3x8>^}yC(tJ|49Ju1#hkpI0SJo;Ng0#5YCJL#Jqv+wMcQuu zD7cPCh@J2YQby*YYt%97m_pOzGU%4lj6i4B^=~ApBP^k`?fLC&2HAtL9Kus|*S*iB z(l|QKdEo5;?|Jg;KRlw{te1LAXLAXS*=yOD)PCj*a$1O28y#EpQco4@lUAd;Us0*< zDUf8HOmw?c2%0lM%@FUZ^Wn}!hUZp}P4?m+Rl*FJmAsnY@GpCuhQQHJ9sP`o9Wq(O zS@>tP*aN-jDLWiU!)8~f#eaB`R^G2c*qbymQ<#EK|MdDO2C3T>>Y0(MbTk@DLtezT zSd5r@s>>E>9)B_8Iv>iXt)0PSXE(zw5uVQG#DUomLA+|x6`R~o-d;nd%L!r&8E9J5 zLwfeh5f3MqEB!wb^zCCBJuA%RsyY{cJ84#2+`v@CUJh1qU!5VMn1Q7TtSy1!wW1tQ z0*OQDBHfv3LD-^6>6)Q%+vnvUk%O6NMS@jp+0jT-l!mXKJ+my-lM@_hJ*j!I5irxT zFTI=40SRCUM43L!j!xSAdfAHJsPs&H^NVtZjM<$?`0-@B z^0epUAv)BwG7*O@u;OG@xz7oMznf9&cZH9hI+k~56?KWIy=}me;Fxa&m8hPi(sl%R z*&|M{Qf5yF_^_%>3}eLjX!5tEMdo1~*|NT$os;nIVFMWq#6G{nt2nMKAygD;pFrk4 zVr0h%Y9|;O0-Y0jJiMY>;IDIvI3%rWW$`c1<`OkP3C z&;zr{Tfm8x2G~G5^ZY!g2^w+WvMAJevV9N6r& z92g()HmFn{<+7HX;%WWfLt|69ST+=45UhdLnS&o=Tl2@$v|$k*GpOp^i;t-VK5F)R zn03xn!E?zOfhpaun)8uVQx#txBus#IC8Q&_ToK}oW_zwl>2rE(UdZf)>ckQ2rXQ#B zH6ym0FfaB^Md!x>44AjMI0l`UZKtjX(A>94wF`!(^l(BG+B0{Jr(MFiEt|UCt;(QL&d4Gdjz93I|pm zMdffl(_R6c>@R{Zd=8g8XW)3RD)(EuH-18Siv@~$w#JK;`u3fQx`6$-H%rdalb1ak zFx3-4%kLy?k?VR+C0%W=kIo6%m7pKEGMYCukG=xiYPR4rU_<{vJ8>shszQ0laQG1P zClD}A)SL!lB(Phb)fE?mo+`cz1PCM{0o(!QuOuP3r8jA2mhrx}IKL}9=XYTD1+Yf4 zTZvhf&Pp4Y@NUfo_Em4r3aN__P+mLR##Odl_cP-K_La`J3qg3QLpAgEIwH!HA_S`p z`&duNom<$WzY_I+QbFJLGj zQQ==@8}1hv4F6>G(!?*KUj_u7_h)2RdvC**lx!%}nf}`iyqn?soQPc=4F^r4)f@Z7 z3v7UkYCr>|mn{IPrN7+TErjB*GjQPeIf2CwUK%*x;l_CQ#L|XVf1CG<{g>#a`|@qq z#>OsT_8r$DlrtRdOYTauB)nd$yaa&yLIki=nx&-YF)gdc#**1@y{Vf40dR{?b57hz zB1WQIV&Jd5ddsTl^oFMT4(^uu0kV{|U9o^yb`jM_%A5+|n(RXN0oi02adAfvs9 zgLkw!NEveQE+}5ei`!}kkso0ly16=ueL3)^+6FWmTCg(&FKPLdX)OCTiid_|Ag6-@qzWWjkg-)AkgkJeFDs7!N{o0>Yl0cm!k-9E+F<6X4GAQx3OZ+F@Zlq`Z95El zy!Oq^56>UC*rAF1xQ$PF*N|h?j;NRsPX?Lfs!V-Ze<6&LS}}#Y;O6u-(h)j3(oZ#g z;>KcaeB+6V717fsn|14>R!cah-Xt8x21+o1GsLMPZkO6!@_a%+S83dPitY~iO;M-> z2&B}1pam-0tmWfoHL6xNvXy z3EsS{1C)ar?@#JjNq%%a0fR4|{sHdA-nXu7Y6<`a!QZ^Yc@Qcr1isLmF z`2thBtk-NU?|nenb&H|4YTTV%QqLC%Yq6ncwvgT%!i`3EQx-@{hS$g-=Tss9G3q`5 zR%~`UDs!!nFMr=m!1nRH=L@~2;ZxH?fJE*Md*I|fnqhflj*?(o+3M^lay8GFXK$K z!%$Y4*<8XCeo}-{z9k_i2$%$bk(N zH2Cdq2oGGwH!;0eLv(F48E{A6z1z0g;o}?i8z2g-1_W}RG4|p@ov-;AK|1&`%x^XgWE>l8Y4zfS8+mA(zuV)Isp643-F+z26RG(xw)fj^e z70TMsCJC$+j3Xk(HF1t$NL!WYNHg+FcDsf`5@(24^#z{gI@^ZPZ7JbsLYd}REbiTZg{1aMZ3un@J22o#nKc+DpW1sE@ zliZ%P`8ZLmj<{V5x#XeHty$>1u)}T;ZoKmY52ZAQ+jTUL$1l{Ra<9wC|B$Zd!lM=) zAwHAU0AfOK$jJS|H11~s@??Xyk>I1#`?NL0@WWrrb9`&;=#%i4uV=I}b$q71mKsdIq|nx`iw`+9vN_feo0@03 zfFWpWMEVP%f{@*|P2<3SF+Er@TPR{)AR#l;MK!Htn8M2ZG~aLwX90~W9|Y5> zut;vx0USXaaySq;vz}8DG%!x#3k*l?MGW<3M(A7!%jY230~loHZexbUYWy6-s2w`- z8^Dmxe_NvmitZRUI1|mg)uldfs>OuDWcIKQo9Mx(<(TJx_6hAbjoVi7n$2)-si44U zyCCS9!7oR0Jh|+U$YJ2rEQdOsB^Y}ih@71kYQJ&B2h)z}5cGxZDYEQ4KOd(TDeB9D z^fD`4tLMoX@4C(qvjH(UJM!AxlQJ$r1f9DPu6oft<*9U3I=j35OoV>C==Hv=7M+o< z(M9FUNpe`~WaGAgUW}6h+7M*L1J295KNS(1OH~n5r0QfMqLmdn=;WJKUyR7#OR7Uw zzO?L1)#l12nyjln$O$}8sOJHjY{XHQ_LyYmjV&tcjF&j|)D|nS)%-GZ=od0eYlf!h zoG=xF8+Z!q+JojMv7BFkm2yir5+Arn$Y1Tf)wg@j6q^h zA@^%fL(ewyO^EJbHis54f`Fg=@sEI`K?x)0W$NtPb@rvQ-Hk_yNgHjX6C;b^N2!q0 zY8`l$-EOJm9NQK#c&Hw;qS*6f=!=YkO}zuSfs%9-=S^ zHP%^K27A0%<3)X8`E$6fGZ*7G@A4R%;2wx+gkkgwW^dwYD*WXkO{WFknLZ$(Pxg_OYMGy!$277R@!t z12B!?i&Dpi+jtK(uK)%#Rmh$=6~pUa#Nl{$PmPAnBv0^nCCWiHO3=J(_+EEu!)7Lv z(TynZYYpa*p(0Rc5e&an4PR#L!1eNGQ=fvCrxT4=FSxtNv=WurE-Y!mdDrWM$GMF>*S*C6Hil5&AWyddy}ovT#2R%Bq(S zpQ^PV*KJcYlr)=)i+HHJs|1hsq4wp)T9oj+quC@YK_f24nrYV=?UT*0 zV?&2)lUr9)sda(3Joh{)_d#RLRaz><<{7tnr!LW}bi&qK`jWqX{gGBNDU8jpoS7qc zmAZp7Rc>>Ec~Q>=VLmTB5K5@ul2vDG00KgT>%RD3#~cdIv*O`3+$RRYwW*qvXRd7S zL)CJuO@R=`nTa5oRBNY!0&p$fq{=$nNRdxkFu~i^V0*kGUsn1;RIR4E@W#`-_~)tpz7mr z;B0V;;4kr3&yQ5`s*^kU-W_engIK>z#nx%>FR(FV$x>aR*M8@D(6xcqvTA`K^Q%Io zwjBk;GjEh*c#%sH`ZMd_O+g-s{iPR+9ks45Da#&a(S8+53p1N1l<}9%*ELx7PvU8It8Lj7B^iI z2R8VD2B7;GWMI9;HHrWDK{^0n&mlJGlrWL z2+gh!g-sd^H`rKt8p%YZp83CJ^+tt#&N9`&In2Fv-g0M|pZZN;Z!DWyQ3i>yz8jRB zIO9p-xuVlfn_^zDQ&%|>hRPbL8l~R+%uS>V-)b29oylhBiA&UdF<{jbFfrz!mhm_h z^aaEd;6@dg$phyaEyUivvH7d^+SrJwle8aqU#MznnN4x{z@1B~rH*dKdQ^oCEf`dj zu~vvEumW3+cD`u6*wX2~$SSnKKG@dst@Z-ci9x=_XDxKjF9GdbzNlRQi;R`CD;M#t z+4aKF9V@U_J$6MGzJfts3PH!rqY8)vpp3!AjqhoM~)YK z@BYgJn7!=Z6T>6mWb)5)2ax@?k zyzGEm|Fi(SYhFvZ((GP6luZA=m;O&Y!2g5{1SE=%R>=o{>#3>(mkSFA#8-)X*Bfmf z0(a+E@3;P&z=A;YgvVR2uBzd&OZ>w_uHHGb|E@;hEqnRjkrSE!a|DAg9Lxf@th`V0PqR_;0 ztK_yH26t~<5@BQB-@Fotu=qtE5`zK~S<6~N?3J-Ew@I-{Sc8pE0Q#!~xL<~F#7;FA&t{&3r(JSG064J-F@xs=N`*_V3#o;9wa1{q9fa60n&+ z5LO+Jk~*s!6)?E&g?A9P#3c7S^HKGnsbU5lR>^@MxXYj<&4Q!;0|a?`BO8cW0bDbX zngu>%=crl;8bbUfBstXmm$EqE*nKs$|K0C`z)^Oc>gUEL;Z?=#OMo2yYB8=-|6e~z z|3j?zn1NXh_4DGtLK!bU{gs6$9CER_2#25UUpWYZ0vvvVQ8lpbu3CGxlFU!--iRN< z#hK|2PUQAAEHBiOn00K=R^p24Xz%t$>e>t12sR}e)Aw;-n{a})xrs6C=`Bo*ys>d9 zskV-x{X}c?vC-utEr-fPYun{WF_Qh zG);U_!PlP(nQJ7y#%Ge;-99?1j|m-)Tsq0}CeswcdEj)?G`?6aJuLj(ZZ^|U+QDc< z{OiPZDDQI`JO4NQCkw@xrkew~3wDqtgrCh>OqIJam74iXnswi|EGq`n>|G{x#D^}l zFc7iz4q!N<>sro%-CCmnlkAV|8TnJIfeMwLO)wg}4k10eo_yNd%myXKXAlE{Q#zj1 zZ}tgm{f+&KCF9vX%1EHcayC_S%T8oIR)k>$64#yR!4NRIm{yjtt}JO(KW~}lXuJKY z#HDiNdu}t#N~Sf@Zkj}s>>~{bE3iBgeKQPI-#1*Vd$J}tJ<;p<_NB@vdIc9 zsJ*&FDf+S0L<#<1W7cw|>(0)wS7Y)9yA=1yIUJPJz7ylo-zGK7&U@#Qef^}Ao@spTDV3vok)JOOqZ1uf57Oj0&p5;y^9HOry0??oUYMU!Pf9Rs z?}0rCEgc9+l=bx&=fT0-{NGmQG+uP$e(~MQWPKJv_BiC>(RL z&(uBVQ(_&QL=`rNWTzQg{UvK|a*b6B%4HN?Us2i}>OGf3{|+Vzk;;}Q!;p?F(Ab$9 zhLLu-)|rNiDm_eXva$Ho@n(i<{g%RD{SgB{1{&y;-zsPHCCLdL>})J_opeKVZyR*T zP^OH7YlN$c=Fjg=z?YL(xzT?wwc@zo(C-0hp+Q|;jH z#di&#Ui*-xQ?pa;R*X`EWGlXXeg-de-Sc#JC+J9w4$HYbe`0#J8R*^RRY~5xcoHb^ zr!`w3NRNMZfA+OMU3vS3HrTUCVzl}zm?0vfM34Zdp!;sKrEm2c%^k+`1=<jN74}Y*YB_KT{B?1*-*02 z6?_&bpu6XT=8@)udAxBLd3lZZPFF~|;h;|va^ z@oc1gUS=N%D~QEd>W2@V&(917-TaXl(tT%ta5idS;jxcw*mY^#VD9Dz zz6$T-cXOY7PDR)sAv}!_-`Rlj9u*5aEw)H6HM<_AR90Y#5=uk&OtQ_n*2MhV<2m77()y;pfJ=BE@FJMwJWvwP*)*J01cQhcq_N^&#sAm$-+r`&b<{&YWi z1ziDE@?65(Dsa9r%-N#LCERgZa0MW~sDk$?p7jDgT52`{B5el`sL0jGKb`-$b7pvK z-`I7Xn%Mqa&F2EM0cOh?&!llTzMHU#;^*9UrxDkO=J-qdA$AYv2q^nwL2D#?Z6KYp zMV(tPFyjX=Y%`_x&Zp7#)8cPFhV>l|W(6Auwr{W-SmMqpWdqmB_mU?M8o=6WuZf&0 zbYIcEG>7WElH#`e?Z5y!9y0m8n$N&$d|}^nVr42eeWWS6kF2gh2${{1de)@C6cnuT z@&uZjHD4WQD2&5wzS**9K4xYHiM0J_I^0 z@ohRS`Kn?BC)m!y6QJ>{C#dLZNnbrjP@v(f;_Em63!o_?Y&Z#ned|?mM3Fta+6TtR zk1?5llO-G?g7?xe0HS1M__zKA_F71X>{W(V@Ku+AhvK0lJ}%$Slm9B0EFo*;=f=O9 zyVPvL{|A>pxCfWySH=GuFkdMzNf{)^e6Nbhw8k}4AILm^j+XhG)YGwl^HB8S(N!=0 z4&N1C|NCCNINo@5RgIZbHa~Kh=AY(JU)+iR>3R=N;D4Hh{}s$@u5%_y&!WS>pXZc( z5f*@a{D}1bn=C09>ydLjmH(%w|7|al)`tHzjX?l`3;omFk^CyvKe&9tS^j&#{tKAz z`UkR}8=5PMSIsEs$=g{VA3oYV`_Jgbt; zKh6CK@8$Ui7iJv5KuC#*o*5b@Xa3XK-hyQh%EF|u@+G)_xv?hV`o%sJSGxXp%C)St z+?t{gdgqBs{3{uOnQhRbiLG77;M?)b&v2&FZWZ(u-x%rEW;L9TQ)}LQ-MP@Cc z6-{fjVPRn#P5vU2}TW3wG_^xLKH z?}*P2evlpl++q7)Ftl{(E0*0TladOq!FHE$N2`5PUtizQuQZPVEN@8x>pL|0-wtuQ zaANh{8l$@POe>a;KUXtcP8Mn1kbxliyU6F;1EsKl73L8&-c{WkqU>d7N@%ILY3B+j zufd_8$}|af{KidMQOAg|u$utxul5((&Ub1M$ija*Vy-(^zU)F7Ug~Swx%wpVTaxzj za8QM5QwZ?C5*1(|$ALcC4sZ6_rac~am6yd`)*NQP#i6or0&D-!_OBvB-(t5*=t(KK z>HV~U)1{sclUFwwAKTH<(RKNm{X{csOGB5AFCY+RMLdUtr1|dp9Upkzt zOt-RX%wtmj67HeXSG-z21{p_}k9imER%y?c*DtE>7S4dV5_mZS%XB46khR*I6(ftP zt8u-2TnUw&&~jY~&D9Ro1iI}ELcMlfi5l%yt%Mg`dO>;;_1ddu3I1GO0Yxi6RT(t4 z8mqOtvm{B%#|}$kO@Qpul{wXM!V>eZ+9%;9%4NENxmyexCFa^EaS0{ex`EdDTQYh$ zqhg5mc#?jJx!y@>LP@w@pkqQ};`_5jC|ILO=5)$3A|_@WxIQXS`vKT&Bjabsignz8 zes6O$00!TWq@+wVns|(Bi@xb_@R>D-En@b3fYBEeyk7~&HfOp^1zn1N{`?skZvt>| zez#=>nbmryS2cnC}ZrnTLEoKt31)O*Kp&Tl;fm|nnax64eTltZT= zTQQmnn3PDi%x}{*Ho$zFxU{_+8q$=p(q5l`{p0=Bgx+w*UZQ|X0Cv7>DKq!^=n;gCPl zIVmBZpR20Dm9WQ&Dc1!RUsh|QN{%K~6K<^EHPR@VGnAM4KQet~MXWNMh_M^3drj+% zrvAHiT>;=jAC&}1k;cipt*|vzC+9tFHaIv4rHJ^uXXSj>6dTf&E?>4s(0y)S-XB?# zZ@=Q&5!dmUj{Y)x3c3HX8T{7`_WvJMibgu%E8o`*KaVjD*AYBaL*(X&yZ%0Y@_cKN z{xwhBS1^Ld1^ZdUh)!n+?)Sj{CjO%$eF}>>QRWxWHRzFRKp*#G&&X%x)|7FR`7hv6 z^MfI?jn|^dE97I0We&DD+CHva2Z2Ao;hq#M&WZh8vO3jYQKYvbd5M*m$-U>--SA5P z8KJ4q_(>!$Awvb1QR+Iw&+qV;-HG%vX0g33j2;zmKf@QkPu^Pc+^;BoYb;~_t|lV! zc}lzuVJ!DcxuhcFTc5ktW4^9EXg-KY-|lhm-CDv?OsWkx4WJhk0(# zs*wl8B0^QC_nfhb)NqgXw}fz2$xqe#axH+Xu5}xAPsMrQ5qj|c-dJ$?=%a$*^1*)6 zh;38ZkU;9(iP!qMI(OswuGj6XJW#lgs#ksabwQy|UV)6U0jFk<*fy&5wveUYc9Cr{ zaaB(Y__9~&Wl5CP2|!p@QcCaS^W6P<%kecY!?tDOba-zV+WCUh(Y%3{t6khhYJfp` zEn8z>(H8gY{@#Znp*}UgNlOWFUW4?wLxFaVmEI=qUJw15SDt}YqUKVGu*=bH0a5g0 zed&|?pph(phnPh~0A4raZfG8ZA7o+Nb*Z0ii{PnF*yPBed1;0||V zvRR|=(u2<_hO0r*>SaB-KbbNpbfglr7BbQ~C8yPnJ)#v%-+b`hP0y@&xM~KodaOjZ zuIb;oSM*%{wCYLwpp$`KrYz-3j_P59Y%3RmQvJP1|BB@|Sx{qchK1)h36-PvB1-}G zveta$(N2}71BvcgdPO#UA#$ik(@Bnff2c98UAIDik;r4h>L_+`dpD5HSX!y8dBqj#%9IwjifOX_<%4REh$K9XRc2mzQNi|J_az)2+ z)63};QcAgnZI7h(B*>)ss*zdquYoRQrFX;)C0fL~Mk ztoxs;+{HxQnlQp)Y>%+CM8ls9L3#(aSC^8gf!)mSr7Qv9)p4dn&P;p4%SLBzM)lI* zSf+8glW1s>bi7MAc%IBIVrxh2;oVpkVgX&Bu2M?M==kfq_%&Yz6ulx!Ue{je^!4|A zT^P_|h7=0zVRSt>tF&u=7YNNnoruS0rBwd(hn_U6BvkHWz+ZCYt5LP01*%G;Hj)IYe=R6 z{B}1PH~kF#Ymq+zxowuzwQ8EP8KHHz2Zo%w$V{KIT6+rPCM=VPlIqA*sswjekJ_bN zjimfUs;47I)j=w;LQ_js>Bi8cn=qvQ>I#O#+D5ZZ-cRb|^2~y|uX=wTkg z=u)z}+JC{FGQZv#1jThG)+`@!It4GdV2+ARq_-cf@E^~xwtb8f!KAUh8t}uU6fTdb zXj{#BInrFT5VY%C)t$Py{K{@1Z}y%PY87|2mcMU7N2wJ)%Rb54a$@+aFV88_Bkwit zrKb~bUrkg`#;VcUdcDP<6NoZJqgfW*O>-1+Vpzy4WkhEsxh+|y@2lgvIYfs5+^4N9 zL^!$Qo2(;vUfWm^CJva{n|jus@7@y@vOy2sxlQW*8MkgRf7w}`sMU?UNVfPBE?IE= zm8CQAr5C3r-z*2;lRj`IHl_lp@dIyN+6J=`eh4dDJ{V?fwqXz6?)SQ!Ju!HbAaVx#ZB78nhy+D1t% zgAsts(#+kY)%sZ{F8f-P%ZT!wopGa|g{_aQpPZ7b$?HlIM5|ZaL{R(RH;L$((yh{{ ztLc~JnSj|n3RxB8VWm2Ga(bjvP(K&zO2$3v-?oLvd}fzr&fBP%!=DITl(SIGiUWRdykA;Nmhm;qS&YDSag`745#W~sIAXzpvCiLaW|93 zx$vn;Vh$nE5fNz21Kvrp37gDmF;*WS6yHwtOIeZf9!$oRs6v>38QVB}_-~7I+VyA6 zJb`V7hf7TTwNj0KpHsX$>xb(<$?~?3BG|H4$JV?P{TQ>zQwT|EF&~L)N0H-7GqyGh z&psGs*d>PFC}!4QBmYhq<>O^d?Vk=2*oH|2>Pii><{-YA=S(&lEyozGD4ab{NUGkW zX;(^2UhYlFXzis2Ld*zkXR)qydRJcdQTGujM>gV@3<|>?P(2XP@g}~}+T7v_cS~YG z^(S@a75rgQg+!FVc8uU&?+b@i>Vr=YbPz{@yhl^1Q~F^=%{JiJ^v@URw3d!pDX~%W z?oKs~EhU0Rnr}sM>w=w6Z5TH6Vvzzm*^Vizt#Y)9)zWkUm|fD!=&2=J0sA%(!nz>x zA~zrddej*L;vBH}>Cn#g^Vvo~;-ZOToy8c3`}C`(d33TMY}47|e3Yd#F4`xG{3uS_JRq=3x{srN4iXlB}I_X{$K znwGg$bm-ISt79~^bklI|cd=^d{bse}jho!0y|+v5hZTvfv3SG|e!MVfjsx>Fu&ZbA z&qZQ>q_rtNKX6*Mvhmz4d)!vvXYkt~BX#RG#%m_Y!h$#<<>(>ctKwnJcV}kSonfJP zKDH>TBdOF6i+ftHJQZzqwNnwXhWHIe_&X;``Tk{nBh#gM9ndx{=6M#5W6~$l>RWha zVoLfgpWU)exORLxbc5;RWx+K*qEH?~x-n(~@rsMobcc5>@XmO?slOg=C^+)C2jC>oXrTnP(`} zdJroxc^$6M2vceg?yT#z8~VX#CcT-bDRrNtxH>U1pH~x`Gty^Nc-J#Dlvi6{Z7)h5 z59U^If-+*GiZlKk&JJEk1x~)BlI>a1;Jx7>p{d@^?MzHURkc9)ZiDeU*~o>OrC*Dk zRm;B!bZPOc(j5b<4rPq)SLqAM29$)ybSOePC;^LDhKF z0tgWxFgomik&aq6&dNB_-nu+7TcjxSKXJ$2!^J;DWb8P>GHoJNDp*-PCK3Xvd@D4w zZ1&UHeX$Wwc)toX6{5>7U0T%|!p>`%51{KuK;AfDGro+hoc$>|^UU?*AWKjSKm zvxBbhylr>OlzAsVRG~(WS)dS*WI!eP{7xZQ)3C>+`6!p52dc%xCoD&&9sA+(7~z$G zx{he&v2gIKGZM>U=9{_JY8ZXH`TJshxDa{yRlsIv=p~QGs&?e;Y-bF82zY@eI8ol_ zH+e3PoY2cu;qRAUh3zNh17Uf0>`u757N0D>!4aqFh7>|0oDAxJA3Dg4u3RhAhug0P zz0a_xVcJ=ZyRp51!E0)yoNo1~+Oc=G7;Zp3*n>WBJejU;@@X%GHr_R; zmx*;So8<(PmL)o9r?rf-%Ri}x-q!DNAN^B5+jjAFj4AYHg1T%)cW?O`cVQ1&6v8y4 zG8r-a90C7eh7fj@N&3=a@My5(Vt>CYfwkecH!Q~C@@HWWHR?IjZe3^fy@@cJ;o1VT zr};fu)tdcREYsms(vIOfTFvH@42}dAK>UoZCgdA+a|k=e&6rM0%=b>bChqkm%ID-y z+U``k^Pu|?ug#NRrT6Znrbc*+2OGsr^umQ1USzyaCxiKml-rtSQ|~{zT-ilfrCi)a z``2}B1bdewO+sTlC6T197!;*2N)5WdM$a(Q;h@)^E*ax8j`Jcq<>~u$(R86oKDQRA zxilK^_7XYl?ss8lbrzswZZ$F+!+sGIcG~OlMeQ>pX5HNk|;9Vr-!Vn{#qPpFks^ zsvpvZiPN+QG~JNt?FFO461csH~A2Sp9a(O&}MrO#zi zBJCqCfxntuwcuPz3T9(kxIX%(FwE51JJpKRgWeZe^x`$#*9KT7MY|V#+L6;*#U;_SM`c zMfjO2uvwp9V*7vz?^_YhadtxOQ?B?E8+>oIKNV+;zS{+RVe_hec*j%98h+-`%0EG? zYO*W;UUJizcZdks^ezNnRg`RqB#aeXkP-n50~esvWOZae#f5mc9sDbc;l~7gE*QD! zqrfn3qG>=HOUlGvttH{Eo}NhUSuKr`RC;fDdNnlyP~iNdlPb-(ddgm3 zUo1`0Fz{AWHlF?5g>VJ;^yV6$!X=MC?Vz;*Cb zoV7r*-%F?=fqIq%8+ogYvzOVg{jS6q{2NK>O}~6kq-1_~I)g;IV(yOPTB-)z&*nH9 zG3C`I9AD;5G+jMB_{C6q=W}y8!uGekRyaJm>!0npHpnSkI?j%;vzoyM6kgW6o|^C> zuNImFe0QtgI^+B)i4hcAYP*ht-r;Mj)rIqwyY~VMGSSbJD22)8bSv3uCiV@CD8~^t zYFlepxQNyrX^r|eMcvK3)hqfkFo>&qtvy)gO#R~*eYZbV!&zWHgaP_oF0LoHlhBT2+4N>`#e$N5KN50|;u>&-__m$_3V#G1CF{sQYO zxiL!h4b0ApiIwbIrw5x!~veQOx2Jj zltEl@`!>CcQFSd>(av!pcRav@yXI3YyyQL@JzdI-XJGVcp%)i&;B23uRG4Ncl$ffz zczkc1^o+_cK`*Lx3v?SAdAn;G-*VU^=w@bM^O7MaN%+b7d(Z|Kj$qp1Kx3T*;%-JU z^}G-i__|^%tD^Qu_mARJ1(RBBgH_xBmut5`&Z5gJddi~*h8tRP^vc%1BUgB=+N{OO zcP1=X4OrVKrI}1+{8yVaFS35*dU(!+jVTruj#OD7>%`$sbR=mWX1ot9cj^_SVD3AO zA7h9?M+-VV*^~TYyr$N+L`o9yK4vbb$C>jG{NHXKhcnq0F46uR?X9M2*sbW*7bGxu z)_rmXRmesfb(l+WlfTq$;@-z*Zn-} za$iHJC10Os5;#1=9GZJs=?2GHwe~HRl^YvTpFP1$yA_QJ#d_R5G6G5dxcO)n{7+ z)AyZzOb=gV_9jT4o2x3u=Hlg7c~e%T*?+Qxa@t`4o@l>f3Gzp{RtFAv@h_O`xfi`D zo0eMK%OsQZZ>VUI=a4cwH8=h-JfSFM^EGI&m@v`--<`^!`EBD*9OpY~VmK(|iS>xy zS<=niX|g&o^-Vp;{)t)uF!%{XARxc(&oCRq`uK%MvMhZ`yQt>91Gs&k}){Aw1`4MM|@n-Rw3fz0aoGk)O_D{9itLkW;vg(pzri7cWC(Um3Dg!H2z zdep!rqO^Klub|lVG40eABx(mu_2+$z!EH@K*bs3qPMk|&-54Z9yehABp4`iy=f`j% z(>=$rzGG6+@`_MDslN`J2hZA*mdy^^E>!Dj=H6mF47`1PRRK0uGTPi?Gof)FeB*4k zX~uVu4eEHESC-J-SD0m%Q!y&mf@Zp_$l6pLA}E)4RJ+^u+E~J@eHdcf14S77sE*-APc@7ZQ2lw5cM9Qlo3rCDYt zB15b@+N;u5X%E>o!(OQ)={RM?;}Y4D4No8F#AD^ugD4(*oq%P1i_IM(DL9PQ72gx>lFGxw zHauVh)RjvdI?FE=b?j_>&fcAWgJusbz(p#01epVGSa)^X5lP1@$?AHqwRo-e>I@xm zQO&?=W_EaQo@-hw(t%t2>0FWP9Qki}{2onyWYUzCrrwP(***<9H0#Y<8VTd)8f3a` zT#v#y4U3rc7F`t|i#DukeJ}O0(#(XhE-#R80A?7FHJNKt+3oREQ9uwUjGKp^$5M>? zMliny2KlM>p1g@f-Z{fIuG!QJ@&3i>b4k53*W`C;wfIea&aLQ6`p$2>odvt1X-JFf zf5JVUu6~r-EQ_2X>(IJ$@j`>1Hy3&L|_nbJI&kDefpy42QN98e3*;+#guJw zMRA!@Yd4~Fe0>IlNaV?r_^AAp4qnPrCirItRd+hTY|=EZ@oVl7_~cTVtSLsf<5oC2v%AkiU?jyA&30Fjv6&q; zm;K0m%v+V+TG`@K&6M-`p9Pc-UdNF zn7D~wVqV-LDzY_i*9V#~@Qt?v7ffy^@{msue0NM^i$$}Li!2k<;|G#?B*27DjhQ)r zJs&M*VoD)L0tX-IC&dK^ibk!pKF8=FF-2+`k-(PO&6n`Z&P3}($}3VbRt|iggKI95 zlGjUzS_iEGi*ZI4mIv0*O6miPAiZAzg(q#tnzbU~0?&pa>w z`5EO>JN!EjBD^A8k_^!e$=2I9XR@Cr?eW`(C?Bryv^k^M0J~ z>LP@0_yP_tXjP12xBGIMBVQ10<)F{M21?(jC@;R@MDL2tpA|iur{WEC*M?%hd+sRL zguH*)w4heTjN2uxrCn?xO*R5JQ0!wm$j#N$D(O`x4=Ghcr=@wd6z9=nTyCQ$nX)Yw zPDD20XlcU$75sTE;m4&r6Ea0Glke;9HD@G%b}_*a9FS|XlaF^YMT>|SOp{(72-qW# zqVuGlD2{B%g||v_+2lnwzF=f#{xJmPq}l?YfSzt~DvI*ujA(CynTf;XSx*?mXx^7A z0G|;s93xw$!GipjQ(lc(;EplP-sGz|VoIc-h8o~#Xfx36B*LiG?XMa1z0v~%sKDE> zKbi#`fS2UnzLe7wUo3P>2$zDmZ4^}I0UJt`OaPB$C!|$6AxHn_TwbE#xZx9ZO%Zdk zoA!3epESUzss}77;QXQ6UW*@Z&x17{NTDiHK4rGd6u-ehw&Qt zwhsY8OIqN3l~d@v*p$6VyB@HnP~w1k8u}GvfthH1>)xLywSMd zz_#s0a}vhk=0M(R1jtQMe|jZ3Od!~dPP;e!qp3v(LofXTtgp3rB?h#m1@rE;1TAV$ zoJuG;`1Y+8`O}bZW+sE5*T{;+{!Irm!t#=@8x<(2JD)E%t$K+hf5{Cl1432{qj>)b zobGe`YZmkNv`05dHQ+MgAITMlvE>R8Dl<$xfj`$Sd1%79>HFQO>hLUoc#|$?Vu+-= zJ^@|&2TOFNtuGM?aVe&50CP@w*9=`jD1Z%hX0=ayER_+qAg8d&JQH=eq+O63Q z`m>nHG4s-PH(FAZNEUUlsUTe)whT60$A3slIr@n(IkZm+SZU*SlWFU?R3wEbrT>PU zL8YGF8Y;iE(c5sY0T{fguMaMcd4*dD$zkT}m+O0(F$9}`_z?FhRyoyE?hluyGeVkI z3BK8`AdeK3B=jfIjmAI<{=(9V67ulTu%sx3yXIPatD zdsmC%HEF=Ey6oQJJJziExY=|_qvqbOI?Dk)E&9sOLY|BpITMNIxi(*KT&0aB582x< zJ*`^m+v#FFgeQb76TZ8gh60zpo89AkQQeFYyafx|Rx&EQ;VrA$SO!yjgM>Pamla?I z@f+$7Sx586{YHF1=%~CE1k>L+v}ODW=KLaLY$ThSDeB6pBB9SzXdvQb$}a|0X~TRc zdt;IK{6mPpNj@Zv*WrqVnrk2j6UWJJAtIhF72KNtF|mPb<+#NKUOxejzF$5*q} z0!7hZpiWX@wZ{kWr8H$AW?jhX*C{;`Qq)~HeC6s3 zJ~9qlmM?~=8CqEi`U-R@28jH*=ed1^%B-&E*z@jxeDCg_>R=r=`IyMW@7jFpau5Ez zfo4Hv0m-5>evM-d(W1o*a(d^0l8VE@cJ=nFX2t65Kjcpo9!|%n8D&7$>KWE=+$pW| zW2<-~W{198kvcdOAr>G1`oeiAQrnmia%a<2scu=S^txIZuD_q%x)&bx;*SSO`gCfrEb5FA?Ckf80bS=_Tg2X?r&V z0%TH9?2_~&j7727{^*sp8>?p)sr&m=-U_1>P{_4Fztibi)^wd0Nj;YNQs;-^22hOi zp1j5lz3%uOK5g@h&jH@}3WldoEY5Z#HRA@)CCO{LX~KDA)^fViiKmtl zscuKA%JG^$2YKV8qh~uC+-h7K(34r8&I&COFg`p)l@GbY}R#n)&{T8$8(f@le34)DZ2(C^i|V#}Wlc*x-{j{dB0%gckJe{3Qeq+{`O;vE@e zLD6q>(sv?iD|80r`>Gq;I`DY=zAN%Pa?AMgQM2a_xVprZ&ik4D3$u8^;PDKzt?8ckvAep-w;BcHhA}$4+w;1SI?G z2ops71!eqOh)xgeaaI*94nIUh<0TV=$%l&NU51%%HchLy{pw~OzdZBkK+efw?gA~5 zh(Z}K{4LFVp3euUsxMYmI8;9IxlzKaGT|U+ayI_h_^9M5qWFSC!D}w%)Mk1U^JjUQ zM`XTIKA15MxT3hnMdR0=AJ#RuK3crE9(>m&>5|3*ANVv(TZ(*YKRX{gr;QcY7^i3` zi8ZFvyQGh%ajowkkzyroILz2xysTAQc3d@hAYImVhS-6oDrbt&L}fVIJNVLT)Ug$E zs>wOyBV2~fS2vf0w)~(hUe>TLob(o7=y)HEL@3mK>urLj4>q=iEg$w5LE;)e6QxK8 z#WOhLdGu95_B|!)S6Y4@GBB(Z*LKl=-`8X?Q_URpYm&3U#M*`?ee-%MwWZ#VJ+dxUhkR{|7!Sw&!#mM45QQmoaJD<>|N83NNMbAn>|}CbHB=!f*L;-X}WftSc73w zdtC^tQ{a;k%~KR<{Ch9?+V}j5uq-nDy7Ui_-cT%@5!NmD)pk3Vf{y6Ve%rt>i4Zya z9Zaqjxq6_R-r?43u<$lvsmurr>E?TV8{lMWEHT?3{b^s|dbN;7B0be}JGLcjI4KZ@ zI6yPBbX>o^ZJfKJQ7|nAZa@3`wS#Tm6`N9oFweqzd6^i%%3QHB{F zWGIa-h^e;6#5Hr=m4akMBA034+1#fRmCBX=Q~$ji86zAKP(r=fl!>NnO9^G!sJ|Ns zIbEB*zjLU5^6w2{xX$L>-=uPt3*VgEo6=^g~{?wR7AD-n4(3ZxSE8=+fbO zY_t;2sE-K}lnl5SsT7I>wH1b!+Za0$k@7tZk7 z?KV7_N!#V~^JELTxjHy;x*iav!m8~qE%V21s3>IOL|+#adk|W=G6oIGq-KnP|@mwilfkA z*VA1^+3dDm{n_J`Y!(h0iuUjiXN;1vnt}JXrwz-cb(k}_=%fSaDuKK6NkBkXmAHiW z3rqWsX(-PJb=#%!O4jLpt013MN8#Bf9m?+aKNDIFuF6MwJ!&io-DHFoy*!^Lds)8t zH+*0n8X<@P6SmBz1^F~)r|pj4Utr#O(6)P-*pdm?u0wv1k@Qzuwh0ZiY>q8RV&S(uV52d}i;2BZs>A1}-qR zwoc-`8t#Y^7GCYY?1Hof(TWV&brA@9_I6fX)$M-F4jp7DQJA0=F9XX{0yytc5w0dx_k&5>T$@We&4D=}bE>(=?_$4Q_zeb@J-S zP=^D=955g<@1(s^X&e^>~Lce?6 zKW&5>autP*XNEQtI>;Rc40Jt+r}?B;;vYT)8T#gkykEH$Ms{QjP3=89JN*1fo4z$S z<@1}=>o$9Agv;Y9$j)~!o-_q0^o`aSi0C@pGIpp9-=XngE59K5oi({f7MjGsq7$-b zHbHj9Jmg6;U3uTZ$BaE-Ys)ywEzl@@nXYj=iShU@I|@a~m4Zj8~O_=_Qp&e1G&(FCpEadzaaA+yjfxE7u9p=lp2j|Vr>)ZcEfV}(Oop?pW&mmYnt zmv05u?hIRN*~B3u=wOBPl7#$4T{!(kIiYQ`T`OBoF~=vu1SeU)@fM~x&8MZB^3KY5hC!giE1gZjyu8R>8yKs_37aS2gGx@{r)}l+`at3>*4Xa zng}E5%ad!{`O|TyY@s=H!wrpAX5Zl7|04|&kM%=h(z!6~$Kg>1D>_lGRFN{6Mm9gi zZ==W~siNgFrz9eN$zLD35=MB*>PL$)+4x6Tjc|bTbH;K|8m70QO>G^w9H&_U&z?W$ zCP5+6%v^06x8`3v!1WazT-%3^%@zX&f^@jI-zi9F5vymHlL5C^DYurc!}N5W8~yzIa}Ho^Xj zu2N|(m#L2b_K^L4OedN+wd9ftbmWR0dO4X#5whp3h(~9f2|W%MdfG%Xh1*}Igjo6A zm|Htx2F|<`>Pc}II8lRBL^e@JjhLbk?3S|R5pY9fgCbtBrpfpea+6cm*vrWwfs&Gn zljzUuv-EmUVWFsF1ehR5-m2T#1!Y+ATuS3#GukChGIF$RpU>n6Esd>!UVl@zeUZdw zjxjs^?$7?%*}@LDgzRaVki|c0&$y)+HPunCC7A0(MNWuOWllhxO=Ps;uh|Tbxu-_D zzi98aH1H=u(&QMEGuVSqn@Jf&3E3z%El&!j);EsRj{Ati&)E7d(~xATY?Ur&-kiwd zjfTlpU%lGyO@8xBjeoI~<6jLsXq<-Xj0j97^xp^%lMWlE+yRaGeO0!zjRgS1I|%Az zoosqJB|>RGo7^M3FF)!x8lZy#od7T+d{s;ndX7T*&0ZTO*4+iL{zK3Dv{@Z$lL~x+RsCuikIGW^d8Sekl1N~2(^#8}U%j2bc ZEV-@mkjRI2=)bdUyp>m#E0Zw|`Y&h}P)q;- literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate3.png b/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate3.png new file mode 100644 index 0000000000000000000000000000000000000000..ecefade79f45571ff7f1f40d8e1af9a59c1321fd GIT binary patch literal 60047 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 W(d-TJm$>eF@myV7twO~-_&)%dR&!zi literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate4.png b/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate4.png new file mode 100644 index 0000000000000000000000000000000000000000..3c86f1c4ad98ad3e3de0a8fa027d4882d20ad657 GIT binary patch literal 59009 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˙1)i4 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 literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate5.png b/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate5.png new file mode 100644 index 0000000000000000000000000000000000000000..dc87798ecc4d9f85b75451f54642fa400a7dd0d8 GIT binary patch literal 60143 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 z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly sK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlya8uyF0ninHE&u=k literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/arduino/data/custom.txt b/lib/PsychicHttp/examples/arduino/data/custom.txt new file mode 100644 index 0000000..d3db23d --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/data/custom.txt @@ -0,0 +1 @@ +Custom text file. \ No newline at end of file diff --git a/lib/PsychicHttp/examples/arduino/data/img/request_flow.png b/lib/PsychicHttp/examples/arduino/data/img/request_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..1005a38895f77797f39ecd018d5fd6a746502a62 GIT binary patch literal 30509 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 + + + + + PsychicHTTP SoftAP Demo + + + +

+

SoftAP Demo

+

You are connected to the ESP in SoftAP mode.

+
+ + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/arduino/data/www/alien.png b/lib/PsychicHttp/examples/arduino/data/www/alien.png new file mode 100644 index 0000000000000000000000000000000000000000..a030da0761a60fbfc813fbef0716f801b7aa5ca6 GIT binary patch literal 28598 zcmYJaWmuG5*Dws@07I9c(v3)ofOHIvbW4}Clyr9^NOyN5-5@DFv~)>#H+&~v_w#_92^|Fq=bkf931@l^B)8e_|M?S$Wr;jgr@UiH3RbUqIq6nxmxyTg3pllF+C|;?J8R22|OHS|$5C&}~C$Cp7CMXc++U4Q4NgN|sM6}41E?6Tp^Y-YI z6#fhiLo&L$mU?MXaTZi`JA7SWs9of%+UM(=S3bi+!8kp6V9v*Az4tWod4Y=G#KQ2K zDcB2cxINq5-$Y_hu6%la!EBxtD%w8(hSb9e7Pwy4*Eb&|887A+KvZ>oyX1?J- zM_ibe8Z;Ta%NmvTHtCP(e8mmObB2XPqVT@qFQ_-LKpD z4T?X3%T@C>*_O^`P3Wj<7WhmK$V3NUz+mPR`cM4KCr_Q)UaR$Yx(0YN%qF?DZ&y`N zAGIOqU_mk>5Z1YkX;uX#^55%EvUo4Qj6EtfAk?lN4O`*o5JZ5poyLV8JS&{(Mbn>< zPX+E(pQQT2yBS@Z13j>QB*B5TH)n+hSOA@+1Vb@U^q_?yjMU z2~%{?Q*7L)c~U=7T$BaddgOG67^VOH%SVnbMvG+wfrm5+(-4@EWOT?a39i3J(fMud z%doaamh8Zr9~Utk%RLx+ZlaL!XTy>H=3cr z^tlDkmg$ILOCXLaEFjp1f~;drXNd6l^U4k(HtE%O+S*0|5bJ9YXdDoR6psUhR~e*p zR7Vh`eK)eTaTZ4Rx+QqIV=*g2R3HQyvbx^*=o>lZq0au<&P=SA?v8o{G0U$*5?K%? z7YZT%LRl#i{D%PHQC9gENY)

&213^-9%ncHPP24Z3H*pr~$xg!|8Chf_Iiynyz> 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 c1aJ>H;&fJ5{Z*>gh@M|f|*Vo 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 BlhptK literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/arduino/data/www/index.html b/lib/PsychicHttp/examples/arduino/data/www/index.html new file mode 100644 index 0000000..4ee2491 --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/data/www/index.html @@ -0,0 +1,236 @@ + + + + + + PsychicHTTP Demo + + + +

+ + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/arduino/data/www/text.txt b/lib/PsychicHttp/examples/arduino/data/www/text.txt new file mode 100644 index 0000000..5375816 --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/data/www/text.txt @@ -0,0 +1 @@ +Test File. diff --git a/lib/PsychicHttp/examples/arduino/secret.h b/lib/PsychicHttp/examples/arduino/secret.h new file mode 100644 index 0000000..6d4bb15 --- /dev/null +++ b/lib/PsychicHttp/examples/arduino/secret.h @@ -0,0 +1,2 @@ +#define WIFI_SSID "Your_SSID" +#define WIFI_PASS "Your_PASS" \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/.gitignore b/lib/PsychicHttp/examples/esp-idf/.gitignore new file mode 100644 index 0000000..3413246 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/.gitignore @@ -0,0 +1,4 @@ +build/ +sdkconfig +sdkconfig.old +components/ diff --git a/lib/PsychicHttp/examples/esp-idf/CMakeLists.txt b/lib/PsychicHttp/examples/esp-idf/CMakeLists.txt new file mode 100644 index 0000000..634d58e --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/CMakeLists.txt @@ -0,0 +1,19 @@ +# 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) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if(DEFINED ENV{HTTP_PATH}) + set(HTTP_PATH $ENV{HTTP_PATH}) +else() + #these both work + set(HTTP_PATH "../../") + #set(HTTP_PATH ${CMAKE_CURRENT_LIST_DIR}/../../../) + + #this does not work for me... + #set(HTTP_PATH ${CMAKE_CURRENT_LIST_DIR}/../../../PsychicHttp) +endif(DEFINED ENV{HTTP_PATH}) + +set(EXTRA_COMPONENT_DIRS ${HTTP_PATH}) + +project(PsychicHttp_IDF) \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/README.md b/lib/PsychicHttp/examples/esp-idf/README.md new file mode 100644 index 0000000..ef91f3c --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/README.md @@ -0,0 +1,7 @@ +# PsychicHttp - ESP IDF Example +* Download and install [ESP IDF 4.4.7](https://github.com/espressif/esp-idf/releases/tag/v4.4.7) (or later version) +* Clone the project: ```git clone --recursive git@github.com:hoeken/PsychicHttp.git``` +* Run build command: ```cd PsychicHttp/examples/esp-idf``` and then ```idf.py build``` +* Flash the LittleFS filesystem: ```esptool.py write_flash --flash_mode dio --flash_freq 40m --flash_size 4MB 0x317000 build/littlefs.bin``` +* Flash the app firmware: ```idf.py flash monitor``` and visit the IP address shown in the console with a web browser. +* Learn more about [Arduino as ESP-IDF Component](https://docs.espressif.com/projects/arduino-esp32/en/latest/esp-idf_component.html) diff --git a/lib/PsychicHttp/examples/esp-idf/data/custom.txt b/lib/PsychicHttp/examples/esp-idf/data/custom.txt new file mode 100644 index 0000000..d3db23d --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/data/custom.txt @@ -0,0 +1 @@ +Custom text file. \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/data/img/request_flow.png b/lib/PsychicHttp/examples/esp-idf/data/img/request_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..1005a38895f77797f39ecd018d5fd6a746502a62 GIT binary patch literal 30509 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 + + + + + PsychicHTTP SoftAP Demo + + + +
+

SoftAP Demo

+

You are connected to the ESP in SoftAP mode.

+
+ + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/data/www/alien.png b/lib/PsychicHttp/examples/esp-idf/data/www/alien.png new file mode 100644 index 0000000000000000000000000000000000000000..a030da0761a60fbfc813fbef0716f801b7aa5ca6 GIT binary patch literal 28598 zcmYJaWmuG5*Dws@07I9c(v3)ofOHIvbW4}Clyr9^NOyN5-5@DFv~)>#H+&~v_w#_92^|Fq=bkf931@l^B)8e_|M?S$Wr;jgr@UiH3RbUqIq6nxmxyTg3pllF+C|;?J8R22|OHS|$5C&}~C$Cp7CMXc++U4Q4NgN|sM6}41E?6Tp^Y-YI z6#fhiLo&L$mU?MXaTZi`JA7SWs9of%+UM(=S3bi+!8kp6V9v*Az4tWod4Y=G#KQ2K zDcB2cxINq5-$Y_hu6%la!EBxtD%w8(hSb9e7Pwy4*Eb&|887A+KvZ>oyX1?J- zM_ibe8Z;Ta%NmvTHtCP(e8mmObB2XPqVT@qFQ_-LKpD z4T?X3%T@C>*_O^`P3Wj<7WhmK$V3NUz+mPR`cM4KCr_Q)UaR$Yx(0YN%qF?DZ&y`N zAGIOqU_mk>5Z1YkX;uX#^55%EvUo4Qj6EtfAk?lN4O`*o5JZ5poyLV8JS&{(Mbn>< zPX+E(pQQT2yBS@Z13j>QB*B5TH)n+hSOA@+1Vb@U^q_?yjMU z2~%{?Q*7L)c~U=7T$BaddgOG67^VOH%SVnbMvG+wfrm5+(-4@EWOT?a39i3J(fMud z%doaamh8Zr9~Utk%RLx+ZlaL!XTy>H=3cr z^tlDkmg$ILOCXLaEFjp1f~;drXNd6l^U4k(HtE%O+S*0|5bJ9YXdDoR6psUhR~e*p zR7Vh`eK)eTaTZ4Rx+QqIV=*g2R3HQyvbx^*=o>lZq0au<&P=SA?v8o{G0U$*5?K%? z7YZT%LRl#i{D%PHQC9gENY)

&213^-9%ncHPP24Z3H*pr~$xg!|8Chf_Iiynyz> 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 c1aJ>H;&fJ5{Z*>gh@M|f|*Vo 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 BlhptK literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/esp-idf/data/www/index.html b/lib/PsychicHttp/examples/esp-idf/data/www/index.html new file mode 100644 index 0000000..4ee2491 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/data/www/index.html @@ -0,0 +1,236 @@ + + + + + + PsychicHTTP Demo + + + +

+

Basic Request Examples

+ + +

Static Serving

+

+ + +

+

Text File

+ +

Simple POST Form

+
+ + +
+ + +
+ +
+ +

Basic File Upload

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

Multipart POST Form

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

Websocket Demo

+ + + +
+ +
+ + + +

EventSource Demo

+ +
+ +
+ + +
+ + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/data/www/text.txt b/lib/PsychicHttp/examples/esp-idf/data/www/text.txt new file mode 100644 index 0000000..5375816 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/data/www/text.txt @@ -0,0 +1 @@ +Test File. diff --git a/lib/PsychicHttp/examples/esp-idf/include/README b/lib/PsychicHttp/examples/esp-idf/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/PsychicHttp/examples/esp-idf/lib/README b/lib/PsychicHttp/examples/esp-idf/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/examples/esp-idf/main/CMakeLists.txt b/lib/PsychicHttp/examples/esp-idf/main/CMakeLists.txt new file mode 100644 index 0000000..ad22981 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/main/CMakeLists.txt @@ -0,0 +1,8 @@ +# This file was automatically generated for projects +# without default 'CMakeLists.txt' file. + +idf_component_register( + SRCS "main.cpp" + INCLUDE_DIRS ".") + +littlefs_create_partition_image(littlefs ${project_dir}/data) \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/main/main.cpp b/lib/PsychicHttp/examples/esp-idf/main/main.cpp new file mode 100644 index 0000000..21c18e4 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/main/main.cpp @@ -0,0 +1,510 @@ +/* + PsychicHTTP Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +/********************************************************************************************** +* Note: this demo relies on the following libraries (Install via Library Manager) +* ArduinoJson UrlEncode +**********************************************************************************************/ + +/********************************************************************************************** +* Note: this demo relies on various files to be uploaded on the LittleFS partition +* Follow instructions here: https://randomnerdtutorials.com/esp32-littlefs-arduino-ide/ +**********************************************************************************************/ + +#include +#include +#include +#include +#include +#include "secret.h" +#include +#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE //set this to y in menuconfig to enable SSL +#include +#endif + +#ifndef WIFI_SSID + #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." +#endif + +//Enter your WIFI credentials in secret.h +const char *ssid = WIFI_SSID; +const char *password = WIFI_PASS; + +// Set your SoftAP credentials +const char *softap_ssid = "PsychicHttp"; +const char *softap_password = ""; +IPAddress softap_ip(10, 0, 0, 1); + +//credentials for the /auth-basic and /auth-digest examples +const char *app_user = "admin"; +const char *app_pass = "admin"; +const char *app_name = "Your App"; + +//hostname for mdns (psychic.local) +const char *local_hostname = "psychic"; + +//#define CONFIG_ESP_HTTPS_SERVER_ENABLE to enable ssl +#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE + bool app_enable_ssl = true; + String server_cert; + String server_key; +#endif + +//our main server object +#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE + PsychicHttpsServer server; +#else + PsychicHttpServer server; +#endif +PsychicWebSocketHandler websocketHandler; +PsychicEventSource eventSource; + +bool connectToWifi() +{ + //dual client and AP mode + WiFi.mode(WIFI_AP_STA); + + // Configure SoftAP + WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00 + WiFi.softAP(softap_ssid, softap_password); + IPAddress myIP = WiFi.softAPIP(); + Serial.print("SoftAP IP Address: "); + Serial.println(myIP); + + Serial.println(); + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + WiFi.begin(ssid, password); + // Auto reconnect is set true as default + // To set auto connect off, use the following function + // WiFi.setAutoReconnect(false); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void setup() +{ + Serial.begin(115200); + delay(10); + + // We start by connecting to a WiFi network + // To debug, please enable Core Debug Level to Verbose + if (connectToWifi()) + { + //set up our esp32 to listen on the local_hostname.local domain + if (!MDNS.begin(local_hostname)) { + Serial.println("Error starting mDNS"); + return; + } + MDNS.addService("http", "tcp", 80); + + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + //look up our keys? + #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE + if (app_enable_ssl) + { + File fp = LittleFS.open("/server.crt"); + if (fp) + { + server_cert = fp.readString(); + + // Serial.println("Server Cert:"); + // Serial.println(server_cert); + } + else + { + Serial.println("server.pem not found, SSL not available"); + app_enable_ssl = false; + } + fp.close(); + + File fp2 = LittleFS.open("/server.key"); + if (fp2) + { + server_key = fp2.readString(); + + // Serial.println("Server Key:"); + // Serial.println(server_key); + } + else + { + Serial.println("server.key not found, SSL not available"); + app_enable_ssl = false; + } + fp2.close(); + } + #endif + + //setup server config stuff here + server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + + #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE + server.ssl_config.httpd.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + + //do we want secure or not? + if (app_enable_ssl) + { + server.listen(443, server_cert.c_str(), server_key.c_str()); + + //this creates a 2nd server listening on port 80 and redirects all requests HTTPS + PsychicHttpServer *redirectServer = new PsychicHttpServer(); + redirectServer->config.ctrl_port = 20424; // just a random port different from the default one + redirectServer->listen(80); + redirectServer->onNotFound([](PsychicRequest *request) { + String url = "https://" + request->host() + request->url(); + return request->redirect(url.c_str()); + }); + } + else + server.listen(80); + #else + server.listen(80); + #endif + + //serve static files from LittleFS/www on / only to clients on same wifi network + //this is where our /index.html file lives + server.serveStatic("/", LittleFS, "/www/")->setFilter(ON_STA_FILTER); + + //serve static files from LittleFS/www-ap on / only to clients on SoftAP + //this is where our /index.html file lives + server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); + + //serve static files from LittleFS/img on /img + //it's more efficient to serve everything from a single www directory, but this is also possible. + server.serveStatic("/img", LittleFS, "/img/"); + + //you can also serve single files + server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); + + //example callback everytime a connection is opened + server.onOpen([](PsychicClient *client) { + Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); + }); + + //example callback everytime a connection is closed + server.onClose([](PsychicClient *client) { + Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); + }); + + //api - json message passed in as post body + server.on("/api", HTTP_POST, [](PsychicRequest *request) + { + //load our JSON request + JsonDocument json; + String body = request->body(); + DeserializationError err = deserializeJson(json, body); + + //create our response json + JsonDocument output; + output["msg"] = "status"; + + //did it parse? + if (err) + { + output["status"] = "failure"; + output["error"] = err.c_str(); + } + else + { + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (json.containsKey("foo")) + { + String foo = json["foo"]; + output["foo"] = foo; + } + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + return request->reply(200, "application/json", jsonBuffer.c_str()); + }); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/ip", HTTP_GET, [](PsychicRequest *request) + { + String output = "Your IP is: " + request->client()->remoteIP().toString(); + return request->reply(output.c_str()); + }); + + //api - parameters passed in via query eg. /api/endpoint?foo=bar + server.on("/api", HTTP_GET, [](PsychicRequest *request) + { + //create a response object + JsonDocument output; + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (request->hasParam("foo")) + { + String foo = request->getParam("foo")->name(); + output["foo"] = foo; + } + + //serialize and return + String jsonBuffer; + serializeJson(output, jsonBuffer); + return request->reply(200, "application/json", jsonBuffer.c_str()); + }); + + //how to redirect a request + server.on("/redirect", HTTP_GET, [](PsychicRequest *request) + { + return request->redirect("/alien.png"); + }); + + //how to do basic auth + server.on("/auth-basic", HTTP_GET, [](PsychicRequest *request) + { + if (!request->authenticate(app_user, app_pass)) + return request->requestAuthentication(BASIC_AUTH, app_name, "You must log in."); + return request->reply("Auth Basic Success!"); + }); + + //how to do digest auth + server.on("/auth-digest", HTTP_GET, [](PsychicRequest *request) + { + if (!request->authenticate(app_user, app_pass)) + return request->requestAuthentication(DIGEST_AUTH, app_name, "You must log in."); + return request->reply("Auth Digest Success!"); + }); + + //example of getting / setting cookies + server.on("/cookies", HTTP_GET, [](PsychicRequest *request) + { + PsychicResponse response(request); + + int counter = 0; + if (request->hasCookie("counter")) + { + counter = std::stoi(request->getCookie("counter").c_str()); + counter++; + } + + char cookie[12]; + sprintf(cookie, "%i", counter); + + response.setCookie("counter", cookie); + response.setContent(cookie); + return response.send(); + }); + + //example of getting POST variables + server.on("/post", HTTP_POST, [](PsychicRequest *request) + { + String output; + output += "Param 1: " + request->getParam("param1")->value() + "
\n"; + output += "Param 2: " + request->getParam("param2")->value() + "
\n"; + + return request->reply(output.c_str()); + }); + + //you can set up a custom 404 handler. + server.onNotFound([](PsychicRequest *request) + { + return request->reply(404, "text/html", "Custom 404 Handler"); + }); + + //handle a very basic upload as post body + PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); + uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + Serial.printf("Writing %d/%d bytes to: %s\n", (int)index+(int)len, request->contentLength(), path.c_str()); + + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + uploadHandler->onRequest([](PsychicRequest *request) + { + String url = "/" + request->getFilename(); + String output = "" + url + ""; + + return request->reply(output.c_str()); + }); + + //wildcard basic file upload - POST to /upload/filename.ext + server.on("/upload/*", HTTP_POST, uploadHandler); + + //a little bit more complicated multipart form + PsychicUploadHandler *multipartHandler = new PsychicUploadHandler(); + multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + //some progress over serial. + Serial.printf("Writing %d bytes to: %s\n", (int)len, path.c_str()); + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + multipartHandler->onRequest([](PsychicRequest *request) + { + PsychicWebParameter *file = request->getParam("file_upload"); + + String url = "/" + file->value(); + String output; + + output += "" + url + "
\n"; + output += "Bytes: " + String(file->size()) + "
\n"; + output += "Param 1: " + request->getParam("param1")->value() + "
\n"; + output += "Param 2: " + request->getParam("param2")->value() + "
\n"; + + return request->reply(output.c_str()); + }); + + //wildcard basic file upload - POST to /upload/filename.ext + server.on("/multipart", HTTP_POST, multipartHandler); + + //a websocket echo server + websocketHandler.onOpen([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); + client->sendMessage("Hello!"); + }); + websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { + Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); + return request->reply(frame); + }); + websocketHandler.onClose([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); + }); + server.on("/ws", &websocketHandler); + + //EventSource server + eventSource.onOpen([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); + client->send("Hello user!", NULL, millis(), 1000); + }); + eventSource.onClose([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); + }); + server.on("/events", &eventSource); + } +} + +unsigned long lastUpdate = 0; +char output[60]; + +void loop() +{ + if (millis() - lastUpdate > 2000) + { + sprintf(output, "Millis: %lu\n", millis()); + websocketHandler.sendAll(output); + + sprintf(output, "%lu", millis()); + eventSource.send(output, "millis", millis(), 0); + + lastUpdate = millis(); + } + vTaskDelay(1 / portTICK_PERIOD_MS); // Feed WDT +} \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/main/secret.h b/lib/PsychicHttp/examples/esp-idf/main/secret.h new file mode 100644 index 0000000..97048cf --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/main/secret.h @@ -0,0 +1,2 @@ +#define WIFI_SSID "WIFI_SSID" +#define WIFI_PASS "WIFI_PASS" \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/partitions_custom.csv b/lib/PsychicHttp/examples/esp-idf/partitions_custom.csv new file mode 100644 index 0000000..57f4354 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/partitions_custom.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x11000, 0xC000 +otadata, data, ota, 0x1D000, 0x2000 +phy_init, data, phy, 0x1F000, 0x1000 +app0, app, ota_0, 0x20000, 0x177000 +app1, app, ota_1, 0x1A0000, 0x177000 +littlefs, data, spiffs, 0x317000, 0xE1000 \ No newline at end of file diff --git a/lib/PsychicHttp/examples/esp-idf/sdkconfig.defaults b/lib/PsychicHttp/examples/esp-idf/sdkconfig.defaults new file mode 100644 index 0000000..6b0dc83 --- /dev/null +++ b/lib/PsychicHttp/examples/esp-idf/sdkconfig.defaults @@ -0,0 +1,64 @@ +CONFIG_AUTOSTART_ARDUINO=y +# CONFIG_WS2812_LED_ENABLE is not set +CONFIG_FREERTOS_HZ=1000 + +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y + +# +# Serial flasher config +# +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" + +# +# Partition Table +# +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_custom.csv" +#CONFIG_PARTITION_TABLE_FILENAME="partitions_custom.csv" +#CONFIG_PARTITION_TABLE_OFFSET=0xE000 +CONFIG_PARTITION_TABLE_MD5=y + +# +# ESP HTTPS OTA +# +CONFIG_ESP_HTTPS_OTA_DECRYPT_CB=y +CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y +# end of ESP HTTPS OTA + + +# +# ESP HTTP client +# +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y +# end of ESP HTTP client + +# +# HTTP Server +# +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +# CONFIG_HTTPD_LOG_PURGE_DATA is not set +CONFIG_HTTPD_WS_SUPPORT=y +# end of HTTP Server + +# +# ESP HTTPS server +# +CONFIG_ESP_HTTPS_SERVER_ENABLE=n +# end of ESP HTTPS server + + +# +# TLS Key Exchange Methods +# +# 2 option require for arduino Arduino +CONFIG_MBEDTLS_PSK_MODES=y +CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y + diff --git a/lib/PsychicHttp/examples/old/esp_ota_http_server/.gitignore b/lib/PsychicHttp/examples/old/esp_ota_http_server/.gitignore new file mode 100644 index 0000000..be2b7e8 --- /dev/null +++ b/lib/PsychicHttp/examples/old/esp_ota_http_server/.gitignore @@ -0,0 +1,39 @@ +.pioenvs +.clang_complete +.gcc-flags.json +# Compiled Object files +*.slo +*.lo +*.o +*.obj +# Precompiled Headers +*.gch +*.pch +# Compiled Dynamic libraries +*.so +*.dylib +*.dll +# Fortran module files +*.mod +# Compiled Static libraries +*.lai +*.la +*.a +*.lib +# Executables +*.exe +*.out +*.app +# Visual Studio/VisualMicro stuff +Visual\ Micro +*.sdf +*.opensdf +*.suo +.pioenvs +.piolibdeps +.pio +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/settings.json +.vscode/.browse.c_cpp.db* +.vscode/ipch \ No newline at end of file diff --git a/lib/PsychicHttp/examples/old/esp_ota_http_server/include/README b/lib/PsychicHttp/examples/old/esp_ota_http_server/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/lib/PsychicHttp/examples/old/esp_ota_http_server/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/PsychicHttp/examples/old/esp_ota_http_server/lib/README b/lib/PsychicHttp/examples/old/esp_ota_http_server/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/PsychicHttp/examples/old/esp_ota_http_server/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/examples/old/esp_ota_http_server/platformio.ini b/lib/PsychicHttp/examples/old/esp_ota_http_server/platformio.ini new file mode 100644 index 0000000..8a0c120 --- /dev/null +++ b/lib/PsychicHttp/examples/old/esp_ota_http_server/platformio.ini @@ -0,0 +1,74 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[common] +lib_deps = ArduinoMongoose +monitor_speed = 115200 +monitor_port = /dev/ttyUSB1 +build_flags = + -DENABLE_DEBUG +# -DCS_ENABLE_STDIO + -DMG_ENABLE_HTTP_STREAMING_MULTIPART=1 + +build_flags_secure = + -DSIMPLE_SERVER_SECURE + -DMG_ENABLE_SSL=1 + +# -DMG_SSL_IF=MG_SSL_IF_OPENSSL +# -DKR_VERSION + + -DMG_SSL_MBED_DUMMY_RANDOM=1 + -DMG_SSL_IF=MG_SSL_IF_MBEDTLS + -DMG_SSL_IF_MBEDTLS_FREE_CERTS=1 + -DMG_SSL_IF_MBEDTLS_MAX_FRAG_LEN=2048 + +build_flags_auth = + -DADMIN_USER='"admin"' + -DADMIN_PASS='"admin"' + -DADMIN_REALM='"esp_ota_http_server"' + +#[env:huzzah] +#platform = espressif8266 +#board = huzzah +#framework = arduino +#monitor_speed = ${common.monitor_speed} +#monitor_port = ${common.monitor_port} +#lib_deps = ${common.lib_deps} +#build_flags = ${common.build_flags} + +[env:esp-wrover-kit] +platform = espressif32 +framework = arduino +board = esp-wrover-kit +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} + +[env:esp-wrover-kit-secure] +platform = espressif32 +framework = arduino +board = esp-wrover-kit +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} ${common.build_flags_secure} + +[env:esp-wrover-kit-auth] +extends = env:esp-wrover-kit +build_flags = ${common.build_flags} ${common.build_flags_auth} -ggdb + +#[env:linux_x86_64] +#platform = linux_x86_64 +#framework = arduino +#board = generic +#lib_deps = ${common.lib_deps} +#build_flags = ${common.build_flags} +#build_flags = -DSERIAL_TO_CONSOLE \ No newline at end of file diff --git a/lib/PsychicHttp/examples/old/esp_ota_http_server/src/esp_ota_http_server.cpp b/lib/PsychicHttp/examples/old/esp_ota_http_server/src/esp_ota_http_server.cpp new file mode 100644 index 0000000..9d9d35b --- /dev/null +++ b/lib/PsychicHttp/examples/old/esp_ota_http_server/src/esp_ota_http_server.cpp @@ -0,0 +1,229 @@ +// +// A simple server implementation showing how to: +// * serve static messages +// * read GET and POST parameters +// * handle missing pages / 404s +// + +#include +#include +#include +#include + +#ifdef ESP32 +#include +#define START_ESP_WIFI +#elif defined(ESP8266) +#include +#define START_ESP_WIFI +#else +#error Platform not supported +#endif + +MongooseHttpServer server; + +const char *ssid = "wifi"; +const char *password = "password"; + +const char *server_pem = +"-----BEGIN CERTIFICATE-----\r\n" +"MIIDDjCCAfagAwIBAgIBBDANBgkqhkiG9w0BAQsFADA/MRkwFwYDVQQDDBB0ZXN0\r\n" +"LmNlc2FudGEuY29tMRAwDgYDVQQKDAdDZXNhbnRhMRAwDgYDVQQLDAd0ZXN0aW5n\r\n" +"MB4XDTE2MTExMzEzMTgwMVoXDTI2MDgxMzEzMTgwMVowFDESMBAGA1UEAwwJbG9j\r\n" +"YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAro8CW1X0xaGm\r\n" +"GkDaMxKbXWA5Lw+seA61tioGrSIQzuqLYeJoFnwVgF0jB5PTj+3EiGMBcA/mh73V\r\n" +"AthTFmJBxj+agIp7/cvUBpgfLClmSYL2fZi6Fodz+f9mcry3XRw7O6vlamtWfTX8\r\n" +"TAmMSR6PXVBHLgjs5pDOFFmrNAsM5sLYU1/1MFvE2Z9InTI5G437IE1WchRSbpYd\r\n" +"HchC39XzpDGoInZB1a3OhcHm+xUtLpMJ0G0oE5VFEynZreZoEIY4JxspQ7LPsay9\r\n" +"fx3Tlk09gEMQgVCeCNiQwUxZdtLau2x61LNcdZCKN7FbFLJszv1U2uguELsTmi7E\r\n" +"6pHrTziosQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUE\r\n" +"DDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB\r\n" +"AQBUw0hbTcT6crzODO4QAXU7z4Xxn0LkxbXEsoThG1QCVgMc4Bhpx8gyz5CLyHYz\r\n" +"AiJOBFEeV0XEqoGTNMMFelR3Q5Tg9y1TYO3qwwAWxe6/brVzpts6NiG1uEMBnBFg\r\n" +"oN1x3I9x4NpOxU5MU1dlIxvKs5HQCoNJ8D0SqOX9BV/pZqwEgiCbuWDWQAlxkFpn\r\n" +"iLonlkVI5hTuybCSBsa9FEI9M6JJn9LZmlH90FYHeS4t6P8eOJCeekHL0jUG4Iae\r\n" +"DMP12h8Sd0yxIKmmZ+Q/p/D/BkuHf5Idv3hgyLkZ4mNznjK49wHaYM+BgBoL3Zeg\r\n" +"gJ2sWjUlokrbHswSBLLbUJIF\r\n" +"-----END CERTIFICATE-----\r\n"; + +const char *server_key = +"-----BEGIN PRIVATE KEY-----\r\n" +"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCujwJbVfTFoaYa\r\n" +"QNozEptdYDkvD6x4DrW2KgatIhDO6oth4mgWfBWAXSMHk9OP7cSIYwFwD+aHvdUC\r\n" +"2FMWYkHGP5qAinv9y9QGmB8sKWZJgvZ9mLoWh3P5/2ZyvLddHDs7q+Vqa1Z9NfxM\r\n" +"CYxJHo9dUEcuCOzmkM4UWas0CwzmwthTX/UwW8TZn0idMjkbjfsgTVZyFFJulh0d\r\n" +"yELf1fOkMagidkHVrc6Fweb7FS0ukwnQbSgTlUUTKdmt5mgQhjgnGylDss+xrL1/\r\n" +"HdOWTT2AQxCBUJ4I2JDBTFl20tq7bHrUs1x1kIo3sVsUsmzO/VTa6C4QuxOaLsTq\r\n" +"ketPOKixAgMBAAECggEAI+uNwpnHirue4Jwjyoqzqd1ZJxQEm5f7UIcJZKsz5kBh\r\n" +"ej0KykWybv27bZ2/1UhKPv6QlyzOdXRc1v8I6fxCKLeB5Z2Zsjo1YT4AfCfwwoPO\r\n" +"kT3SXTx2YyVpQYcP/HsIvVi8FtALtixbxJHaall9iugwHYr8pN17arihAE6d0wZC\r\n" +"JXtXRjUWwjKzXP8FoH4KhyadhHbDwIbbJe3cyLfdvp54Gr0YHha0JcOxYgDYNya4\r\n" +"OKxlCluI+hPF31iNzOmFLQVrdYynyPcR6vY5XOiANKE2iNbqCzRb54CvW9WMqObX\r\n" +"RD9t3DMOxGsbVNIwyzZndWy13HoQMGnrHfnGak9ueQKBgQDiVtOqYfLnUnTxvJ/b\r\n" +"qlQZr2ZmsYPZztxlP+DSqZGPD+WtGSo9+rozWfzjTv3KGIDLvf+GFVmjVHwlLQfd\r\n" +"u7eTemWHFc4HK68wruzPO/FdyVpQ4w9v3Usg+ll4a/PDEId0fDMjAr6kk4LC6t8y\r\n" +"9fJR0HjOz57jVnlrDt3v50G8BwKBgQDFbw+jRiUxXnBbDyXZLi+I4iGBGdC+CbaJ\r\n" +"CmsM6/TsOFc+GRsPwQF1gCGqdaURw76noIVKZJOSc8I+yiwU6izyh/xaju5JiWQd\r\n" +"kwbU1j4DE6GnxmT3ARmB7VvCxjaEZEAtICWs1QTKRz7PcTV8yr7Ng1A3VIy+NSpo\r\n" +"LFMMmk83hwKBgQDVCEwpLg/mUeHoNVVw95w4oLKNLb+gHeerFLiTDy8FrDzM88ai\r\n" +"l37yHly7xflxYia3nZkHpsi7xiUjCINC3BApKyasQoWskh1OgRY653yCfaYYQ96f\r\n" +"t3WjEH9trI2+p6wWo1+uMEMnu/9zXoW9/WeaQdGzNg+igh29+jxCNTPVuQKBgGV4\r\n" +"CN9vI5pV4QTLqjYOSJvfLDz/mYqxz0BrPE1tz3jAFAZ0PLZCCY/sBGFpCScyJQBd\r\n" +"vWNYgYeZOtGuci1llSgov4eDQfBFTlDsyWwFl+VY55IkoqtXw1ZFOQ3HdSlhpKIM\r\n" +"jZBgApA7QYq3sjeqs5lHzahCKftvs5XKgfxOKjxtAoGBALdnYe6xkDvGLvI51Yr+\r\n" +"Dy0TNcB5W84SxUKvM7DVEomy1QPB57ZpyQaoBq7adOz0pWJXfp7qo4950ZOhBGH1\r\n" +"hKbZ6c4ggwVJy2j49EgMok5NGCKvPAtabbR6H8Mz8DW9aXURxhWJvij+Qw1fWK4b\r\n" +"7G/qUI9iE5iUU7MkIcLIbTf/\r\n" +"-----END PRIVATE KEY-----\r\n"; + +const char* server_index = +"" +"
" + "" + "" +"
" +"
progress: 0%
" +""; + +#include + +bool updateCompleted = false; + +static void updateError(MongooseHttpServerRequest *request) +{ + MongooseHttpServerResponseStream *resp = request->beginResponseStream(); + resp->setCode(500); + resp->setContentType("text/plain"); + resp->printf("Error: %d", Update.getError()); + request->send(resp); + + // Anoyingly this uses Stream rather than Print... + Update.printError(Serial); +} + +void setup() +{ + Serial.begin(115200); + +#ifdef START_ESP_WIFI + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) + { + Serial.printf("WiFi Failed!\n"); + return; + } + + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + Serial.print("Hostname: "); +#ifdef ESP32 + Serial.println(WiFi.getHostname()); +#elif defined(ESP8266) + Serial.println(WiFi.hostname()); +#endif +#endif + + Mongoose.begin(); + +#ifdef SIMPLE_SERVER_SECURE + if(false == server.begin(443, server_pem, server_key)) { + Serial.print("Failed to start server"); + return; + } +#else + server.begin(80); +#endif + + server.on("/$", HTTP_GET, [](MongooseHttpServerRequest *request) { +#if defined(ADMIN_USER) && defined(ADMIN_PASS) && defined(ADMIN_REALM) + if(false == request->authenticate(ADMIN_USER, ADMIN_PASS)) { + request->requestAuthentication(ADMIN_REALM); + return; + } +#endif + + request->send(200, "text/html", server_index); + }); + + server.on("/update$", HTTP_POST)-> + onRequest([](MongooseHttpServerRequest *request) { +#if defined(ADMIN_USER) && defined(ADMIN_PASS) && defined(ADMIN_REALM) + if(false == request->authenticate(ADMIN_USER, ADMIN_PASS)) { + request->requestAuthentication(ADMIN_REALM); + return; + } +#endif + updateCompleted = false; + })-> + onUpload([](MongooseHttpServerRequest *request, int ev, MongooseString filename, uint64_t index, uint8_t *data, size_t len) + { + if(MG_EV_HTTP_PART_BEGIN == ev) { + Serial.printf("Update Start: %s\n", filename.c_str()); + + if (!Update.begin()) { //start with max available size + updateError(request); + } + } + + if(!Update.hasError()) + { + Serial.printf("Update Writing %llu\n", index); + if(Update.write(data, len) != len) { + updateError(request); + } + } + + if(MG_EV_HTTP_PART_END == ev) { + Serial.println("Data finished"); + if(Update.end(true)) { + Serial.printf("Update Success: %lluB\n", index+len); + request->send(200, "text/plain", "OK"); + updateCompleted = true; + } else { + updateError(request); + } + } + + return len; + })-> + onClose([](MongooseHttpServerRequest *request) + { + if(updateCompleted) { + ESP.restart(); + } + }); +} + +void loop() +{ + Mongoose.poll(1000); +} diff --git a/lib/PsychicHttp/examples/old/esp_ota_http_server/test/README b/lib/PsychicHttp/examples/old/esp_ota_http_server/test/README new file mode 100644 index 0000000..df5066e --- /dev/null +++ b/lib/PsychicHttp/examples/old/esp_ota_http_server/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/lib/PsychicHttp/examples/old/simple_http_server/.gitignore b/lib/PsychicHttp/examples/old/simple_http_server/.gitignore new file mode 100644 index 0000000..be2b7e8 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simple_http_server/.gitignore @@ -0,0 +1,39 @@ +.pioenvs +.clang_complete +.gcc-flags.json +# Compiled Object files +*.slo +*.lo +*.o +*.obj +# Precompiled Headers +*.gch +*.pch +# Compiled Dynamic libraries +*.so +*.dylib +*.dll +# Fortran module files +*.mod +# Compiled Static libraries +*.lai +*.la +*.a +*.lib +# Executables +*.exe +*.out +*.app +# Visual Studio/VisualMicro stuff +Visual\ Micro +*.sdf +*.opensdf +*.suo +.pioenvs +.piolibdeps +.pio +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/settings.json +.vscode/.browse.c_cpp.db* +.vscode/ipch \ No newline at end of file diff --git a/lib/PsychicHttp/examples/old/simple_http_server/include/README b/lib/PsychicHttp/examples/old/simple_http_server/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simple_http_server/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/PsychicHttp/examples/old/simple_http_server/lib/README b/lib/PsychicHttp/examples/old/simple_http_server/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simple_http_server/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/examples/old/simple_http_server/platformio.ini b/lib/PsychicHttp/examples/old/simple_http_server/platformio.ini new file mode 100644 index 0000000..5da8a5a --- /dev/null +++ b/lib/PsychicHttp/examples/old/simple_http_server/platformio.ini @@ -0,0 +1,64 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[common] +lib_deps = ArduinoMongoose +monitor_speed = 115200 +monitor_port = /dev/ttyUSB1 +build_flags = + -DENABLE_DEBUG +# -DCS_ENABLE_STDIO + +build_flags_secure = + -DSIMPLE_SERVER_SECURE + -DMG_ENABLE_SSL=1 + +# -DMG_SSL_IF=MG_SSL_IF_OPENSSL +# -DKR_VERSION + + -DMG_SSL_MBED_DUMMY_RANDOM=1 + -DMG_SSL_IF=MG_SSL_IF_MBEDTLS + -DMG_SSL_IF_MBEDTLS_FREE_CERTS=1 + -DMG_SSL_IF_MBEDTLS_MAX_FRAG_LEN=2048 + +[env:huzzah] +platform = espressif8266 +board = huzzah +framework = arduino +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} + +[env:esp-wrover-kit] +platform = espressif32 +framework = arduino +board = esp-wrover-kit +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} + +[env:esp-wrover-kit-secure] +platform = espressif32 +framework = arduino +board = esp-wrover-kit +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} ${common.build_flags_secure} + +#[env:linux_x86_64] +#platform = linux_x86_64 +#framework = arduino +#board = generic +#lib_deps = ${common.lib_deps} +#build_flags = ${common.build_flags} +#build_flags = -DSERIAL_TO_CONSOLE \ No newline at end of file diff --git a/lib/PsychicHttp/examples/old/simple_http_server/src/simple_http_server.cpp b/lib/PsychicHttp/examples/old/simple_http_server/src/simple_http_server.cpp new file mode 100644 index 0000000..ffa1e4d --- /dev/null +++ b/lib/PsychicHttp/examples/old/simple_http_server/src/simple_http_server.cpp @@ -0,0 +1,211 @@ +// +// A simple server implementation showing how to: +// * serve static messages +// * read GET and POST parameters +// * handle missing pages / 404s +// + +#include +#include +#include + +#ifdef ESP32 +#include +#define START_ESP_WIFI +#elif defined(ESP8266) +#include +#define START_ESP_WIFI +#else +#error Platform not supported +#endif + +MongooseHttpServer server; + +const char *ssid = "wifi"; +const char *password = "password"; + +const char *PARAM_MESSAGE = "message"; + +const char *server_pem = +"-----BEGIN CERTIFICATE-----\r\n" +"MIIDDjCCAfagAwIBAgIBBDANBgkqhkiG9w0BAQsFADA/MRkwFwYDVQQDDBB0ZXN0\r\n" +"LmNlc2FudGEuY29tMRAwDgYDVQQKDAdDZXNhbnRhMRAwDgYDVQQLDAd0ZXN0aW5n\r\n" +"MB4XDTE2MTExMzEzMTgwMVoXDTI2MDgxMzEzMTgwMVowFDESMBAGA1UEAwwJbG9j\r\n" +"YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAro8CW1X0xaGm\r\n" +"GkDaMxKbXWA5Lw+seA61tioGrSIQzuqLYeJoFnwVgF0jB5PTj+3EiGMBcA/mh73V\r\n" +"AthTFmJBxj+agIp7/cvUBpgfLClmSYL2fZi6Fodz+f9mcry3XRw7O6vlamtWfTX8\r\n" +"TAmMSR6PXVBHLgjs5pDOFFmrNAsM5sLYU1/1MFvE2Z9InTI5G437IE1WchRSbpYd\r\n" +"HchC39XzpDGoInZB1a3OhcHm+xUtLpMJ0G0oE5VFEynZreZoEIY4JxspQ7LPsay9\r\n" +"fx3Tlk09gEMQgVCeCNiQwUxZdtLau2x61LNcdZCKN7FbFLJszv1U2uguELsTmi7E\r\n" +"6pHrTziosQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUE\r\n" +"DDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB\r\n" +"AQBUw0hbTcT6crzODO4QAXU7z4Xxn0LkxbXEsoThG1QCVgMc4Bhpx8gyz5CLyHYz\r\n" +"AiJOBFEeV0XEqoGTNMMFelR3Q5Tg9y1TYO3qwwAWxe6/brVzpts6NiG1uEMBnBFg\r\n" +"oN1x3I9x4NpOxU5MU1dlIxvKs5HQCoNJ8D0SqOX9BV/pZqwEgiCbuWDWQAlxkFpn\r\n" +"iLonlkVI5hTuybCSBsa9FEI9M6JJn9LZmlH90FYHeS4t6P8eOJCeekHL0jUG4Iae\r\n" +"DMP12h8Sd0yxIKmmZ+Q/p/D/BkuHf5Idv3hgyLkZ4mNznjK49wHaYM+BgBoL3Zeg\r\n" +"gJ2sWjUlokrbHswSBLLbUJIF\r\n" +"-----END CERTIFICATE-----\r\n"; + +const char *server_key = +"-----BEGIN PRIVATE KEY-----\r\n" +"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCujwJbVfTFoaYa\r\n" +"QNozEptdYDkvD6x4DrW2KgatIhDO6oth4mgWfBWAXSMHk9OP7cSIYwFwD+aHvdUC\r\n" +"2FMWYkHGP5qAinv9y9QGmB8sKWZJgvZ9mLoWh3P5/2ZyvLddHDs7q+Vqa1Z9NfxM\r\n" +"CYxJHo9dUEcuCOzmkM4UWas0CwzmwthTX/UwW8TZn0idMjkbjfsgTVZyFFJulh0d\r\n" +"yELf1fOkMagidkHVrc6Fweb7FS0ukwnQbSgTlUUTKdmt5mgQhjgnGylDss+xrL1/\r\n" +"HdOWTT2AQxCBUJ4I2JDBTFl20tq7bHrUs1x1kIo3sVsUsmzO/VTa6C4QuxOaLsTq\r\n" +"ketPOKixAgMBAAECggEAI+uNwpnHirue4Jwjyoqzqd1ZJxQEm5f7UIcJZKsz5kBh\r\n" +"ej0KykWybv27bZ2/1UhKPv6QlyzOdXRc1v8I6fxCKLeB5Z2Zsjo1YT4AfCfwwoPO\r\n" +"kT3SXTx2YyVpQYcP/HsIvVi8FtALtixbxJHaall9iugwHYr8pN17arihAE6d0wZC\r\n" +"JXtXRjUWwjKzXP8FoH4KhyadhHbDwIbbJe3cyLfdvp54Gr0YHha0JcOxYgDYNya4\r\n" +"OKxlCluI+hPF31iNzOmFLQVrdYynyPcR6vY5XOiANKE2iNbqCzRb54CvW9WMqObX\r\n" +"RD9t3DMOxGsbVNIwyzZndWy13HoQMGnrHfnGak9ueQKBgQDiVtOqYfLnUnTxvJ/b\r\n" +"qlQZr2ZmsYPZztxlP+DSqZGPD+WtGSo9+rozWfzjTv3KGIDLvf+GFVmjVHwlLQfd\r\n" +"u7eTemWHFc4HK68wruzPO/FdyVpQ4w9v3Usg+ll4a/PDEId0fDMjAr6kk4LC6t8y\r\n" +"9fJR0HjOz57jVnlrDt3v50G8BwKBgQDFbw+jRiUxXnBbDyXZLi+I4iGBGdC+CbaJ\r\n" +"CmsM6/TsOFc+GRsPwQF1gCGqdaURw76noIVKZJOSc8I+yiwU6izyh/xaju5JiWQd\r\n" +"kwbU1j4DE6GnxmT3ARmB7VvCxjaEZEAtICWs1QTKRz7PcTV8yr7Ng1A3VIy+NSpo\r\n" +"LFMMmk83hwKBgQDVCEwpLg/mUeHoNVVw95w4oLKNLb+gHeerFLiTDy8FrDzM88ai\r\n" +"l37yHly7xflxYia3nZkHpsi7xiUjCINC3BApKyasQoWskh1OgRY653yCfaYYQ96f\r\n" +"t3WjEH9trI2+p6wWo1+uMEMnu/9zXoW9/WeaQdGzNg+igh29+jxCNTPVuQKBgGV4\r\n" +"CN9vI5pV4QTLqjYOSJvfLDz/mYqxz0BrPE1tz3jAFAZ0PLZCCY/sBGFpCScyJQBd\r\n" +"vWNYgYeZOtGuci1llSgov4eDQfBFTlDsyWwFl+VY55IkoqtXw1ZFOQ3HdSlhpKIM\r\n" +"jZBgApA7QYq3sjeqs5lHzahCKftvs5XKgfxOKjxtAoGBALdnYe6xkDvGLvI51Yr+\r\n" +"Dy0TNcB5W84SxUKvM7DVEomy1QPB57ZpyQaoBq7adOz0pWJXfp7qo4950ZOhBGH1\r\n" +"hKbZ6c4ggwVJy2j49EgMok5NGCKvPAtabbR6H8Mz8DW9aXURxhWJvij+Qw1fWK4b\r\n" +"7G/qUI9iE5iUU7MkIcLIbTf/\r\n" +"-----END PRIVATE KEY-----\r\n"; + +static void notFound(MongooseHttpServerRequest *request); + +#include + +void setup() +{ + Serial.begin(115200); + +#ifdef START_ESP_WIFI + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) + { + Serial.printf("WiFi Failed!\n"); + return; + } + + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + Serial.print("Hostname: "); +#ifdef ESP32 + Serial.println(WiFi.getHostname()); +#elif defined(ESP8266) + Serial.println(WiFi.hostname()); +#endif +#endif + + Mongoose.begin(); + +#ifdef SIMPLE_SERVER_SECURE + if(false == server.begin(443, server_pem, server_key)) { + Serial.print("Failed to start server"); + return; + } +#else + server.begin(80); +#endif + + server.on("/$", HTTP_GET, [](MongooseHttpServerRequest *request) { + request->send(200, "text/plain", "Hello world"); + }); + + // Send a GET request to /get?message= + server.on("/get$", HTTP_GET, [](MongooseHttpServerRequest *request) { + String message; + if (request->hasParam(PARAM_MESSAGE)) + { + message = request->getParam(PARAM_MESSAGE); + } + else + { + message = "No message sent"; + } + request->send(200, "text/plain", "Hello, GET: " + message); + }); + + // Send a POST request to /post with a form field message set to + server.on("/post$", HTTP_POST, [](MongooseHttpServerRequest *request) { + String message; + if (request->hasParam(PARAM_MESSAGE)) + { + message = request->getParam(PARAM_MESSAGE); + } + else + { + message = "No message sent"; + } + request->send(200, "text/plain", "Hello, POST: " + message); + }); + + // Test the basic response class + server.on("/basic$", HTTP_GET, [](MongooseHttpServerRequest *request) { + MongooseHttpServerResponseBasic *resp = request->beginResponse(); + resp->setCode(200); + resp->setContentType("text/html"); + resp->addHeader("Cache-Control", "max-age=300"); + resp->addHeader("X-hello", "world"); + resp->setContent( + "\n" + "\n" + "Basic Page\n" + "\n" + "\n" + "

Basic Page

\n" + "

\n" + "This page has been sent using the MongooseHttpServerResponseBasic class\n" + "

\n" + "\n" + "\n"); + request->send(resp); + }); + + // Test the stream response class + server.on("/stream$", HTTP_GET, [](MongooseHttpServerRequest *request) { + MongooseHttpServerResponseStream *resp = request->beginResponseStream(); + resp->setCode(200); + resp->setContentType("text/html"); + resp->addHeader("Cache-Control", "max-age=300"); + resp->addHeader("X-hello", "world"); + + resp->println(""); + resp->println(""); + resp->println("Stream Page"); + resp->println(""); + resp->println(""); + resp->println("

Stream Page

"); + resp->println("

"); + resp->println("This page has been sent using the MongooseHttpServerResponseStream class"); + resp->println("

"); + resp->println("

"); + resp->printf("micros = %lu
", micros()); + resp->printf("free = %u
", ESP.getFreeHeap()); + resp->println("

"); + resp->println(""); + resp->println(""); + + request->send(resp); + }); + + server.onNotFound(notFound); +} + +void loop() +{ + Mongoose.poll(1000); + Serial.printf("Free memory %u\n", ESP.getFreeHeap()); +} + +static void notFound(MongooseHttpServerRequest *request) +{ + request->send(404, "text/plain", "Not found"); +} diff --git a/lib/PsychicHttp/examples/old/simple_http_server/test/README b/lib/PsychicHttp/examples/old/simple_http_server/test/README new file mode 100644 index 0000000..df5066e --- /dev/null +++ b/lib/PsychicHttp/examples/old/simple_http_server/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/lib/PsychicHttp/examples/old/simple_http_server/test/tests.rest b/lib/PsychicHttp/examples/old/simple_http_server/test/tests.rest new file mode 100644 index 0000000..4072428 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simple_http_server/test/tests.rest @@ -0,0 +1,35 @@ +# Name: REST Client +# Id: humao.rest-client +# Description: REST Client for Visual Studio Code +# Version: 0.21.3 +# Publisher: Huachao Mao +# VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=humao.rest-client + +@baseUrl = http://172.16.0.87 + +### + +GET {{baseUrl}}/ HTTP/1.1 + +### + +GET {{baseUrl}}/get?message=Hello+World HTTP/1.1 + +### + +POST {{baseUrl}}/post HTTP/1.1 +Content-Type: application/x-www-form-urlencoded;charset=UTF-8 + +message=Hello+World + +### + +GET {{baseUrl}}/someRandomFile HTTP/1.1 + +### + +GET {{baseUrl}}/basic HTTP/1.1 + +### + +GET {{baseUrl}}/stream HTTP/1.1 diff --git a/lib/PsychicHttp/examples/old/simplest_web_server_esp/.gitignore b/lib/PsychicHttp/examples/old/simplest_web_server_esp/.gitignore new file mode 100644 index 0000000..be2b7e8 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simplest_web_server_esp/.gitignore @@ -0,0 +1,39 @@ +.pioenvs +.clang_complete +.gcc-flags.json +# Compiled Object files +*.slo +*.lo +*.o +*.obj +# Precompiled Headers +*.gch +*.pch +# Compiled Dynamic libraries +*.so +*.dylib +*.dll +# Fortran module files +*.mod +# Compiled Static libraries +*.lai +*.la +*.a +*.lib +# Executables +*.exe +*.out +*.app +# Visual Studio/VisualMicro stuff +Visual\ Micro +*.sdf +*.opensdf +*.suo +.pioenvs +.piolibdeps +.pio +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/settings.json +.vscode/.browse.c_cpp.db* +.vscode/ipch \ No newline at end of file diff --git a/lib/PsychicHttp/examples/old/simplest_web_server_esp/.travis.yml b/lib/PsychicHttp/examples/old/simplest_web_server_esp/.travis.yml new file mode 100644 index 0000000..7c486f1 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simplest_web_server_esp/.travis.yml @@ -0,0 +1,67 @@ +# Continuous Integration (CI) is the practice, in software +# engineering, of merging all developer working copies with a shared mainline +# several times a day < https://docs.platformio.org/page/ci/index.html > +# +# Documentation: +# +# * Travis CI Embedded Builds with PlatformIO +# < https://docs.travis-ci.com/user/integration/platformio/ > +# +# * PlatformIO integration with Travis CI +# < https://docs.platformio.org/page/ci/travis.html > +# +# * User Guide for `platformio ci` command +# < https://docs.platformio.org/page/userguide/cmd_ci.html > +# +# +# Please choose one of the following templates (proposed below) and uncomment +# it (remove "# " before each line) or use own configuration according to the +# Travis CI documentation (see above). +# + + +# +# Template #1: General project. Test it using existing `platformio.ini`. +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# install: +# - pip install -U platformio +# - platformio update +# +# script: +# - platformio run + + +# +# Template #2: The project is intended to be used as a library with examples. +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# env: +# - PLATFORMIO_CI_SRC=path/to/test/file.c +# - PLATFORMIO_CI_SRC=examples/file.ino +# - PLATFORMIO_CI_SRC=path/to/test/directory +# +# install: +# - pip install -U platformio +# - platformio update +# +# script: +# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N diff --git a/lib/PsychicHttp/examples/old/simplest_web_server_esp/include/README b/lib/PsychicHttp/examples/old/simplest_web_server_esp/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simplest_web_server_esp/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/PsychicHttp/examples/old/simplest_web_server_esp/lib/README b/lib/PsychicHttp/examples/old/simplest_web_server_esp/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simplest_web_server_esp/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/examples/old/simplest_web_server_esp/platformio.ini b/lib/PsychicHttp/examples/old/simplest_web_server_esp/platformio.ini new file mode 100644 index 0000000..d02c3bc --- /dev/null +++ b/lib/PsychicHttp/examples/old/simplest_web_server_esp/platformio.ini @@ -0,0 +1,40 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[common] +lib_deps = ArduinoMongoose +monitor_speed = 115200 +build_flags = + +[espressif8266] +build_flags = -DMG_ESP8266 + +[espressif32] +build_flags = + +[env:huzzah] +platform = espressif8266 +framework = arduino +board = huzzah +monitor_speed = ${common.monitor_speed} +lib_deps = ${common.lib_deps} +build_flags = + ${espressif8266.build_flags} + ${common.build_flags} + +[env:espwroverkit] +platform = espressif32 +framework = arduino +board = esp-wrover-kit +monitor_speed = ${common.monitor_speed} +lib_deps = ${common.lib_deps} +build_flags = + ${espressif32.build_flags} + ${common.build_flags} diff --git a/lib/PsychicHttp/examples/old/simplest_web_server_esp/src/simplest_web_server.cpp b/lib/PsychicHttp/examples/old/simplest_web_server_esp/src/simplest_web_server.cpp new file mode 100644 index 0000000..cedb9c2 --- /dev/null +++ b/lib/PsychicHttp/examples/old/simplest_web_server_esp/src/simplest_web_server.cpp @@ -0,0 +1,97 @@ +// Copyright (c) 2015 Cesanta Software Limited +// All rights reserved + +#include + +#ifdef ESP32 +#include +#elif defined(ESP8266) +#include +#else +#error Platform not supported +#endif + +#include "mongoose.h" + +const char* ssid = "my-ssid"; +const char* password = "my-password"; + +static const char *s_http_port = "80"; +//static struct mg_serve_http_opts s_http_server_opts; + +static void ev_handler(struct mg_connection *nc, int ev, void *p, void *d) { + static const char *reply_fmt = + "HTTP/1.0 200 OK\r\n" + "Connection: close\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + "Hello %s\n"; + + switch (ev) { + case MG_EV_ACCEPT: { + char addr[32]; + mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), + MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); + Serial.printf("Connection %p from %s\n", nc, addr); + break; + } + case MG_EV_HTTP_REQUEST: { + char addr[32]; + struct http_message *hm = (struct http_message *) p; + mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), + MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); + Serial.printf("HTTP request from %s: %.*s %.*s\n", addr, (int) hm->method.len, + hm->method.p, (int) hm->uri.len, hm->uri.p); + mg_printf(nc, reply_fmt, addr); + nc->flags |= MG_F_SEND_AND_CLOSE; + break; + } + case MG_EV_CLOSE: { + Serial.printf("Connection %p closed\n", nc); + break; + } + } +} + +struct mg_mgr mgr; +struct mg_connection *nc; + +void setup() +{ + Serial.begin(115200); + + Serial.print("Connecting to "); + Serial.println(ssid); + + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println("WiFi connected"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); + + mg_mgr_init(&mgr, NULL); + Serial.printf("Starting web server on port %s\n", s_http_port); + nc = mg_bind(&mgr, s_http_port, ev_handler, NULL); + if (nc == NULL) { + Serial.printf("Failed to create listener\n"); + return; + } + + // Set up HTTP server parameters + mg_set_protocol_http_websocket(nc); +// s_http_server_opts.document_root = "."; // Serve current directory +// s_http_server_opts.enable_directory_listing = "yes"; +} + +static uint32_t count = 0; +void loop() +{ + mg_mgr_poll(&mgr, 1000); + //Serial.println(count++); +} diff --git a/lib/PsychicHttp/examples/old/simplest_web_server_esp/test/README b/lib/PsychicHttp/examples/old/simplest_web_server_esp/test/README new file mode 100644 index 0000000..df5066e --- /dev/null +++ b/lib/PsychicHttp/examples/old/simplest_web_server_esp/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/lib/PsychicHttp/examples/old/websocket_chat/.gitignore b/lib/PsychicHttp/examples/old/websocket_chat/.gitignore new file mode 100644 index 0000000..be2b7e8 --- /dev/null +++ b/lib/PsychicHttp/examples/old/websocket_chat/.gitignore @@ -0,0 +1,39 @@ +.pioenvs +.clang_complete +.gcc-flags.json +# Compiled Object files +*.slo +*.lo +*.o +*.obj +# Precompiled Headers +*.gch +*.pch +# Compiled Dynamic libraries +*.so +*.dylib +*.dll +# Fortran module files +*.mod +# Compiled Static libraries +*.lai +*.la +*.a +*.lib +# Executables +*.exe +*.out +*.app +# Visual Studio/VisualMicro stuff +Visual\ Micro +*.sdf +*.opensdf +*.suo +.pioenvs +.piolibdeps +.pio +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/settings.json +.vscode/.browse.c_cpp.db* +.vscode/ipch \ No newline at end of file diff --git a/lib/PsychicHttp/examples/old/websocket_chat/include/README b/lib/PsychicHttp/examples/old/websocket_chat/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/lib/PsychicHttp/examples/old/websocket_chat/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/PsychicHttp/examples/old/websocket_chat/lib/README b/lib/PsychicHttp/examples/old/websocket_chat/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/PsychicHttp/examples/old/websocket_chat/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/examples/old/websocket_chat/platformio.ini b/lib/PsychicHttp/examples/old/websocket_chat/platformio.ini new file mode 100644 index 0000000..165b7ac --- /dev/null +++ b/lib/PsychicHttp/examples/old/websocket_chat/platformio.ini @@ -0,0 +1,64 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[common] +lib_deps = ArduinoMongoose +monitor_speed = 115200 +monitor_port = /dev/ttyUSB2 +build_flags = + -DENABLE_DEBUG +# -DCS_ENABLE_STDIO + +build_flags_secure = + -DSIMPLE_SERVER_SECURE + -DMG_ENABLE_SSL=1 + +# -DMG_SSL_IF=MG_SSL_IF_OPENSSL +# -DKR_VERSION + + -DMG_SSL_MBED_DUMMY_RANDOM=1 + -DMG_SSL_IF=MG_SSL_IF_MBEDTLS + -DMG_SSL_IF_MBEDTLS_FREE_CERTS=1 + -DMG_SSL_IF_MBEDTLS_MAX_FRAG_LEN=2048 + +[env:huzzah] +platform = espressif8266 +board = huzzah +framework = arduino +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} + +[env:esp-wrover-kit] +platform = espressif32 +framework = arduino +board = esp-wrover-kit +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} + +[env:esp-wrover-kit-secure] +platform = espressif32 +framework = arduino +board = esp-wrover-kit +monitor_speed = ${common.monitor_speed} +monitor_port = ${common.monitor_port} +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} ${common.build_flags_secure} + +#[env:linux_x86_64] +#platform = linux_x86_64 +#framework = arduino +#board = generic +#lib_deps = ${common.lib_deps} +#build_flags = ${common.build_flags} +#build_flags = -DSERIAL_TO_CONSOLE \ No newline at end of file diff --git a/lib/PsychicHttp/examples/old/websocket_chat/src/websocket_chat.cpp b/lib/PsychicHttp/examples/old/websocket_chat/src/websocket_chat.cpp new file mode 100644 index 0000000..47a04cf --- /dev/null +++ b/lib/PsychicHttp/examples/old/websocket_chat/src/websocket_chat.cpp @@ -0,0 +1,235 @@ +// +// A simple server implementation showing how to: +// * serve static messages +// * read GET and POST parameters +// * handle missing pages / 404s +// + +#include +#include +#include + +#ifdef ESP32 +#include +#define START_ESP_WIFI +#elif defined(ESP8266) +#include +#define START_ESP_WIFI +#else +#error Platform not supported +#endif + +MongooseHttpServer server; + +const char *ssid = "wifi"; +const char *password = "password"; + +const char *server_pem = +"-----BEGIN CERTIFICATE-----\r\n" +"MIIDDjCCAfagAwIBAgIBBDANBgkqhkiG9w0BAQsFADA/MRkwFwYDVQQDDBB0ZXN0\r\n" +"LmNlc2FudGEuY29tMRAwDgYDVQQKDAdDZXNhbnRhMRAwDgYDVQQLDAd0ZXN0aW5n\r\n" +"MB4XDTE2MTExMzEzMTgwMVoXDTI2MDgxMzEzMTgwMVowFDESMBAGA1UEAwwJbG9j\r\n" +"YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAro8CW1X0xaGm\r\n" +"GkDaMxKbXWA5Lw+seA61tioGrSIQzuqLYeJoFnwVgF0jB5PTj+3EiGMBcA/mh73V\r\n" +"AthTFmJBxj+agIp7/cvUBpgfLClmSYL2fZi6Fodz+f9mcry3XRw7O6vlamtWfTX8\r\n" +"TAmMSR6PXVBHLgjs5pDOFFmrNAsM5sLYU1/1MFvE2Z9InTI5G437IE1WchRSbpYd\r\n" +"HchC39XzpDGoInZB1a3OhcHm+xUtLpMJ0G0oE5VFEynZreZoEIY4JxspQ7LPsay9\r\n" +"fx3Tlk09gEMQgVCeCNiQwUxZdtLau2x61LNcdZCKN7FbFLJszv1U2uguELsTmi7E\r\n" +"6pHrTziosQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUE\r\n" +"DDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB\r\n" +"AQBUw0hbTcT6crzODO4QAXU7z4Xxn0LkxbXEsoThG1QCVgMc4Bhpx8gyz5CLyHYz\r\n" +"AiJOBFEeV0XEqoGTNMMFelR3Q5Tg9y1TYO3qwwAWxe6/brVzpts6NiG1uEMBnBFg\r\n" +"oN1x3I9x4NpOxU5MU1dlIxvKs5HQCoNJ8D0SqOX9BV/pZqwEgiCbuWDWQAlxkFpn\r\n" +"iLonlkVI5hTuybCSBsa9FEI9M6JJn9LZmlH90FYHeS4t6P8eOJCeekHL0jUG4Iae\r\n" +"DMP12h8Sd0yxIKmmZ+Q/p/D/BkuHf5Idv3hgyLkZ4mNznjK49wHaYM+BgBoL3Zeg\r\n" +"gJ2sWjUlokrbHswSBLLbUJIF\r\n" +"-----END CERTIFICATE-----\r\n"; + +const char *server_key = +"-----BEGIN PRIVATE KEY-----\r\n" +"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCujwJbVfTFoaYa\r\n" +"QNozEptdYDkvD6x4DrW2KgatIhDO6oth4mgWfBWAXSMHk9OP7cSIYwFwD+aHvdUC\r\n" +"2FMWYkHGP5qAinv9y9QGmB8sKWZJgvZ9mLoWh3P5/2ZyvLddHDs7q+Vqa1Z9NfxM\r\n" +"CYxJHo9dUEcuCOzmkM4UWas0CwzmwthTX/UwW8TZn0idMjkbjfsgTVZyFFJulh0d\r\n" +"yELf1fOkMagidkHVrc6Fweb7FS0ukwnQbSgTlUUTKdmt5mgQhjgnGylDss+xrL1/\r\n" +"HdOWTT2AQxCBUJ4I2JDBTFl20tq7bHrUs1x1kIo3sVsUsmzO/VTa6C4QuxOaLsTq\r\n" +"ketPOKixAgMBAAECggEAI+uNwpnHirue4Jwjyoqzqd1ZJxQEm5f7UIcJZKsz5kBh\r\n" +"ej0KykWybv27bZ2/1UhKPv6QlyzOdXRc1v8I6fxCKLeB5Z2Zsjo1YT4AfCfwwoPO\r\n" +"kT3SXTx2YyVpQYcP/HsIvVi8FtALtixbxJHaall9iugwHYr8pN17arihAE6d0wZC\r\n" +"JXtXRjUWwjKzXP8FoH4KhyadhHbDwIbbJe3cyLfdvp54Gr0YHha0JcOxYgDYNya4\r\n" +"OKxlCluI+hPF31iNzOmFLQVrdYynyPcR6vY5XOiANKE2iNbqCzRb54CvW9WMqObX\r\n" +"RD9t3DMOxGsbVNIwyzZndWy13HoQMGnrHfnGak9ueQKBgQDiVtOqYfLnUnTxvJ/b\r\n" +"qlQZr2ZmsYPZztxlP+DSqZGPD+WtGSo9+rozWfzjTv3KGIDLvf+GFVmjVHwlLQfd\r\n" +"u7eTemWHFc4HK68wruzPO/FdyVpQ4w9v3Usg+ll4a/PDEId0fDMjAr6kk4LC6t8y\r\n" +"9fJR0HjOz57jVnlrDt3v50G8BwKBgQDFbw+jRiUxXnBbDyXZLi+I4iGBGdC+CbaJ\r\n" +"CmsM6/TsOFc+GRsPwQF1gCGqdaURw76noIVKZJOSc8I+yiwU6izyh/xaju5JiWQd\r\n" +"kwbU1j4DE6GnxmT3ARmB7VvCxjaEZEAtICWs1QTKRz7PcTV8yr7Ng1A3VIy+NSpo\r\n" +"LFMMmk83hwKBgQDVCEwpLg/mUeHoNVVw95w4oLKNLb+gHeerFLiTDy8FrDzM88ai\r\n" +"l37yHly7xflxYia3nZkHpsi7xiUjCINC3BApKyasQoWskh1OgRY653yCfaYYQ96f\r\n" +"t3WjEH9trI2+p6wWo1+uMEMnu/9zXoW9/WeaQdGzNg+igh29+jxCNTPVuQKBgGV4\r\n" +"CN9vI5pV4QTLqjYOSJvfLDz/mYqxz0BrPE1tz3jAFAZ0PLZCCY/sBGFpCScyJQBd\r\n" +"vWNYgYeZOtGuci1llSgov4eDQfBFTlDsyWwFl+VY55IkoqtXw1ZFOQ3HdSlhpKIM\r\n" +"jZBgApA7QYq3sjeqs5lHzahCKftvs5XKgfxOKjxtAoGBALdnYe6xkDvGLvI51Yr+\r\n" +"Dy0TNcB5W84SxUKvM7DVEomy1QPB57ZpyQaoBq7adOz0pWJXfp7qo4950ZOhBGH1\r\n" +"hKbZ6c4ggwVJy2j49EgMok5NGCKvPAtabbR6H8Mz8DW9aXURxhWJvij+Qw1fWK4b\r\n" +"7G/qUI9iE5iUU7MkIcLIbTf/\r\n" +"-----END PRIVATE KEY-----\r\n"; + +const char *index_page = +"\n" +"\n" +"\n" +" \n" +" WebSocket Test\n" +" \n" +" \n" +"\n" +"\n" +"\n" +"\n" +"
\n" +"

Websocket PubSub Demonstration

\n" +"\n" +"

\n" +" This page demonstrates how Mongoose could be used to implement\n" +" \n" +" publish–subscribe pattern. Open this page in several browser\n" +" windows. Each window initiates persistent\n" +" WebSocket\n" +" connection with the server, making each browser window a websocket client.\n" +" Send messages, and see messages sent by other clients.\n" +"

\n" +"\n" +"
\n" +"
\n" +"\n" +"

\n" +" \n" +" \n" +"

\n" +"
\n" +"\n" +"\n"; + +#include + +void broadcast(MongooseHttpWebSocketConnection *from, MongooseString msg) +{ + char buf[500]; + char addr[32]; + mg_sock_addr_to_str(from->getRemoteAddress(), addr, sizeof(addr), + MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); + + snprintf(buf, sizeof(buf), "%s %.*s", addr, (int) msg.length(), msg.c_str()); + printf("%s\n", buf); + server.sendAll(from, buf); +} + +void setup() +{ + Serial.begin(115200); + +#ifdef START_ESP_WIFI + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) + { + Serial.printf("WiFi Failed!\n"); + return; + } + + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + Serial.print("Hostname: "); +#ifdef ESP32 + Serial.println(WiFi.getHostname()); +#elif defined(ESP8266) + Serial.println(WiFi.hostname()); +#endif +#endif + + Mongoose.begin(); + +#ifdef SIMPLE_SERVER_SECURE + if(false == server.begin(443, server_pem, server_key)) { + Serial.print("Failed to start server"); + return; + } +#else + server.begin(80); +#endif + + server.on("/$", HTTP_GET, [](MongooseHttpServerRequest *request) { + request->send(200, "text/html", index_page); + }); + + // Test the stream response class + server.on("/ws$")-> + onConnect([](MongooseHttpWebSocketConnection *connection) { + broadcast(connection, MongooseString("++ joined")); + })-> + onClose([](MongooseHttpServerRequest *c) { + MongooseHttpWebSocketConnection *connection = static_cast(c); + broadcast(connection, MongooseString("++ left")); + })-> + onFrame([](MongooseHttpWebSocketConnection *connection, int flags, uint8_t *data, size_t len) { + broadcast(connection, MongooseString((const char *)data, len)); + }); +} + +void loop() +{ + Mongoose.poll(1000); + Serial.printf("Free memory %u\n", ESP.getFreeHeap()); +} diff --git a/lib/PsychicHttp/examples/old/websocket_chat/test/README b/lib/PsychicHttp/examples/old/websocket_chat/test/README new file mode 100644 index 0000000..df5066e --- /dev/null +++ b/lib/PsychicHttp/examples/old/websocket_chat/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/lib/PsychicHttp/examples/old/websocket_chat/test/tests.rest b/lib/PsychicHttp/examples/old/websocket_chat/test/tests.rest new file mode 100644 index 0000000..4072428 --- /dev/null +++ b/lib/PsychicHttp/examples/old/websocket_chat/test/tests.rest @@ -0,0 +1,35 @@ +# Name: REST Client +# Id: humao.rest-client +# Description: REST Client for Visual Studio Code +# Version: 0.21.3 +# Publisher: Huachao Mao +# VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=humao.rest-client + +@baseUrl = http://172.16.0.87 + +### + +GET {{baseUrl}}/ HTTP/1.1 + +### + +GET {{baseUrl}}/get?message=Hello+World HTTP/1.1 + +### + +POST {{baseUrl}}/post HTTP/1.1 +Content-Type: application/x-www-form-urlencoded;charset=UTF-8 + +message=Hello+World + +### + +GET {{baseUrl}}/someRandomFile HTTP/1.1 + +### + +GET {{baseUrl}}/basic HTTP/1.1 + +### + +GET {{baseUrl}}/stream HTTP/1.1 diff --git a/lib/PsychicHttp/examples/platformio/.gitignore b/lib/PsychicHttp/examples/platformio/.gitignore new file mode 100644 index 0000000..9e5f911 --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/lib/PsychicHttp/examples/platformio/data/custom.txt b/lib/PsychicHttp/examples/platformio/data/custom.txt new file mode 100644 index 0000000..d3db23d --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/data/custom.txt @@ -0,0 +1 @@ +Custom text file. \ No newline at end of file diff --git a/lib/PsychicHttp/examples/platformio/data/img/request_flow.png b/lib/PsychicHttp/examples/platformio/data/img/request_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..1005a38895f77797f39ecd018d5fd6a746502a62 GIT binary patch literal 30509 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 + + + + + PsychicHTTP SoftAP Demo + + + +
+

SoftAP Demo

+

You are connected to the ESP in SoftAP mode.

+
+ + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/platformio/data/www/alien.png b/lib/PsychicHttp/examples/platformio/data/www/alien.png new file mode 100644 index 0000000000000000000000000000000000000000..a030da0761a60fbfc813fbef0716f801b7aa5ca6 GIT binary patch literal 28598 zcmYJaWmuG5*Dws@07I9c(v3)ofOHIvbW4}Clyr9^NOyN5-5@DFv~)>#H+&~v_w#_92^|Fq=bkf931@l^B)8e_|M?S$Wr;jgr@UiH3RbUqIq6nxmxyTg3pllF+C|;?J8R22|OHS|$5C&}~C$Cp7CMXc++U4Q4NgN|sM6}41E?6Tp^Y-YI z6#fhiLo&L$mU?MXaTZi`JA7SWs9of%+UM(=S3bi+!8kp6V9v*Az4tWod4Y=G#KQ2K zDcB2cxINq5-$Y_hu6%la!EBxtD%w8(hSb9e7Pwy4*Eb&|887A+KvZ>oyX1?J- zM_ibe8Z;Ta%NmvTHtCP(e8mmObB2XPqVT@qFQ_-LKpD z4T?X3%T@C>*_O^`P3Wj<7WhmK$V3NUz+mPR`cM4KCr_Q)UaR$Yx(0YN%qF?DZ&y`N zAGIOqU_mk>5Z1YkX;uX#^55%EvUo4Qj6EtfAk?lN4O`*o5JZ5poyLV8JS&{(Mbn>< zPX+E(pQQT2yBS@Z13j>QB*B5TH)n+hSOA@+1Vb@U^q_?yjMU z2~%{?Q*7L)c~U=7T$BaddgOG67^VOH%SVnbMvG+wfrm5+(-4@EWOT?a39i3J(fMud z%doaamh8Zr9~Utk%RLx+ZlaL!XTy>H=3cr z^tlDkmg$ILOCXLaEFjp1f~;drXNd6l^U4k(HtE%O+S*0|5bJ9YXdDoR6psUhR~e*p zR7Vh`eK)eTaTZ4Rx+QqIV=*g2R3HQyvbx^*=o>lZq0au<&P=SA?v8o{G0U$*5?K%? z7YZT%LRl#i{D%PHQC9gENY)

&213^-9%ncHPP24Z3H*pr~$xg!|8Chf_Iiynyz> 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 c1aJ>H;&fJ5{Z*>gh@M|f|*Vo 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 BlhptK literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/platformio/data/www/index.html b/lib/PsychicHttp/examples/platformio/data/www/index.html new file mode 100644 index 0000000..76f14a9 --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/data/www/index.html @@ -0,0 +1,236 @@ + + + + + + PsychicHTTP Demo + + + +

+

Basic Request Examples

+ + +

Static Serving

+

+ + +

+

Text File

+ +

Simple POST Form

+
+ + +
+ + +
+ +
+ +

Basic File Upload

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

Multipart POST Form

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

Websocket Demo

+ + + +
+ +
+ + + +

EventSource Demo

+ +
+ +
+ + +
+ + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/platformio/data/www/text.txt b/lib/PsychicHttp/examples/platformio/data/www/text.txt new file mode 100644 index 0000000..5375816 --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/data/www/text.txt @@ -0,0 +1 @@ +Test File. diff --git a/lib/PsychicHttp/examples/platformio/include/README b/lib/PsychicHttp/examples/platformio/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/PsychicHttp/examples/platformio/lib/README b/lib/PsychicHttp/examples/platformio/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/examples/platformio/platformio.ini b/lib/PsychicHttp/examples/platformio/platformio.ini new file mode 100644 index 0000000..77a33bc --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/platformio.ini @@ -0,0 +1,30 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +lib_deps = + ; devmode: with this disabled make a symlink from platformio/lib to the PsychicHttp directory + ;hoeken/PsychicHttp + bblanchon/ArduinoJson +board_build.filesystem = littlefs + +[env:default] +build_flags = + -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_WARN + ;-D ENABLE_ASYNC + +; [env:arduino3] +; platform = https://github.com/platformio/platform-espressif32.git +; platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32#master \ No newline at end of file diff --git a/lib/PsychicHttp/examples/platformio/src/main.cpp b/lib/PsychicHttp/examples/platformio/src/main.cpp new file mode 100644 index 0000000..eb52479 --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/src/main.cpp @@ -0,0 +1,583 @@ +/* + PsychicHTTP Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +/********************************************************************************************** +* Note: this demo relies on various files to be uploaded on the LittleFS partition +* PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image +**********************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include "_secret.h" +#include +//#include //uncomment this to enable HTTPS / SSL + +#ifndef WIFI_SSID + #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." +#endif + +//Enter your WIFI credentials in secret.h +const char *ssid = WIFI_SSID; +const char *password = WIFI_PASS; + +// Set your SoftAP credentials +const char *softap_ssid = "PsychicHttp"; +const char *softap_password = ""; +IPAddress softap_ip(10, 0, 0, 1); + +//credentials for the /auth-basic and /auth-digest examples +const char *app_user = "admin"; +const char *app_pass = "admin"; +const char *app_name = "Your App"; + +//hostname for mdns (psychic.local) +const char *local_hostname = "psychic"; + +//#define PSY_ENABLE_SSL to enable ssl +#ifdef PSY_ENABLE_SSL + bool app_enable_ssl = true; + String server_cert; + String server_key; +#endif + +//our main server object +#ifdef PSY_ENABLE_SSL + PsychicHttpsServer server; +#else + PsychicHttpServer server; +#endif +PsychicWebSocketHandler websocketHandler; +PsychicEventSource eventSource; + +//NTP server stuff +const char *ntpServer1 = "pool.ntp.org"; +const char *ntpServer2 = "time.nist.gov"; +const long gmtOffset_sec = 0; +const int daylightOffset_sec = 0; +struct tm timeinfo; + +// Callback function (gets called when time adjusts via NTP) +void timeAvailable(struct timeval *t) +{ + if (!getLocalTime(&timeinfo)) { + Serial.println("Failed to obtain time"); + return; + } + + Serial.print("NTP update: "); + char buffer[40]; + strftime(buffer, 40, "%FT%T%z", &timeinfo); + Serial.println(buffer); +} + +bool connectToWifi() +{ + //dual client and AP mode + WiFi.mode(WIFI_AP_STA); + + // Configure SoftAP + WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00 + WiFi.softAP(softap_ssid, softap_password); + IPAddress myIP = WiFi.softAPIP(); + Serial.print("SoftAP IP Address: "); + Serial.println(myIP); + + Serial.println(); + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + WiFi.begin(ssid, password); + // Auto reconnect is set true as default + // To set auto connect off, use the following function + // WiFi.setAutoReconnect(false); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void setup() +{ + Serial.begin(115200); + delay(10); + + // We start by connecting to a WiFi network + // To debug, please enable Core Debug Level to Verbose + if (connectToWifi()) + { + //Setup our NTP to get the current time. + sntp_set_time_sync_notification_cb(timeAvailable); + sntp_servermode_dhcp(1); // (optional) + configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2); + + //set up our esp32 to listen on the local_hostname.local domain + if (!MDNS.begin(local_hostname)) { + Serial.println("Error starting mDNS"); + return; + } + MDNS.addService("http", "tcp", 80); + + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + //look up our keys? + #ifdef PSY_ENABLE_SSL + if (app_enable_ssl) + { + File fp = LittleFS.open("/server.crt"); + if (fp) + { + server_cert = fp.readString(); + + // Serial.println("Server Cert:"); + // Serial.println(server_cert); + } + else + { + Serial.println("server.pem not found, SSL not available"); + app_enable_ssl = false; + } + fp.close(); + + File fp2 = LittleFS.open("/server.key"); + if (fp2) + { + server_key = fp2.readString(); + + // Serial.println("Server Key:"); + // Serial.println(server_key); + } + else + { + Serial.println("server.key not found, SSL not available"); + app_enable_ssl = false; + } + fp2.close(); + } + #endif + + //setup server config stuff here + server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + + #ifdef PSY_ENABLE_SSL + server.ssl_config.httpd.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) + + //do we want secure or not? + if (app_enable_ssl) + { + server.listen(443, server_cert.c_str(), server_key.c_str()); + + //this creates a 2nd server listening on port 80 and redirects all requests HTTPS + PsychicHttpServer *redirectServer = new PsychicHttpServer(); + redirectServer->config.ctrl_port = 20424; // just a random port different from the default one + redirectServer->listen(80); + redirectServer->onNotFound([](PsychicRequest *request) { + String url = "https://" + request->host() + request->url(); + return request->redirect(url.c_str()); + }); + } + else + server.listen(80); + #else + server.listen(80); + #endif + + DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); + + //serve static files from LittleFS/www on / only to clients on same wifi network + //this is where our /index.html file lives + // curl -i http://psychic.local/ + PsychicStaticFileHandler* handler = server.serveStatic("/", LittleFS, "/www/"); + handler->setFilter(ON_STA_FILTER); + handler->setCacheControl("max-age=60"); + + //serve static files from LittleFS/www-ap on / only to clients on SoftAP + //this is where our /index.html file lives + server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); + + //serve static files from LittleFS/img on /img + //it's more efficient to serve everything from a single www directory, but this is also possible. + // curl -i http://psychic.local/img/request_flow.png + server.serveStatic("/img", LittleFS, "/img/"); + + //you can also serve single files + // curl -i http://psychic.local/myfile.txt + server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); + + //example callback everytime a connection is opened + server.onOpen([](PsychicClient *client) { + Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); + }); + + //example callback everytime a connection is closed + server.onClose([](PsychicClient *client) { + Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); + }); + + //api - json message passed in as post body + // curl -i -X POST -H "Content-Type: application/json" -d '{"foo":"bar"}' http://psychic.local/api + server.on("/api", HTTP_POST, [](PsychicRequest *request, JsonVariant &json) + { + JsonObject input = json.as(); + + //create our response json + PsychicJsonResponse response = PsychicJsonResponse(request); + JsonObject output = response.getRoot(); + + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (input.containsKey("foo")) + { + String foo = input["foo"]; + output["foo"] = foo; + } + + return response.send(); + }); + + //ip - get info about the client + // curl -i http://psychic.local/ip + server.on("/ip", HTTP_GET, [](PsychicRequest *request) + { + String output = "Your IP is: " + request->client()->remoteIP().toString(); + return request->reply(output.c_str()); + }); + + //client connect/disconnect to a url + // curl -i http://psychic.local/handler + PsychicWebHandler *connectionHandler = new PsychicWebHandler(); + connectionHandler->onRequest([](PsychicRequest *request) + { + return request->reply("OK"); + }); + connectionHandler->onOpen([](PsychicClient *client) { + Serial.printf("[handler] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); + }); + connectionHandler->onClose([](PsychicClient *client) { + Serial.printf("[handler] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); + }); + + //add it to our server + server.on("/handler", connectionHandler); + + //api - parameters passed in via query eg. /api?foo=bar + // curl -i 'http://psychic.local/api?foo=bar' + server.on("/api", HTTP_GET, [](PsychicRequest *request) + { + //showcase some of the variables + Serial.println(request->host()); + Serial.println(request->uri()); + Serial.println(request->path()); + Serial.println(request->queryString()); + + //create a response object + //create our response json + PsychicJsonResponse response = PsychicJsonResponse(request); + JsonObject output = response.getRoot(); + + output["msg"] = "status"; + output["status"] = "success"; + output["millis"] = millis(); + + //work with some params + if (request->hasParam("foo")) + { + String foo = request->getParam("foo")->value(); + output["foo"] = foo; + } + + return response.send(); + }); + + //JsonResponse example + // curl -i http://psychic.local/json + server.on("/json", HTTP_GET, [](PsychicRequest *request) + { + PsychicJsonResponse response = PsychicJsonResponse(request); + + char key[16]; + char value[32]; + JsonObject root = response.getRoot(); + for (int i=0; i<100; i++) + { + sprintf(key, "key%d", i); + sprintf(value, "value is %d", i); + root[key] = value; + } + + return response.send(); + }); + + //how to redirect a request + // curl -i http://psychic.local/redirect + server.on("/redirect", HTTP_GET, [](PsychicRequest *request) + { + return request->redirect("/alien.png"); + }); + + //how to do basic auth + // curl -i --user admin:admin http://psychic.local/auth-basic + server.on("/auth-basic", HTTP_GET, [](PsychicRequest *request) + { + if (!request->authenticate(app_user, app_pass)) + return request->requestAuthentication(BASIC_AUTH, app_name, "You must log in."); + return request->reply("Auth Basic Success!"); + }); + + //how to do digest auth + // curl -i --user admin:admin http://psychic.local/auth-digest + server.on("/auth-digest", HTTP_GET, [](PsychicRequest *request) + { + if (!request->authenticate(app_user, app_pass)) + return request->requestAuthentication(DIGEST_AUTH, app_name, "You must log in."); + return request->reply("Auth Digest Success!"); + }); + + //example of getting / setting cookies + // curl -i -b cookie.txt -c cookie.txt http://psychic.local/cookies + server.on("/cookies", HTTP_GET, [](PsychicRequest *request) + { + PsychicResponse response(request); + + int counter = 0; + if (request->hasCookie("counter")) + { + counter = std::stoi(request->getCookie("counter").c_str()); + counter++; + } + + char cookie[10]; + sprintf(cookie, "%i", counter); + + response.setCookie("counter", cookie); + response.setContent(cookie); + return response.send(); + }); + + //example of getting POST variables + // curl -i -d "param1=value1¶m2=value2" -X POST http://psychic.local/post + server.on("/post", HTTP_POST, [](PsychicRequest *request) + { + String output; + output += "Param 1: " + request->getParam("param1")->value() + "
\n"; + output += "Param 2: " + request->getParam("param2")->value() + "
\n"; + + return request->reply(output.c_str()); + }); + + //you can set up a custom 404 handler. + // curl -i http://psychic.local/404 + server.onNotFound([](PsychicRequest *request) + { + return request->reply(404, "text/html", "Custom 404 Handler"); + }); + + //handle a very basic upload as post body + PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); + uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + Serial.printf("Writing %d/%d bytes to: %s\n", (int)index+(int)len, request->contentLength(), path.c_str()); + + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + uploadHandler->onRequest([](PsychicRequest *request) + { + String url = "/" + request->getFilename(); + String output = "" + url + ""; + + return request->reply(output.c_str()); + }); + + //wildcard basic file upload - POST to /upload/filename.ext + // use http://psychic.local/ to test + server.on("/upload/*", HTTP_POST, uploadHandler); + + //a little bit more complicated multipart form + PsychicUploadHandler *multipartHandler = new PsychicUploadHandler(); + multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { + File file; + String path = "/www/" + filename; + + //some progress over serial. + Serial.printf("Writing %d bytes to: %s\n", (int)len, path.c_str()); + if (last) + Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); + + //our first call? + if (!index) + file = LittleFS.open(path, FILE_WRITE); + else + file = LittleFS.open(path, FILE_APPEND); + + if(!file) { + Serial.println("Failed to open file"); + return ESP_FAIL; + } + + if(!file.write(data, len)) { + Serial.println("Write failed"); + return ESP_FAIL; + } + + return ESP_OK; + }); + + //gets called after upload has been handled + multipartHandler->onRequest([](PsychicRequest *request) + { + if (request->hasParam("file_upload")) + { + PsychicWebParameter *file = request->getParam("file_upload"); + + String url = "/" + file->value(); + String output; + + output += "" + url + "
\n"; + output += "Bytes: " + String(file->size()) + "
\n"; + output += "Param 1: " + request->getParam("param1")->value() + "
\n"; + output += "Param 2: " + request->getParam("param2")->value() + "
\n"; + + return request->reply(output.c_str()); + } + else + return request->reply("No upload."); + }); + + //wildcard basic file upload - POST to /upload/filename.ext + // use http://psychic.local/ to test + server.on("/multipart", HTTP_POST, multipartHandler); + + //a websocket echo server + // npm install -g wscat + // wscat -c ws://psychic.local/ws + websocketHandler.onOpen([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); + client->sendMessage("Hello!"); + }); + websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { + Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); + return request->reply(frame); + }); + websocketHandler.onClose([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); + }); + server.on("/ws", &websocketHandler); + + //EventSource server + // curl -i -N http://psychic.local/events + eventSource.onOpen([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); + client->send("Hello user!", NULL, millis(), 1000); + }); + eventSource.onClose([](PsychicEventSourceClient *client) { + Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); + }); + server.on("/events", &eventSource); + } +} + +unsigned long lastUpdate = 0; +char output[60]; + +void loop() +{ + if (millis() - lastUpdate > 2000) + { + sprintf(output, "Millis: %d\n", millis()); + websocketHandler.sendAll(output); + + sprintf(output, "%d", millis()); + eventSource.send(output, "millis", millis(), 0); + + lastUpdate = millis(); + } +} \ No newline at end of file diff --git a/lib/PsychicHttp/examples/platformio/src/secret.h b/lib/PsychicHttp/examples/platformio/src/secret.h new file mode 100644 index 0000000..6d4bb15 --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/src/secret.h @@ -0,0 +1,2 @@ +#define WIFI_SSID "Your_SSID" +#define WIFI_PASS "Your_PASS" \ No newline at end of file diff --git a/lib/PsychicHttp/examples/platformio/test/README b/lib/PsychicHttp/examples/platformio/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/lib/PsychicHttp/examples/platformio/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/lib/PsychicHttp/examples/websockets/.gitignore b/lib/PsychicHttp/examples/websockets/.gitignore new file mode 100644 index 0000000..e37ccaa --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/.gitignore @@ -0,0 +1,8 @@ +.pio +.vscode/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +src/_secret.h +lib/PsychicHttp/ \ No newline at end of file diff --git a/lib/PsychicHttp/examples/websockets/data/www/favicon.ico b/lib/PsychicHttp/examples/websockets/data/www/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bdf785c1d706e64c1fa5cc305893a54d16c87fc7 GIT binary patch literal 67646 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 BlhptK literal 0 HcmV?d00001 diff --git a/lib/PsychicHttp/examples/websockets/data/www/index.html b/lib/PsychicHttp/examples/websockets/data/www/index.html new file mode 100644 index 0000000..c4ba4d9 --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/data/www/index.html @@ -0,0 +1,76 @@ + + + + + + PsychicHTTP Websocket Demo + + + +
+

Websocket Demo

+ + + +
+ +
+ + +
+ + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/websockets/include/README b/lib/PsychicHttp/examples/websockets/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/PsychicHttp/examples/websockets/lib/README b/lib/PsychicHttp/examples/websockets/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/PsychicHttp/examples/websockets/platformio.ini b/lib/PsychicHttp/examples/websockets/platformio.ini new file mode 100644 index 0000000..4849f9e --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/platformio.ini @@ -0,0 +1,25 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env] +platform = espressif32 +framework = arduino +board = esp32dev +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +lib_deps = + ; devmode: with this disabled make a symlink from platformio/lib to the PsychicHttp directory + ;hoeken/PsychicHttp + bblanchon/ArduinoJson +board_build.filesystem = littlefs + +[env:default] +build_flags = + -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_WARN \ No newline at end of file diff --git a/lib/PsychicHttp/examples/websockets/src/main.cpp b/lib/PsychicHttp/examples/websockets/src/main.cpp new file mode 100644 index 0000000..a7c79b4 --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/src/main.cpp @@ -0,0 +1,225 @@ +/* + PsychicHTTP Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +/********************************************************************************************** +* Note: this demo relies on various files to be uploaded on the LittleFS partition +* PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image +**********************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include "_secret.h" +#include +#include + +#ifndef WIFI_SSID + #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." +#endif + +//Enter your WIFI credentials in secret.h +const char *ssid = WIFI_SSID; +const char *password = WIFI_PASS; + +//hostname for mdns (psychic.local) +const char *local_hostname = "psychic"; + +PsychicHttpServer server; +PsychicWebSocketHandler websocketHandler; + +typedef struct { + int socket; + char *buffer; + size_t len; +} WebsocketMessage; + +QueueHandle_t wsMessages; + +bool connectToWifi() +{ + Serial.print("[WiFi] Connecting to "); + Serial.println(ssid); + + //setup our wifi + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + // Will try for about 10 seconds (20x 500ms) + int tryDelay = 500; + int numberOfTries = 20; + + // Wait for the WiFi event + while (true) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + Serial.println("[WiFi] SSID not found"); + break; + case WL_CONNECT_FAILED: + Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); + return false; + break; + case WL_CONNECTION_LOST: + Serial.println("[WiFi] Connection was lost"); + break; + case WL_SCAN_COMPLETED: + Serial.println("[WiFi] Scan is completed"); + break; + case WL_DISCONNECTED: + Serial.println("[WiFi] WiFi is disconnected"); + break; + case WL_CONNECTED: + Serial.println("[WiFi] WiFi is connected!"); + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); + return true; + break; + default: + Serial.print("[WiFi] WiFi Status: "); + Serial.println(WiFi.status()); + break; + } + delay(tryDelay); + + if (numberOfTries <= 0) + { + Serial.print("[WiFi] Failed to connect to WiFi!"); + // Use disconnect function to force stop trying to connect + WiFi.disconnect(); + return false; + } + else + { + numberOfTries--; + } + } + + return false; +} + +void setup() +{ + Serial.begin(115200); + delay(10); + + //prepare our message queue of 10 messages + wsMessages = xQueueCreate(10, sizeof(WebsocketMessage)); + if (wsMessages == 0) + Serial.printf("Failed to create queue= %p\n", wsMessages); + + // We start by connecting to a WiFi network + // To debug, please enable Core Debug Level to Verbose + if (connectToWifi()) + { + //set up our esp32 to listen on the local_hostname.local domain + if (!MDNS.begin(local_hostname)) { + Serial.println("Error starting mDNS"); + return; + } + MDNS.addService("http", "tcp", 80); + + if(!LittleFS.begin()) + { + Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); + return; + } + + server.listen(80); + + //this is where our /index.html file lives + // curl -i http://psychic.local/ + PsychicStaticFileHandler* handler = server.serveStatic("/", LittleFS, "/www/"); + + //a websocket echo server + // npm install -g wscat + // wscat -c ws://psychic.local/ws + websocketHandler.onOpen([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); + client->sendMessage("Hello!"); + }); + websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) + { + Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); + + //we are allocating memory here, and the worker will free it + WebsocketMessage wm; + wm.socket = request->client()->socket(); + wm.len = frame->len; + wm.buffer = (char *)malloc(frame->len); + + //did we flame out? + if (wm.buffer == NULL) + { + Serial.printf("Queue message: unable to allocate %d bytes\n", frame->len); + return ESP_FAIL; + } + + //okay, copy it over + memcpy(wm.buffer, frame->payload, frame->len); + + //try to throw it in our queue + if (xQueueSend(wsMessages, &wm, 1) != pdTRUE) + { + Serial.printf("[socket] queue full #%d\n", wm.socket); + + //free the memory... no worker to do it for us. + free(wm.buffer); + } + + //send a throttle message if we're full + if (!uxQueueSpacesAvailable(wsMessages)) + return request->reply("Queue Full"); + + return ESP_OK; + }); + websocketHandler.onClose([](PsychicWebSocketClient *client) { + Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); + }); + server.on("/ws", &websocketHandler); + } +} + +unsigned long lastUpdate = 0; +char output[60]; + +void loop() +{ + //process our websockets outside the callback. + WebsocketMessage message; + while (xQueueReceive(wsMessages, &message, 0) == pdTRUE) + { + //make sure our client is still good. + PsychicWebSocketClient *client = websocketHandler.getClient(message.socket); + if (client == NULL) { + Serial.printf("[socket] client #%d bad, bailing\n", message.socket); + return; + } + + //echo it back to the client. + //alternatively, this is where you would deserialize a json message, parse it, and generate a response if needed + client->sendMessage(HTTPD_WS_TYPE_TEXT, message.buffer, message.len); + + //make sure to release our memory! + free(message.buffer); + } + + //send a periodic update to all clients + if (millis() - lastUpdate > 2000) + { + sprintf(output, "Millis: %d\n", millis()); + websocketHandler.sendAll(output); + + lastUpdate = millis(); + } +} \ No newline at end of file diff --git a/lib/PsychicHttp/examples/websockets/src/secret.h b/lib/PsychicHttp/examples/websockets/src/secret.h new file mode 100644 index 0000000..6d4bb15 --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/src/secret.h @@ -0,0 +1,2 @@ +#define WIFI_SSID "Your_SSID" +#define WIFI_PASS "Your_PASS" \ No newline at end of file diff --git a/lib/PsychicHttp/examples/websockets/test/README b/lib/PsychicHttp/examples/websockets/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/lib/PsychicHttp/examples/websockets/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/lib/PsychicHttp/library.json b/lib/PsychicHttp/library.json new file mode 100644 index 0000000..e732d31 --- /dev/null +++ b/lib/PsychicHttp/library.json @@ -0,0 +1,53 @@ +{ + "name": "PsychicHttp", + "version": "1.2.1", + "description": "Arduino style wrapper around ESP-IDF HTTP library. HTTP server with SSL + websockets. Works on esp32 and probably esp8266", + "keywords": "network,http,https,tcp,ssl,tls,websocket,espasyncwebserver", + "repository": + { + "type": "git", + "url": "https://github.com/hoeken/PsychicHttp" + }, + "authors": + [ + { + "name": "Zach Hoeken", + "email": "hoeken@gmail.com", + "maintainer": true + } + ], + "license" : "MIT", + "examples": [ + { + "name": "platformio", + "base": "examples/platformio", + "files": [ + "src/main.cpp" + ] + } + ], + "frameworks": "arduino", + "platforms": "espressif32", + "dependencies": [ + { + "owner": "bblanchon", + "name": "ArduinoJson", + "version": "^7.0.4" + }, + { + "owner": "plageoj", + "name" : "UrlEncode", + "version" : "^1.0.1" + } + ], + "export": { + "include": [ + "examples/platformio", + "src", + "library.json", + "library.properties", + "LICENSE", + "README.md" + ] + } +} diff --git a/lib/PsychicHttp/library.properties b/lib/PsychicHttp/library.properties new file mode 100644 index 0000000..885afb5 --- /dev/null +++ b/lib/PsychicHttp/library.properties @@ -0,0 +1,11 @@ +name=PsychicHttp +version=1.2.1 +author=Zach Hoeken +maintainer=Zach Hoeken +sentence=PsychicHttp is a robust webserver that supports http/https + websockets. +paragraph=This library is based on the ESP-IDF HTTP Server library which is asynchronous, does http / https+ssl and supports websockets. +category=Communication +architectures=esp32 +url=https://github.com/hoeken/PsychicHttp +includes=PsychicHttp.h +depends=ArduinoJson,UrlEncode \ No newline at end of file diff --git a/lib/PsychicHttp/request flow.drawio b/lib/PsychicHttp/request flow.drawio new file mode 100644 index 0000000..8915583 --- /dev/null +++ b/lib/PsychicHttp/request flow.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/lib/PsychicHttp/src/ChunkPrinter.cpp b/lib/PsychicHttp/src/ChunkPrinter.cpp new file mode 100644 index 0000000..b3f8713 --- /dev/null +++ b/lib/PsychicHttp/src/ChunkPrinter.cpp @@ -0,0 +1,85 @@ + +#include "ChunkPrinter.h" + +ChunkPrinter::ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len) : + _response(response), + _buffer(buffer), + _length(len), + _pos(0) +{} + +ChunkPrinter::~ChunkPrinter() +{ + flush(); +} + +size_t ChunkPrinter::write(uint8_t c) +{ + esp_err_t err; + + //if we're full, send a chunk + if (_pos == _length) + { + _pos = 0; + err = _response->sendChunk(_buffer, _length); + + if (err != ESP_OK) + return 0; + } + + _buffer[_pos] = c; + _pos++; + return 1; +} + +size_t ChunkPrinter::write(const uint8_t *buffer, size_t size) +{ + size_t written = 0; + + while (written < size) + { + size_t space = _length - _pos; + size_t blockSize = std::min(space, size - written); + + memcpy(_buffer + _pos, buffer + written, blockSize); + _pos += blockSize; + + if (_pos == _length) + { + _pos = 0; + + if (_response->sendChunk(_buffer, _length) != ESP_OK) + return written; + } + written += blockSize; //Update if sent correctly. + } + return written; +} + +void ChunkPrinter::flush() +{ + if (_pos) + { + _response->sendChunk(_buffer, _pos); + _pos = 0; + } +} + +size_t ChunkPrinter::copyFrom(Stream &stream) +{ + size_t count = 0; + + while (stream.available()){ + + if (_pos == _length) + { + _response->sendChunk(_buffer, _length); + _pos = 0; + } + + size_t readBytes = stream.readBytes(_buffer + _pos, _length - _pos); + _pos += readBytes; + count += readBytes; + } + return count; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/ChunkPrinter.h b/lib/PsychicHttp/src/ChunkPrinter.h new file mode 100644 index 0000000..f63fac0 --- /dev/null +++ b/lib/PsychicHttp/src/ChunkPrinter.h @@ -0,0 +1,27 @@ +#ifndef ChunkPrinter_h +#define ChunkPrinter_h + +#include "PsychicResponse.h" +#include + +class ChunkPrinter : public Print +{ + private: + PsychicResponse *_response; + uint8_t *_buffer; + size_t _length; + size_t _pos; + + public: + ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len); + ~ChunkPrinter(); + + size_t write(uint8_t c) override; + size_t write(const uint8_t *buffer, size_t size) override; + + size_t copyFrom(Stream &stream); + + void flush() override; +}; + +#endif \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicClient.cpp b/lib/PsychicHttp/src/PsychicClient.cpp new file mode 100644 index 0000000..fd7820d --- /dev/null +++ b/lib/PsychicHttp/src/PsychicClient.cpp @@ -0,0 +1,72 @@ +#include "PsychicClient.h" +#include "PsychicHttpServer.h" +#include + +PsychicClient::PsychicClient(httpd_handle_t server, int socket) : + _server(server), + _socket(socket), + _friend(NULL), + isNew(false) +{} + +PsychicClient::~PsychicClient() { +} + +httpd_handle_t PsychicClient::server() { + return _server; +} + +int PsychicClient::socket() { + return _socket; +} + +// I'm not sure this is entirely safe to call. I was having issues with race conditions when highly loaded using this. +esp_err_t PsychicClient::close() +{ + esp_err_t err = httpd_sess_trigger_close(_server, _socket); + //PsychicHttpServer::closeCallback(_server, _socket); // call this immediately so the client is taken off the list. + + return err; +} + +IPAddress PsychicClient::localIP() +{ + IPAddress address(0,0,0,0); + + char ipstr[INET6_ADDRSTRLEN]; + struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing + socklen_t addr_size = sizeof(addr); + + if (getsockname(_socket, (struct sockaddr *)&addr, &addr_size) < 0) { + ESP_LOGE(PH_TAG, "Error getting client IP"); + return address; + } + + // Convert to IPv4 string + inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr)); + //ESP_LOGD(PH_TAG, "Client Local IP => %s", ipstr); + address.fromString(ipstr); + + return address; +} + +IPAddress PsychicClient::remoteIP() +{ + IPAddress address(0,0,0,0); + + char ipstr[INET6_ADDRSTRLEN]; + struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing + socklen_t addr_size = sizeof(addr); + + if (getpeername(_socket, (struct sockaddr *)&addr, &addr_size) < 0) { + ESP_LOGE(PH_TAG, "Error getting client IP"); + return address; + } + + // Convert to IPv4 string + inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr)); + //ESP_LOGD(PH_TAG, "Client Remote IP => %s", ipstr); + address.fromString(ipstr); + + return address; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicClient.h b/lib/PsychicHttp/src/PsychicClient.h new file mode 100644 index 0000000..b823df7 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicClient.h @@ -0,0 +1,35 @@ +#ifndef PsychicClient_h +#define PsychicClient_h + +#include "PsychicCore.h" + +/* +* PsychicClient :: Generic wrapper around the ESP-IDF socket +*/ + +class PsychicClient { + protected: + httpd_handle_t _server; + int _socket; + + public: + PsychicClient(httpd_handle_t server, int socket); + ~PsychicClient(); + + //no idea if this is the right way to do it or not, but lets see. + //pointer to our derived class (eg. PsychicWebSocketConnection) + void *_friend; + + bool isNew = false; + + bool operator==(PsychicClient& rhs) const { return _socket == rhs.socket(); } + + httpd_handle_t server(); + int socket(); + esp_err_t close(); + + IPAddress localIP(); + IPAddress remoteIP(); +}; + +#endif \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicCore.h b/lib/PsychicHttp/src/PsychicCore.h new file mode 100644 index 0000000..fe88b38 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicCore.h @@ -0,0 +1,107 @@ +#ifndef PsychicCore_h +#define PsychicCore_h + +#define PH_TAG "psychic" + +//version numbers +#define PSYCHIC_HTTP_VERSION_MAJOR 1 +#define PSYCHIC_HTTP_VERSION_MINOR 1 +#define PSYCHIC_HTTP_VERSION_PATCH 0 + +#ifndef MAX_COOKIE_SIZE + #define MAX_COOKIE_SIZE 512 +#endif + +#ifndef FILE_CHUNK_SIZE + #define FILE_CHUNK_SIZE 8*1024 +#endif + +#ifndef STREAM_CHUNK_SIZE + #define STREAM_CHUNK_SIZE 1024 +#endif + +#ifndef MAX_UPLOAD_SIZE + #define MAX_UPLOAD_SIZE (2048*1024) // 2MB +#endif + +#ifndef MAX_REQUEST_BODY_SIZE + #define MAX_REQUEST_BODY_SIZE (16*1024) //16K +#endif + +#ifdef ARDUINO + #include +#endif + +#include +#include +#include +#include +#include "esp_random.h" +#include "MD5Builder.h" +#include +#include "FS.h" +#include + +enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH }; + +String urlDecode(const char* encoded); + +class PsychicHttpServer; +class PsychicRequest; +class PsychicWebSocketRequest; +class PsychicClient; + +//filter function definition +typedef std::function PsychicRequestFilterFunction; + +//client connect callback +typedef std::function PsychicClientCallback; + +//callback definitions +typedef std::function PsychicHttpRequestCallback; +typedef std::function PsychicJsonRequestCallback; + +struct HTTPHeader { + char * field; + char * value; +}; + +class DefaultHeaders { + std::list _headers; + +public: + DefaultHeaders() {} + + void addHeader(const String& field, const String& value) + { + addHeader(field.c_str(), value.c_str()); + } + + void addHeader(const char * field, const char * value) + { + HTTPHeader header; + + //these are just going to stick around forever. + header.field =(char *)malloc(strlen(field)+1); + header.value = (char *)malloc(strlen(value)+1); + + strlcpy(header.field, field, strlen(field)+1); + strlcpy(header.value, value, strlen(value)+1); + + _headers.push_back(header); + } + + const std::list& getHeaders() { return _headers; } + + //delete the copy constructor, singleton class + DefaultHeaders(DefaultHeaders const &) = delete; + DefaultHeaders &operator=(DefaultHeaders const &) = delete; + + //single static class interface + static DefaultHeaders &Instance() { + static DefaultHeaders instance; + return instance; + } +}; + +#endif //PsychicCore_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicEndpoint.cpp b/lib/PsychicHttp/src/PsychicEndpoint.cpp new file mode 100644 index 0000000..e692658 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicEndpoint.cpp @@ -0,0 +1,90 @@ +#include "PsychicEndpoint.h" +#include "PsychicHttpServer.h" + +PsychicEndpoint::PsychicEndpoint() : + _server(NULL), + _uri(""), + _method(HTTP_GET), + _handler(NULL) +{ +} + +PsychicEndpoint::PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri) : + _server(server), + _uri(uri), + _method(method), + _handler(NULL) +{ +} + +PsychicEndpoint * PsychicEndpoint::setHandler(PsychicHandler *handler) +{ + //clean up old / default handler + if (_handler != NULL) + delete _handler; + + //get our new pointer + _handler = handler; + + //keep a pointer to the server + _handler->_server = _server; + + return this; +} + +PsychicHandler * PsychicEndpoint::handler() +{ + return _handler; +} + +String PsychicEndpoint::uri() { + return _uri; +} + +esp_err_t PsychicEndpoint::requestCallback(httpd_req_t *req) +{ + #ifdef ENABLE_ASYNC + if (is_on_async_worker_thread() == false) { + if (submit_async_req(req, PsychicEndpoint::requestCallback) == ESP_OK) { + return ESP_OK; + } else { + httpd_resp_set_status(req, "503 Busy"); + httpd_resp_sendstr(req, "No workers available. Server busy."); + return ESP_OK; + } + } + #endif + + PsychicEndpoint *self = (PsychicEndpoint *)req->user_ctx; + PsychicHandler *handler = self->handler(); + PsychicRequest request(self->_server, req); + + //make sure we have a handler + if (handler != NULL) + { + if (handler->filter(&request) && handler->canHandle(&request)) + { + //check our credentials + if (handler->needsAuthentication(&request)) + return handler->authenticate(&request); + + //pass it to our handler + return handler->handleRequest(&request); + } + //pass it to our generic handlers + else + return PsychicHttpServer::notFoundHandler(req, HTTPD_500_INTERNAL_SERVER_ERROR); + } + else + return request.reply(500, "text/html", "No handler registered."); +} + +PsychicEndpoint* PsychicEndpoint::setFilter(PsychicRequestFilterFunction fn) { + _handler->setFilter(fn); + return this; +} + +PsychicEndpoint* PsychicEndpoint::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) { + _handler->setAuthentication(username, password, method, realm, authFailMsg); + return this; +}; \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicEndpoint.h b/lib/PsychicHttp/src/PsychicEndpoint.h new file mode 100644 index 0000000..540b294 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicEndpoint.h @@ -0,0 +1,37 @@ +#ifndef PsychicEndpoint_h +#define PsychicEndpoint_h + +#include "PsychicCore.h" + +class PsychicHandler; + +#ifdef ENABLE_ASYNC + #include "async_worker.h" +#endif + +class PsychicEndpoint +{ + friend PsychicHttpServer; + + private: + PsychicHttpServer *_server; + String _uri; + http_method _method; + PsychicHandler *_handler; + + public: + PsychicEndpoint(); + PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri); + + PsychicEndpoint *setHandler(PsychicHandler *handler); + PsychicHandler *handler(); + + PsychicEndpoint* setFilter(PsychicRequestFilterFunction fn); + PsychicEndpoint* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = "", const char *authFailMsg = ""); + + String uri(); + + static esp_err_t requestCallback(httpd_req_t *req); +}; + +#endif // PsychicEndpoint_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicEventSource.cpp b/lib/PsychicHttp/src/PsychicEventSource.cpp new file mode 100644 index 0000000..25f947d --- /dev/null +++ b/lib/PsychicHttp/src/PsychicEventSource.cpp @@ -0,0 +1,225 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "PsychicEventSource.h" + +/*****************************************/ +// PsychicEventSource - Handler +/*****************************************/ + +PsychicEventSource::PsychicEventSource() : + PsychicHandler(), + _onOpen(NULL), + _onClose(NULL) +{} + +PsychicEventSource::~PsychicEventSource() { +} + +PsychicEventSourceClient * PsychicEventSource::getClient(int socket) +{ + PsychicClient *client = PsychicHandler::getClient(socket); + + if (client == NULL) + return NULL; + + return (PsychicEventSourceClient *)client->_friend; +} + +PsychicEventSourceClient * PsychicEventSource::getClient(PsychicClient *client) { + return getClient(client->socket()); +} + +esp_err_t PsychicEventSource::handleRequest(PsychicRequest *request) +{ + //start our open ended HTTP response + PsychicEventSourceResponse response(request); + esp_err_t err = response.send(); + + //lookup our client + PsychicClient *client = checkForNewClient(request->client()); + if (client->isNew) + { + //did we get our last id? + if(request->hasHeader("Last-Event-ID")) + { + PsychicEventSourceClient *buddy = getClient(client); + buddy->_lastId = atoi(request->header("Last-Event-ID").c_str()); + } + + //let our handler know. + openCallback(client); + } + + return err; +} + +PsychicEventSource * PsychicEventSource::onOpen(PsychicEventSourceClientCallback fn) { + _onOpen = fn; + return this; +} + +PsychicEventSource * PsychicEventSource::onClose(PsychicEventSourceClientCallback fn) { + _onClose = fn; + return this; +} + +void PsychicEventSource::addClient(PsychicClient *client) { + client->_friend = new PsychicEventSourceClient(client); + PsychicHandler::addClient(client); +} + +void PsychicEventSource::removeClient(PsychicClient *client) { + PsychicHandler::removeClient(client); + delete (PsychicEventSourceClient*)client->_friend; + client->_friend = NULL; +} + +void PsychicEventSource::openCallback(PsychicClient *client) { + PsychicEventSourceClient *buddy = getClient(client); + if (buddy == NULL) + { + return; + } + + if (_onOpen != NULL) + _onOpen(buddy); +} + +void PsychicEventSource::closeCallback(PsychicClient *client) { + PsychicEventSourceClient *buddy = getClient(client); + if (buddy == NULL) + { + return; + } + + if (_onClose != NULL) + _onClose(getClient(buddy)); +} + +void PsychicEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) +{ + String ev = generateEventMessage(message, event, id, reconnect); + for(PsychicClient *c : _clients) { + ((PsychicEventSourceClient*)c->_friend)->sendEvent(ev.c_str()); + } +} + +/*****************************************/ +// PsychicEventSourceClient +/*****************************************/ + +PsychicEventSourceClient::PsychicEventSourceClient(PsychicClient *client) : + PsychicClient(client->server(), client->socket()), + _lastId(0) +{ +} + +PsychicEventSourceClient::~PsychicEventSourceClient(){ +} + +void PsychicEventSourceClient::send(const char *message, const char *event, uint32_t id, uint32_t reconnect){ + String ev = generateEventMessage(message, event, id, reconnect); + sendEvent(ev.c_str()); +} + +void PsychicEventSourceClient::sendEvent(const char *event) { + int result; + do { + result = httpd_socket_send(this->server(), this->socket(), event, strlen(event), 0); + } while (result == HTTPD_SOCK_ERR_TIMEOUT); + + //if (result < 0) + //error log here +} + +/*****************************************/ +// PsychicEventSourceResponse +/*****************************************/ + +PsychicEventSourceResponse::PsychicEventSourceResponse(PsychicRequest *request) + : PsychicResponse(request) +{ +} + +esp_err_t PsychicEventSourceResponse::send() { + + //build our main header + String out = String(); + out.concat("HTTP/1.1 200 OK\r\n"); + out.concat("Content-Type: text/event-stream\r\n"); + out.concat("Cache-Control: no-cache\r\n"); + out.concat("Connection: keep-alive\r\n"); + + //get our global headers out of the way first + for (HTTPHeader header : DefaultHeaders::Instance().getHeaders()) + out.concat(String(header.field) + ": " + String(header.value) + "\r\n"); + + //separator + out.concat("\r\n"); + + int result; + do { + result = httpd_send(_request->request(), out.c_str(), out.length()); + } while (result == HTTPD_SOCK_ERR_TIMEOUT); + + if (result < 0) + ESP_LOGE(PH_TAG, "EventSource send failed with %s", esp_err_to_name(result)); + + if (result > 0) + return ESP_OK; + else + return ESP_ERR_HTTPD_RESP_SEND; +} + +/*****************************************/ +// Event Message Generator +/*****************************************/ + +String generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + String ev = ""; + + if(reconnect){ + ev += "retry: "; + ev += String(reconnect); + ev += "\r\n"; + } + + if(id){ + ev += "id: "; + ev += String(id); + ev += "\r\n"; + } + + if(event != NULL){ + ev += "event: "; + ev += String(event); + ev += "\r\n"; + } + + if(message != NULL){ + ev += "data: "; + ev += String(message); + ev += "\r\n"; + } + ev += "\r\n"; + + return ev; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicEventSource.h b/lib/PsychicHttp/src/PsychicEventSource.h new file mode 100644 index 0000000..ab31980 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicEventSource.h @@ -0,0 +1,82 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef PsychicEventSource_H_ +#define PsychicEventSource_H_ + +#include "PsychicCore.h" +#include "PsychicHandler.h" +#include "PsychicClient.h" +#include "PsychicResponse.h" + +class PsychicEventSource; +class PsychicEventSourceResponse; +class PsychicEventSourceClient; +class PsychicResponse; + +typedef std::function PsychicEventSourceClientCallback; + +class PsychicEventSourceClient : public PsychicClient { + friend PsychicEventSource; + + protected: + uint32_t _lastId; + + public: + PsychicEventSourceClient(PsychicClient *client); + ~PsychicEventSourceClient(); + + uint32_t lastId() const { return _lastId; } + void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); + void sendEvent(const char *event); +}; + +class PsychicEventSource : public PsychicHandler { + private: + PsychicEventSourceClientCallback _onOpen; + PsychicEventSourceClientCallback _onClose; + + public: + PsychicEventSource(); + ~PsychicEventSource(); + + PsychicEventSourceClient * getClient(int socket) override; + PsychicEventSourceClient * getClient(PsychicClient *client) override; + void addClient(PsychicClient *client) override; + void removeClient(PsychicClient *client) override; + void openCallback(PsychicClient *client) override; + void closeCallback(PsychicClient *client) override; + + PsychicEventSource *onOpen(PsychicEventSourceClientCallback fn); + PsychicEventSource *onClose(PsychicEventSourceClientCallback fn); + + esp_err_t handleRequest(PsychicRequest *request) override final; + + void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); +}; + +class PsychicEventSourceResponse: public PsychicResponse { + public: + PsychicEventSourceResponse(PsychicRequest *request); + virtual esp_err_t send() override; +}; + +String generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect); + +#endif /* PsychicEventSource_H_ */ \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicFileResponse.cpp b/lib/PsychicHttp/src/PsychicFileResponse.cpp new file mode 100644 index 0000000..5fc9822 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicFileResponse.cpp @@ -0,0 +1,159 @@ +#include "PsychicFileResponse.h" +#include "PsychicResponse.h" +#include "PsychicRequest.h" + + +PsychicFileResponse::PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType, bool download) + : PsychicResponse(request) { + //_code = 200; + String _path(path); + + if(!download && !fs.exists(_path) && fs.exists(_path+".gz")){ + _path = _path+".gz"; + addHeader("Content-Encoding", "gzip"); + } + + _content = fs.open(_path, "r"); + _contentLength = _content.size(); + + if(contentType == "") + _setContentType(path); + else + setContentType(contentType.c_str()); + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + // set filename and force download + snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); + } else { + // set filename and force rendering + snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); + } + addHeader("Content-Disposition", buf); +} + +PsychicFileResponse::PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType, bool download) + : PsychicResponse(request) { + String _path(path); + + if(!download && String(content.name()).endsWith(".gz") && !path.endsWith(".gz")){ + addHeader("Content-Encoding", "gzip"); + } + + _content = content; + _contentLength = _content.size(); + + if(contentType == "") + _setContentType(path); + else + setContentType(contentType.c_str()); + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); + } else { + snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); + } + addHeader("Content-Disposition", buf); +} + +PsychicFileResponse::~PsychicFileResponse() +{ + if(_content) + _content.close(); +} + +void PsychicFileResponse::_setContentType(const String& path){ + const char *_contentType; + + if (path.endsWith(".html")) _contentType = "text/html"; + else if (path.endsWith(".htm")) _contentType = "text/html"; + else if (path.endsWith(".css")) _contentType = "text/css"; + else if (path.endsWith(".json")) _contentType = "application/json"; + else if (path.endsWith(".js")) _contentType = "application/javascript"; + else if (path.endsWith(".png")) _contentType = "image/png"; + else if (path.endsWith(".gif")) _contentType = "image/gif"; + else if (path.endsWith(".jpg")) _contentType = "image/jpeg"; + else if (path.endsWith(".ico")) _contentType = "image/x-icon"; + else if (path.endsWith(".svg")) _contentType = "image/svg+xml"; + else if (path.endsWith(".eot")) _contentType = "font/eot"; + else if (path.endsWith(".woff")) _contentType = "font/woff"; + else if (path.endsWith(".woff2")) _contentType = "font/woff2"; + else if (path.endsWith(".ttf")) _contentType = "font/ttf"; + else if (path.endsWith(".xml")) _contentType = "text/xml"; + else if (path.endsWith(".pdf")) _contentType = "application/pdf"; + else if (path.endsWith(".zip")) _contentType = "application/zip"; + else if(path.endsWith(".gz")) _contentType = "application/x-gzip"; + else _contentType = "text/plain"; + + setContentType(_contentType); +} + +esp_err_t PsychicFileResponse::send() +{ + esp_err_t err = ESP_OK; + + //just send small files directly + size_t size = getContentLength(); + if (size < FILE_CHUNK_SIZE) + { + uint8_t *buffer = (uint8_t *)malloc(size); + if (buffer == NULL) + { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); + return ESP_FAIL; + } + + size_t readSize = _content.readBytes((char *)buffer, size); + + this->setContent(buffer, readSize); + err = PsychicResponse::send(); + + free(buffer); + } + else + { + /* Retrieve the pointer to scratch buffer for temporary storage */ + char *chunk = (char *)malloc(FILE_CHUNK_SIZE); + if (chunk == NULL) + { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); + return ESP_FAIL; + } + + this->sendHeaders(); + + size_t chunksize; + do { + /* Read file in chunks into the scratch buffer */ + chunksize = _content.readBytes(chunk, FILE_CHUNK_SIZE); + if (chunksize > 0) + { + err = this->sendChunk((uint8_t *)chunk, chunksize); + if (err != ESP_OK) + break; + } + + /* Keep looping till the whole file is sent */ + } while (chunksize != 0); + + //keep track of our memory + free(chunk); + + if (err == ESP_OK) + { + ESP_LOGD(PH_TAG, "File sending complete"); + this->finishChunking(); + } + } + + return err; +} diff --git a/lib/PsychicHttp/src/PsychicFileResponse.h b/lib/PsychicHttp/src/PsychicFileResponse.h new file mode 100644 index 0000000..8eb75fc --- /dev/null +++ b/lib/PsychicHttp/src/PsychicFileResponse.h @@ -0,0 +1,23 @@ +#ifndef PsychicFileResponse_h +#define PsychicFileResponse_h + +#include "PsychicCore.h" +#include "PsychicResponse.h" + +class PsychicRequest; + +class PsychicFileResponse: public PsychicResponse +{ + using File = fs::File; + using FS = fs::FS; + private: + File _content; + void _setContentType(const String& path); + public: + PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType=String(), bool download=false); + PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType=String(), bool download=false); + ~PsychicFileResponse(); + esp_err_t send(); +}; + +#endif // PsychicFileResponse_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicHandler.cpp b/lib/PsychicHttp/src/PsychicHandler.cpp new file mode 100644 index 0000000..097b30e --- /dev/null +++ b/lib/PsychicHttp/src/PsychicHandler.cpp @@ -0,0 +1,111 @@ +#include "PsychicHandler.h" + +PsychicHandler::PsychicHandler() : + _filter(NULL), + _server(NULL), + _username(""), + _password(""), + _method(DIGEST_AUTH), + _realm(""), + _authFailMsg(""), + _subprotocol("") + {} + +PsychicHandler::~PsychicHandler() { + // actual PsychicClient deletion handled by PsychicServer + // for (PsychicClient *client : _clients) + // delete(client); + _clients.clear(); +} + +PsychicHandler* PsychicHandler::setFilter(PsychicRequestFilterFunction fn) { + _filter = fn; + return this; +} + +bool PsychicHandler::filter(PsychicRequest *request){ + return _filter == NULL || _filter(request); +} + +void PsychicHandler::setSubprotocol(const String& subprotocol) { + this->_subprotocol = subprotocol; +} +const char* PsychicHandler::getSubprotocol() const { + return _subprotocol.c_str(); +} + +PsychicHandler* PsychicHandler::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) { + _username = String(username); + _password = String(password); + _method = method; + _realm = String(realm); + _authFailMsg = String(authFailMsg); + return this; +}; + +bool PsychicHandler::needsAuthentication(PsychicRequest *request) { + return (_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str()); +} + +esp_err_t PsychicHandler::authenticate(PsychicRequest *request) { + return request->requestAuthentication(_method, _realm.c_str(), _authFailMsg.c_str()); +} + +PsychicClient * PsychicHandler::checkForNewClient(PsychicClient *client) +{ + PsychicClient *c = PsychicHandler::getClient(client); + if (c == NULL) + { + c = client; + addClient(c); + c->isNew = true; + } + else + c->isNew = false; + + return c; +} + +void PsychicHandler::checkForClosedClient(PsychicClient *client) +{ + if (hasClient(client)) + { + closeCallback(client); + removeClient(client); + } +} + +void PsychicHandler::addClient(PsychicClient *client) { + _clients.push_back(client); +} + +void PsychicHandler::removeClient(PsychicClient *client) { + _clients.remove(client); +} + +PsychicClient * PsychicHandler::getClient(int socket) +{ + //make sure the server has it too. + if (!_server->hasClient(socket)) + return NULL; + + //what about us? + for (PsychicClient *client : _clients) + if (client->socket() == socket) + return client; + + //nothing found. + return NULL; +} + +PsychicClient * PsychicHandler::getClient(PsychicClient *client) { + return PsychicHandler::getClient(client->socket()); +} + +bool PsychicHandler::hasClient(PsychicClient *socket) { + return PsychicHandler::getClient(socket) != NULL; +} + +const std::list& PsychicHandler::getClientList() { + return _clients; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicHandler.h b/lib/PsychicHttp/src/PsychicHandler.h new file mode 100644 index 0000000..bad62bf --- /dev/null +++ b/lib/PsychicHttp/src/PsychicHandler.h @@ -0,0 +1,66 @@ +#ifndef PsychicHandler_h +#define PsychicHandler_h + +#include "PsychicCore.h" +#include "PsychicRequest.h" + +class PsychicEndpoint; +class PsychicHttpServer; + +/* +* HANDLER :: Can be attached to any endpoint or as a generic request handler. +*/ + +class PsychicHandler { + friend PsychicEndpoint; + + protected: + PsychicRequestFilterFunction _filter; + PsychicHttpServer *_server; + + String _username; + String _password; + HTTPAuthMethod _method; + String _realm; + String _authFailMsg; + + String _subprotocol; + + std::list _clients; + + public: + PsychicHandler(); + virtual ~PsychicHandler(); + + PsychicHandler* setFilter(PsychicRequestFilterFunction fn); + bool filter(PsychicRequest *request); + + PsychicHandler* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = "", const char *authFailMsg = ""); + bool needsAuthentication(PsychicRequest *request); + esp_err_t authenticate(PsychicRequest *request); + + virtual bool isWebSocket() { return false; }; + + void setSubprotocol(const String& subprotocol); + const char* getSubprotocol() const; + + PsychicClient * checkForNewClient(PsychicClient *client); + void checkForClosedClient(PsychicClient *client); + + virtual void addClient(PsychicClient *client); + virtual void removeClient(PsychicClient *client); + virtual PsychicClient * getClient(int socket); + virtual PsychicClient * getClient(PsychicClient *client); + virtual void openCallback(PsychicClient *client) {}; + virtual void closeCallback(PsychicClient *client) {}; + + bool hasClient(PsychicClient *client); + int count() { return _clients.size(); }; + const std::list& getClientList(); + + //derived classes must implement these functions + virtual bool canHandle(PsychicRequest *request) { return true; }; + virtual esp_err_t handleRequest(PsychicRequest *request) = 0; +}; + +#endif \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicHttp.h b/lib/PsychicHttp/src/PsychicHttp.h new file mode 100644 index 0000000..3e9da55 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicHttp.h @@ -0,0 +1,24 @@ +#ifndef PsychicHttp_h +#define PsychicHttp_h + +//#define ENABLE_ASYNC // This is something added in ESP-IDF 5.1.x where each request can be handled in its own thread + +#include +#include "PsychicHttpServer.h" +#include "PsychicRequest.h" +#include "PsychicResponse.h" +#include "PsychicEndpoint.h" +#include "PsychicHandler.h" +#include "PsychicStaticFileHandler.h" +#include "PsychicFileResponse.h" +#include "PsychicStreamResponse.h" +#include "PsychicUploadHandler.h" +#include "PsychicWebSocket.h" +#include "PsychicEventSource.h" +#include "PsychicJson.h" + +#ifdef ENABLE_ASYNC + #include "async_worker.h" +#endif + +#endif /* PsychicHttp_h */ \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicHttpServer.cpp b/lib/PsychicHttp/src/PsychicHttpServer.cpp new file mode 100644 index 0000000..628f38b --- /dev/null +++ b/lib/PsychicHttp/src/PsychicHttpServer.cpp @@ -0,0 +1,366 @@ +#include "PsychicHttpServer.h" +#include "PsychicEndpoint.h" +#include "PsychicHandler.h" +#include "PsychicWebHandler.h" +#include "PsychicStaticFileHandler.h" +#include "PsychicWebSocket.h" +#include "PsychicJson.h" +#include "WiFi.h" + +PsychicHttpServer::PsychicHttpServer() : + _onOpen(NULL), + _onClose(NULL) +{ + maxRequestBodySize = MAX_REQUEST_BODY_SIZE; + maxUploadSize = MAX_UPLOAD_SIZE; + + defaultEndpoint = new PsychicEndpoint(this, HTTP_GET, ""); + onNotFound(PsychicHttpServer::defaultNotFoundHandler); + + //for a regular server + config = HTTPD_DEFAULT_CONFIG(); + config.open_fn = PsychicHttpServer::openCallback; + config.close_fn = PsychicHttpServer::closeCallback; + config.uri_match_fn = httpd_uri_match_wildcard; + config.global_user_ctx = this; + config.global_user_ctx_free_fn = destroy; + config.max_uri_handlers = 20; + + #ifdef ENABLE_ASYNC + // It is advisable that httpd_config_t->max_open_sockets > MAX_ASYNC_REQUESTS + // Why? This leaves at least one socket still available to handle + // quick synchronous requests. Otherwise, all the sockets will + // get taken by the long async handlers, and your server will no + // longer be responsive. + config.max_open_sockets = ASYNC_WORKER_COUNT + 1; + config.lru_purge_enable = true; + #endif +} + +PsychicHttpServer::~PsychicHttpServer() +{ + for (auto *client : _clients) + delete(client); + _clients.clear(); + + for (auto *endpoint : _endpoints) + delete(endpoint); + _endpoints.clear(); + + for (auto *handler : _handlers) + delete(handler); + _handlers.clear(); + + delete defaultEndpoint; +} + +void PsychicHttpServer::destroy(void *ctx) +{ + // do not release any resource for PsychicHttpServer in order to be able to restart it after stopping +} + +esp_err_t PsychicHttpServer::listen(uint16_t port) +{ + this->_use_ssl = false; + this->config.server_port = port; + + return this->_start(); +} + +esp_err_t PsychicHttpServer::_start() +{ + esp_err_t ret; + + #ifdef ENABLE_ASYNC + // start workers + start_async_req_workers(); + #endif + + //fire it up. + ret = _startServer(); + if (ret != ESP_OK) + { + ESP_LOGE(PH_TAG, "Server start failed (%s)", esp_err_to_name(ret)); + return ret; + } + + // Register handler + ret = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, PsychicHttpServer::notFoundHandler); + if (ret != ESP_OK) + ESP_LOGE(PH_TAG, "Add 404 handler failed (%s)", esp_err_to_name(ret)); + + return ret; +} + +esp_err_t PsychicHttpServer::_startServer() { + return httpd_start(&this->server, &this->config); +} + +void PsychicHttpServer::stop() +{ + httpd_stop(this->server); +} + +PsychicHandler& PsychicHttpServer::addHandler(PsychicHandler* handler){ + _handlers.push_back(handler); + return *handler; +} + +void PsychicHttpServer::removeHandler(PsychicHandler *handler){ + _handlers.remove(handler); +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri) { + return on(uri, HTTP_GET); +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method) +{ + PsychicWebHandler *handler = new PsychicWebHandler(); + + return on(uri, method, handler); +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHandler *handler) +{ + return on(uri, HTTP_GET, handler); +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHandler *handler) +{ + //make our endpoint + PsychicEndpoint *endpoint = new PsychicEndpoint(this, method, uri); + + //set our handler + endpoint->setHandler(handler); + + // URI handler structure + httpd_uri_t my_uri { + .uri = uri, + .method = method, + .handler = PsychicEndpoint::requestCallback, + .user_ctx = endpoint, + .is_websocket = handler->isWebSocket(), + .supported_subprotocol = handler->getSubprotocol() + }; + + // Register endpoint with ESP-IDF server + esp_err_t ret = httpd_register_uri_handler(this->server, &my_uri); + if (ret != ESP_OK) + ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret)); + + //save it for later + _endpoints.push_back(endpoint); + + return endpoint; +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHttpRequestCallback fn) +{ + return on(uri, HTTP_GET, fn); +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHttpRequestCallback fn) +{ + //these basic requests need a basic web handler + PsychicWebHandler *handler = new PsychicWebHandler(); + handler->onRequest(fn); + + return on(uri, method, handler); +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicJsonRequestCallback fn) +{ + return on(uri, HTTP_GET, fn); +} + +PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicJsonRequestCallback fn) +{ + //these basic requests need a basic web handler + PsychicJsonHandler *handler = new PsychicJsonHandler(); + handler->onRequest(fn); + + return on(uri, method, handler); +} + +void PsychicHttpServer::onNotFound(PsychicHttpRequestCallback fn) +{ + PsychicWebHandler *handler = new PsychicWebHandler(); + handler->onRequest(fn == nullptr ? PsychicHttpServer::defaultNotFoundHandler : fn); + + this->defaultEndpoint->setHandler(handler); +} + +esp_err_t PsychicHttpServer::notFoundHandler(httpd_req_t *req, httpd_err_code_t err) +{ + PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle); + PsychicRequest request(server, req); + + //loop through our global handlers and see if anyone wants it + for(auto *handler: server->_handlers) + { + //are we capable of handling this? + if (handler->filter(&request) && handler->canHandle(&request)) + { + //check our credentials + if (handler->needsAuthentication(&request)) + return handler->authenticate(&request); + else + return handler->handleRequest(&request); + } + } + + //nothing found, give it to our defaultEndpoint + PsychicHandler *handler = server->defaultEndpoint->handler(); + if (handler->filter(&request) && handler->canHandle(&request)) + return handler->handleRequest(&request); + + //not sure how we got this far. + return ESP_ERR_HTTPD_INVALID_REQ; +} + +esp_err_t PsychicHttpServer::defaultNotFoundHandler(PsychicRequest *request) +{ + request->reply(404, "text/html", "That URI does not exist."); + + return ESP_OK; +} + +void PsychicHttpServer::onOpen(PsychicClientCallback handler) { + this->_onOpen = handler; +} + +esp_err_t PsychicHttpServer::openCallback(httpd_handle_t hd, int sockfd) +{ + ESP_LOGD(PH_TAG, "New client connected %d", sockfd); + + //get our global server reference + PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); + + //lookup our client + PsychicClient *client = server->getClient(sockfd); + if (client == NULL) + { + client = new PsychicClient(hd, sockfd); + server->addClient(client); + } + + //user callback + if (server->_onOpen != NULL) + server->_onOpen(client); + + return ESP_OK; +} + +void PsychicHttpServer::onClose(PsychicClientCallback handler) { + this->_onClose = handler; +} + +void PsychicHttpServer::closeCallback(httpd_handle_t hd, int sockfd) +{ + ESP_LOGD(PH_TAG, "Client disconnected %d", sockfd); + + PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); + + //lookup our client + PsychicClient *client = server->getClient(sockfd); + if (client != NULL) + { + //give our handlers a chance to handle a disconnect first + for (PsychicEndpoint * endpoint : server->_endpoints) + { + PsychicHandler *handler = endpoint->handler(); + handler->checkForClosedClient(client); + } + + //do we have a callback attached? + if (server->_onClose != NULL) + server->_onClose(client); + + //remove it from our list + server->removeClient(client); + } + else + ESP_LOGE(PH_TAG, "No client record %d", sockfd); + + //finally close it out. + close(sockfd); +} + +PsychicStaticFileHandler* PsychicHttpServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control) +{ + PsychicStaticFileHandler* handler = new PsychicStaticFileHandler(uri, fs, path, cache_control); + this->addHandler(handler); + + return handler; +} + +void PsychicHttpServer::addClient(PsychicClient *client) { + _clients.push_back(client); +} + +void PsychicHttpServer::removeClient(PsychicClient *client) { + _clients.remove(client); + delete client; +} + +PsychicClient * PsychicHttpServer::getClient(int socket) { + for (PsychicClient * client : _clients) + if (client->socket() == socket) + return client; + + return NULL; +} + +PsychicClient * PsychicHttpServer::getClient(httpd_req_t *req) { + return getClient(httpd_req_to_sockfd(req)); +} + +bool PsychicHttpServer::hasClient(int socket) { + return getClient(socket) != NULL; +} + +const std::list& PsychicHttpServer::getClientList() { + return _clients; +} + +bool ON_STA_FILTER(PsychicRequest *request) { + return WiFi.localIP() == request->client()->localIP(); +} + +bool ON_AP_FILTER(PsychicRequest *request) { + return WiFi.softAPIP() == request->client()->localIP(); +} + +String urlDecode(const char* encoded) +{ + size_t length = strlen(encoded); + char* decoded = (char*)malloc(length + 1); + if (!decoded) { + return ""; + } + + size_t i, j = 0; + for (i = 0; i < length; ++i) { + if (encoded[i] == '%' && isxdigit(encoded[i + 1]) && isxdigit(encoded[i + 2])) { + // Valid percent-encoded sequence + int hex; + sscanf(encoded + i + 1, "%2x", &hex); + decoded[j++] = (char)hex; + i += 2; // Skip the two hexadecimal characters + } else if (encoded[i] == '+') { + // Convert '+' to space + decoded[j++] = ' '; + } else { + // Copy other characters as they are + decoded[j++] = encoded[i]; + } + } + + decoded[j] = '\0'; // Null-terminate the decoded string + + String output(decoded); + free(decoded); + + return output; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicHttpServer.h b/lib/PsychicHttp/src/PsychicHttpServer.h new file mode 100644 index 0000000..40e5442 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicHttpServer.h @@ -0,0 +1,81 @@ +#ifndef PsychicHttpServer_h +#define PsychicHttpServer_h + +#include "PsychicCore.h" +#include "PsychicClient.h" +#include "PsychicHandler.h" + +class PsychicEndpoint; +class PsychicHandler; +class PsychicStaticFileHandler; + +class PsychicHttpServer +{ + protected: + bool _use_ssl = false; + std::list _endpoints; + std::list _handlers; + std::list _clients; + + PsychicClientCallback _onOpen; + PsychicClientCallback _onClose; + + esp_err_t _start(); + virtual esp_err_t _startServer(); + + public: + PsychicHttpServer(); + virtual ~PsychicHttpServer(); + + //esp-idf specific stuff + httpd_handle_t server; + httpd_config_t config; + + //some limits on what we will accept + unsigned long maxUploadSize; + unsigned long maxRequestBodySize; + + PsychicEndpoint *defaultEndpoint; + + static void destroy(void *ctx); + + esp_err_t listen(uint16_t port); + + virtual void stop(); + + PsychicHandler& addHandler(PsychicHandler* handler); + void removeHandler(PsychicHandler* handler); + + void addClient(PsychicClient *client); + void removeClient(PsychicClient *client); + PsychicClient* getClient(int socket); + PsychicClient* getClient(httpd_req_t *req); + bool hasClient(int socket); + int count() { return _clients.size(); }; + const std::list& getClientList(); + + PsychicEndpoint* on(const char* uri); + PsychicEndpoint* on(const char* uri, http_method method); + PsychicEndpoint* on(const char* uri, PsychicHandler *handler); + PsychicEndpoint* on(const char* uri, http_method method, PsychicHandler *handler); + PsychicEndpoint* on(const char* uri, PsychicHttpRequestCallback onRequest); + PsychicEndpoint* on(const char* uri, http_method method, PsychicHttpRequestCallback onRequest); + PsychicEndpoint* on(const char* uri, PsychicJsonRequestCallback onRequest); + PsychicEndpoint* on(const char* uri, http_method method, PsychicJsonRequestCallback onRequest); + + static esp_err_t notFoundHandler(httpd_req_t *req, httpd_err_code_t err); + static esp_err_t defaultNotFoundHandler(PsychicRequest *request); + void onNotFound(PsychicHttpRequestCallback fn); + + void onOpen(PsychicClientCallback handler); + void onClose(PsychicClientCallback handler); + static esp_err_t openCallback(httpd_handle_t hd, int sockfd); + static void closeCallback(httpd_handle_t hd, int sockfd); + + PsychicStaticFileHandler* serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); +}; + +bool ON_STA_FILTER(PsychicRequest *request); +bool ON_AP_FILTER(PsychicRequest *request); + +#endif // PsychicHttpServer_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicHttpsServer.cpp b/lib/PsychicHttp/src/PsychicHttpsServer.cpp new file mode 100644 index 0000000..5df225e --- /dev/null +++ b/lib/PsychicHttp/src/PsychicHttpsServer.cpp @@ -0,0 +1,61 @@ +#include "PsychicHttpsServer.h" + +#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE + +PsychicHttpsServer::PsychicHttpsServer() : PsychicHttpServer() +{ + //for a SSL server + ssl_config = HTTPD_SSL_CONFIG_DEFAULT(); + ssl_config.httpd.open_fn = PsychicHttpServer::openCallback; + ssl_config.httpd.close_fn = PsychicHttpServer::closeCallback; + ssl_config.httpd.uri_match_fn = httpd_uri_match_wildcard; + ssl_config.httpd.global_user_ctx = this; + ssl_config.httpd.global_user_ctx_free_fn = destroy; + ssl_config.httpd.max_uri_handlers = 20; + + // each SSL connection takes about 45kb of heap + // a barebones sketch with PsychicHttp has ~150kb of heap available + // if we set it higher than 2 and use all the connections, we get lots of memory errors. + // not to mention there is no heap left over for the program itself. + ssl_config.httpd.max_open_sockets = 2; +} + +PsychicHttpsServer::~PsychicHttpsServer() {} + +esp_err_t PsychicHttpsServer::listen(uint16_t port, const char *cert, const char *private_key) +{ + this->_use_ssl = true; + + this->ssl_config.port_secure = port; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2) + this->ssl_config.servercert = (uint8_t *)cert; + this->ssl_config.servercert_len = strlen(cert)+1; +#else + this->ssl_config.cacert_pem = (uint8_t *)cert; + this->ssl_config.cacert_len = strlen(cert)+1; +#endif + + this->ssl_config.prvtkey_pem = (uint8_t *)private_key; + this->ssl_config.prvtkey_len = strlen(private_key)+1; + + return this->_start(); +} + +esp_err_t PsychicHttpsServer::_startServer() +{ + if (this->_use_ssl) + return httpd_ssl_start(&this->server, &this->ssl_config); + else + return httpd_start(&this->server, &this->config); +} + +void PsychicHttpsServer::stop() +{ + if (this->_use_ssl) + httpd_ssl_stop(this->server); + else + httpd_stop(this->server); +} + +#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicHttpsServer.h b/lib/PsychicHttp/src/PsychicHttpsServer.h new file mode 100644 index 0000000..51f8210 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicHttpsServer.h @@ -0,0 +1,39 @@ +#ifndef PsychicHttpsServer_h +#define PsychicHttpsServer_h + +#include + +#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE + +#include "PsychicCore.h" +#include "PsychicHttpServer.h" +#include +#if !CONFIG_HTTPD_WS_SUPPORT + #error PsychicHttpsServer cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration +#endif + +#define PSY_ENABLE_SSL //you can use this define in your code to enable/disable these features + +class PsychicHttpsServer : public PsychicHttpServer +{ + protected: + bool _use_ssl = false; + + public: + PsychicHttpsServer(); + ~PsychicHttpsServer(); + + httpd_ssl_config_t ssl_config; + + using PsychicHttpServer::listen; //keep the regular version + esp_err_t listen(uint16_t port, const char *cert, const char *private_key); + + virtual esp_err_t _startServer() override final; + virtual void stop() override final; +}; + +#endif // PsychicHttpsServer_h + +#else + #warning ESP-IDF https server support not enabled. +#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicJson.cpp b/lib/PsychicHttp/src/PsychicJson.cpp new file mode 100644 index 0000000..ca0f0a5 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicJson.cpp @@ -0,0 +1,133 @@ +#include "PsychicJson.h" + +#ifdef ARDUINOJSON_6_COMPATIBILITY + PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray, size_t maxJsonBufferSize) : + PsychicResponse(request), + _jsonBuffer(maxJsonBufferSize) + { + setContentType(JSON_MIMETYPE); + if (isArray) + _root = _jsonBuffer.createNestedArray(); + else + _root = _jsonBuffer.createNestedObject(); + } +#else + PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray) : PsychicResponse(request) + { + setContentType(JSON_MIMETYPE); + if (isArray) + _root = _jsonBuffer.add(); + else + _root = _jsonBuffer.add(); + } +#endif + +JsonVariant &PsychicJsonResponse::getRoot() { return _root; } + +size_t PsychicJsonResponse::getLength() +{ + return measureJson(_root); +} + +esp_err_t PsychicJsonResponse::send() +{ + esp_err_t err = ESP_OK; + size_t length = getLength(); + size_t buffer_size; + char *buffer; + + //how big of a buffer do we want? + if (length < JSON_BUFFER_SIZE) + buffer_size = length+1; + else + buffer_size = JSON_BUFFER_SIZE; + + buffer = (char *)malloc(buffer_size); + if (buffer == NULL) { + httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); + return ESP_FAIL; + } + + //send it in one shot or no? + if (length < JSON_BUFFER_SIZE) + { + serializeJson(_root, buffer, buffer_size); + + this->setContent((uint8_t *)buffer, length); + this->setContentType(JSON_MIMETYPE); + + err = PsychicResponse::send(); + } + else + { + //helper class that acts as a stream to print chunked responses + ChunkPrinter dest(this, (uint8_t *)buffer, buffer_size); + + //keep our headers + this->sendHeaders(); + + serializeJson(_root, dest); + + //send the last bits + dest.flush(); + + //done with our chunked response too + err = this->finishChunking(); + } + + //let the buffer go + free(buffer); + + return err; +} + +#ifdef ARDUINOJSON_6_COMPATIBILITY + PsychicJsonHandler::PsychicJsonHandler(size_t maxJsonBufferSize) : + _onRequest(NULL), + _maxJsonBufferSize(maxJsonBufferSize) + {}; + + PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize) : + _onRequest(onRequest), + _maxJsonBufferSize(maxJsonBufferSize) + {} +#else + PsychicJsonHandler::PsychicJsonHandler() : + _onRequest(NULL) + {}; + + PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest) : + _onRequest(onRequest) + {} +#endif + +void PsychicJsonHandler::onRequest(PsychicJsonRequestCallback fn) { _onRequest = fn; } + +esp_err_t PsychicJsonHandler::handleRequest(PsychicRequest *request) +{ + //process basic stuff + PsychicWebHandler::handleRequest(request); + + if (_onRequest) + { + #ifdef ARDUINOJSON_6_COMPATIBILITY + DynamicJsonDocument jsonBuffer(this->_maxJsonBufferSize); + DeserializationError error = deserializeJson(jsonBuffer, request->body()); + if (error) + return request->reply(400); + + JsonVariant json = jsonBuffer.as(); + #else + JsonDocument jsonBuffer; + DeserializationError error = deserializeJson(jsonBuffer, request->body()); + if (error) + return request->reply(400); + + JsonVariant json = jsonBuffer.as(); + #endif + + return _onRequest(request, json); + } + else + return request->reply(500); +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicJson.h b/lib/PsychicHttp/src/PsychicJson.h new file mode 100644 index 0000000..95ca894 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicJson.h @@ -0,0 +1,89 @@ +// PsychicJson.h +/* + Async Response to use with ArduinoJson and AsyncWebServer + Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon. + Ported to PsychicHttp by Zach Hoeken + +*/ +#ifndef PSYCHIC_JSON_H_ +#define PSYCHIC_JSON_H_ + +#include "PsychicRequest.h" +#include "PsychicWebHandler.h" +#include "ChunkPrinter.h" +#include + +#if ARDUINOJSON_VERSION_MAJOR == 6 + #define ARDUINOJSON_6_COMPATIBILITY + #ifndef DYNAMIC_JSON_DOCUMENT_SIZE + #define DYNAMIC_JSON_DOCUMENT_SIZE 4096 + #endif +#endif + + +#ifndef JSON_BUFFER_SIZE + #define JSON_BUFFER_SIZE 4*1024 +#endif + +constexpr const char *JSON_MIMETYPE = "application/json"; + +/* + * Json Response + * */ + +class PsychicJsonResponse : public PsychicResponse +{ + protected: + #ifdef ARDUINOJSON_5_COMPATIBILITY + DynamicJsonBuffer _jsonBuffer; + #elif ARDUINOJSON_VERSION_MAJOR == 6 + DynamicJsonDocument _jsonBuffer; + #else + JsonDocument _jsonBuffer; + #endif + + JsonVariant _root; + size_t _contentLength; + + public: + #ifdef ARDUINOJSON_5_COMPATIBILITY + PsychicJsonResponse(PsychicRequest *request, bool isArray = false); + #elif ARDUINOJSON_VERSION_MAJOR == 6 + PsychicJsonResponse(PsychicRequest *request, bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); + #else + PsychicJsonResponse(PsychicRequest *request, bool isArray = false); + #endif + + ~PsychicJsonResponse() {} + + JsonVariant &getRoot(); + size_t getLength(); + + virtual esp_err_t send() override; +}; + +class PsychicJsonHandler : public PsychicWebHandler +{ + protected: + PsychicJsonRequestCallback _onRequest; + #if ARDUINOJSON_VERSION_MAJOR == 6 + const size_t _maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE; + #endif + + public: + #ifdef ARDUINOJSON_5_COMPATIBILITY + PsychicJsonHandler(); + PsychicJsonHandler(PsychicJsonRequestCallback onRequest); + #elif ARDUINOJSON_VERSION_MAJOR == 6 + PsychicJsonHandler(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); + PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); + #else + PsychicJsonHandler(); + PsychicJsonHandler(PsychicJsonRequestCallback onRequest); + #endif + + void onRequest(PsychicJsonRequestCallback fn); + virtual esp_err_t handleRequest(PsychicRequest *request) override; +}; + +#endif \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicRequest.cpp b/lib/PsychicHttp/src/PsychicRequest.cpp new file mode 100644 index 0000000..4244358 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicRequest.cpp @@ -0,0 +1,542 @@ +#include "PsychicRequest.h" +#include "http_status.h" +#include "PsychicHttpServer.h" + + +PsychicRequest::PsychicRequest(PsychicHttpServer *server, httpd_req_t *req) : + _server(server), + _req(req), + _method(HTTP_GET), + _query(""), + _body(""), + _tempObject(NULL) +{ + //load up our client. + this->_client = server->getClient(req); + + //handle our session data + if (req->sess_ctx != NULL) + this->_session = (SessionData *)req->sess_ctx; + else + { + this->_session = new SessionData(); + req->sess_ctx = this->_session; + } + + //callback for freeing the session later + req->free_ctx = this->freeSession; + + //load up some data + this->_uri = String(this->_req->uri); +} + +PsychicRequest::~PsychicRequest() +{ + //temorary user object + if (_tempObject != NULL) + free(_tempObject); + + //our web parameters + for (auto *param : _params) + delete(param); + _params.clear(); +} + +void PsychicRequest::freeSession(void *ctx) +{ + if (ctx != NULL) + { + SessionData *session = (SessionData*)ctx; + delete session; + } +} + +PsychicHttpServer * PsychicRequest::server() { + return _server; +} + +httpd_req_t * PsychicRequest::request() { + return _req; +} + +PsychicClient * PsychicRequest::client() { + return _client; +} + +const String PsychicRequest::getFilename() +{ + //parse the content-disposition header + if (this->hasHeader("Content-Disposition")) + { + ContentDisposition cd = this->getContentDisposition(); + if (cd.filename != "") + return cd.filename; + } + + //fall back to passed in query string + PsychicWebParameter *param = getParam("_filename"); + if (param != NULL) + return param->name(); + + //fall back to parsing it from url (useful for wildcard uploads) + String uri = this->uri(); + int filenameStart = uri.lastIndexOf('/') + 1; + String filename = uri.substring(filenameStart); + if (filename != "") + return filename; + + //finally, unknown. + ESP_LOGE(PH_TAG, "Did not get a valid filename from the upload."); + return "unknown.txt"; +} + +const ContentDisposition PsychicRequest::getContentDisposition() +{ + ContentDisposition cd; + String header = this->header("Content-Disposition"); + int start; + int end; + + if (header.indexOf("form-data") == 0) + cd.disposition = FORM_DATA; + else if (header.indexOf("attachment") == 0) + cd.disposition = ATTACHMENT; + else if (header.indexOf("inline") == 0) + cd.disposition = INLINE; + else + cd.disposition = NONE; + + start = header.indexOf("filename="); + if (start) + { + end = header.indexOf('"', start+10); + cd.filename = header.substring(start+10, end-1); + } + + start = header.indexOf("name="); + if (start) + { + end = header.indexOf('"', start+6); + cd.name = header.substring(start+6, end-1); + } + + return cd; +} + +esp_err_t PsychicRequest::loadBody() +{ + esp_err_t err = ESP_OK; + + this->_body = String(); + + size_t remaining = this->_req->content_len; + size_t actuallyReceived = 0; + char *buf = (char *)malloc(remaining + 1); + if (buf == NULL) { + ESP_LOGE(PH_TAG, "Failed to allocate memory for body"); + return ESP_FAIL; + } + + while (remaining > 0) { + int received = httpd_req_recv(this->_req, buf + actuallyReceived, remaining); + + if (received == HTTPD_SOCK_ERR_TIMEOUT) { + continue; + } + else if (received == HTTPD_SOCK_ERR_FAIL) { + ESP_LOGE(PH_TAG, "Failed to receive data."); + err = ESP_FAIL; + break; + } + + remaining -= received; + actuallyReceived += received; + } + + buf[actuallyReceived] = '\0'; + this->_body = String(buf); + free(buf); + return err; +} + +http_method PsychicRequest::method() { + return (http_method)this->_req->method; +} + +const String PsychicRequest::methodStr() { + return String(http_method_str((http_method)this->_req->method)); +} + +const String PsychicRequest::path() { + int index = _uri.indexOf("?"); + if(index == -1) + return _uri; + else + return _uri.substring(0, index); +} + +const String& PsychicRequest::uri() { + return this->_uri; +} + +const String& PsychicRequest::query() { + return this->_query; +} + +// no way to get list of headers yet.... +// int PsychicRequest::headers() +// { +// } + +const String PsychicRequest::header(const char *name) +{ + size_t header_len = httpd_req_get_hdr_value_len(this->_req, name); + + //if we've got one, allocated it and load it + if (header_len) + { + char header[header_len+1]; + httpd_req_get_hdr_value_str(this->_req, name, header, sizeof(header)); + return String(header); + } + else + return ""; +} + +bool PsychicRequest::hasHeader(const char *name) +{ + return httpd_req_get_hdr_value_len(this->_req, name) > 0; +} + +const String PsychicRequest::host() { + return this->header("Host"); +} + +const String PsychicRequest::contentType() { + return header("Content-Type"); +} + +size_t PsychicRequest::contentLength() { + return this->_req->content_len; +} + +const String& PsychicRequest::body() +{ + return this->_body; +} + +bool PsychicRequest::isMultipart() +{ + const String& type = this->contentType(); + + return (this->contentType().indexOf("multipart/form-data") >= 0); +} + +esp_err_t PsychicRequest::redirect(const char *url) +{ + PsychicResponse response(this); + response.setCode(301); + response.addHeader("Location", url); + + return response.send(); +} + +bool PsychicRequest::hasCookie(const char *key) +{ + char cookie[MAX_COOKIE_SIZE]; + size_t cookieSize = MAX_COOKIE_SIZE; + esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize); + + //did we get anything? + if (err == ESP_OK) + return true; + else if (err == ESP_ERR_HTTPD_RESULT_TRUNC) + ESP_LOGE(PH_TAG, "cookie too large (%d bytes).\n", cookieSize); + + return false; +} + +const String PsychicRequest::getCookie(const char *key) +{ + char cookie[MAX_COOKIE_SIZE]; + size_t cookieSize = MAX_COOKIE_SIZE; + esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize); + + //did we get anything? + if (err == ESP_OK) + return String(cookie); + else + return ""; +} + +void PsychicRequest::loadParams() +{ + //did we get a query string? + size_t query_len = httpd_req_get_url_query_len(_req); + if (query_len) + { + char query[query_len+1]; + httpd_req_get_url_query_str(_req, query, sizeof(query)); + _query.concat(query); + + //parse them. + _addParams(_query, false); + } + + //did we get form data as body? + if (this->method() == HTTP_POST && this->contentType().startsWith("application/x-www-form-urlencoded")) + { + _addParams(_body, true); + } +} + +void PsychicRequest::_addParams(const String& params, bool post){ + size_t start = 0; + while (start < params.length()){ + int end = params.indexOf('&', start); + if (end < 0) end = params.length(); + int equal = params.indexOf('=', start); + if (equal < 0 || equal > end) equal = end; + String name = params.substring(start, equal); + String value = equal + 1 < end ? params.substring(equal + 1, end) : String(); + addParam(name, value, true, post); + start = end + 1; + } +} + +PsychicWebParameter * PsychicRequest::addParam(const String &name, const String &value, bool decode, bool post) +{ + if (decode) + return addParam(new PsychicWebParameter(urlDecode(name.c_str()), urlDecode(value.c_str()), post)); + else + return addParam(new PsychicWebParameter(name, value, post)); +} + +PsychicWebParameter * PsychicRequest::addParam(PsychicWebParameter *param) { + // ESP_LOGD(PH_TAG, "Adding param: '%s' = '%s'", param->name().c_str(), param->value().c_str()); + _params.push_back(param); + return param; +} + +bool PsychicRequest::hasParam(const char *key) +{ + return getParam(key) != NULL; +} + +PsychicWebParameter * PsychicRequest::getParam(const char *key) +{ + for (auto *param : _params) + if (param->name().equals(key)) + return param; + + return NULL; +} + +bool PsychicRequest::hasSessionKey(const String& key) +{ + return this->_session->find(key) != this->_session->end(); +} + +const String PsychicRequest::getSessionKey(const String& key) +{ + auto it = this->_session->find(key); + if (it != this->_session->end()) + return it->second; + else + return ""; +} + +void PsychicRequest::setSessionKey(const String& key, const String& value) +{ + this->_session->insert(std::pair(key, value)); +} + +static const String md5str(const String &in){ + MD5Builder md5 = MD5Builder(); + md5.begin(); + md5.add(in); + md5.calculate(); + return md5.toString(); +} + +bool PsychicRequest::authenticate(const char * username, const char * password) +{ + if(hasHeader("Authorization")) + { + String authReq = header("Authorization"); + if(authReq.startsWith("Basic")){ + authReq = authReq.substring(6); + authReq.trim(); + char toencodeLen = strlen(username)+strlen(password)+1; + char *toencode = new char[toencodeLen + 1]; + if(toencode == NULL){ + authReq = ""; + return false; + } + char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; + if(encoded == NULL){ + authReq = ""; + delete[] toencode; + return false; + } + sprintf(toencode, "%s:%s", username, password); + if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded)) { + authReq = ""; + delete[] toencode; + delete[] encoded; + return true; + } + delete[] toencode; + delete[] encoded; + } + else if(authReq.startsWith(F("Digest"))) + { + authReq = authReq.substring(7); + String _username = _extractParam(authReq,F("username=\""),'\"'); + if(!_username.length() || _username != String(username)) { + authReq = ""; + return false; + } + // extracting required parameters for RFC 2069 simpler Digest + String _realm = _extractParam(authReq, F("realm=\""),'\"'); + String _nonce = _extractParam(authReq, F("nonce=\""),'\"'); + String _uri = _extractParam(authReq, F("uri=\""),'\"'); + String _resp = _extractParam(authReq, F("response=\""),'\"'); + String _opaque = _extractParam(authReq, F("opaque=\""),'\"'); + + if((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_resp.length()) || (!_opaque.length())) { + authReq = ""; + return false; + } + if((_opaque != this->getSessionKey("opaque")) || (_nonce != this->getSessionKey("nonce")) || (_realm != this->getSessionKey("realm"))) + { + authReq = ""; + return false; + } + // parameters for the RFC 2617 newer Digest + String _nc,_cnonce; + if(authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) { + _nc = _extractParam(authReq, F("nc="), ','); + _cnonce = _extractParam(authReq, F("cnonce=\""),'\"'); + } + + String _H1 = md5str(String(username) + ':' + _realm + ':' + String(password)); + //ESP_LOGD(PH_TAG, "Hash of user:realm:pass=%s", _H1.c_str()); + + String _H2 = ""; + if(_method == HTTP_GET){ + _H2 = md5str(String(F("GET:")) + _uri); + }else if(_method == HTTP_POST){ + _H2 = md5str(String(F("POST:")) + _uri); + }else if(_method == HTTP_PUT){ + _H2 = md5str(String(F("PUT:")) + _uri); + }else if(_method == HTTP_DELETE){ + _H2 = md5str(String(F("DELETE:")) + _uri); + }else{ + _H2 = md5str(String(F("GET:")) + _uri); + } + //ESP_LOGD(PH_TAG, "Hash of GET:uri=%s", _H2.c_str()); + + String _responsecheck = ""; + if(authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2); + } else { + _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2); + } + + //ESP_LOGD(PH_TAG, "The Proper response=%s", _responsecheck.c_str()); + if(_resp == _responsecheck){ + authReq = ""; + return true; + } + } + authReq = ""; + } + return false; +} + +const String PsychicRequest::_extractParam(const String& authReq, const String& param, const char delimit) +{ + int _begin = authReq.indexOf(param); + if (_begin == -1) + return ""; + return authReq.substring(_begin+param.length(),authReq.indexOf(delimit,_begin+param.length())); +} + +const String PsychicRequest::_getRandomHexString() +{ + char buffer[33]; // buffer to hold 32 Hex Digit + /0 + int i; + for(i = 0; i < 4; i++) { + sprintf (buffer + (i*8), "%08lx", (unsigned long int)esp_random()); + } + return String(buffer); +} + +esp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg) +{ + //what is thy realm, sire? + if(!strcmp(realm, "")) + this->setSessionKey("realm", "Login Required"); + else + this->setSessionKey("realm", realm); + + PsychicResponse response(this); + String authStr; + + //what kind of auth? + if(mode == BASIC_AUTH) + { + authStr = "Basic realm=\"" + this->getSessionKey("realm") + "\""; + response.addHeader("WWW-Authenticate", authStr.c_str()); + } + else + { + //only make new ones if we havent sent them yet + if (this->getSessionKey("nonce").isEmpty()) + this->setSessionKey("nonce", _getRandomHexString()); + if (this->getSessionKey("opaque").isEmpty()) + this->setSessionKey("opaque", _getRandomHexString()); + + authStr = "Digest realm=\"" + this->getSessionKey("realm") + "\", qop=\"auth\", nonce=\"" + this->getSessionKey("nonce") + "\", opaque=\"" + this->getSessionKey("opaque") + "\""; + response.addHeader("WWW-Authenticate", authStr.c_str()); + } + + response.setCode(401); + response.setContentType("text/html"); + response.setContent(authStr.c_str()); + return response.send(); +} + +esp_err_t PsychicRequest::reply(int code) +{ + PsychicResponse response(this); + + response.setCode(code); + response.setContentType("text/plain"); + response.setContent(http_status_reason(code)); + + return response.send(); +} + +esp_err_t PsychicRequest::reply(const char *content) +{ + PsychicResponse response(this); + + response.setCode(200); + response.setContentType("text/html"); + response.setContent(content); + + return response.send(); +} + +esp_err_t PsychicRequest::reply(int code, const char *contentType, const char *content) +{ + PsychicResponse response(this); + + response.setCode(code); + response.setContentType(contentType); + response.setContent(content); + + return response.send(); +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicRequest.h b/lib/PsychicHttp/src/PsychicRequest.h new file mode 100644 index 0000000..fe48a1b --- /dev/null +++ b/lib/PsychicHttp/src/PsychicRequest.h @@ -0,0 +1,98 @@ +#ifndef PsychicRequest_h +#define PsychicRequest_h + +#include "PsychicCore.h" +#include "PsychicHttpServer.h" +#include "PsychicClient.h" +#include "PsychicWebParameter.h" +#include "PsychicResponse.h" + +typedef std::map SessionData; + +enum Disposition { NONE, INLINE, ATTACHMENT, FORM_DATA}; + +struct ContentDisposition { + Disposition disposition; + String filename; + String name; +}; + +class PsychicRequest { + friend PsychicHttpServer; + + protected: + PsychicHttpServer *_server; + httpd_req_t *_req; + SessionData *_session; + PsychicClient *_client; + + http_method _method; + String _uri; + String _query; + String _body; + + std::list _params; + + void _addParams(const String& params, bool post); + void _parseGETParams(); + void _parsePOSTParams(); + + const String _extractParam(const String& authReq, const String& param, const char delimit); + const String _getRandomHexString(); + + public: + PsychicRequest(PsychicHttpServer *server, httpd_req_t *req); + virtual ~PsychicRequest(); + + void *_tempObject; + + PsychicHttpServer * server(); + httpd_req_t * request(); + virtual PsychicClient * client(); + + bool isMultipart(); + esp_err_t loadBody(); + + const String header(const char *name); + bool hasHeader(const char *name); + + static void freeSession(void *ctx); + bool hasSessionKey(const String& key); + const String getSessionKey(const String& key); + void setSessionKey(const String& key, const String& value); + + bool hasCookie(const char * key); + const String getCookie(const char * key); + + http_method method(); // returns the HTTP method used as enum value (eg. HTTP_GET) + const String methodStr(); // returns the HTTP method used as a string (eg. "GET") + const String path(); // returns the request path (eg /page?foo=bar returns "/page") + const String& uri(); // returns the full request uri (eg /page?foo=bar) + const String& query(); // returns the request query data (eg /page?foo=bar returns "foo=bar") + const String host(); // returns the requested host (request to http://psychic.local/foo will return "psychic.local") + const String contentType(); // returns the Content-Type header value + size_t contentLength(); // returns the Content-Length header value + const String& body(); // returns the body of the request + const ContentDisposition getContentDisposition(); + + const String& queryString() { return query(); } //compatability function. same as query() + const String& url() { return uri(); } //compatability function. same as uri() + + void loadParams(); + PsychicWebParameter * addParam(PsychicWebParameter *param); + PsychicWebParameter * addParam(const String &name, const String &value, bool decode = true, bool post = false); + bool hasParam(const char *key); + PsychicWebParameter * getParam(const char *name); + + const String getFilename(); + + bool authenticate(const char * username, const char * password); + esp_err_t requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg); + + esp_err_t redirect(const char *url); + esp_err_t reply(int code); + esp_err_t reply(const char *content); + esp_err_t reply(int code, const char *contentType, const char *content); +}; + +#endif // PsychicRequest_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicResponse.cpp b/lib/PsychicHttp/src/PsychicResponse.cpp new file mode 100644 index 0000000..5046441 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicResponse.cpp @@ -0,0 +1,162 @@ +#include "PsychicResponse.h" +#include "PsychicRequest.h" +#include + +PsychicResponse::PsychicResponse(PsychicRequest *request) : + _request(request), + _code(200), + _status(""), + _contentLength(0), + _body("") +{ +} + +PsychicResponse::~PsychicResponse() +{ + //clean up our header variables. we have to do this on desctruct since httpd_resp_send doesn't store copies + for (HTTPHeader header : _headers) + { + free(header.field); + free(header.value); + } + _headers.clear(); +} + +void PsychicResponse::addHeader(const char *field, const char *value) +{ + //these get freed after send by the destructor + HTTPHeader header; + header.field =(char *)malloc(strlen(field)+1); + header.value = (char *)malloc(strlen(value)+1); + + strlcpy(header.field, field, strlen(field)+1); + strlcpy(header.value, value, strlen(value)+1); + + _headers.push_back(header); +} + +void PsychicResponse::setCookie(const char *name, const char *value, unsigned long secondsFromNow, const char *extras) +{ + time_t now = time(nullptr); + + String output; + output = urlEncode(name) + "=" + urlEncode(value); + + //if current time isn't modern, default to using max age + if (now < 1700000000) + output += "; Max-Age=" + String(secondsFromNow); + //otherwise, set an expiration date + else + { + time_t expirationTimestamp = now + secondsFromNow; + + // Convert the expiration timestamp to a formatted string for the "expires" attribute + struct tm* tmInfo = gmtime(&expirationTimestamp); + char expires[30]; + strftime(expires, sizeof(expires), "%a, %d %b %Y %H:%M:%S GMT", tmInfo); + output += "; Expires=" + String(expires); + } + + //did we get any extras? + if (strlen(extras)) + output += "; " + String(extras); + + //okay, add it in. + addHeader("Set-Cookie", output.c_str()); +} + +void PsychicResponse::setCode(int code) +{ + _code = code; +} + +void PsychicResponse::setContentType(const char *contentType) +{ + httpd_resp_set_type(_request->request(), contentType); +} + +void PsychicResponse::setContent(const char *content) +{ + _body = content; + setContentLength(strlen(content)); +} + +void PsychicResponse::setContent(const uint8_t *content, size_t len) +{ + _body = (char *)content; + setContentLength(len); +} + +const char * PsychicResponse::getContent() +{ + return _body; +} + +size_t PsychicResponse::getContentLength() +{ + return _contentLength; +} + +esp_err_t PsychicResponse::send() +{ + //esp-idf makes you set the whole status. + sprintf(_status, "%u %s", _code, http_status_reason(_code)); + httpd_resp_set_status(_request->request(), _status); + + //our headers too + this->sendHeaders(); + + //now send it off + esp_err_t err = httpd_resp_send(_request->request(), getContent(), getContentLength()); + + //did something happen? + if (err != ESP_OK) + ESP_LOGE(PH_TAG, "Send response failed (%s)", esp_err_to_name(err)); + + return err; +} + +void PsychicResponse::sendHeaders() +{ + //get our global headers out of the way first + for (HTTPHeader header : DefaultHeaders::Instance().getHeaders()) + httpd_resp_set_hdr(_request->request(), header.field, header.value); + + //now do our individual headers + for (HTTPHeader header : _headers) + httpd_resp_set_hdr(this->_request->request(), header.field, header.value); + + // DO NOT RELEASE HEADERS HERE... released in the PsychicResponse destructor after they have been sent. + // httpd_resp_set_hdr just passes on the pointer, but its needed after this call. + // clean up our header variables after send + // for (HTTPHeader header : _headers) + // { + // free(header.field); + // free(header.value); + // } + // _headers.clear(); +} + +esp_err_t PsychicResponse::sendChunk(uint8_t *chunk, size_t chunksize) +{ + /* Send the buffer contents as HTTP response chunk */ + esp_err_t err = httpd_resp_send_chunk(this->_request->request(), (char *)chunk, chunksize); + if (err != ESP_OK) + { + ESP_LOGE(PH_TAG, "File sending failed (%s)", esp_err_to_name(err)); + + /* Abort sending file */ + httpd_resp_sendstr_chunk(this->_request->request(), NULL); + + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + } + + return err; +} + +esp_err_t PsychicResponse::finishChunking() +{ + /* Respond with an empty chunk to signal HTTP response completion */ + return httpd_resp_send_chunk(this->_request->request(), NULL, 0); +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicResponse.h b/lib/PsychicHttp/src/PsychicResponse.h new file mode 100644 index 0000000..a36de65 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicResponse.h @@ -0,0 +1,46 @@ +#ifndef PsychicResponse_h +#define PsychicResponse_h + +#include "PsychicCore.h" +#include "time.h" + +class PsychicRequest; + +class PsychicResponse +{ + protected: + PsychicRequest *_request; + + int _code; + char _status[60]; + std::list _headers; + int64_t _contentLength; + const char * _body; + + public: + PsychicResponse(PsychicRequest *request); + virtual ~PsychicResponse(); + + void setCode(int code); + + void setContentType(const char *contentType); + void setContentLength(int64_t contentLength) { _contentLength = contentLength; } + int64_t getContentLength(int64_t contentLength) { return _contentLength; } + + void addHeader(const char *field, const char *value); + + void setCookie(const char *key, const char *value, unsigned long max_age = 60*60*24*30, const char *extras = ""); + + void setContent(const char *content); + void setContent(const uint8_t *content, size_t len); + + const char * getContent(); + size_t getContentLength(); + + virtual esp_err_t send(); + void sendHeaders(); + esp_err_t sendChunk(uint8_t *chunk, size_t chunksize); + esp_err_t finishChunking(); +}; + +#endif // PsychicResponse_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicStaticFileHander.cpp b/lib/PsychicHttp/src/PsychicStaticFileHander.cpp new file mode 100644 index 0000000..54fafc4 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicStaticFileHander.cpp @@ -0,0 +1,181 @@ +#include "PsychicStaticFileHandler.h" + +/*************************************/ +/* PsychicStaticFileHandler */ +/*************************************/ + +PsychicStaticFileHandler::PsychicStaticFileHandler(const char* uri, FS& fs, const char* path, const char* cache_control) + : _fs(fs), _uri(uri), _path(path), _default_file("index.html"), _cache_control(cache_control), _last_modified("") +{ + // Ensure leading '/' + if (_uri.length() == 0 || _uri[0] != '/') _uri = "/" + _uri; + if (_path.length() == 0 || _path[0] != '/') _path = "/" + _path; + + // If path ends with '/' we assume a hint that this is a directory to improve performance. + // However - if it does not end with '/' we, can't assume a file, path can still be a directory. + _isDir = _path[_path.length()-1] == '/'; + + // Remove the trailing '/' so we can handle default file + // Notice that root will be "" not "/" + if (_uri[_uri.length()-1] == '/') _uri = _uri.substring(0, _uri.length()-1); + if (_path[_path.length()-1] == '/') _path = _path.substring(0, _path.length()-1); + + // Reset stats + _gzipFirst = false; + _gzipStats = 0xF8; +} + +PsychicStaticFileHandler& PsychicStaticFileHandler::setIsDir(bool isDir){ + _isDir = isDir; + return *this; +} + +PsychicStaticFileHandler& PsychicStaticFileHandler::setDefaultFile(const char* filename){ + _default_file = String(filename); + return *this; +} + +PsychicStaticFileHandler& PsychicStaticFileHandler::setCacheControl(const char* cache_control){ + _cache_control = String(cache_control); + return *this; +} + +PsychicStaticFileHandler& PsychicStaticFileHandler::setLastModified(const char* last_modified){ + _last_modified = String(last_modified); + return *this; +} + +PsychicStaticFileHandler& PsychicStaticFileHandler::setLastModified(struct tm* last_modified){ + char result[30]; + strftime (result,30,"%a, %d %b %Y %H:%M:%S %Z", last_modified); + return setLastModified((const char *)result); +} + +bool PsychicStaticFileHandler::canHandle(PsychicRequest *request) +{ + if(request->method() != HTTP_GET || !request->uri().startsWith(_uri) ) + return false; + + if (_getFile(request)) + return true; + + return false; +} + +bool PsychicStaticFileHandler::_getFile(PsychicRequest *request) +{ + // Remove the found uri + String path = request->uri().substring(_uri.length()); + + // We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/' + bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length()-1] == '/'); + + path = _path + path; + + // Do we have a file or .gz file + if (!canSkipFileCheck && _fileExists(path)) + return true; + + // Can't handle if not default file + if (_default_file.length() == 0) + return false; + + // Try to add default file, ensure there is a trailing '/' ot the path. + if (path.length() == 0 || path[path.length()-1] != '/') + path += "/"; + path += _default_file; + + return _fileExists(path); +} + +#define FILE_IS_REAL(f) (f == true && !f.isDirectory()) + +bool PsychicStaticFileHandler::_fileExists(const String& path) +{ + bool fileFound = false; + bool gzipFound = false; + + String gzip = path + ".gz"; + + if (_gzipFirst) { + _file = _fs.open(gzip, "r"); + gzipFound = FILE_IS_REAL(_file); + if (!gzipFound){ + _file = _fs.open(path, "r"); + fileFound = FILE_IS_REAL(_file); + } + } else { + _file = _fs.open(path, "r"); + fileFound = FILE_IS_REAL(_file); + if (!fileFound){ + _file = _fs.open(gzip, "r"); + gzipFound = FILE_IS_REAL(_file); + } + } + + bool found = fileFound || gzipFound; + + if (found) + { + _filename = path; + + // Calculate gzip statistic + _gzipStats = (_gzipStats << 1) + (gzipFound ? 1 : 0); + if (_gzipStats == 0x00) _gzipFirst = false; // All files are not gzip + else if (_gzipStats == 0xFF) _gzipFirst = true; // All files are gzip + else _gzipFirst = _countBits(_gzipStats) > 4; // IF we have more gzip files - try gzip first + } + + return found; +} + +uint8_t PsychicStaticFileHandler::_countBits(const uint8_t value) const +{ + uint8_t w = value; + uint8_t n; + for (n=0; w!=0; n++) w&=w-1; + return n; +} + +esp_err_t PsychicStaticFileHandler::handleRequest(PsychicRequest *request) +{ + if (_file == true) + { + //is it not modified? + String etag = String(_file.size()); + if (_last_modified.length() && _last_modified == request->header("If-Modified-Since")) + { + _file.close(); + request->reply(304); // Not modified + } + //does our Etag match? + else if (_cache_control.length() && request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag)) + { + _file.close(); + + PsychicResponse response(request); + response.addHeader("Cache-Control", _cache_control.c_str()); + response.addHeader("ETag", etag.c_str()); + response.setCode(304); + response.send(); + } + //nope, send them the full file. + else + { + PsychicFileResponse response(request, _fs, _filename); + + if (_last_modified.length()) + response.addHeader("Last-Modified", _last_modified.c_str()); + if (_cache_control.length()) { + response.addHeader("Cache-Control", _cache_control.c_str()); + response.addHeader("ETag", etag.c_str()); + } + + return response.send(); + } + } else { + return request->reply(404); + } + + return ESP_OK; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicStaticFileHandler.h b/lib/PsychicHttp/src/PsychicStaticFileHandler.h new file mode 100644 index 0000000..f757111 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicStaticFileHandler.h @@ -0,0 +1,41 @@ +#ifndef PsychicStaticFileHandler_h +#define PsychicStaticFileHandler_h + +#include "PsychicCore.h" +#include "PsychicWebHandler.h" +#include "PsychicRequest.h" +#include "PsychicResponse.h" +#include "PsychicFileResponse.h" + +class PsychicStaticFileHandler : public PsychicWebHandler { + using File = fs::File; + using FS = fs::FS; + private: + bool _getFile(PsychicRequest *request); + bool _fileExists(const String& path); + uint8_t _countBits(const uint8_t value) const; + protected: + FS _fs; + File _file; + String _filename; + String _uri; + String _path; + String _default_file; + String _cache_control; + String _last_modified; + bool _isDir; + bool _gzipFirst; + uint8_t _gzipStats; + public: + PsychicStaticFileHandler(const char* uri, FS& fs, const char* path, const char* cache_control); + bool canHandle(PsychicRequest *request) override; + esp_err_t handleRequest(PsychicRequest *request) override; + PsychicStaticFileHandler& setIsDir(bool isDir); + PsychicStaticFileHandler& setDefaultFile(const char* filename); + PsychicStaticFileHandler& setCacheControl(const char* cache_control); + PsychicStaticFileHandler& setLastModified(const char* last_modified); + PsychicStaticFileHandler& setLastModified(struct tm* last_modified); + //PsychicStaticFileHandler& setTemplateProcessor(AwsTemplateProcessor newCallback) {_callback = newCallback; return *this;} +}; + +#endif /* PsychicHttp_h */ \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicStreamResponse.cpp b/lib/PsychicHttp/src/PsychicStreamResponse.cpp new file mode 100644 index 0000000..5132104 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicStreamResponse.cpp @@ -0,0 +1,94 @@ +#include "PsychicStreamResponse.h" +#include "PsychicResponse.h" +#include "PsychicRequest.h" + +PsychicStreamResponse::PsychicStreamResponse(PsychicRequest *request, const String& contentType) + : PsychicResponse(request), _buffer(NULL) { + + setContentType(contentType.c_str()); + addHeader("Content-Disposition", "inline"); +} + + +PsychicStreamResponse::PsychicStreamResponse(PsychicRequest *request, const String& contentType, const String& name) + : PsychicResponse(request), _buffer(NULL) { + + setContentType(contentType.c_str()); + + char buf[26+name.length()]; + snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", name.c_str()); + addHeader("Content-Disposition", buf); +} + + +PsychicStreamResponse::~PsychicStreamResponse() +{ + endSend(); +} + + +esp_err_t PsychicStreamResponse::beginSend() +{ + if(_buffer) + return ESP_OK; + + //Buffer to hold ChunkPrinter and stream buffer. Using placement new will keep us at a single allocation. + _buffer = (uint8_t*)malloc(STREAM_CHUNK_SIZE + sizeof(ChunkPrinter)); + + if(!_buffer) + { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); + return ESP_FAIL; + } + + _printer = new (_buffer) ChunkPrinter(this, _buffer + sizeof(ChunkPrinter), STREAM_CHUNK_SIZE); + + sendHeaders(); + return ESP_OK; +} + + +esp_err_t PsychicStreamResponse::endSend() +{ + esp_err_t err = ESP_OK; + + if(!_buffer) + err = ESP_FAIL; + else + { + _printer->~ChunkPrinter(); //flushed on destruct + err = finishChunking(); + free(_buffer); + _buffer = NULL; + } + return err; +} + + +void PsychicStreamResponse::flush() +{ + if(_buffer) + _printer->flush(); +} + + +size_t PsychicStreamResponse::write(uint8_t data) +{ + return _buffer ? _printer->write(data) : 0; +} + + +size_t PsychicStreamResponse::write(const uint8_t *buffer, size_t size) +{ + return _buffer ? _printer->write(buffer, size) : 0; +} + + +size_t PsychicStreamResponse::copyFrom(Stream &stream) +{ + if(_buffer) + return _printer->copyFrom(stream); + + return 0; +} diff --git a/lib/PsychicHttp/src/PsychicStreamResponse.h b/lib/PsychicHttp/src/PsychicStreamResponse.h new file mode 100644 index 0000000..888ec9c --- /dev/null +++ b/lib/PsychicHttp/src/PsychicStreamResponse.h @@ -0,0 +1,35 @@ +#ifndef PsychicStreamResponse_h +#define PsychicStreamResponse_h + +#include "PsychicCore.h" +#include "PsychicResponse.h" +#include "ChunkPrinter.h" + +class PsychicRequest; + +class PsychicStreamResponse : public PsychicResponse, public Print +{ + private: + ChunkPrinter *_printer; + uint8_t *_buffer; + public: + + PsychicStreamResponse(PsychicRequest *request, const String& contentType); + PsychicStreamResponse(PsychicRequest *request, const String& contentType, const String& name); //Download + + ~PsychicStreamResponse(); + + esp_err_t beginSend(); + esp_err_t endSend(); + + void flush() override; + + size_t write(uint8_t data) override; + size_t write(const uint8_t *buffer, size_t size) override; + + size_t copyFrom(Stream &stream); + + using Print::write; +}; + +#endif // PsychicStreamResponse_h diff --git a/lib/PsychicHttp/src/PsychicUploadHandler.cpp b/lib/PsychicHttp/src/PsychicUploadHandler.cpp new file mode 100644 index 0000000..d8a55e3 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicUploadHandler.cpp @@ -0,0 +1,395 @@ +#include "PsychicUploadHandler.h" + +PsychicUploadHandler::PsychicUploadHandler() : + PsychicWebHandler() + , _temp() + , _parsedLength(0) + , _multiParseState(EXPECT_BOUNDARY) + , _boundaryPosition(0) + , _itemStartIndex(0) + , _itemSize(0) + , _itemName() + , _itemFilename() + , _itemType() + , _itemValue() + , _itemBuffer(0) + , _itemBufferIndex(0) + , _itemIsFile(false) + {} +PsychicUploadHandler::~PsychicUploadHandler() {} + +bool PsychicUploadHandler::canHandle(PsychicRequest *request) { + return true; +} + +esp_err_t PsychicUploadHandler::handleRequest(PsychicRequest *request) +{ + esp_err_t err = ESP_OK; + + //save it for later (multipart) + _request = request; + _parsedLength = 0; + /* File cannot be larger than a limit */ + if (request->contentLength() > request->server()->maxUploadSize) + { + ESP_LOGE(PH_TAG, "File too large : %d bytes", request->contentLength()); + + /* Respond with 400 Bad Request */ + char error[50]; + sprintf(error, "File size must be less than %lu bytes!", request->server()->maxUploadSize); + httpd_resp_send_err(request->request(), HTTPD_400_BAD_REQUEST, error); + + /* Return failure to close underlying connection else the incoming file content will keep the socket busy */ + return ESP_FAIL; + } + + //we might want to access some of these params + request->loadParams(); + + //TODO: support for the 100 header. not sure if we can do it. + // if (request->header("Expect").equals("100-continue")) + // { + // char response[] = "100 Continue"; + // httpd_socket_send(self->server, httpd_req_to_sockfd(req), response, strlen(response), 0); + // } + + //2 types of upload requests + if (request->isMultipart()) + err = _multipartUploadHandler(request); + else + err = _basicUploadHandler(request); + + //we can also call onRequest for some final processing and response + if (err == ESP_OK) + { + if (_requestCallback != NULL) + err = _requestCallback(request); + else + err = request->reply("Upload Successful."); + } + else + request->reply(500, "text/html", "Error processing upload."); + + return err; +} + +esp_err_t PsychicUploadHandler::_basicUploadHandler(PsychicRequest *request) +{ + esp_err_t err = ESP_OK; + + String filename = request->getFilename(); + + /* Retrieve the pointer to scratch buffer for temporary storage */ + char *buf = (char *)malloc(FILE_CHUNK_SIZE); + int received; + unsigned long index = 0; + + /* Content length of the request gives the size of the file being uploaded */ + int remaining = request->contentLength(); + + while (remaining > 0) + { + #ifdef ENABLE_ASYNC + httpd_sess_update_lru_counter(request->server()->server, request->client()->socket()); + #endif + + //ESP_LOGD(PH_TAG, "Remaining size : %d", remaining); + + /* Receive the file part by part into a buffer */ + if ((received = httpd_req_recv(request->request(), buf, min(remaining, FILE_CHUNK_SIZE))) <= 0) + { + /* Retry if timeout occurred */ + if (received == HTTPD_SOCK_ERR_TIMEOUT) + continue; + //bail if we got an error + else if (received == HTTPD_SOCK_ERR_FAIL) + { + ESP_LOGE(PH_TAG, "Socket error"); + err = ESP_FAIL; + break; + } + } + + //call our upload callback here. + if (_uploadCallback != NULL) + { + err = _uploadCallback(request, filename, index, (uint8_t *)buf, received, (remaining - received == 0)); + if (err != ESP_OK) + break; + } + else + { + ESP_LOGE(PH_TAG, "No upload callback specified!"); + err = ESP_FAIL; + break; + } + + /* Keep track of remaining size of the file left to be uploaded */ + remaining -= received; + index += received; + } + + //dont forget to free our buffer + free(buf); + + return err; +} + +esp_err_t PsychicUploadHandler::_multipartUploadHandler(PsychicRequest *request) +{ + esp_err_t err = ESP_OK; + + String value = request->header("Content-Type"); + if (value.startsWith("multipart/")){ + _boundary = value.substring(value.indexOf('=')+1); + _boundary.replace("\"",""); + } else { + ESP_LOGE(PH_TAG, "No multipart boundary found."); + return request->reply(400, "text/html", "No multipart boundary found."); + } + + char *buf = (char *)malloc(FILE_CHUNK_SIZE); + int received; + unsigned long index = 0; + + /* Content length of the request gives the size of the file being uploaded */ + int remaining = request->contentLength(); + + while (remaining > 0) + { + #ifdef ENABLE_ASYNC + httpd_sess_update_lru_counter(request->server()->server, request->client()->socket()); + #endif + + //ESP_LOGD(PH_TAG, "Remaining size : %d", remaining); + + /* Receive the file part by part into a buffer */ + if ((received = httpd_req_recv(request->request(), buf, min(remaining, FILE_CHUNK_SIZE))) <= 0) + { + /* Retry if timeout occurred */ + if (received == HTTPD_SOCK_ERR_TIMEOUT) + continue; + //bail if we got an error + else if (received == HTTPD_SOCK_ERR_FAIL) + { + ESP_LOGE(PH_TAG, "Socket error"); + err = ESP_FAIL; + break; + } + } + + //parse it 1 byte at a time. + for (int i=0; i 12 && _temp.substring(0, 12).equalsIgnoreCase("Content-Type")){ + _itemType = _temp.substring(14); + _itemIsFile = true; + } else if(_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase("Content-Disposition")){ + _temp = _temp.substring(_temp.indexOf(';') + 2); + while(_temp.indexOf(';') > 0){ + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1); + if(name == "name"){ + _itemName = nameVal; + } else if(name == "filename"){ + _itemFilename = nameVal; + _itemIsFile = true; + } + _temp = _temp.substring(_temp.indexOf(';') + 2); + } + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1); + if(name == "name"){ + _itemName = nameVal; + } else if(name == "filename"){ + _itemFilename = nameVal; + _itemIsFile = true; + } + } + _temp = String(); + } else { + _multiParseState = WAIT_FOR_RETURN1; + //value starts from here + _itemSize = 0; + _itemStartIndex = _parsedLength; + _itemValue = String(); + if(_itemIsFile){ + if(_itemBuffer) + free(_itemBuffer); + _itemBuffer = (uint8_t*)malloc(FILE_CHUNK_SIZE); + if(_itemBuffer == NULL){ + ESP_LOGE(PH_TAG, "Multipart: Failed to allocate buffer"); + _multiParseState = PARSE_ERROR; + return; + } + _itemBufferIndex = 0; + } + } + } + } else if(_multiParseState == EXPECT_FEED1){ + if(data != '\n'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH1; + } + } else if(_multiParseState == EXPECT_DASH1){ + if(data != '-'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH2; + } + } else if(_multiParseState == EXPECT_DASH2){ + if(data != '-'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = BOUNDARY_OR_DATA; + _boundaryPosition = 0; + } + } else if(_multiParseState == BOUNDARY_OR_DATA){ + if(_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; + for(i=0; i<_boundaryPosition; i++) + itemWriteByte(_boundary.c_str()[i]); + _parseMultipartPostByte(data, last); + } else if(_boundaryPosition == _boundary.length() - 1){ + _multiParseState = DASH3_OR_RETURN2; + if(!_itemIsFile){ + _request->addParam(_itemName, _itemValue); + //_addParam(new AsyncWebParameter(_itemName, _itemValue, true)); + } else { + if(_itemSize){ + if(_uploadCallback) + _uploadCallback(_request, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); + _itemBufferIndex = 0; + _request->addParam(new PsychicWebParameter(_itemName, _itemFilename, true, true, _itemSize)); + } + free(_itemBuffer); + _itemBuffer = NULL; + } + + } else { + _boundaryPosition++; + } + } else if(_multiParseState == DASH3_OR_RETURN2){ + if(data == '-' && (_request->contentLength() - _parsedLength - 4) != 0){ + ESP_LOGE(PH_TAG, "ERROR: The parser got to the end of the POST but is expecting more bytes!"); + _multiParseState = PARSE_ERROR; + return; + } + if(data == '\r'){ + _multiParseState = EXPECT_FEED2; + } else if(data == '-' && _request->contentLength() == (_parsedLength + 4)){ + _multiParseState = PARSING_FINISHED; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); + _parseMultipartPostByte(data, last); + } + } else if(_multiParseState == EXPECT_FEED2){ + if(data == '\n'){ + _multiParseState = PARSE_HEADERS; + _itemIsFile = false; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); + itemWriteByte('\r'); _parseMultipartPostByte(data, last); + } + } +} diff --git a/lib/PsychicHttp/src/PsychicUploadHandler.h b/lib/PsychicHttp/src/PsychicUploadHandler.h new file mode 100644 index 0000000..59e62b6 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicUploadHandler.h @@ -0,0 +1,68 @@ +#ifndef PsychicUploadHandler_h +#define PsychicUploadHandler_h + +#include "PsychicCore.h" +#include "PsychicHttpServer.h" +#include "PsychicRequest.h" +#include "PsychicWebHandler.h" +#include "PsychicWebParameter.h" + +//callback definitions +typedef std::function PsychicUploadCallback; + +/* +* HANDLER :: Can be attached to any endpoint or as a generic request handler. +*/ + +class PsychicUploadHandler : public PsychicWebHandler { + protected: + PsychicUploadCallback _uploadCallback; + + PsychicRequest *_request; + + String _temp; + size_t _parsedLength; + uint8_t _multiParseState; + String _boundary; + uint8_t _boundaryPosition; + size_t _itemStartIndex; + size_t _itemSize; + String _itemName; + String _itemFilename; + String _itemType; + String _itemValue; + uint8_t *_itemBuffer; + size_t _itemBufferIndex; + bool _itemIsFile; + + esp_err_t _basicUploadHandler(PsychicRequest *request); + esp_err_t _multipartUploadHandler(PsychicRequest *request); + + void _handleUploadByte(uint8_t data, bool last); + void _parseMultipartPostByte(uint8_t data, bool last); + + public: + PsychicUploadHandler(); + ~PsychicUploadHandler(); + + bool canHandle(PsychicRequest *request) override; + esp_err_t handleRequest(PsychicRequest *request) override; + + PsychicUploadHandler * onUpload(PsychicUploadCallback fn); +}; + +enum { + EXPECT_BOUNDARY, + PARSE_HEADERS, + WAIT_FOR_RETURN1, + EXPECT_FEED1, + EXPECT_DASH1, + EXPECT_DASH2, + BOUNDARY_OR_DATA, + DASH3_OR_RETURN2, + EXPECT_FEED2, + PARSING_FINISHED, + PARSE_ERROR +}; + +#endif // PsychicUploadHandler_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicWebHandler.cpp b/lib/PsychicHttp/src/PsychicWebHandler.cpp new file mode 100644 index 0000000..1e4997e --- /dev/null +++ b/lib/PsychicHttp/src/PsychicWebHandler.cpp @@ -0,0 +1,74 @@ +#include "PsychicWebHandler.h" + +PsychicWebHandler::PsychicWebHandler() : + PsychicHandler(), + _requestCallback(NULL), + _onOpen(NULL), + _onClose(NULL) + {} +PsychicWebHandler::~PsychicWebHandler() {} + +bool PsychicWebHandler::canHandle(PsychicRequest *request) { + return true; +} + +esp_err_t PsychicWebHandler::handleRequest(PsychicRequest *request) +{ + //lookup our client + PsychicClient *client = checkForNewClient(request->client()); + if (client->isNew) + openCallback(client); + + /* Request body cannot be larger than a limit */ + if (request->contentLength() > request->server()->maxRequestBodySize) + { + ESP_LOGE(PH_TAG, "Request body too large : %d bytes", request->contentLength()); + + /* Respond with 400 Bad Request */ + char error[60]; + sprintf(error, "Request body must be less than %lu bytes!", request->server()->maxRequestBodySize); + httpd_resp_send_err(request->request(), HTTPD_400_BAD_REQUEST, error); + + /* Return failure to close underlying connection else the incoming file content will keep the socket busy */ + return ESP_FAIL; + } + + //get our body loaded up. + esp_err_t err = request->loadBody(); + if (err != ESP_OK) + return err; + + //load our params in. + request->loadParams(); + + //okay, pass on to our callback. + if (this->_requestCallback != NULL) + err = this->_requestCallback(request); + + return err; +} + +PsychicWebHandler * PsychicWebHandler::onRequest(PsychicHttpRequestCallback fn) { + _requestCallback = fn; + return this; +} + +void PsychicWebHandler::openCallback(PsychicClient *client) { + if (_onOpen != NULL) + _onOpen(client); +} + +void PsychicWebHandler::closeCallback(PsychicClient *client) { + if (_onClose != NULL) + _onClose(getClient(client)); +} + +PsychicWebHandler * PsychicWebHandler::onOpen(PsychicClientCallback fn) { + _onOpen = fn; + return this; +} + +PsychicWebHandler * PsychicWebHandler::onClose(PsychicClientCallback fn) { + _onClose = fn; + return this; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicWebHandler.h b/lib/PsychicHttp/src/PsychicWebHandler.h new file mode 100644 index 0000000..07a6780 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicWebHandler.h @@ -0,0 +1,34 @@ +#ifndef PsychicWebHandler_h +#define PsychicWebHandler_h + +// #include "PsychicCore.h" +// #include "PsychicHttpServer.h" +// #include "PsychicRequest.h" +#include "PsychicHandler.h" + +/* +* HANDLER :: Can be attached to any endpoint or as a generic request handler. +*/ + +class PsychicWebHandler : public PsychicHandler { + protected: + PsychicHttpRequestCallback _requestCallback; + PsychicClientCallback _onOpen; + PsychicClientCallback _onClose; + + public: + PsychicWebHandler(); + ~PsychicWebHandler(); + + virtual bool canHandle(PsychicRequest *request) override; + virtual esp_err_t handleRequest(PsychicRequest *request) override; + PsychicWebHandler * onRequest(PsychicHttpRequestCallback fn); + + virtual void openCallback(PsychicClient *client); + virtual void closeCallback(PsychicClient *client); + + PsychicWebHandler *onOpen(PsychicClientCallback fn); + PsychicWebHandler *onClose(PsychicClientCallback fn); +}; + +#endif \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicWebParameter.h b/lib/PsychicHttp/src/PsychicWebParameter.h new file mode 100644 index 0000000..e09136b --- /dev/null +++ b/lib/PsychicHttp/src/PsychicWebParameter.h @@ -0,0 +1,25 @@ +#ifndef PsychicWebParameter_h +#define PsychicWebParameter_h + +/* + * PARAMETER :: Chainable object to hold GET/POST and FILE parameters + * */ + +class PsychicWebParameter { + private: + String _name; + String _value; + size_t _size; + bool _isForm; + bool _isFile; + + public: + PsychicWebParameter(const String& name, const String& value, bool form=false, bool file=false, size_t size=0): _name(name), _value(value), _size(size), _isForm(form), _isFile(file){} + const String& name() const { return _name; } + const String& value() const { return _value; } + size_t size() const { return _size; } + bool isPost() const { return _isForm; } + bool isFile() const { return _isFile; } +}; + +#endif //PsychicWebParameter_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/PsychicWebSocket.cpp b/lib/PsychicHttp/src/PsychicWebSocket.cpp new file mode 100644 index 0000000..7a3c6b1 --- /dev/null +++ b/lib/PsychicHttp/src/PsychicWebSocket.cpp @@ -0,0 +1,258 @@ +#include "PsychicWebSocket.h" + +/*************************************/ +/* PsychicWebSocketRequest */ +/*************************************/ + +PsychicWebSocketRequest::PsychicWebSocketRequest(PsychicRequest *req) : + PsychicRequest(req->server(), req->request()), + _client(req->client()) +{ +} + +PsychicWebSocketRequest::~PsychicWebSocketRequest() +{ +} + +PsychicWebSocketClient * PsychicWebSocketRequest::client() { + return &_client; +} + +esp_err_t PsychicWebSocketRequest::reply(httpd_ws_frame_t * ws_pkt) +{ + return httpd_ws_send_frame(this->_req, ws_pkt); +} + +esp_err_t PsychicWebSocketRequest::reply(httpd_ws_type_t op, const void *data, size_t len) +{ + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + + ws_pkt.payload = (uint8_t*)data; + ws_pkt.len = len; + ws_pkt.type = op; + + return this->reply(&ws_pkt); +} + +esp_err_t PsychicWebSocketRequest::reply(const char *buf) +{ + return this->reply(HTTPD_WS_TYPE_TEXT, buf, strlen(buf)); +} + +/*************************************/ +/* PsychicWebSocketClient */ +/*************************************/ + +PsychicWebSocketClient::PsychicWebSocketClient(PsychicClient *client) + : PsychicClient(client->server(), client->socket()) +{ +} + +PsychicWebSocketClient::~PsychicWebSocketClient() { +} + +esp_err_t PsychicWebSocketClient::sendMessage(httpd_ws_frame_t * ws_pkt) +{ + return httpd_ws_send_frame_async(this->server(), this->socket(), ws_pkt); +} + +esp_err_t PsychicWebSocketClient::sendMessage(httpd_ws_type_t op, const void *data, size_t len) +{ + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + + ws_pkt.payload = (uint8_t*)data; + ws_pkt.len = len; + ws_pkt.type = op; + + return this->sendMessage(&ws_pkt); +} + +esp_err_t PsychicWebSocketClient::sendMessage(const char *buf) +{ + return this->sendMessage(HTTPD_WS_TYPE_TEXT, buf, strlen(buf)); +} + +PsychicWebSocketHandler::PsychicWebSocketHandler() : + PsychicHandler(), + _onOpen(NULL), + _onFrame(NULL), + _onClose(NULL) + { + } + +PsychicWebSocketHandler::~PsychicWebSocketHandler() { +} + +PsychicWebSocketClient * PsychicWebSocketHandler::getClient(int socket) +{ + PsychicClient *client = PsychicHandler::getClient(socket); + if (client == NULL) + return NULL; + + if (client->_friend == NULL) + { + return NULL; + } + + return (PsychicWebSocketClient *)client->_friend; +} + +PsychicWebSocketClient * PsychicWebSocketHandler::getClient(PsychicClient *client) { + return getClient(client->socket()); +} + +void PsychicWebSocketHandler::addClient(PsychicClient *client) { + client->_friend = new PsychicWebSocketClient(client); + PsychicHandler::addClient(client); +} + +void PsychicWebSocketHandler::removeClient(PsychicClient *client) { + PsychicHandler::removeClient(client); + delete (PsychicWebSocketClient*)client->_friend; + client->_friend = NULL; +} + +void PsychicWebSocketHandler::openCallback(PsychicClient *client) { + PsychicWebSocketClient *buddy = getClient(client); + if (buddy == NULL) + { + return; + } + + if (_onOpen != NULL) + _onOpen(getClient(buddy)); +} + +void PsychicWebSocketHandler::closeCallback(PsychicClient *client) { + PsychicWebSocketClient *buddy = getClient(client); + if (buddy == NULL) + { + return; + } + + if (_onClose != NULL) + _onClose(getClient(buddy)); +} + +bool PsychicWebSocketHandler::isWebSocket() { return true; } + +esp_err_t PsychicWebSocketHandler::handleRequest(PsychicRequest *request) +{ + //lookup our client + PsychicClient *client = checkForNewClient(request->client()); + + // beginning of the ws URI handler and our onConnect hook + if (request->method() == HTTP_GET) + { + if (client->isNew) + openCallback(client); + + return ESP_OK; + } + + //prep our request + PsychicWebSocketRequest wsRequest(request); + + //init our memory for storing the packet + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + uint8_t *buf = NULL; + + /* Set max_len = 0 to get the frame len */ + esp_err_t ret = httpd_ws_recv_frame(wsRequest.request(), &ws_pkt, 0); + if (ret != ESP_OK) { + ESP_LOGE(PH_TAG, "httpd_ws_recv_frame failed to get frame len with %s", esp_err_to_name(ret)); + return ret; + } + + //okay, now try to load the packet + //ESP_LOGD(PH_TAG, "frame len is %d", ws_pkt.len); + if (ws_pkt.len) { + /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */ + buf = (uint8_t*) calloc(1, ws_pkt.len + 1); + if (buf == NULL) { + ESP_LOGE(PH_TAG, "Failed to calloc memory for buf"); + return ESP_ERR_NO_MEM; + } + ws_pkt.payload = buf; + /* Set max_len = ws_pkt.len to get the frame payload */ + ret = httpd_ws_recv_frame(wsRequest.request(), &ws_pkt, ws_pkt.len); + if (ret != ESP_OK) { + ESP_LOGE(PH_TAG, "httpd_ws_recv_frame failed with %s", esp_err_to_name(ret)); + free(buf); + return ret; + } + //ESP_LOGD(PH_TAG, "Got packet with message: %s", ws_pkt.payload); + } + + // Text messages are our payload. + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT || ws_pkt.type == HTTPD_WS_TYPE_BINARY) + { + if (this->_onFrame != NULL) + ret = this->_onFrame(&wsRequest, &ws_pkt); + } + + //logging housekeeping + if (ret != ESP_OK) + ESP_LOGE(PH_TAG, "httpd_ws_send_frame failed with %s", esp_err_to_name(ret)); + // ESP_LOGD(PH_TAG, "ws_handler: httpd_handle_t=%p, sockfd=%d, client_info:%d", + // request->server(), + // httpd_req_to_sockfd(request->request()), + // httpd_ws_get_fd_info(request->server()->server, httpd_req_to_sockfd(request->request()))); + + //dont forget to release our buffer memory + free(buf); + + return ret; +} + +PsychicWebSocketHandler * PsychicWebSocketHandler::onOpen(PsychicWebSocketClientCallback fn) { + _onOpen = fn; + return this; +} + +PsychicWebSocketHandler * PsychicWebSocketHandler::onFrame(PsychicWebSocketFrameCallback fn) { + _onFrame = fn; + return this; +} + +PsychicWebSocketHandler * PsychicWebSocketHandler::onClose(PsychicWebSocketClientCallback fn) { + _onClose = fn; + return this; +} + +void PsychicWebSocketHandler::sendAll(httpd_ws_frame_t * ws_pkt) +{ + for (PsychicClient *client : _clients) + { + //ESP_LOGD(PH_TAG, "Active client (fd=%d) -> sending async message", client->socket()); + + if (client->_friend == NULL) + { + return; + } + + if (((PsychicWebSocketClient*)client->_friend)->sendMessage(ws_pkt) != ESP_OK) + break; + } +} + +void PsychicWebSocketHandler::sendAll(httpd_ws_type_t op, const void *data, size_t len) +{ + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + + ws_pkt.payload = (uint8_t*)data; + ws_pkt.len = len; + ws_pkt.type = op; + + this->sendAll(&ws_pkt); +} + +void PsychicWebSocketHandler::sendAll(const char *buf) +{ + this->sendAll(HTTPD_WS_TYPE_TEXT, buf, strlen(buf)); +} diff --git a/lib/PsychicHttp/src/PsychicWebSocket.h b/lib/PsychicHttp/src/PsychicWebSocket.h new file mode 100644 index 0000000..01917de --- /dev/null +++ b/lib/PsychicHttp/src/PsychicWebSocket.h @@ -0,0 +1,70 @@ +#ifndef PsychicWebSocket_h +#define PsychicWebSocket_h + +#include "PsychicCore.h" +#include "PsychicRequest.h" + +class PsychicWebSocketRequest; +class PsychicWebSocketClient; + +//callback function definitions +typedef std::function PsychicWebSocketClientCallback; +typedef std::function PsychicWebSocketFrameCallback; + +class PsychicWebSocketClient : public PsychicClient +{ + public: + PsychicWebSocketClient(PsychicClient *client); + ~PsychicWebSocketClient(); + + esp_err_t sendMessage(httpd_ws_frame_t * ws_pkt); + esp_err_t sendMessage(httpd_ws_type_t op, const void *data, size_t len); + esp_err_t sendMessage(const char *buf); +}; + +class PsychicWebSocketRequest : public PsychicRequest +{ + private: + PsychicWebSocketClient _client; + + public: + PsychicWebSocketRequest(PsychicRequest *req); + virtual ~PsychicWebSocketRequest(); + + PsychicWebSocketClient * client() override; + + esp_err_t reply(httpd_ws_frame_t * ws_pkt); + esp_err_t reply(httpd_ws_type_t op, const void *data, size_t len); + esp_err_t reply(const char *buf); +}; + +class PsychicWebSocketHandler : public PsychicHandler { + protected: + PsychicWebSocketClientCallback _onOpen; + PsychicWebSocketFrameCallback _onFrame; + PsychicWebSocketClientCallback _onClose; + + public: + PsychicWebSocketHandler(); + ~PsychicWebSocketHandler(); + + PsychicWebSocketClient * getClient(int socket) override; + PsychicWebSocketClient * getClient(PsychicClient *client) override; + void addClient(PsychicClient *client) override; + void removeClient(PsychicClient *client) override; + void openCallback(PsychicClient *client) override; + void closeCallback(PsychicClient *client) override; + + bool isWebSocket() override final; + esp_err_t handleRequest(PsychicRequest *request) override; + + PsychicWebSocketHandler *onOpen(PsychicWebSocketClientCallback fn); + PsychicWebSocketHandler *onFrame(PsychicWebSocketFrameCallback fn); + PsychicWebSocketHandler *onClose(PsychicWebSocketClientCallback fn); + + void sendAll(httpd_ws_frame_t * ws_pkt); + void sendAll(httpd_ws_type_t op, const void *data, size_t len); + void sendAll(const char *buf); +}; + +#endif // PsychicWebSocket_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/TemplatePrinter.cpp b/lib/PsychicHttp/src/TemplatePrinter.cpp new file mode 100644 index 0000000..5c05904 --- /dev/null +++ b/lib/PsychicHttp/src/TemplatePrinter.cpp @@ -0,0 +1,90 @@ + /************************************************************ + + TemplatePrinter Class + + A basic templating engine for a stream of text. + This wraps the Arduino Print interface and writes to any + Print interface. + + Written by Christopher Andrews (https://github.com/Chris--A) + + ************************************************************/ + +#include "TemplatePrinter.h" + +void TemplatePrinter::resetParam(bool flush){ + if(flush && _inParam){ + _stream.write(_delimiter); + + if(_paramPos) + _stream.print(_paramBuffer); + } + + memset(_paramBuffer, 0, sizeof(_paramBuffer)); + _paramPos = 0; + _inParam = false; +} + + +void TemplatePrinter::flush(){ + resetParam(true); + _stream.flush(); +} + +size_t TemplatePrinter::write(uint8_t data){ + + if(data == _delimiter){ + + // End of parameter, send to callback + if(_inParam){ + + // On false, return the parameter place holder as is: not a parameter + // Bug fix: ignore parameters that are zero length. + if(!_paramPos || !_cb(_stream, _paramBuffer)){ + resetParam(true); + _stream.write(data); + }else{ + resetParam(false); + } + + // Start collecting parameter + }else{ + _inParam = true; + } + }else{ + + // Are we collecting + if(_inParam){ + + // Is param still valid + if(isalnum(data) || data == '_'){ + + // Total param len must be 63, 1 for null. + if(_paramPos < sizeof(_paramBuffer) - 1){ + _paramBuffer[_paramPos++] = data; + + // Not a valid param + }else{ + resetParam(true); + } + }else{ + resetParam(true); + _stream.write(data); + } + + // Just output + }else{ + _stream.write(data); + } + } + return 1; +} + +size_t TemplatePrinter::copyFrom(Stream &stream){ + size_t count = 0; + + while(stream.available()) + count += this->write(stream.read()); + + return count; +} diff --git a/lib/PsychicHttp/src/TemplatePrinter.h b/lib/PsychicHttp/src/TemplatePrinter.h new file mode 100644 index 0000000..6adf14a --- /dev/null +++ b/lib/PsychicHttp/src/TemplatePrinter.h @@ -0,0 +1,51 @@ +#ifndef TemplatePrinter_h + #define TemplatePrinter_h + + #include "PsychicCore.h" + #include + + /************************************************************ + + TemplatePrinter Class + + A basic templating engine for a stream of text. + This wraps the Arduino Print interface and writes to any + Print interface. + + Written by Christopher Andrews (https://github.com/Chris--A) + + ************************************************************/ + + class TemplatePrinter; + + typedef std::function TemplateCallback; + typedef std::function TemplateSourceCallback; + + class TemplatePrinter : public Print{ + private: + bool _inParam; + char _paramBuffer[64]; + uint8_t _paramPos; + Print &_stream; + TemplateCallback _cb; + char _delimiter; + + void resetParam(bool flush); + + public: + using Print::write; + + static void start(Print &stream, TemplateCallback cb, TemplateSourceCallback entry){ + TemplatePrinter printer(stream, cb); + entry(printer); + } + + TemplatePrinter(Print &stream, TemplateCallback cb, const char delimeter = '%') : _stream(stream), _cb(cb), _delimiter(delimeter) { resetParam(false); } + ~TemplatePrinter(){ flush(); } + + void flush() override; + size_t write(uint8_t data) override; + size_t copyFrom(Stream &stream); + }; + +#endif diff --git a/lib/PsychicHttp/src/async_worker.cpp b/lib/PsychicHttp/src/async_worker.cpp new file mode 100644 index 0000000..d7310cb --- /dev/null +++ b/lib/PsychicHttp/src/async_worker.cpp @@ -0,0 +1,203 @@ +#include "async_worker.h" + +bool is_on_async_worker_thread(void) +{ + // is our handle one of the known async handles? + TaskHandle_t handle = xTaskGetCurrentTaskHandle(); + for (int i = 0; i < ASYNC_WORKER_COUNT; i++) { + if (worker_handles[i] == handle) { + return true; + } + } + return false; +} + +// Submit an HTTP req to the async worker queue +esp_err_t submit_async_req(httpd_req_t *req, httpd_req_handler_t handler) +{ + // must create a copy of the request that we own + httpd_req_t* copy = NULL; + esp_err_t err = httpd_req_async_handler_begin(req, ©); + if (err != ESP_OK) { + return err; + } + + httpd_async_req_t async_req = { + .req = copy, + .handler = handler, + }; + + // How should we handle resource exhaustion? + // In this example, we immediately respond with an + // http error if no workers are available. + int ticks = 0; + + // counting semaphore: if success, we know 1 or + // more asyncReqTaskWorkers are available. + if (xSemaphoreTake(worker_ready_count, ticks) == false) { + ESP_LOGE(PH_TAG, "No workers are available"); + httpd_req_async_handler_complete(copy); // cleanup + return ESP_FAIL; + } + + // Since worker_ready_count > 0 the queue should already have space. + // But lets wait up to 100ms just to be safe. + if (xQueueSend(async_req_queue, &async_req, pdMS_TO_TICKS(100)) == false) { + ESP_LOGE(PH_TAG, "worker queue is full"); + httpd_req_async_handler_complete(copy); // cleanup + return ESP_FAIL; + } + + return ESP_OK; +} + +void async_req_worker_task(void *p) +{ + ESP_LOGI(PH_TAG, "starting async req task worker"); + + while (true) { + + // counting semaphore - this signals that a worker + // is ready to accept work + xSemaphoreGive(worker_ready_count); + + // wait for a request + httpd_async_req_t async_req; + if (xQueueReceive(async_req_queue, &async_req, portMAX_DELAY)) { + + ESP_LOGI(PH_TAG, "invoking %s", async_req.req->uri); + + // call the handler + async_req.handler(async_req.req); + + // Inform the server that it can purge the socket used for + // this request, if needed. + if (httpd_req_async_handler_complete(async_req.req) != ESP_OK) { + ESP_LOGE(PH_TAG, "failed to complete async req"); + } + } + } + + ESP_LOGW(PH_TAG, "worker stopped"); + vTaskDelete(NULL); +} + +void start_async_req_workers(void) +{ + + // counting semaphore keeps track of available workers + worker_ready_count = xSemaphoreCreateCounting( + ASYNC_WORKER_COUNT, // Max Count + 0); // Initial Count + if (worker_ready_count == NULL) { + ESP_LOGE(PH_TAG, "Failed to create workers counting Semaphore"); + return; + } + + // create queue + async_req_queue = xQueueCreate(1, sizeof(httpd_async_req_t)); + if (async_req_queue == NULL){ + ESP_LOGE(PH_TAG, "Failed to create async_req_queue"); + vSemaphoreDelete(worker_ready_count); + return; + } + + // start worker tasks + for (int i = 0; i < ASYNC_WORKER_COUNT; i++) { + + bool success = xTaskCreate(async_req_worker_task, "async_req_worker", + ASYNC_WORKER_TASK_STACK_SIZE, // stack size + (void *)0, // argument + ASYNC_WORKER_TASK_PRIORITY, // priority + &worker_handles[i]); + + if (!success) { + ESP_LOGE(PH_TAG, "Failed to start asyncReqWorker"); + continue; + } + } +} + +/**** + * + * This code is backported from the 5.1.x branch + * +****/ + +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + +/* Calculate the maximum size needed for the scratch buffer */ +#define HTTPD_SCRATCH_BUF MAX(HTTPD_MAX_REQ_HDR_LEN, HTTPD_MAX_URI_LEN) + +/** + * @brief Auxiliary data structure for use during reception and processing + * of requests and temporarily keeping responses + */ +struct httpd_req_aux { + struct sock_db *sd; /*!< Pointer to socket database */ + char scratch[HTTPD_SCRATCH_BUF + 1]; /*!< Temporary buffer for our operations (1 byte extra for null termination) */ + size_t remaining_len; /*!< Amount of data remaining to be fetched */ + char *status; /*!< HTTP response's status code */ + char *content_type; /*!< HTTP response's content type */ + bool first_chunk_sent; /*!< Used to indicate if first chunk sent */ + unsigned req_hdrs_count; /*!< Count of total headers in request packet */ + unsigned resp_hdrs_count; /*!< Count of additional headers in response packet */ + struct resp_hdr { + const char *field; + const char *value; + } *resp_hdrs; /*!< Additional headers in response packet */ + struct http_parser_url url_parse_res; /*!< URL parsing result, used for retrieving URL elements */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + bool ws_handshake_detect; /*!< WebSocket handshake detection flag */ + httpd_ws_type_t ws_type; /*!< WebSocket frame type */ + bool ws_final; /*!< WebSocket FIN bit (final frame or not) */ + uint8_t mask_key[4]; /*!< WebSocket mask key for this payload */ +#endif +}; + +esp_err_t httpd_req_async_handler_begin(httpd_req_t *r, httpd_req_t **out) +{ + if (r == NULL || out == NULL) { + return ESP_ERR_INVALID_ARG; + } + + // alloc async req + httpd_req_t *async = (httpd_req_t *)malloc(sizeof(httpd_req_t)); + if (async == NULL) { + return ESP_ERR_NO_MEM; + } + memcpy((void *)async, (void *)r, sizeof(httpd_req_t)); + + // alloc async aux + async->aux = (httpd_req_aux *)malloc(sizeof(struct httpd_req_aux)); + if (async->aux == NULL) { + free(async); + return ESP_ERR_NO_MEM; + } + memcpy(async->aux, r->aux, sizeof(struct httpd_req_aux)); + + // not available in 4.4.x + // mark socket as "in use" + // struct httpd_req_aux *ra = r->aux; + //ra->sd->for_async_req = true; + + *out = async; + + return ESP_OK; +} + +esp_err_t httpd_req_async_handler_complete(httpd_req_t *r) +{ + if (r == NULL) { + return ESP_ERR_INVALID_ARG; + } + + // not available in 4.4.x + // struct httpd_req_aux *ra = (httpd_req_aux *)r->aux; + // ra->sd->for_async_req = false; + + free(r->aux); + free(r); + + return ESP_OK; +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/async_worker.h b/lib/PsychicHttp/src/async_worker.h new file mode 100644 index 0000000..e77c4a6 --- /dev/null +++ b/lib/PsychicHttp/src/async_worker.h @@ -0,0 +1,36 @@ +#ifndef async_worker_h +#define async_worker_h + +#include "PsychicCore.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#define ASYNC_WORKER_TASK_PRIORITY 5 +#define ASYNC_WORKER_TASK_STACK_SIZE (4*1024) +#define ASYNC_WORKER_COUNT 8 + +// Async requests are queued here while they wait to be processed by the workers +static QueueHandle_t async_req_queue; + +// Track the number of free workers at any given time +static SemaphoreHandle_t worker_ready_count; + +// Each worker has its own thread +static TaskHandle_t worker_handles[ASYNC_WORKER_COUNT]; + +typedef esp_err_t (*httpd_req_handler_t)(httpd_req_t *req); + +typedef struct { + httpd_req_t* req; + httpd_req_handler_t handler; +} httpd_async_req_t; + +bool is_on_async_worker_thread(void); +esp_err_t submit_async_req(httpd_req_t *req, httpd_req_handler_t handler); +void async_req_worker_task(void *p); +void start_async_req_workers(void); + +esp_err_t httpd_req_async_handler_begin(httpd_req_t *r, httpd_req_t **out); +esp_err_t httpd_req_async_handler_complete(httpd_req_t *r); + +#endif //async_worker_h \ No newline at end of file diff --git a/lib/PsychicHttp/src/http_status.cpp b/lib/PsychicHttp/src/http_status.cpp new file mode 100644 index 0000000..64e0c07 --- /dev/null +++ b/lib/PsychicHttp/src/http_status.cpp @@ -0,0 +1,194 @@ +#include "http_status.h" + +bool http_informational(int code) +{ + return code >= 100 && code < 200; +} + +bool http_success(int code) +{ + return code >= 200 && code < 300; +} + +bool http_redirection(int code) +{ + return code >= 300 && code < 400; +} + +bool http_client_error(int code) +{ + return code >= 400 && code < 500; +} + +bool http_server_error(int code) +{ + return code >= 500 && code < 600; +} + +bool http_failure(int code) +{ + return code >= 400 && code < 600; +} + +const char *http_status_group(int code) +{ + if (http_informational(code)) + return "Informational"; + + if (http_success(code)) + return "Success"; + + if (http_redirection(code)) + return "Redirection"; + + if (http_client_error(code)) + return "Client Error"; + + if (http_server_error(code)) + return "Server Error"; + + return "Unknown"; +} + +const char *http_status_reason(int code) +{ + switch (code) + { + /*####### 1xx - Informational #######*/ + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 102: + return "Processing"; + case 103: + return "Early Hints"; + + /*####### 2xx - Successful #######*/ + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authoritative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 207: + return "Multi-Status"; + case 208: + return "Already Reported"; + case 226: + return "IM Used"; + + /*####### 3xx - Redirection #######*/ + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + case 307: + return "Temporary Redirect"; + case 308: + return "Permanent Redirect"; + + /*####### 4xx - Client Error #######*/ + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Content Too Large"; + case 414: + return "URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 418: + return "I'm a teapot"; + case 421: + return "Misdirected Request"; + case 422: + return "Unprocessable Content"; + case 423: + return "Locked"; + case 424: + return "Failed Dependency"; + case 425: + return "Too Early"; + case 426: + return "Upgrade Required"; + case 428: + return "Precondition Required"; + case 429: + return "Too Many Requests"; + case 431: + return "Request Header Fields Too Large"; + case 451: + return "Unavailable For Legal Reasons"; + + /*####### 5xx - Server Error #######*/ + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + case 506: + return "Variant Also Negotiates"; + case 507: + return "Insufficient Storage"; + case 508: + return "Loop Detected"; + case 510: + return "Not Extended"; + case 511: + return "Network Authentication Required"; + + default: + return "Unknown"; + } +} \ No newline at end of file diff --git a/lib/PsychicHttp/src/http_status.h b/lib/PsychicHttp/src/http_status.h new file mode 100644 index 0000000..e03b735 --- /dev/null +++ b/lib/PsychicHttp/src/http_status.h @@ -0,0 +1,15 @@ +#ifndef MICRO_HTTP_STATUS_H +#define MICRO_HTTP_STATUS_H + +#include + +bool http_informational(int code); +bool http_success(int code); +bool http_redirection(int code); +bool http_client_error(int code); +bool http_server_error(int code); +bool http_failure(int code); +const char *http_status_group(int code); +const char *http_status_reason(int code); + +#endif // MICRO_HTTP_STATUS_H \ No newline at end of file From 89393cee586bab2ce832bbcf335cacdd7fdb3cee Mon Sep 17 00:00:00 2001 From: iranl Date: Mon, 26 Aug 2024 23:41:54 +0200 Subject: [PATCH 02/30] PsychicHTTP --- lib/MqttLogger/src/MqttLogger.cpp | 4 +- lib/MqttLogger/src/MqttLogger.h | 2 +- lib/PsychicHttp/src/PsychicHttpServer.cpp | 8 + lib/PsychicHttp/src/PsychicRequest.cpp | 17 + lib/PsychicHttp/src/PsychicRequest.h | 4 +- lib/WiFiManager/WiFiManager.cpp | 225 +- lib/WiFiManager/WiFiManager.h | 44 +- sdkconfig.defaults | 11 +- src/Config.h | 2 +- src/NukiNetwork.h | 2 - src/NukiNetworkLock.h | 2 - src/WebCfgServer.cpp | 2513 +++++++++++---------- src/WebCfgServer.h | 85 +- src/main.cpp | 33 +- updater/platformio.ini | 6 +- updater/sdkconfig.defaults | 11 +- 16 files changed, 1522 insertions(+), 1447 deletions(-) diff --git a/lib/MqttLogger/src/MqttLogger.cpp b/lib/MqttLogger/src/MqttLogger.cpp index d1b1c71..d8d08f7 100644 --- a/lib/MqttLogger/src/MqttLogger.cpp +++ b/lib/MqttLogger/src/MqttLogger.cpp @@ -90,8 +90,8 @@ void MqttLogger::sendBuffer() } if (doWebSerial) { - WebSerial.write(this->buffer, this->bufferCnt); - WebSerial.println(); + //WebSerial.write(this->buffer, this->bufferCnt); + //WebSerial.println(); } this->bufferCnt=0; } diff --git a/lib/MqttLogger/src/MqttLogger.h b/lib/MqttLogger/src/MqttLogger.h index 6f30a6f..d82e62a 100644 --- a/lib/MqttLogger/src/MqttLogger.h +++ b/lib/MqttLogger/src/MqttLogger.h @@ -12,7 +12,7 @@ #include #include #include -#include "MycilaWebSerial.h" +//#include "MycilaWebSerial.h" #define MQTT_MAX_PACKET_SIZE 1024 diff --git a/lib/PsychicHttp/src/PsychicHttpServer.cpp b/lib/PsychicHttp/src/PsychicHttpServer.cpp index 628f38b..f6cbb7f 100644 --- a/lib/PsychicHttp/src/PsychicHttpServer.cpp +++ b/lib/PsychicHttp/src/PsychicHttpServer.cpp @@ -325,11 +325,19 @@ const std::list& PsychicHttpServer::getClientList() { } bool ON_STA_FILTER(PsychicRequest *request) { + #ifndef CONFIG_IDF_TARGET_ESP32H2 return WiFi.localIP() == request->client()->localIP(); + #else + return false; + #endif } bool ON_AP_FILTER(PsychicRequest *request) { + #ifndef CONFIG_IDF_TARGET_ESP32H2 return WiFi.softAPIP() == request->client()->localIP(); + #else + return false; + #endif } String urlDecode(const char* encoded) diff --git a/lib/PsychicHttp/src/PsychicRequest.cpp b/lib/PsychicHttp/src/PsychicRequest.cpp index 4244358..2005d91 100644 --- a/lib/PsychicHttp/src/PsychicRequest.cpp +++ b/lib/PsychicHttp/src/PsychicRequest.cpp @@ -318,6 +318,11 @@ PsychicWebParameter * PsychicRequest::addParam(PsychicWebParameter *param) { return param; } +int PsychicRequest::params() +{ + return _params.size(); +} + bool PsychicRequest::hasParam(const char *key) { return getParam(key) != NULL; @@ -332,6 +337,18 @@ PsychicWebParameter * PsychicRequest::getParam(const char *key) return NULL; } +PsychicWebParameter * PsychicRequest::getParam(int index) +{ + if (_params.size() > index){ + std::list::iterator it = _params.begin(); + for(int i=0; i_session->find(key) != this->_session->end(); diff --git a/lib/PsychicHttp/src/PsychicRequest.h b/lib/PsychicHttp/src/PsychicRequest.h index fe48a1b..5b54d6e 100644 --- a/lib/PsychicHttp/src/PsychicRequest.h +++ b/lib/PsychicHttp/src/PsychicRequest.h @@ -81,9 +81,11 @@ class PsychicRequest { void loadParams(); PsychicWebParameter * addParam(PsychicWebParameter *param); PsychicWebParameter * addParam(const String &name, const String &value, bool decode = true, bool post = false); + int params(); bool hasParam(const char *key); PsychicWebParameter * getParam(const char *name); - + PsychicWebParameter * getParam(int index); + const String getFilename(); bool authenticate(const char * username, const char * password); diff --git a/lib/WiFiManager/WiFiManager.cpp b/lib/WiFiManager/WiFiManager.cpp index d7aa82c..34c6868 100644 --- a/lib/WiFiManager/WiFiManager.cpp +++ b/lib/WiFiManager/WiFiManager.cpp @@ -627,30 +627,28 @@ boolean WiFiManager::configPortalHasTimeout(){ } void WiFiManager::setupHTTPServer(){ - - server.reset(new WM_WebServer(_httpPort)); + server->listen(_httpPort); /* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */ - server->on(String(FPSTR(R_wifi)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,true)); - server->on(String(FPSTR(R_wifinoscan)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,false)); - server->on(String(FPSTR(R_erase)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleErase, this,std::placeholders::_1,false)); + server->on(String(FPSTR(R_wifi)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,true)); + server->on(String(FPSTR(R_wifinoscan)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,false)); + server->on(String(FPSTR(R_erase)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleErase, this,std::placeholders::_1,false)); { using namespace std::placeholders; - server->on(String(FPSTR(R_root)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleRoot, this,_1)); - server->on(String(FPSTR(R_wifisave)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWifiSave, this,_1)); - server->on(String(FPSTR(R_info)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleInfo, this,_1)); - server->on(String(FPSTR(R_param)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleParam, this,_1)); - server->on(String(FPSTR(R_paramsave)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleParamSave, this,_1)); - server->on(String(FPSTR(R_restart)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleReset, this,_1)); - server->on(String(FPSTR(R_exit)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleExit, this,_1)); - server->on(String(FPSTR(R_close)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleClose, this,_1)); - server->on(String(FPSTR(R_status)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWiFiStatus, this,_1)); - server->onNotFound (std::bind(&WiFiManager::handleNotFound, this, _1)); + server->on(String(FPSTR(R_root)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleRoot, this,_1)); + server->on(String(FPSTR(R_wifisave)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifiSave, this,_1)); + server->on(String(FPSTR(R_info)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleInfo, this,_1)); + server->on(String(FPSTR(R_param)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParam, this,_1)); + server->on(String(FPSTR(R_paramsave)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParamSave, this,_1)); + server->on(String(FPSTR(R_restart)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleReset, this,_1)); + server->on(String(FPSTR(R_exit)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleExit, this,_1)); + server->on(String(FPSTR(R_close)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleClose, this,_1)); + server->on(String(FPSTR(R_status)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWiFiStatus, this,_1)); + server->onNotFound ((PsychicHttpRequestCallback)std::bind(&WiFiManager::handleNotFound, this, _1)); - server->on(String(FPSTR(R_update)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleUpdate, this, _1)); - server->on(String(FPSTR(R_updatedone)).c_str(), HTTP_POST,std::bind(&WiFiManager::handleUpdateDone, this, _1), std::bind(&WiFiManager::handleUpdating, this, _1,_2,_3,_4,_5,_6)); + server->on(String(FPSTR(R_update)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleUpdate, this, _1)); + //server->on(String(FPSTR(R_updatedone)).c_str(), HTTP_POST, std::bind(&WiFiManager::handleUpdateDone, this, _1), std::bind(&WiFiManager::handleUpdating, this, _1,_2,_3,_4,_5,_6)); } - server->begin(); // Web server start } void WiFiManager::teardownHTTPServer(){ @@ -979,7 +977,7 @@ bool WiFiManager::shutdownConfigPortal(){ // @todo what is the proper way to shutdown and free the server up // debug - many open issues aobut port not clearing for use with other servers #ifdef WM_ASYNCWEBSERVER - server->end(); + server->stop(); #else server->stop(); #endif @@ -1423,10 +1421,13 @@ String WiFiManager::getHTTPHead(String title){ return page; } -void WiFiManager::HTTPSend(AsyncWebServerRequest *request, String page){ - AsyncWebServerResponse *response = request->beginResponse(200,FPSTR(HTTP_HEAD_CT), page); - response->addHeader(FPSTR(HTTP_HEAD_CL), String(page.length())); - request->send(response); +esp_err_t WiFiManager::HTTPSend(PsychicRequest *request, String page){ + PsychicResponse response(request); + response.addHeader(HTTP_HEAD_CL, ((String)page.length()).c_str()); + response.setCode(200); + response.setContentType(HTTP_HEAD_CT); + response.setContent(page.c_str()); + return response.send(); } /** @@ -1446,12 +1447,12 @@ void WiFiManager::handleRequest() { /** * HTTPD CALLBACK root or redirect to captive portal */ -void WiFiManager::handleRoot(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleRoot(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Root")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page handleRequest(); String page = getHTTPHead(_title); // @token options @todo replace options with title String str = FPSTR(HTTP_ROOT_MAIN); // @todo custom title @@ -1463,28 +1464,29 @@ void WiFiManager::handleRoot(AsyncWebServerRequest *request) { reportStatus(page); page += FPSTR(HTTP_END); - HTTPSend(request,page); if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime,true); // preload wifiscan throttled, async // @todo buggy, captive portals make a query on every page load, causing this to run every time in addition to the real page load // I dont understand why, when you are already in the captive portal, I guess they want to know that its still up and not done or gone // if we can detect these and ignore them that would be great, since they come from the captive portal redirect maybe there is a refferer + + return HTTPSend(request,page); } /** * HTTPD CALLBACK Wifi config page handler */ -void WiFiManager::handleWifi(AsyncWebServerRequest *request,bool scan = true) { +esp_err_t WiFiManager::handleWifi(PsychicRequest *request,bool scan = true) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Wifi")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titlewifi)); // @token titlewifi if (scan) { #ifdef WM_DEBUG_LEVEL // DEBUG_WM(WM_DEBUG_DEV,"refresh flag:",request->hasArg(F("refresh"))); #endif - WiFi_scanNetworks(request->hasArg(F("refresh")),true); //wifiscan, force if arg refresh + WiFi_scanNetworks(request->hasParam("refresh")); //wifiscan, force if arg refresh page += getScanItemOut(); } String pitem = ""; @@ -1520,21 +1522,21 @@ void WiFiManager::handleWifi(AsyncWebServerRequest *request,bool scan = true) { reportStatus(page); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent config page")); #endif + + return HTTPSend(request,page); } /** * HTTPD CALLBACK Wifi param page handler */ -void WiFiManager::handleParam(AsyncWebServerRequest *request){ +esp_err_t WiFiManager::handleParam(PsychicRequest *request){ #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Param")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titleparam)); // @token titlewifi @@ -1550,11 +1552,11 @@ void WiFiManager::handleParam(AsyncWebServerRequest *request){ reportStatus(page); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent param page")); #endif + + return HTTPSend(request,page); } @@ -1914,34 +1916,35 @@ String WiFiManager::getParamOut(){ return page; } -void WiFiManager::handleWiFiStatus(AsyncWebServerRequest *request){ +esp_err_t WiFiManager::handleWiFiStatus(PsychicRequest *request){ #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi status ")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page; // String page = "{\"result\":true,\"count\":1}"; #ifdef WM_JSTEST page = FPSTR(HTTP_JS); #endif - HTTPSend(request,page); + + return HTTPSend(request,page); } /** * HTTPD CALLBACK save form and redirect to WLAN config page again */ -void WiFiManager::handleWifiSave(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleWifiSave(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi save ")); DEBUG_WM(WM_DEBUG_DEV,F("Method:"),request->method() == HTTP_GET ? (String)FPSTR(S_GET) : (String)FPSTR(S_POST)); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); //SAVE/connect here - _ssid = request->arg(F("s")).c_str(); - _pass = request->arg(F("p")).c_str(); + _ssid = request->getParam("s")->value(); + _pass = request->getParam("p")->value(); if(_ssid == "" && _pass != ""){ _ssid = WiFi_SSID(true); // password change, placeholder ssid, @todo compare pass to old?, confirm ssid is clean @@ -1957,40 +1960,40 @@ void WiFiManager::handleWifiSave(AsyncWebServerRequest *request) { requestinfo += "\nMethod: "; requestinfo += (request->method() == HTTP_GET) ? "GET" : "POST"; requestinfo += "\nArguments: "; - requestinfo += request->args(); + requestinfo += request->params(); requestinfo += "\n"; - for (uint8_t i = 0; i < request->args(); i++) { - requestinfo += " " + request->argName(i) + ": " + request->arg(i) + "\n"; + for (uint8_t i = 0; i < request->params(); i++) { + requestinfo += " " + request->getParam(i)->name() + ": " + request->getParam(i)->value() + "\n"; } DEBUG_WM(WM_DEBUG_MAX,requestinfo); #endif // set static ips from server args - if (request->arg(FPSTR(S_ip)) != "") { + if (request->getParam(S_ip)->value() != "") { //_sta_static_ip.fromString(request->arg(FPSTR(S_ip)); - String ip = request->arg(FPSTR(S_ip)); + String ip = request->getParam(S_ip)->value(); optionalIPFromString(&_sta_static_ip, ip.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static ip:"),ip); #endif } - if (request->arg(FPSTR(S_gw)) != "") { - String gw = request->arg(FPSTR(S_gw)); + if (request->getParam(S_gw)->value() != "") { + String gw = request->getParam(S_gw)->value(); optionalIPFromString(&_sta_static_gw, gw.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static gateway:"),gw); #endif } - if (request->arg(FPSTR(S_sn)) != "") { - String sn = request->arg(FPSTR(S_sn)); + if (request->getParam(S_sn)->value() != "") { + String sn = request->getParam(S_sn)->value(); optionalIPFromString(&_sta_static_sn, sn.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static netmask:"),sn); #endif } - if (request->arg(FPSTR(S_dns)) != "") { - String dns = request->arg(FPSTR(S_dns)); + if (request->getParam(S_dns)->value() != "") { + String dns = request->getParam(S_dns)->value(); optionalIPFromString(&_sta_static_dns, dns.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static DNS:"),dns); @@ -2018,16 +2021,17 @@ void WiFiManager::handleWifiSave(AsyncWebServerRequest *request) { page += FPSTR(HTTP_END); //server->sendHeader(FPSTR(HTTP_HEAD_CORS), FPSTR(HTTP_HEAD_CORS_ALLOW_ALL)); // @HTTPHEAD send cors - HTTPSend(request,page); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent wifi save page")); #endif connect = true; //signal ready to connect/reset process in processConfigPortal + + return HTTPSend(request,page); } -void WiFiManager::handleParamSave(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleParamSave(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Param save ")); @@ -2035,7 +2039,7 @@ void WiFiManager::handleParamSave(AsyncWebServerRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Method:"),request->method() == HTTP_GET ? (String)FPSTR(S_GET) : (String)FPSTR(S_POST)); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); doParamSave(request); @@ -2045,14 +2049,14 @@ void WiFiManager::handleParamSave(AsyncWebServerRequest *request) { if(_showBack) page += FPSTR(HTTP_BACKBTN); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent param save page")); #endif + + return HTTPSend(request,page); } -void WiFiManager::doParamSave(AsyncWebServerRequest *request){ +void WiFiManager::doParamSave(PsychicRequest *request){ // @todo use new callback for before paramsaves, is this really needed? if ( _presaveparamscallback != NULL) { _presaveparamscallback(); // @CALLBACK @@ -2075,10 +2079,10 @@ void WiFiManager::doParamSave(AsyncWebServerRequest *request){ //read parameter from server String name = (String)FPSTR(S_parampre)+(String)i; String value; - if(request->hasArg(name.c_str())) { - value = request->arg(name); + if(request->hasParam(name.c_str())) { + value = request->getParam(name.c_str())->value(); } else { - value = request->arg(_params[i]->getID()); + value = request->getParam(_params[i]->getID())->value(); } //store it in params array @@ -2101,11 +2105,11 @@ void WiFiManager::doParamSave(AsyncWebServerRequest *request){ /** * HTTPD CALLBACK info page */ -void WiFiManager::handleInfo(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleInfo(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Info")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); //handleRequest(); String page = getHTTPHead(FPSTR(S_titleinfo)); // @token titleinfo reportStatus(page); @@ -2203,11 +2207,11 @@ void WiFiManager::handleInfo(AsyncWebServerRequest *request) { page += FPSTR(HTTP_HELP); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent info page")); #endif + + return HTTPSend(request,page); } String WiFiManager::getInfoData(String id){ @@ -2447,41 +2451,45 @@ String WiFiManager::getInfoData(String id){ /** * HTTPD CALLBACK exit, closes configportal if blocking, if non blocking undefined */ -void WiFiManager::handleExit(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleExit(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Exit")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titleexit)); // @token titleexit page += FPSTR(S_exiting); // @token exiting // ('Logout', 401, {'WWW-Authenticate': 'Basic realm="Login required"'}) - AsyncWebServerResponse *response = request->beginResponse(200,FPSTR(HTTP_HEAD_CT), page); - response->addHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); - request->send(response); delay(2000); abort = true; + + PsychicResponse response(request); + response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setCode(200); + response.setContentType(HTTP_HEAD_CT); + response.setContent(page.c_str()); + return response.send(); } /** * HTTPD CALLBACK reset page */ -void WiFiManager::handleReset(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleReset(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Reset")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titlereset)); //@token titlereset page += FPSTR(S_resetting); //@token resetting page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(F("RESETTING ESP")); #endif _rebootNeeded = true; + + return HTTPSend(request,page); } /** @@ -2491,11 +2499,11 @@ void WiFiManager::handleReset(AsyncWebServerRequest *request) { // void WiFiManager::handleErase() { // handleErase(false); // } -void WiFiManager::handleErase(AsyncWebServerRequest *request,bool opt = false) { +esp_err_t WiFiManager::handleErase(PsychicRequest *request,bool opt = false) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_NOTIFY,F("<- HTTP Erase")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titleerase)); // @token titleerase @@ -2510,18 +2518,19 @@ void WiFiManager::handleErase(AsyncWebServerRequest *request,bool opt = false) { } page += FPSTR(HTTP_END); - HTTPSend(request,page); if(ret){ _rebootNeeded = true; } + + return HTTPSend(request,page); } /** * HTTPD CALLBACK 404 */ -void WiFiManager::handleNotFound(AsyncWebServerRequest *request) { - if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page +esp_err_t WiFiManager::handleNotFound(PsychicRequest *request) { + if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page handleRequest(); String message = FPSTR(S_notfound); // @token notfound @@ -2532,18 +2541,22 @@ void WiFiManager::handleNotFound(AsyncWebServerRequest *request) { message += FPSTR(S_method); // @token method message += ( request->method() == HTTP_GET ) ? FPSTR(S_GET) : FPSTR(S_POST); message += FPSTR(S_args); // @token args - message += request->args(); + message += request->params(); message += F("\n"); - for ( uint8_t i = 0; i < request->args(); i++ ) { - message += " " + request->argName ( i ) + ": " + request->arg ( i ) + "\n"; + for ( uint8_t i = 0; i < request->params(); i++ ) { + message += " " + request->getParam(i)->name() + ": " + request->getParam(i)->value() + "\n"; } } - AsyncWebServerResponse *response = request->beginResponse(404,FPSTR(HTTP_HEAD_CT2), message); - response->addHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); - response->addHeader(F("Pragma"), F("no-cache")); - response->addHeader(F("Expires"), F("-1")); - request->send(response); + + PsychicResponse response(request); + response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.addHeader("Pragma", "no-cache"); + response.addHeader("Expires", "-1"); + response.setCode(404); + response.setContentType(HTTP_HEAD_CT2); + response.setContent(message.c_str()); + return response.send(); } /** @@ -2551,7 +2564,7 @@ void WiFiManager::handleNotFound(AsyncWebServerRequest *request) { * Redirect to captive portal if we got a request for another domain. * Return true in that case so the page handler do not try to handle the request again. */ -boolean WiFiManager::captivePortal(AsyncWebServerRequest *request) { +boolean WiFiManager::captivePortal(PsychicRequest *request) { if(!_enableCaptivePortal || !configPortalActive) return false; // skip redirections if cp not enabled or not in ap mode @@ -2578,9 +2591,11 @@ boolean WiFiManager::captivePortal(AsyncWebServerRequest *request) { DEBUG_WM(WM_DEBUG_VERBOSE,F("<- Request redirected to captive portal")); DEBUG_WM(WM_DEBUG_DEV,"serverLoc " + serverLoc); #endif - AsyncWebServerResponse *response = request->beginResponse(302,FPSTR(HTTP_HEAD_CT2), ""); - response->addHeader(F("Location"), (String)F("http://") + serverLoc); - request->send(response); + PsychicResponse response(request); + response.addHeader("Location", ((String)("http://") + serverLoc).c_str()); + response.setCode(302); + response.setContentType(HTTP_HEAD_CT2); + response.send(); return true; } return false; @@ -2592,9 +2607,9 @@ void WiFiManager::stopCaptivePortal(){ } // HTTPD CALLBACK, handle close, stop captive portal, if not enabled undefined -void WiFiManager::handleClose(AsyncWebServerRequest *request){ +esp_err_t WiFiManager::handleClose(PsychicRequest *request){ DEBUG_WM(WM_DEBUG_VERBOSE,F("Disabling Captive Portal")); - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); stopCaptivePortal(); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP close")); @@ -2602,7 +2617,7 @@ void WiFiManager::handleClose(AsyncWebServerRequest *request){ handleRequest(); String page = getHTTPHead(FPSTR(S_titleclose)); // @token titleclose page += FPSTR(S_closing); // @token closing - HTTPSend(request,page); + return HTTPSend(request,page); } void WiFiManager::reportStatus(String &page){ @@ -4011,12 +4026,12 @@ void WiFiManager::WiFi_autoReconnect(){ } // Called when /update is requested -void WiFiManager::handleUpdate(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleUpdate(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- Handle update")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page String page = getHTTPHead(_title); // @token options String str = FPSTR(HTTP_ROOT_MAIN); str.replace(FPSTR(T_t), _title); @@ -4026,12 +4041,12 @@ void WiFiManager::handleUpdate(AsyncWebServerRequest *request) { page += FPSTR(HTTP_UPDATE); page += FPSTR(HTTP_END); - HTTPSend(request,page); + return HTTPSend(request,page); } // upload via /u POST -void WiFiManager::handleUpdating(AsyncWebServerRequest *request,String filename, size_t index, uint8_t *data, size_t len, bool final){ +void WiFiManager::handleUpdating(String filename, size_t index, uint8_t *data, size_t len, bool final){ // @todo // cannot upload files in captive portal, file select is not allowed, show message with link or hide // cannot upload if softreset after upload, maybe check for hard reset at least for dev, ERROR[11]: Invalid bootstrapping state, reset ESP8266 before updating @@ -4123,10 +4138,10 @@ void WiFiManager::handleUpdating(AsyncWebServerRequest *request,String filename, } // upload and ota done, show status -void WiFiManager::handleUpdateDone(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleUpdateDone(PsychicRequest *request) { DEBUG_WM(WM_DEBUG_VERBOSE, F("<- Handle update done")); // if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); String page = getHTTPHead(FPSTR(S_options)); // @token options String str = FPSTR(HTTP_ROOT_MAIN); str.replace(FPSTR(T_t),_title); @@ -4148,13 +4163,13 @@ void WiFiManager::handleUpdateDone(AsyncWebServerRequest *request) { } page += FPSTR(HTTP_END); - HTTPSend(request,page); - // delay(1000); // send page if (!Update.hasError()) { //ESP.restart(); _rebootNeeded = true; } + + return HTTPSend(request,page); } #endif diff --git a/lib/WiFiManager/WiFiManager.h b/lib/WiFiManager/WiFiManager.h index 9f0eac5..38f9bfb 100644 --- a/lib/WiFiManager/WiFiManager.h +++ b/lib/WiFiManager/WiFiManager.h @@ -97,8 +97,8 @@ #define WM_WIFIOPEN WIFI_AUTH_OPEN #ifdef WM_ASYNCWEBSERVER - #include - #include + #include + #include #else #ifndef WEBSERVER_H #ifdef WM_WEBSERVERSHIM @@ -521,7 +521,7 @@ class WiFiManager #if defined(ESP32) && defined(WM_WEBSERVERSHIM) #ifdef WM_ASYNCWEBSERVER - using WM_WebServer = AsyncWebServer; + using WM_WebServer = PsychicHttpServer; #else using WM_WebServer = WebServer; #endif @@ -694,32 +694,32 @@ public: bool WiFi_scanNetworks(bool force,bool async); protected: // webserver handlers - void handleRoot(AsyncWebServerRequest *request); - void handleWifi(AsyncWebServerRequest *request,bool scan); - void handleWifiSave(AsyncWebServerRequest *request); - void handleInfo(AsyncWebServerRequest *request); - void handleReset(AsyncWebServerRequest *request); - void handleNotFound(AsyncWebServerRequest *request); - void handleExit(AsyncWebServerRequest *request); - void handleClose(AsyncWebServerRequest *request); - // void handleErase(AsyncWebServerRequest *request); - void handleErase(AsyncWebServerRequest *request, bool opt); - void handleParam(AsyncWebServerRequest *request); - void handleWiFiStatus(AsyncWebServerRequest *request); - void handleParamSave(AsyncWebServerRequest *request); - void doParamSave(AsyncWebServerRequest *request); + esp_err_t handleRoot(PsychicRequest *request); + esp_err_t handleWifi(PsychicRequest *request,bool scan); + esp_err_t handleWifiSave(PsychicRequest *request); + esp_err_t handleInfo(PsychicRequest *request); + esp_err_t handleReset(PsychicRequest *request); + esp_err_t handleNotFound(PsychicRequest *request); + esp_err_t handleExit(PsychicRequest *request); + esp_err_t handleClose(PsychicRequest *request); + // esp_err_t handleErase(PsychicRequest *request); + esp_err_t handleErase(PsychicRequest *request, bool opt); + esp_err_t handleParam(PsychicRequest *request); + esp_err_t handleWiFiStatus(PsychicRequest *request); + esp_err_t handleParamSave(PsychicRequest *request); + void doParamSave(PsychicRequest *request); void handleRequest(); - void HTTPSend(AsyncWebServerRequest *request, String page); + esp_err_t HTTPSend(PsychicRequest *request, String page); - boolean captivePortal(AsyncWebServerRequest *request); + boolean captivePortal(PsychicRequest *request); boolean configPortalHasTimeout(); uint8_t processConfigPortal(); void stopCaptivePortal(); // OTA Update handler - void handleUpdate(AsyncWebServerRequest *request); - void handleUpdating(AsyncWebServerRequest *request,String filename, size_t index, uint8_t *data, size_t len, bool final); - void handleUpdateDone(AsyncWebServerRequest *request); + esp_err_t handleUpdate(PsychicRequest *request); + void handleUpdating(String filename, size_t index, uint8_t *data, size_t len, bool final); + esp_err_t handleUpdateDone(PsychicRequest *request); // wifi platform abstractions bool WiFi_Mode(WiFiMode_t m); diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 780d167..89c8474 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -82,4 +82,13 @@ CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE=y CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y -CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/github_root_ca.pem" \ No newline at end of file +CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/github_root_ca.pem" +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +CONFIG_HTTPD_WS_SUPPORT=y +CONFIG_ESP_HTTPS_SERVER_ENABLE=y \ No newline at end of file diff --git a/src/Config.h b/src/Config.h index d621c64..3bd746c 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.01" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-08-18" +#define NUKI_HUB_DATE "2024-08-26" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index 65d2921..afc5ca8 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -16,8 +16,6 @@ #include "NukiConstants.h" #endif -#define JSON_BUFFER_SIZE 1024 - class NukiNetwork { public: diff --git a/src/NukiNetworkLock.h b/src/NukiNetworkLock.h index 21f921c..d069ccf 100644 --- a/src/NukiNetworkLock.h +++ b/src/NukiNetworkLock.h @@ -13,8 +13,6 @@ #include "QueryCommand.h" #include "LockActionResult.h" -#define LOCK_LOG_JSON_BUFFER_SIZE 2048 - class NukiNetworkLock : public MqttReceiver { public: diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index a595508..a6dc65d 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -17,7 +17,7 @@ extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_ #include #include "ArduinoJson.h" -WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer) +WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) : _nuki(nuki), _nukiOpener(nukiOpener), _network(network), @@ -25,14 +25,14 @@ WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Nuk _preferences(preferences), _allowRestartToPortal(allowRestartToPortal), _partitionType(partitionType), - _asyncServer(asyncServer) + _psychicServer(psychicServer) #else -WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer) +WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) : _network(network), _preferences(preferences), _allowRestartToPortal(allowRestartToPortal), _partitionType(partitionType), - _asyncServer(asyncServer) + _psychicServer(psychicServer) #endif { _hostname = _preferences->getString(preference_hostname, ""); @@ -72,206 +72,201 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool void WebCfgServer::initialize() { - _response.reserve(8192); - - _asyncServer->on("/", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); #ifndef NUKI_HUB_UPDATER - buildHtml(request); + return buildHtml(request); #else - buildOtaHtml(request); + return buildOtaHtml(request); #endif }); - _asyncServer->on("/style.css", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - sendCss(request); + _psychicServer->on("/style.css", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return sendCss(request); }); - _asyncServer->on("/favicon.ico", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - sendFavicon(request); + _psychicServer->on("/favicon.ico", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return sendFavicon(request); }); #ifndef NUKI_HUB_UPDATER - _asyncServer->on("/import", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/import", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); String message = ""; bool restart = processImport(request, message); - buildConfirmHtml(request, message, 3, true); + return buildConfirmHtml(request, message, 3, true); }); - _asyncServer->on("/export", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - sendSettings(request); + _psychicServer->on("/export", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return sendSettings(request); }); - _asyncServer->on("/impexpcfg", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildImportExportHtml(request); + _psychicServer->on("/impexpcfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildImportExportHtml(request); }); - _asyncServer->on("/status", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildStatusHtml(request); + _psychicServer->on("/status", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildStatusHtml(request); }); - _asyncServer->on("/acclvl", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildAccLvlHtml(request); + _psychicServer->on("/acclvl", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildAccLvlHtml(request); }); - _asyncServer->on("/custntw", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildCustomNetworkConfigHtml(request); + _psychicServer->on("/custntw", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildCustomNetworkConfigHtml(request); }); - _asyncServer->on("/advanced", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildAdvancedConfigHtml(request); + _psychicServer->on("/advanced", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildAdvancedConfigHtml(request); }); - _asyncServer->on("/cred", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildCredHtml(request); + _psychicServer->on("/cred", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildCredHtml(request); }); - _asyncServer->on("/mqttconfig", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildMqttConfigHtml(request); + _psychicServer->on("/mqttconfig", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildMqttConfigHtml(request); }); - _asyncServer->on("/nukicfg", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildNukiConfigHtml(request); + _psychicServer->on("/nukicfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildNukiConfigHtml(request); }); - _asyncServer->on("/gpiocfg", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildGpioConfigHtml(request); + _psychicServer->on("/gpiocfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildGpioConfigHtml(request); }); #ifndef CONFIG_IDF_TARGET_ESP32H2 - _asyncServer->on("/wifi", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildConfigureWifiHtml(request); + _psychicServer->on("/wifi", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildConfigureWifiHtml(request); }); - _asyncServer->on("/wifimanager", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/wifimanager", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); if(_allowRestartToPortal) { - buildConfirmHtml(request, "Restarting. Connect to ESP access point to reconfigure Wi-Fi.", 0); + esp_err_t res = buildConfirmHtml(request, "Restarting. Connect to ESP access point to reconfigure Wi-Fi.", 0); waitAndProcess(false, 1000); _network->reconfigureDevice(); + return res; } }); #endif - _asyncServer->on("/unpairlock", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - processUnpair(request, false); + _psychicServer->on("/unpairlock", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processUnpair(request, false); }); - _asyncServer->on("/unpairopener", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - processUnpair(request, true); + _psychicServer->on("/unpairopener", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processUnpair(request, true); }); - _asyncServer->on("/factoryreset", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - processFactoryReset(request); + _psychicServer->on("/factoryreset", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processFactoryReset(request); }); - _asyncServer->on("/info", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildInfoHtml(request); + _psychicServer->on("/info", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildInfoHtml(request); }); - _asyncServer->on("/debugon", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/debugon", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); _preferences->putBool(preference_publish_debug_info, true); - buildConfirmHtml(request, "Debug On", 3, true); + return buildConfirmHtml(request, "Debug On", 3, true); }); - _asyncServer->on("/debugoff", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/debugoff", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); _preferences->putBool(preference_publish_debug_info, false); - buildConfirmHtml(request, "Debug Off", 3, true); + return buildConfirmHtml(request, "Debug Off", 3, true); }); - _asyncServer->on("/savecfg", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/savecfg", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); String message = ""; bool restart = processArgs(request, message); - buildConfirmHtml(request, message, 3, true); + return buildConfirmHtml(request, message, 3, true); }); - _asyncServer->on("/savegpiocfg", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/savegpiocfg", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); processGpioArgs(request); - buildConfirmHtml(request, "Saving GPIO configuration. Restarting.", 3, true); + esp_err_t res = buildConfirmHtml(request, "Saving GPIO configuration. Restarting.", 3, true); Log->println(F("Restarting")); waitAndProcess(true, 1000); restartEsp(RestartReason::GpioConfigurationUpdated); + return res; }); #endif - _asyncServer->on("/ota", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildOtaHtml(request); + _psychicServer->on("/ota", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildOtaHtml(request); }); - _asyncServer->on("/otadebug", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildOtaHtml(request, true); + _psychicServer->on("/otadebug", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildOtaHtml(request, true); }); - _asyncServer->on("/reboottoota", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildConfirmHtml(request, "Rebooting to other partition", 2, true); + _psychicServer->on("/reboottoota", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + esp_err_t res = buildConfirmHtml(request, "Rebooting to other partition", 2, true); waitAndProcess(true, 1000); esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)); restartEsp(RestartReason::OTAReboot); + return res; }); - _asyncServer->on("/reboot", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildConfirmHtml(request, "Rebooting", 2, true); + _psychicServer->on("/reboot", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + esp_err_t res = buildConfirmHtml(request, "Rebooting", 2, true); waitAndProcess(true, 1000); restartEsp(RestartReason::RequestedViaWebServer); + return res; }); - _asyncServer->on("/autoupdate", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/autoupdate", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); #ifndef NUKI_HUB_UPDATER - processUpdate(request); + return processUpdate(request); #else - request->redirect("/"); + return request->redirect("/"); #endif }); - _asyncServer->on("/uploadota", HTTP_POST, - [&](AsyncWebServerRequest *request) {}, - [&](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) + + /* + _psychicServer->on("/uploadota", HTTP_POST, + [&](PsychicRequest *request) {}, + [&](PsychicRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - handleOtaUpload(request, filename, index, data, len, final); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return handleOtaUpload(request, filename, index, data, len, final); } ); + */ //Update.onProgress(printProgress); } -void WebCfgServer::sendResponse(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) { - AsyncWebServerResponse *response = request->beginChunkedResponse("text/html", - [&](uint8_t *buffer, size_t maxlen, size_t index) -> size_t { - size_t len = min(maxlen, _response.length() - index); - memcpy(buffer, _response.c_str() + index, len); - return len; - }); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); - request->send(response); -} - -void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) -{ - _response = ""; - buildHtmlHeader(); + buildHtmlHeader(&response); bool errored = false; if(request->hasParam("errored")) { - const AsyncWebParameter* p = request->getParam("errored"); + const PsychicWebParameter* p = request->getParam("errored"); if(p->value() != "") errored = true; } - if(errored) _response.concat("
Over-the-air update errored. Please check the logs for more info

"); + if(errored) response.print("
Over-the-air update errored. Please check the logs for more info

"); if(_partitionType == 0) { - _response.concat("

You are currently running Nuki Hub with an outdated partition scheme. Because of this you cannot use OTA to update to 9.00 or higher. Please check GitHub for instructions on how to update to 9.00 and the new partition scheme

"); - _response.concat(""); - return; + response.print("

You are currently running Nuki Hub with an outdated partition scheme. Because of this you cannot use OTA to update to 9.00 or higher. Please check GitHub for instructions on how to update to 9.00 and the new partition scheme

"); + response.print(""); + return response.endSend(); } - _response.concat("
Initiating Over-the-air update. This will take about two minutes, please be patient.
You will be forwarded automatically when the update is complete.
"); - _response.concat("

Update Nuki Hub

"); - _response.concat("Click on the button to reboot and automatically update Nuki Hub and the Nuki Hub updater to the latest versions from GitHub"); - _response.concat("
"); + response.print("
Initiating Over-the-air update. This will take about two minutes, please be patient.
You will be forwarded automatically when the update is complete.
"); + response.print("

Update Nuki Hub

"); + response.print("Click on the button to reboot and automatically update Nuki Hub and the Nuki Hub updater to the latest versions from GitHub"); + response.print("
"); String release_type; @@ -283,18 +278,18 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) #else String build_type = "debug"; #endif - _response.concat("

"); - _response.concat("

"); - _response.concat("

"); - _response.concat("

"); + response.print("

"); + response.print("

"); + response.print("

"); + response.print("

"); - _response.concat("Current version: "); - _response.concat(NUKI_HUB_VERSION); - _response.concat(" ("); - _response.concat(NUKI_HUB_BUILD); - _response.concat("), "); - _response.concat(NUKI_HUB_DATE); - _response.concat("
"); + response.print("Current version: "); + response.print(NUKI_HUB_VERSION); + response.print(" ("); + response.print(NUKI_HUB_BUILD); + response.print("), "); + response.print(NUKI_HUB_DATE); + response.print("
"); #ifndef NUKI_HUB_UPDATER bool manifestSuccess = false; @@ -325,39 +320,39 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) if(!manifestSuccess) { - _response.concat("currentverlatestverdevverbetaver"); + response.print("currentverlatestverdevverbetaver"); } else { - _response.concat("Latest release version: "); - _response.concat(doc["release"]["fullversion"].as()); - _response.concat(" ("); - _response.concat(doc["release"]["build"].as()); - _response.concat("), "); - _response.concat(doc["release"]["time"].as()); - _response.concat("
"); - _response.concat("Latest beta version: "); + response.print("Latest release version: "); + response.print(doc["release"]["fullversion"].as()); + response.print(" ("); + response.print(doc["release"]["build"].as()); + response.print("), "); + response.print(doc["release"]["time"].as()); + response.print("
"); + response.print("Latest beta version: "); if(doc["beta"]["fullversion"] != "No beta available") { - _response.concat(doc["beta"]["fullversion"].as()); - _response.concat(" ("); - _response.concat(doc["beta"]["build"].as()); - _response.concat("), "); - _response.concat(doc["beta"]["time"].as()); + response.print(doc["beta"]["fullversion"].as()); + response.print(" ("); + response.print(doc["beta"]["build"].as()); + response.print(")
, "); + response.print(doc["beta"]["time"].as()); } else { - _response.concat(doc["beta"]["fullversion"].as()); - _response.concat(""); + response.print(doc["beta"]["fullversion"].as()); + response.print(""); } - _response.concat("
"); - _response.concat("Latest development version: "); - _response.concat(doc["master"]["fullversion"].as()); - _response.concat(" ("); - _response.concat(doc["master"]["build"].as()); - _response.concat("), "); - _response.concat(doc["master"]["time"].as()); - _response.concat("
"); + response.print("
"); + response.print("Latest development version: "); + response.print(doc["master"]["fullversion"].as()); + response.print(" ("); + response.print(doc["master"]["build"].as()); + response.print("), "); + response.print(doc["master"]["time"].as()); + response.print("
"); String currentVersion = NUKI_HUB_VERSION; const char* latestVersion; @@ -370,87 +365,88 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) if(strcmp(latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) _preferences->putString(preference_latest_version, latestVersion); } #endif - _response.concat("
"); + response.print("
"); if(_partitionType == 1) { - _response.concat("

Manually update Nuki Hub

"); - _response.concat("

Reboot to Nuki Hub Updater

"); - _response.concat("Click on the button to reboot to the Nuki Hub updater, where you can select the latest Nuki Hub binary to update"); - _response.concat("



"); - _response.concat("

Update Nuki Hub Updater

"); - _response.concat("Select the latest Nuki Hub updater binary to update the Nuki Hub updater"); - _response.concat("
Choose the nuki_hub_updater.bin file to upload:
"); + response.print("

Manually update Nuki Hub

"); + response.print("

Reboot to Nuki Hub Updater

"); + response.print("Click on the button to reboot to the Nuki Hub updater, where you can select the latest Nuki Hub binary to update"); + response.print("


"); + response.print("

Update Nuki Hub Updater

"); + response.print("Select the latest Nuki Hub updater binary to update the Nuki Hub updater"); + response.print("
Choose the nuki_hub_updater.bin file to upload:
"); } else { - _response.concat("
"); - _response.concat("

Reboot to Nuki Hub

"); - _response.concat("Click on the button to reboot to Nuki Hub"); - _response.concat("


"); - _response.concat("

Update Nuki Hub

"); - _response.concat("Select the latest Nuki Hub binary to update Nuki Hub"); - _response.concat("
Choose the nuki_hub.bin file to upload:
"); + response.print("
"); + response.print("

Reboot to Nuki Hub

"); + response.print("Click on the button to reboot to Nuki Hub"); + response.print("


"); + response.print("

Update Nuki Hub

"); + response.print("Select the latest Nuki Hub binary to update Nuki Hub"); + response.print("
Choose the nuki_hub.bin file to upload:
"); } - _response.concat("


"); - _response.concat("
"); - _response.concat("

GitHub


"); - _response.concat(""); - _response.concat("

"); - _response.concat("

"); - _response.concat(""); - _response.concat(""); - sendResponse(request); + response.print("


"); + response.print("
"); + response.print("

GitHub


"); + response.print(""); + response.print("

"); + response.print("

"); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildOtaCompletedHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildOtaCompletedHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); - _response.concat("
Over-the-air update completed.
You will be forwarded automatically.
"); - _response.concat(""); - _response.concat(""); - sendResponse(request); + response.print("
Over-the-air update completed.
You will be forwarded automatically.
"); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildHtmlHeader(String additionalHeader) +void WebCfgServer::buildHtmlHeader(PsychicStreamResponse *response, String additionalHeader) { - _response.concat(""); - _response.concat(""); - if(strcmp(additionalHeader.c_str(), "") != 0) _response.concat(additionalHeader); - _response.concat(""); - _response.concat("Nuki Hub"); + response->print(""); + response->print(""); + if(strcmp(additionalHeader.c_str(), "") != 0) response->print(additionalHeader); + response->print(""); + response->print("Nuki Hub"); } void WebCfgServer::waitAndProcess(const bool blocking, const uint32_t duration) @@ -473,7 +469,7 @@ void WebCfgServer::printProgress(size_t prg, size_t sz) { Log->printf("Progress: %d%%\n", (prg*100)/_otaContentLen); } -void WebCfgServer::handleOtaUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +void WebCfgServer::handleOtaUpload(PsychicRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if(!request->url().endsWith("/uploadota")) return; @@ -535,6 +531,7 @@ void WebCfgServer::handleOtaUpload(AsyncWebServerRequest *request, String filena restartEsp(RestartReason::OTAAborted); } + /* if (final) { AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots"); response->addHeader("Refresh", "20"); @@ -553,11 +550,13 @@ void WebCfgServer::handleOtaUpload(AsyncWebServerRequest *request, String filena restartEsp(RestartReason::OTACompleted); } } + */ } -void WebCfgServer::buildConfirmHtml(AsyncWebServerRequest *request, const String &message, uint32_t redirectDelay, bool redirect) +esp_err_t WebCfgServer::buildConfirmHtml(PsychicRequest *request, const String &message, uint32_t redirectDelay, bool redirect) { - _response = ""; + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); String header; if(!redirect) @@ -570,25 +569,31 @@ void WebCfgServer::buildConfirmHtml(AsyncWebServerRequest *request, const String String delay(redirectDelay * 1000); header = ""; } - buildHtmlHeader(header); - _response.concat(message); - _response.concat(""); - sendResponse(request); + buildHtmlHeader(&response, header); + response.print(message); + response.print(""); + return response.endSend(); } -void WebCfgServer::sendCss(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::sendCss(PsychicRequest *request) { // escaped by https://www.cescaper.com/ - AsyncWebServerResponse *asyncResponse = request->beginResponse(200, "text/css", (const uint8_t*)stylecss, sizeof(stylecss)); - asyncResponse ->addHeader("Cache-Control", "public, max-age=3600"); - request->send(asyncResponse); + PsychicResponse response(request); + response.addHeader("Cache-Control", "public, max-age=3600"); + response.setCode(200); + response.setContentType("text/css"); + response.setContent(stylecss); + return response.send(); } -void WebCfgServer::sendFavicon(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::sendFavicon(PsychicRequest *request) { - AsyncWebServerResponse *asyncResponse = request->beginResponse(200, "image/png", (const uint8_t*)favicon_32x32, sizeof(favicon_32x32)); - asyncResponse->addHeader("Cache-Control", "public, max-age=604800"); - request->send(asyncResponse); + PsychicResponse response(request); + response.addHeader("Cache-Control", "public, max-age=604800"); + response.setCode(200); + response.setContentType("image/png"); + response.setContent((const char*)favicon_32x32); + return response.send(); } String WebCfgServer::generateConfirmCode() @@ -598,19 +603,19 @@ String WebCfgServer::generateConfirmCode() } #ifndef NUKI_HUB_UPDATER -void WebCfgServer::sendSettings(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) { bool redacted = false; bool pairing = false; if(request->hasParam("redacted")) { - const AsyncWebParameter* p = request->getParam("redacted"); + const PsychicWebParameter* p = request->getParam("redacted"); if(p->value() == "1") redacted = true; } if(request->hasParam("pairing")) { - const AsyncWebParameter* p = request->getParam("pairing"); + const PsychicWebParameter* p = request->getParam("pairing"); if(p->value() == "1") pairing = true; } @@ -769,17 +774,10 @@ void WebCfgServer::sendSettings(AsyncWebServerRequest *request) serializeJsonPretty(json, jsonPretty); - AsyncWebServerResponse *response = request->beginChunkedResponse("application/json", - [&](uint8_t *buffer, size_t maxlen, size_t index) -> size_t { - size_t len = min(maxlen, jsonPretty.length() - index); - memcpy(buffer, jsonPretty.c_str() + index, len); - return len; - }); - - request->send(response); + return request->reply(200, "application/json", jsonPretty.c_str()); } -bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) +bool WebCfgServer::processArgs(PsychicRequest *request, String& message) { bool configChanged = false; bool aclLvlChanged = false; @@ -809,13 +807,13 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) for(int index = 0; index < params; index++) { - const AsyncWebParameter* p = request->getParam(index); + const PsychicWebParameter* p = request->getParam(index); String key = p->name(); String value = p->value(); if(index < params -1) { - const AsyncWebParameter* next = request->getParam(index+1); + const PsychicWebParameter* next = request->getParam(index+1); if(key == next->name()) continue; } @@ -2323,7 +2321,7 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) return configChanged; } -bool WebCfgServer::processImport(AsyncWebServerRequest *request, String& message) +bool WebCfgServer::processImport(PsychicRequest *request, String& message) { bool configChanged = false; unsigned char currentBleAddress[6]; @@ -2337,7 +2335,7 @@ bool WebCfgServer::processImport(AsyncWebServerRequest *request, String& message for(int index = 0; index < params; index++) { - const AsyncWebParameter* p = request->getParam(index); + const PsychicWebParameter* p = request->getParam(index); if(p->name() == "importjson") { JsonDocument doc; @@ -2481,14 +2479,14 @@ bool WebCfgServer::processImport(AsyncWebServerRequest *request, String& message return configChanged; } -void WebCfgServer::processGpioArgs(AsyncWebServerRequest *request) +void WebCfgServer::processGpioArgs(PsychicRequest *request) { int params = request->params(); std::vector pinConfiguration; for(int index = 0; index < params; index++) { - const AsyncWebParameter* p = request->getParam(index); + const PsychicWebParameter* p = request->getParam(index); PinRole role = (PinRole)p->value().toInt(); if(role != PinRole::Disabled) { @@ -2502,87 +2500,90 @@ void WebCfgServer::processGpioArgs(AsyncWebServerRequest *request) _gpio->savePinConfiguration(pinConfiguration); } -void WebCfgServer::buildImportExportHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildImportExportHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - - _response.concat("

Import configuration

"); - _response.concat("

"); - _response.concat("


"); - _response.concat("
"); - _response.concat("

Export configuration


"); - _response.concat(""); - _response.concat("

"); - _response.concat("

"); - _response.concat("
"); - sendResponse(request); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print("

Import configuration

"); + response.print("

"); + response.print("


"); + response.print("
"); + response.print("

Export configuration


"); + response.print(""); + response.print("

"); + response.print("

"); + response.print("
"); + return response.endSend(); } -void WebCfgServer::buildCustomNetworkConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildCustomNetworkConfigHtml(PsychicRequest *request) { String header = ""; - _response = ""; - buildHtmlHeader(header); - _response.concat("
"); - _response.concat("

Custom Ethernet Configuration

"); - _response.concat(""); - printDropDown("NWCUSTPHY", "PHY", String(_preferences->getInt(preference_network_custom_phy)), getNetworkCustomPHYOptions(), ""); - printInputField("NWCUSTADDR", "ADDR", _preferences->getInt(preference_network_custom_addr, 1), 6, ""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response, header); + response.print(""); + response.print("

Custom Ethernet Configuration

"); + response.print("
"); + printDropDown(&response, "NWCUSTPHY", "PHY", String(_preferences->getInt(preference_network_custom_phy)), getNetworkCustomPHYOptions(), ""); + printInputField(&response, "NWCUSTADDR", "ADDR", _preferences->getInt(preference_network_custom_addr, 1), 6, ""); #if defined(CONFIG_IDF_TARGET_ESP32) - printDropDown("NWCUSTCLK", "CLK", String(_preferences->getInt(preference_network_custom_clk, 0)), getNetworkCustomCLKOptions(), "internalopt"); - printInputField("NWCUSTPWR", "PWR", _preferences->getInt(preference_network_custom_pwr, 12), 6, "class=\"internalopt\""); - printInputField("NWCUSTMDIO", "MDIO", _preferences->getInt(preference_network_custom_mdio), 6, "class=\"internalopt\""); - printInputField("NWCUSTMDC", "MDC", _preferences->getInt(preference_network_custom_mdc), 6, "class=\"internalopt\""); + printDropDown(&response, "NWCUSTCLK", "CLK", String(_preferences->getInt(preference_network_custom_clk, 0)), getNetworkCustomCLKOptions(), "internalopt"); + printInputField(&response, "NWCUSTPWR", "PWR", _preferences->getInt(preference_network_custom_pwr, 12), 6, "class=\"internalopt\""); + printInputField(&response, "NWCUSTMDIO", "MDIO", _preferences->getInt(preference_network_custom_mdio), 6, "class=\"internalopt\""); + printInputField(&response, "NWCUSTMDC", "MDC", _preferences->getInt(preference_network_custom_mdc), 6, "class=\"internalopt\""); #endif - printInputField("NWCUSTIRQ", "IRQ", _preferences->getInt(preference_network_custom_irq, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTRST", "RST", _preferences->getInt(preference_network_custom_rst, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTCS", "CS", _preferences->getInt(preference_network_custom_cs, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTSCK", "SCK", _preferences->getInt(preference_network_custom_sck, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTMISO", "MISO", _preferences->getInt(preference_network_custom_miso, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTMOSI", "MOSI", _preferences->getInt(preference_network_custom_mosi, -1), 6, "class=\"externalopt\""); - - _response.concat("
"); - - _response.concat("
"); - _response.concat("
"); - _response.concat(""); - sendResponse(request); + printInputField(&response, "NWCUSTIRQ", "IRQ", _preferences->getInt(preference_network_custom_irq, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTRST", "RST", _preferences->getInt(preference_network_custom_rst, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTCS", "CS", _preferences->getInt(preference_network_custom_cs, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTSCK", "SCK", _preferences->getInt(preference_network_custom_sck, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTMISO", "MISO", _preferences->getInt(preference_network_custom_miso, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTMOSI", "MOSI", _preferences->getInt(preference_network_custom_mosi, -1), 6, "class=\"externalopt\""); + response.print(""); + response.print("
"); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildHtml(PsychicRequest *request) { String header = ""; - _response = ""; - buildHtmlHeader(header); - - if(_rebootRequired) _response.concat("
REBOOT REQUIRED TO APPLY SETTINGS
"); - if(_preferences->getBool(preference_webserial_enabled, false)) _response.concat("
WEBSERIAL IS ENABLED, ONLY ENABLE WHEN DEBUGGING AND DISABLE ASAP
"); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response, header); + if(_rebootRequired) + { + response.print("
REBOOT REQUIRED TO APPLY SETTINGS
"); + } + if(_preferences->getBool(preference_webserial_enabled, false)) + { + response.print("
WEBSERIAL IS ENABLED, ONLY ENABLE WHEN DEBUGGING AND DISABLE ASAP
"); + } #ifdef DEBUG_NUKIHUB - _response.concat("
RUNNING DEBUG BUILD, SWITCH TO RELEASE BUILD ASAP
"); + response.print("
RUNNING DEBUG BUILD, SWITCH TO RELEASE BUILD ASAP
"); #endif - - _response.concat("

Info


"); - _response.concat(""); - - printParameter("Hostname", _hostname.c_str(), "", "hostname"); - printParameter("MQTT Connected", _network->mqttConnectionState() > 0 ? "Yes" : "No", "", "mqttState"); + response.print("

Info


"); + response.print("
"); + printParameter(&response, "Hostname", _hostname.c_str(), "", "hostname"); + printParameter(&response, "MQTT Connected", _network->mqttConnectionState() > 0 ? "Yes" : "No", "", "mqttState"); if(_nuki != nullptr) { char lockStateArr[20]; NukiLock::lockstateToString(_nuki->keyTurnerState().lockState, lockStateArr); - printParameter("Nuki Lock paired", _nuki->isPaired() ? ("Yes (BLE Address " + _nuki->getBleAddress().toString() + ")").c_str() : "No", "", "lockPaired"); - printParameter("Nuki Lock state", lockStateArr, "", "lockState"); + printParameter(&response, "Nuki Lock paired", _nuki->isPaired() ? ("Yes (BLE Address " + _nuki->getBleAddress().toString() + ")").c_str() : "No", "", "lockPaired"); + printParameter(&response, "Nuki Lock state", lockStateArr, "", "lockState"); if(_nuki->isPaired()) { String lockState = pinStateToString(_preferences->getInt(preference_lock_pin_status, 4)); - printParameter("Nuki Lock PIN status", lockState.c_str(), "", "lockPin"); + printParameter(&response, "Nuki Lock PIN status", lockState.c_str(), "", "lockPin"); if(_preferences->getBool(preference_official_hybrid, false)) { String offConnected = _nuki->offConnected() ? "Yes": "No"; - printParameter("Nuki Lock hybrid mode connected", offConnected.c_str(), "", "lockHybrid"); + printParameter(&response, "Nuki Lock hybrid mode connected", offConnected.c_str(), "", "lockHybrid"); } } } @@ -2590,234 +2591,247 @@ void WebCfgServer::buildHtml(AsyncWebServerRequest *request) { char openerStateArr[20]; NukiOpener::lockstateToString(_nukiOpener->keyTurnerState().lockState, openerStateArr); - printParameter("Nuki Opener paired", _nukiOpener->isPaired() ? ("Yes (BLE Address " + _nukiOpener->getBleAddress().toString() + ")").c_str() : "No", "", "openerPaired"); - - if(_nukiOpener->keyTurnerState().nukiState == NukiOpener::State::ContinuousMode) printParameter("Nuki Opener state", "Open (Continuous Mode)", "", "openerState"); - else printParameter("Nuki Opener state", openerStateArr, "", "openerState"); + printParameter(&response, "Nuki Opener paired", _nukiOpener->isPaired() ? ("Yes (BLE Address " + _nukiOpener->getBleAddress().toString() + ")").c_str() : "No", "", "openerPaired"); + if(_nukiOpener->keyTurnerState().nukiState == NukiOpener::State::ContinuousMode) + { + printParameter(&response, "Nuki Opener state", "Open (Continuous Mode)", "", "openerState"); + } + else + { + printParameter(&response, "Nuki Opener state", openerStateArr, "", "openerState"); + } if(_nukiOpener->isPaired()) { String openerState = pinStateToString(_preferences->getInt(preference_opener_pin_status, 4)); - printParameter("Nuki Opener PIN status", openerState.c_str(), "", "openerPin"); + printParameter(&response, "Nuki Opener PIN status", openerState.c_str(), "", "openerPin"); } } - printParameter("Firmware", NUKI_HUB_VERSION, "/info", "firmware"); - if(_preferences->getBool(preference_check_updates)) printParameter("Latest Firmware", _preferences->getString(preference_latest_version).c_str(), "/ota", "ota"); - _response.concat("

"); - _response.concat("
    "); - buildNavigationMenuEntry("MQTT and Network Configuration", "/mqttconfig", _brokerConfigured ? "" : "Please configure MQTT broker"); - buildNavigationMenuEntry("Nuki Configuration", "/nukicfg"); - buildNavigationMenuEntry("Access Level Configuration", "/acclvl"); - buildNavigationMenuEntry("Credentials", "/cred", _pinsConfigured ? "" : "Please configure PIN"); - buildNavigationMenuEntry("GPIO Configuration", "/gpiocfg"); - buildNavigationMenuEntry("Firmware update", "/ota"); - buildNavigationMenuEntry("Import/Export Configuration", "/impexpcfg"); + printParameter(&response, "Firmware", NUKI_HUB_VERSION, "/info", "firmware"); + if(_preferences->getBool(preference_check_updates)) + { + printParameter(&response, "Latest Firmware", _preferences->getString(preference_latest_version).c_str(), "/ota", "ota"); + } + response.print("
    "); + response.print("
      "); + buildNavigationMenuEntry(&response, "MQTT and Network Configuration", "/mqttconfig", _brokerConfigured ? "" : "Please configure MQTT broker"); + buildNavigationMenuEntry(&response, "Nuki Configuration", "/nukicfg"); + buildNavigationMenuEntry(&response, "Access Level Configuration", "/acclvl"); + buildNavigationMenuEntry(&response, "Credentials", "/cred", _pinsConfigured ? "" : "Please configure PIN"); + buildNavigationMenuEntry(&response, "GPIO Configuration", "/gpiocfg"); + buildNavigationMenuEntry(&response, "Firmware update", "/ota"); + buildNavigationMenuEntry(&response, "Import/Export Configuration", "/impexpcfg"); if(_preferences->getInt(preference_network_hardware, 0) == 11) { - buildNavigationMenuEntry("Custom Ethernet Configuration", "/custntw"); + buildNavigationMenuEntry(&response, "Custom Ethernet Configuration", "/custntw"); } if (_preferences->getBool(preference_publish_debug_info, false)) { - buildNavigationMenuEntry("Advanced Configuration", "/advanced"); + buildNavigationMenuEntry(&response, "Advanced Configuration", "/advanced"); } if(_preferences->getBool(preference_webserial_enabled, false)) { - buildNavigationMenuEntry("Open Webserial", "/webserial"); + buildNavigationMenuEntry(&response, "Open Webserial", "/webserial"); } #ifndef CONFIG_IDF_TARGET_ESP32H2 - if(_allowRestartToPortal) buildNavigationMenuEntry("Configure Wi-Fi", "/wifi"); + if(_allowRestartToPortal) + { + buildNavigationMenuEntry(&response, "Configure Wi-Fi", "/wifi"); + } #endif - buildNavigationMenuEntry("Reboot Nuki Hub", "/reboot"); - _response.concat("
    "); - sendResponse(request); + buildNavigationMenuEntry(&response, "Reboot Nuki Hub", "/reboot"); + response.print("
"); + return response.endSend(); } -void WebCfgServer::buildCredHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
"); - _response.concat("

Credentials

"); - _response.concat(""); - printInputField("CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, "", false, true); - printInputField("CREDPASS", "Password", "*", 30, "", true, true); - printInputField("CREDPASSRE", "Retype password", "*", 30, "", true); - _response.concat("
"); - _response.concat("
"); - _response.concat("
"); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print("
"); + response.print("

Credentials

"); + response.print(""); + printInputField(&response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, "", false, true); + printInputField(&response, "CREDPASS", "Password", "*", 30, "", true, true); + printInputField(&response, "CREDPASSRE", "Retype password", "*", 30, "", true); + response.print("
"); + response.print("
"); + response.print("
"); if(_nuki != nullptr) { - _response.concat("

"); - _response.concat("

Nuki Lock PIN

"); - _response.concat(""); - printInputField("NUKIPIN", "PIN Code (# to clear)", "*", 20, "", true); - _response.concat("
"); - _response.concat("
"); - _response.concat("
"); + response.print("

"); + response.print("

Nuki Lock PIN

"); + response.print(""); + printInputField(&response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, "", true); + response.print("
"); + response.print("
"); + response.print("
"); } if(_nukiOpener != nullptr) { - _response.concat("

"); - _response.concat("

Nuki Opener PIN

"); - _response.concat(""); - printInputField("NUKIOPPIN", "PIN Code (# to clear)", "*", 20, "", true); - _response.concat("
"); - _response.concat("
"); - _response.concat("
"); + response.print("

"); + response.print("

Nuki Opener PIN

"); + response.print(""); + printInputField(&response, "NUKIOPPIN", "PIN Code (# to clear)", "*", 20, "", true); + response.print("
"); + response.print("
"); + response.print("
"); } if(_nuki != nullptr) { - _response.concat("

Unpair Nuki Lock

"); - _response.concat("
"); - _response.concat(""); + response.print("

Unpair Nuki Lock

"); + response.print(""); + response.print("
"); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm unpair"); - printInputField("CONFIRMTOKEN", message.c_str(), "", 10, ""); - _response.concat("
"); - _response.concat("
"); + printInputField(&response, "CONFIRMTOKEN", message.c_str(), "", 10, ""); + response.print(""); + response.print("
"); } if(_nukiOpener != nullptr) { - _response.concat("

Unpair Nuki Opener

"); - _response.concat("
"); - _response.concat(""); + response.print("

Unpair Nuki Opener

"); + response.print(""); + response.print("
"); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm unpair"); - printInputField("CONFIRMTOKEN", message.c_str(), "", 10, ""); - _response.concat("
"); - _response.concat("
"); + printInputField(&response, "CONFIRMTOKEN", message.c_str(), "", 10, ""); + response.print(""); + response.print("
"); } - _response.concat("

Factory reset Nuki Hub

"); - _response.concat("

This will reset all settings to default and unpair Nuki Lock and/or Opener."); + response.print("

Factory reset Nuki Hub

"); + response.print("

This will reset all settings to default and unpair Nuki Lock and/or Opener."); #ifndef CONFIG_IDF_TARGET_ESP32H2 - _response.concat("Optionally will also reset WiFi settings and reopen WiFi manager portal."); + response.print("Optionally will also reset WiFi settings and reopen WiFi manager portal."); #endif - _response.concat("

"); - _response.concat("
"); - _response.concat(""); + response.print(""); + response.print(""); + response.print("
"); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm factory reset"); - printInputField("CONFIRMTOKEN", message.c_str(), "", 10, ""); + printInputField(&response, "CONFIRMTOKEN", message.c_str(), "", 10, ""); #ifndef CONFIG_IDF_TARGET_ESP32H2 - printCheckBox("WIFI", "Also reset WiFi settings", false, ""); + printCheckBox(&response, "WIFI", "Also reset WiFi settings", false, ""); #endif - _response.concat("
"); - _response.concat("
"); - _response.concat(""); - sendResponse(request); + response.print(""); + response.print("
"); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildMqttConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildMqttConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
"); - _response.concat("

Basic MQTT and Network Configuration

"); - _response.concat(""); - printInputField("HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100, ""); - printInputField("MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100, ""); - printInputField("MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5, ""); - printInputField("MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30, "", false, true); - printInputField("MQTTPASS", "MQTT Password", "*", 30, "", true, true); - _response.concat("

"); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

Basic MQTT and Network Configuration

"); + response.print(""); + printInputField(&response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100, ""); + printInputField(&response, "MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100, ""); + printInputField(&response, "MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5, ""); + printInputField(&response, "MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30, "", false, true); + printInputField(&response, "MQTTPASS", "MQTT Password", "*", 30, "", true, true); + response.print("

"); - _response.concat("

Advanced MQTT and Network Configuration

"); - _response.concat(""); - printInputField("HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30, ""); - printInputField("HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261, ""); - if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox("OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), ""); - printTextarea("MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported(), true); - printTextarea("MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true); - printTextarea("MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported(), true); - printDropDown("NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions(), ""); + response.print("

Advanced MQTT and Network Configuration

"); + response.print("
"); + printInputField(&response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30, ""); + printInputField(&response, "HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261, ""); + if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox(&response, "OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), ""); + printTextarea(&response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported(), true); + printTextarea(&response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true); + printTextarea(&response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported(), true); + printDropDown(&response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions(), ""); #ifndef CONFIG_IDF_TARGET_ESP32H2 - printCheckBox("NWHWWIFIFB", "Disable fallback to Wi-Fi / Wi-Fi config portal", _preferences->getBool(preference_network_wifi_fallback_disabled), ""); - printCheckBox("BESTRSSI", "Connect to AP with the best signal in an environment with multiple APs with the same SSID", _preferences->getBool(preference_find_best_rssi), ""); - printInputField("RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6, ""); + printCheckBox(&response, "NWHWWIFIFB", "Disable fallback to Wi-Fi / Wi-Fi config portal", _preferences->getBool(preference_network_wifi_fallback_disabled), ""); + printCheckBox(&response, "BESTRSSI", "Connect to AP with the best signal in an environment with multiple APs with the same SSID", _preferences->getBool(preference_find_best_rssi), ""); + printInputField(&response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6, ""); #endif - printInputField("NETTIMEOUT", "MQTT Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5, ""); - printCheckBox("RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect), ""); - printCheckBox("RECNWTMQTTDIS", "Reconnect network on MQTT connection failure", _preferences->getBool(preference_recon_netw_on_mqtt_discon), ""); - printCheckBox("MQTTLOG", "Enable MQTT logging", _preferences->getBool(preference_mqtt_log_enabled), ""); - printCheckBox("CHECKUPDATE", "Check for Firmware Updates every 24h", _preferences->getBool(preference_check_updates), ""); - printCheckBox("UPDATEMQTT", "Allow updating using MQTT", _preferences->getBool(preference_update_from_mqtt), ""); - printCheckBox("DISNONJSON", "Disable some extraneous non-JSON topics", _preferences->getBool(preference_disable_non_json), ""); - printCheckBox("OFFHYBRID", "Enable hybrid official MQTT and Nuki Hub setup", _preferences->getBool(preference_official_hybrid), ""); - printCheckBox("HYBRIDACT", "Enable sending actions through official MQTT", _preferences->getBool(preference_official_hybrid_actions), ""); - printInputField("HYBRIDTIMER", "Time between status updates when official MQTT is offline (seconds)", _preferences->getInt(preference_query_interval_hybrid_lockstate), 5, ""); - // printCheckBox("HYBRIDRETRY", "Retry command sent using official MQTT over BLE if failed", _preferences->getBool(preference_official_hybrid_retry), ""); // NOT IMPLEMENTED (YET?) - _response.concat("
"); - _response.concat("* If no encryption is configured for the MQTT broker, leave empty.

"); + printInputField(&response, "NETTIMEOUT", "MQTT Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5, ""); + printCheckBox(&response, "RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect), ""); + printCheckBox(&response, "RECNWTMQTTDIS", "Reconnect network on MQTT connection failure", _preferences->getBool(preference_recon_netw_on_mqtt_discon), ""); + printCheckBox(&response, "MQTTLOG", "Enable MQTT logging", _preferences->getBool(preference_mqtt_log_enabled), ""); + printCheckBox(&response, "CHECKUPDATE", "Check for Firmware Updates every 24h", _preferences->getBool(preference_check_updates), ""); + printCheckBox(&response, "UPDATEMQTT", "Allow updating using MQTT", _preferences->getBool(preference_update_from_mqtt), ""); + printCheckBox(&response, "DISNONJSON", "Disable some extraneous non-JSON topics", _preferences->getBool(preference_disable_non_json), ""); + printCheckBox(&response, "OFFHYBRID", "Enable hybrid official MQTT and Nuki Hub setup", _preferences->getBool(preference_official_hybrid), ""); + printCheckBox(&response, "HYBRIDACT", "Enable sending actions through official MQTT", _preferences->getBool(preference_official_hybrid_actions), ""); + printInputField(&response, "HYBRIDTIMER", "Time between status updates when official MQTT is offline (seconds)", _preferences->getInt(preference_query_interval_hybrid_lockstate), 5, ""); + // printCheckBox(&response, "HYBRIDRETRY", "Retry command sent using official MQTT over BLE if failed", _preferences->getBool(preference_official_hybrid_retry), ""); // NOT IMPLEMENTED (YET?) + response.print(""); + response.print("* If no encryption is configured for the MQTT broker, leave empty.

"); - _response.concat("

IP Address assignment

"); - _response.concat(""); - printCheckBox("DHCPENA", "Enable DHCP", _preferences->getBool(preference_ip_dhcp_enabled), ""); - printInputField("IPADDR", "Static IP address", _preferences->getString(preference_ip_address).c_str(), 15, ""); - printInputField("IPSUB", "Subnet", _preferences->getString(preference_ip_subnet).c_str(), 15, ""); - printInputField("IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15, ""); - printInputField("DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15, ""); - _response.concat("
"); - _response.concat("
"); - _response.concat("
"); - _response.concat(""); - sendResponse(request); + response.print("

IP Address assignment

"); + response.print(""); + printCheckBox(&response, "DHCPENA", "Enable DHCP", _preferences->getBool(preference_ip_dhcp_enabled), ""); + printInputField(&response, "IPADDR", "Static IP address", _preferences->getString(preference_ip_address).c_str(), 15, ""); + printInputField(&response, "IPSUB", "Subnet", _preferences->getString(preference_ip_subnet).c_str(), 15, ""); + printInputField(&response, "IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15, ""); + printInputField(&response, "DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15, ""); + response.print("
"); + response.print("
"); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildAdvancedConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildAdvancedConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
"); - _response.concat("

Advanced Configuration

"); - _response.concat("

Warning: Changing these settings can lead to bootloops that might require you to erase the ESP32 and reflash nukihub using USB/serial

"); - _response.concat(""); - _response.concat(""); - printCheckBox("WEBLOG", "Enable WebSerial logging", _preferences->getBool(preference_webserial_enabled), ""); - printCheckBox("BTLPRST", "Enable Bootloop prevention (Try to reset these settings to default on bootloop)", true, ""); - printInputField("BUFFSIZE", "Char buffer size (min 4096, max 32768)", _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE), 6, ""); - _response.concat(""); - printInputField("TSKNTWK", "Task size Network (min 12288, max 32768)", _preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), 6, ""); - _response.concat(""); - printInputField("TSKNUKI", "Task size Nuki (min 8192, max 32768)", _preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), 6, ""); - printInputField("ALMAX", "Max auth log entries (min 1, max 50)", _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 3, "id=\"inputmaxauthlog\""); - printInputField("KPMAX", "Max keypad entries (min 1, max 100)", _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD), 3, "id=\"inputmaxkeypad\""); - printInputField("TCMAX", "Max timecontrol entries (min 1, max 50)", _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL), 3, "id=\"inputmaxtimecontrol\""); - printInputField("AUTHMAX", "Max authorization entries (min 1, max 50)", _preferences->getInt(preference_auth_max_entries, MAX_AUTH), 3, "id=\"inputmaxauth\""); - printCheckBox("SHOWSECRETS", "Show Pairing secrets on Info page", _preferences->getBool(preference_show_secrets), ""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

Advanced Configuration

"); + response.print("

Warning: Changing these settings can lead to bootloops that might require you to erase the ESP32 and reflash nukihub using USB/serial

"); + response.print("
Current bootloop prevention state"); - _response.concat(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Enabled" : "Disabled"); - _response.concat("
Advised minimum char buffer size based on current settings
Advised minimum network task size based on current settings
"); + response.print(""); + printCheckBox(&response, "WEBLOG", "Enable WebSerial logging", _preferences->getBool(preference_webserial_enabled), ""); + printCheckBox(&response, "BTLPRST", "Enable Bootloop prevention (Try to reset these settings to default on bootloop)", true, ""); + printInputField(&response, "BUFFSIZE", "Char buffer size (min 4096, max 32768)", _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE), 6, ""); + response.print(""); + printInputField(&response, "TSKNTWK", "Task size Network (min 12288, max 32768)", _preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), 6, ""); + response.print(""); + printInputField(&response, "TSKNUKI", "Task size Nuki (min 8192, max 32768)", _preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), 6, ""); + printInputField(&response, "ALMAX", "Max auth log entries (min 1, max 50)", _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 3, "id=\"inputmaxauthlog\""); + printInputField(&response, "KPMAX", "Max keypad entries (min 1, max 100)", _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD), 3, "id=\"inputmaxkeypad\""); + printInputField(&response, "TCMAX", "Max timecontrol entries (min 1, max 50)", _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL), 3, "id=\"inputmaxtimecontrol\""); + printInputField(&response, "AUTHMAX", "Max authorization entries (min 1, max 50)", _preferences->getInt(preference_auth_max_entries, MAX_AUTH), 3, "id=\"inputmaxauth\""); + printCheckBox(&response, "SHOWSECRETS", "Show Pairing secrets on Info page", _preferences->getBool(preference_show_secrets), ""); if(_preferences->getBool(preference_lock_enabled, true)) { - printCheckBox("LCKMANPAIR", "Manually set lock pairing data (enable to save values below)", false, ""); - printInputField("LCKBLEADDR", "currentBleAddress", "", 12, ""); - printInputField("LCKSECRETK", "secretKeyK", "", 64, ""); - printInputField("LCKAUTHID", "authorizationId", "", 8, ""); + printCheckBox(&response, "LCKMANPAIR", "Manually set lock pairing data (enable to save values below)", false, ""); + printInputField(&response, "LCKBLEADDR", "currentBleAddress", "", 12, ""); + printInputField(&response, "LCKSECRETK", "secretKeyK", "", 64, ""); + printInputField(&response, "LCKAUTHID", "authorizationId", "", 8, ""); } if(_preferences->getBool(preference_opener_enabled, false)) { - printCheckBox("OPNMANPAIR", "Manually set opener pairing data (enable to save values below)", false, ""); - printInputField("OPNBLEADDR", "currentBleAddress", "", 12, ""); - printInputField("OPNSECRETK", "secretKeyK", "", 64, ""); - printInputField("OPNAUTHID", "authorizationId", "", 8, ""); + printCheckBox(&response, "OPNMANPAIR", "Manually set opener pairing data (enable to save values below)", false, ""); + printInputField(&response, "OPNBLEADDR", "currentBleAddress", "", 12, ""); + printInputField(&response, "OPNSECRETK", "secretKeyK", "", 64, ""); + printInputField(&response, "OPNAUTHID", "authorizationId", "", 8, ""); } - printInputField("OTAUPD", "Custom URL to update Nuki Hub updater", "", 255, ""); - printInputField("OTAMAIN", "Custom URL to update Nuki Hub", "", 255, ""); - _response.concat("
Current bootloop prevention state"); + response.print(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Enabled" : "Disabled"); + response.print("
Advised minimum char buffer size based on current settings
Advised minimum network task size based on current settings
"); + printInputField(&response, "OTAUPD", "Custom URL to update Nuki Hub updater", "", 255, ""); + printInputField(&response, "OTAMAIN", "Custom URL to update Nuki Hub", "", 255, ""); + response.print(""); - _response.concat("
"); - _response.concat("
"); - _response.concat(""); - sendResponse(request); + response.print("
"); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildStatusHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildStatusHtml(PsychicRequest *request) { - _response = ""; JsonDocument json; - char _resbuf[2048]; + String jsonStr; bool mqttDone = false; bool lockDone = false; bool openerDone = false; @@ -2878,9 +2892,8 @@ void WebCfgServer::buildStatusHtml(AsyncWebServerRequest *request) if(mqttDone && lockDone && openerDone && latestDone) json["stop"] = 1; - serializeJson(json, _resbuf, sizeof(_resbuf)); - _response.concat(_resbuf); - sendResponse(request); + serializeJson(json, jsonStr); + return request->reply(200, "application/json", jsonStr.c_str()); } String WebCfgServer::pinStateToString(uint8_t value) { @@ -2897,36 +2910,37 @@ String WebCfgServer::pinStateToString(uint8_t value) { } } -void WebCfgServer::buildAccLvlHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildAccLvlHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); - _response.concat("
"); - _response.concat(""); - _response.concat("

Nuki General Access Control

"); - _response.concat(""); - printCheckBox("CONFPUB", "Publish Nuki configuration information", _preferences->getBool(preference_conf_info_enabled, true), ""); + response.print(""); + response.print(""); + response.print("

Nuki General Access Control

"); + response.print("
SettingEnabled
"); + printCheckBox(&response, "CONFPUB", "Publish Nuki configuration information", _preferences->getBool(preference_conf_info_enabled, true), ""); if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad())) { - printCheckBox("KPPUB", "Publish keypad entries information", _preferences->getBool(preference_keypad_info_enabled), ""); - printCheckBox("KPPER", "Publish a topic per keypad entry and create HA sensor", _preferences->getBool(preference_keypad_topic_per_entry), ""); - printCheckBox("KPCODE", "Also publish keypad codes (Disadvised for security reasons)", _preferences->getBool(preference_keypad_publish_code, false), ""); - printCheckBox("KPENA", "Add, modify and delete keypad codes", _preferences->getBool(preference_keypad_control_enabled), ""); + printCheckBox(&response, "KPPUB", "Publish keypad entries information", _preferences->getBool(preference_keypad_info_enabled), ""); + printCheckBox(&response, "KPPER", "Publish a topic per keypad entry and create HA sensor", _preferences->getBool(preference_keypad_topic_per_entry), ""); + printCheckBox(&response, "KPCODE", "Also publish keypad codes (Disadvised for security reasons)", _preferences->getBool(preference_keypad_publish_code, false), ""); + printCheckBox(&response, "KPENA", "Add, modify and delete keypad codes", _preferences->getBool(preference_keypad_control_enabled), ""); } - printCheckBox("TCPUB", "Publish time control entries information", _preferences->getBool(preference_timecontrol_info_enabled), ""); - printCheckBox("TCPER", "Publish a topic per time control entry and create HA sensor", _preferences->getBool(preference_timecontrol_topic_per_entry), ""); - printCheckBox("TCENA", "Add, modify and delete time control entries", _preferences->getBool(preference_timecontrol_control_enabled), ""); - printCheckBox("AUTHPUB", "Publish authorization entries information", _preferences->getBool(preference_auth_info_enabled), ""); - printCheckBox("AUTHPER", "Publish a topic per authorization entry and create HA sensor", _preferences->getBool(preference_auth_topic_per_entry), ""); - printCheckBox("AUTHENA", "Modify and delete authorization entries", _preferences->getBool(preference_auth_control_enabled), ""); - printCheckBox("PUBAUTH", "Publish authorization log", _preferences->getBool(preference_publish_authdata), ""); - _response.concat("
SettingEnabled

"); - _response.concat("
"); + printCheckBox(&response, "TCPUB", "Publish time control entries information", _preferences->getBool(preference_timecontrol_info_enabled), ""); + printCheckBox(&response, "TCPER", "Publish a topic per time control entry and create HA sensor", _preferences->getBool(preference_timecontrol_topic_per_entry), ""); + printCheckBox(&response, "TCENA", "Add, modify and delete time control entries", _preferences->getBool(preference_timecontrol_control_enabled), ""); + printCheckBox(&response, "AUTHPUB", "Publish authorization entries information", _preferences->getBool(preference_auth_info_enabled), ""); + printCheckBox(&response, "AUTHPER", "Publish a topic per authorization entry and create HA sensor", _preferences->getBool(preference_auth_topic_per_entry), ""); + printCheckBox(&response, "AUTHENA", "Modify and delete authorization entries", _preferences->getBool(preference_auth_control_enabled), ""); + printCheckBox(&response, "PUBAUTH", "Publish authorization log", _preferences->getBool(preference_publish_authdata), ""); + response.print("
"); + response.print("
"); if(_nuki != nullptr) { @@ -2935,72 +2949,72 @@ void WebCfgServer::buildAccLvlHtml(AsyncWebServerRequest *request) uint32_t advancedLockConfigAclPrefs[22]; _preferences->getBytes(preference_conf_lock_advanced_acl, &advancedLockConfigAclPrefs, sizeof(advancedLockConfigAclPrefs)); - _response.concat("

Nuki Lock Access Control

"); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

Nuki Lock Access Control

"); + response.print(""); + response.print(""); + response.print("
ActionAllowed
"); - printCheckBox("ACLLCKLCK", "Lock", ((int)aclPrefs[0] == 1), "chk_access_lock"); - printCheckBox("ACLLCKUNLCK", "Unlock", ((int)aclPrefs[1] == 1), "chk_access_lock"); - printCheckBox("ACLLCKUNLTCH", "Unlatch", ((int)aclPrefs[2] == 1), "chk_access_lock"); - printCheckBox("ACLLCKLNG", "Lock N Go", ((int)aclPrefs[3] == 1), "chk_access_lock"); - printCheckBox("ACLLCKLNGU", "Lock N Go Unlatch", ((int)aclPrefs[4] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFLLCK", "Full Lock", ((int)aclPrefs[5] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFOB1", "Fob Action 1", ((int)aclPrefs[6] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFOB2", "Fob Action 2", ((int)aclPrefs[7] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFOB3", "Fob Action 3", ((int)aclPrefs[8] == 1), "chk_access_lock"); - _response.concat("
ActionAllowed

"); + printCheckBox(&response, "ACLLCKLCK", "Lock", ((int)aclPrefs[0] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKUNLCK", "Unlock", ((int)aclPrefs[1] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKUNLTCH", "Unlatch", ((int)aclPrefs[2] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKLNG", "Lock N Go", ((int)aclPrefs[3] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKLNGU", "Lock N Go Unlatch", ((int)aclPrefs[4] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFLLCK", "Full Lock", ((int)aclPrefs[5] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFOB1", "Fob Action 1", ((int)aclPrefs[6] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFOB2", "Fob Action 2", ((int)aclPrefs[7] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFOB3", "Fob Action 3", ((int)aclPrefs[8] == 1), "chk_access_lock"); + response.print("
"); - _response.concat("

Nuki Lock Config Control (Requires PIN to be set)

"); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

Nuki Lock Config Control (Requires PIN to be set)

"); + response.print(""); + response.print(""); + response.print("
ChangeAllowed
"); - printCheckBox("CONFLCKNAME", "Name", ((int)basicLockConfigAclPrefs[0] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLAT", "Latitude", ((int)basicLockConfigAclPrefs[1] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLONG", "Longitude", ((int)basicLockConfigAclPrefs[2] == 1), "chk_config_lock"); - printCheckBox("CONFLCKAUNL", "Auto unlatch", ((int)basicLockConfigAclPrefs[3] == 1), "chk_config_lock"); - printCheckBox("CONFLCKPRENA", "Pairing enabled", ((int)basicLockConfigAclPrefs[4] == 1), "chk_config_lock"); - printCheckBox("CONFLCKBTENA", "Button enabled", ((int)basicLockConfigAclPrefs[5] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLEDENA", "LED flash enabled", ((int)basicLockConfigAclPrefs[6] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLEDBR", "LED brightness", ((int)basicLockConfigAclPrefs[7] == 1), "chk_config_lock"); - printCheckBox("CONFLCKTZOFF", "Timezone offset", ((int)basicLockConfigAclPrefs[8] == 1), "chk_config_lock"); - printCheckBox("CONFLCKDSTM", "DST mode", ((int)basicLockConfigAclPrefs[9] == 1), "chk_config_lock"); - printCheckBox("CONFLCKFOB1", "Fob Action 1", ((int)basicLockConfigAclPrefs[10] == 1), "chk_config_lock"); - printCheckBox("CONFLCKFOB2", "Fob Action 2", ((int)basicLockConfigAclPrefs[11] == 1), "chk_config_lock"); - printCheckBox("CONFLCKFOB3", "Fob Action 3", ((int)basicLockConfigAclPrefs[12] == 1), "chk_config_lock"); - printCheckBox("CONFLCKSGLLCK", "Single Lock", ((int)basicLockConfigAclPrefs[13] == 1), "chk_config_lock"); - printCheckBox("CONFLCKADVM", "Advertising Mode", ((int)basicLockConfigAclPrefs[14] == 1), "chk_config_lock"); - printCheckBox("CONFLCKTZID", "Timezone ID", ((int)basicLockConfigAclPrefs[15] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNAME", "Name", ((int)basicLockConfigAclPrefs[0] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLAT", "Latitude", ((int)basicLockConfigAclPrefs[1] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLONG", "Longitude", ((int)basicLockConfigAclPrefs[2] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKAUNL", "Auto unlatch", ((int)basicLockConfigAclPrefs[3] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKPRENA", "Pairing enabled", ((int)basicLockConfigAclPrefs[4] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKBTENA", "Button enabled", ((int)basicLockConfigAclPrefs[5] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLEDENA", "LED flash enabled", ((int)basicLockConfigAclPrefs[6] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLEDBR", "LED brightness", ((int)basicLockConfigAclPrefs[7] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKTZOFF", "Timezone offset", ((int)basicLockConfigAclPrefs[8] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKDSTM", "DST mode", ((int)basicLockConfigAclPrefs[9] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKFOB1", "Fob Action 1", ((int)basicLockConfigAclPrefs[10] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKFOB2", "Fob Action 2", ((int)basicLockConfigAclPrefs[11] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKFOB3", "Fob Action 3", ((int)basicLockConfigAclPrefs[12] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKSGLLCK", "Single Lock", ((int)basicLockConfigAclPrefs[13] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKADVM", "Advertising Mode", ((int)basicLockConfigAclPrefs[14] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKTZID", "Timezone ID", ((int)basicLockConfigAclPrefs[15] == 1), "chk_config_lock"); - printCheckBox("CONFLCKUPOD", "Unlocked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[0] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLPOD", "Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[1] == 1), "chk_config_lock"); - printCheckBox("CONFLCKSLPOD", "Single Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[2] == 1), "chk_config_lock"); - printCheckBox("CONFLCKUTLTOD", "Unlocked To Locked Transition Offset Degrees", ((int)advancedLockConfigAclPrefs[3] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLNGT", "Lock n Go timeout", ((int)advancedLockConfigAclPrefs[4] == 1), "chk_config_lock"); - printCheckBox("CONFLCKSBPA", "Single button press action", ((int)advancedLockConfigAclPrefs[5] == 1), "chk_config_lock"); - printCheckBox("CONFLCKDBPA", "Double button press action", ((int)advancedLockConfigAclPrefs[6] == 1), "chk_config_lock"); - printCheckBox("CONFLCKDC", "Detached cylinder", ((int)advancedLockConfigAclPrefs[7] == 1), "chk_config_lock"); - printCheckBox("CONFLCKBATT", "Battery type", ((int)advancedLockConfigAclPrefs[8] == 1), "chk_config_lock"); - printCheckBox("CONFLCKABTD", "Automatic battery type detection", ((int)advancedLockConfigAclPrefs[9] == 1), "chk_config_lock"); - printCheckBox("CONFLCKUNLD", "Unlatch duration", ((int)advancedLockConfigAclPrefs[10] == 1), "chk_config_lock"); - printCheckBox("CONFLCKALT", "Auto lock timeout", ((int)advancedLockConfigAclPrefs[11] == 1), "chk_config_lock"); - printCheckBox("CONFLCKAUNLD", "Auto unlock disabled", ((int)advancedLockConfigAclPrefs[12] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMENA", "Nightmode enabled", ((int)advancedLockConfigAclPrefs[13] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMST", "Nightmode start time", ((int)advancedLockConfigAclPrefs[14] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMET", "Nightmode end time", ((int)advancedLockConfigAclPrefs[15] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMALENA", "Nightmode auto lock enabled", ((int)advancedLockConfigAclPrefs[16] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMAULD", "Nightmode auto unlock disabled", ((int)advancedLockConfigAclPrefs[17] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMLOS", "Nightmode immediate lock on start", ((int)advancedLockConfigAclPrefs[18] == 1), "chk_config_lock"); - printCheckBox("CONFLCKALENA", "Auto lock enabled", ((int)advancedLockConfigAclPrefs[19] == 1), "chk_config_lock"); - printCheckBox("CONFLCKIALENA", "Immediate auto lock enabled", ((int)advancedLockConfigAclPrefs[20] == 1), "chk_config_lock"); - printCheckBox("CONFLCKAUENA", "Auto update enabled", ((int)advancedLockConfigAclPrefs[21] == 1), "chk_config_lock"); - _response.concat("
ChangeAllowed

"); - _response.concat("
"); + printCheckBox(&response, "CONFLCKUPOD", "Unlocked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[0] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLPOD", "Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[1] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKSLPOD", "Single Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[2] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKUTLTOD", "Unlocked To Locked Transition Offset Degrees", ((int)advancedLockConfigAclPrefs[3] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLNGT", "Lock n Go timeout", ((int)advancedLockConfigAclPrefs[4] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKSBPA", "Single button press action", ((int)advancedLockConfigAclPrefs[5] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKDBPA", "Double button press action", ((int)advancedLockConfigAclPrefs[6] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKDC", "Detached cylinder", ((int)advancedLockConfigAclPrefs[7] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKBATT", "Battery type", ((int)advancedLockConfigAclPrefs[8] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKABTD", "Automatic battery type detection", ((int)advancedLockConfigAclPrefs[9] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKUNLD", "Unlatch duration", ((int)advancedLockConfigAclPrefs[10] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKALT", "Auto lock timeout", ((int)advancedLockConfigAclPrefs[11] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKAUNLD", "Auto unlock disabled", ((int)advancedLockConfigAclPrefs[12] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMENA", "Nightmode enabled", ((int)advancedLockConfigAclPrefs[13] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMST", "Nightmode start time", ((int)advancedLockConfigAclPrefs[14] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMET", "Nightmode end time", ((int)advancedLockConfigAclPrefs[15] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMALENA", "Nightmode auto lock enabled", ((int)advancedLockConfigAclPrefs[16] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMAULD", "Nightmode auto unlock disabled", ((int)advancedLockConfigAclPrefs[17] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMLOS", "Nightmode immediate lock on start", ((int)advancedLockConfigAclPrefs[18] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKALENA", "Auto lock enabled", ((int)advancedLockConfigAclPrefs[19] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKIALENA", "Immediate auto lock enabled", ((int)advancedLockConfigAclPrefs[20] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKAUENA", "Auto update enabled", ((int)advancedLockConfigAclPrefs[21] == 1), "chk_config_lock"); + response.print("
"); + response.print("
"); } if(_nukiOpener != nullptr) { @@ -3009,117 +3023,119 @@ void WebCfgServer::buildAccLvlHtml(AsyncWebServerRequest *request) uint32_t advancedOpenerConfigAclPrefs[20]; _preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs)); - _response.concat("

Nuki Opener Access Control

"); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

Nuki Opener Access Control

"); + response.print(""); + response.print(""); + response.print("
ActionAllowed
"); - printCheckBox("ACLOPNUNLCK", "Activate Ring-to-Open", ((int)aclPrefs[9] == 1), "chk_access_opener"); - printCheckBox("ACLOPNLCK", "Deactivate Ring-to-Open", ((int)aclPrefs[10] == 1), "chk_access_opener"); - printCheckBox("ACLOPNUNLTCH", "Electric Strike Actuation", ((int)aclPrefs[11] == 1), "chk_access_opener"); - printCheckBox("ACLOPNUNLCKCM", "Activate Continuous Mode", ((int)aclPrefs[12] == 1), "chk_access_opener"); - printCheckBox("ACLOPNLCKCM", "Deactivate Continuous Mode", ((int)aclPrefs[13] == 1), "chk_access_opener"); - printCheckBox("ACLOPNFOB1", "Fob Action 1", ((int)aclPrefs[14] == 1), "chk_access_opener"); - printCheckBox("ACLOPNFOB2", "Fob Action 2", ((int)aclPrefs[15] == 1), "chk_access_opener"); - printCheckBox("ACLOPNFOB3", "Fob Action 3", ((int)aclPrefs[16] == 1), "chk_access_opener"); - _response.concat("
ActionAllowed

"); + printCheckBox(&response, "ACLOPNUNLCK", "Activate Ring-to-Open", ((int)aclPrefs[9] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNLCK", "Deactivate Ring-to-Open", ((int)aclPrefs[10] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNUNLTCH", "Electric Strike Actuation", ((int)aclPrefs[11] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNUNLCKCM", "Activate Continuous Mode", ((int)aclPrefs[12] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNLCKCM", "Deactivate Continuous Mode", ((int)aclPrefs[13] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNFOB1", "Fob Action 1", ((int)aclPrefs[14] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNFOB2", "Fob Action 2", ((int)aclPrefs[15] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNFOB3", "Fob Action 3", ((int)aclPrefs[16] == 1), "chk_access_opener"); + response.print("
"); - _response.concat("

Nuki Opener Config Control (Requires PIN to be set)

"); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

Nuki Opener Config Control (Requires PIN to be set)

"); + response.print(""); + response.print(""); + response.print("
ChangeAllowed
"); - printCheckBox("CONFOPNNAME", "Name", ((int)basicOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); - printCheckBox("CONFOPNLAT", "Latitude", ((int)basicOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); - printCheckBox("CONFOPNLONG", "Longitude", ((int)basicOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); - printCheckBox("CONFOPNPRENA", "Pairing enabled", ((int)basicOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); - printCheckBox("CONFOPNBTENA", "Button enabled", ((int)basicOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); - printCheckBox("CONFOPNLEDENA", "LED flash enabled", ((int)basicOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); - printCheckBox("CONFOPNTZOFF", "Timezone offset", ((int)basicOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDSTM", "DST mode", ((int)basicOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); - printCheckBox("CONFOPNFOB1", "Fob Action 1", ((int)basicOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); - printCheckBox("CONFOPNFOB2", "Fob Action 2", ((int)basicOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); - printCheckBox("CONFOPNFOB3", "Fob Action 3", ((int)basicOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); - printCheckBox("CONFOPNOPM", "Operating Mode", ((int)basicOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); - printCheckBox("CONFOPNADVM", "Advertising Mode", ((int)basicOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); - printCheckBox("CONFOPNTZID", "Timezone ID", ((int)basicOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNNAME", "Name", ((int)basicOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNLAT", "Latitude", ((int)basicOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNLONG", "Longitude", ((int)basicOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNPRENA", "Pairing enabled", ((int)basicOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNBTENA", "Button enabled", ((int)basicOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNLEDENA", "LED flash enabled", ((int)basicOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNTZOFF", "Timezone offset", ((int)basicOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDSTM", "DST mode", ((int)basicOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNFOB1", "Fob Action 1", ((int)basicOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNFOB2", "Fob Action 2", ((int)basicOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNFOB3", "Fob Action 3", ((int)basicOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNOPM", "Operating Mode", ((int)basicOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNADVM", "Advertising Mode", ((int)basicOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNTZID", "Timezone ID", ((int)basicOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); - printCheckBox("CONFOPNICID", "Intercom ID", ((int)advancedOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); - printCheckBox("CONFOPNBUSMS", "BUS mode Switch", ((int)advancedOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSCDUR", "Short Circuit Duration", ((int)advancedOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); - printCheckBox("CONFOPNESD", "Eletric Strike Delay", ((int)advancedOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); - printCheckBox("CONFOPNRESD", "Random Electric Strike Delay", ((int)advancedOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); - printCheckBox("CONFOPNESDUR", "Electric Strike Duration", ((int)advancedOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDRTOAR", "Disable RTO after ring", ((int)advancedOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); - printCheckBox("CONFOPNRTOT", "RTO timeout", ((int)advancedOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDRBSUP", "Doorbell suppression", ((int)advancedOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDRBSUPDUR", "Doorbell suppression duration", ((int)advancedOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSRING", "Sound Ring", ((int)advancedOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSOPN", "Sound Open", ((int)advancedOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSRTO", "Sound RTO", ((int)advancedOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSCM", "Sound CM", ((int)advancedOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSCFRM", "Sound confirmation", ((int)advancedOpenerConfigAclPrefs[14] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSLVL", "Sound level", ((int)advancedOpenerConfigAclPrefs[15] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSBPA", "Single button press action", ((int)advancedOpenerConfigAclPrefs[16] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDBPA", "Double button press action", ((int)advancedOpenerConfigAclPrefs[17] == 1), "chk_config_opener"); - printCheckBox("CONFOPNBATT", "Battery type", ((int)advancedOpenerConfigAclPrefs[18] == 1), "chk_config_opener"); - printCheckBox("CONFOPNABTD", "Automatic battery type detection", ((int)advancedOpenerConfigAclPrefs[19] == 1), "chk_config_opener"); - _response.concat("
ChangeAllowed

"); - _response.concat("
"); + printCheckBox(&response, "CONFOPNICID", "Intercom ID", ((int)advancedOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNBUSMS", "BUS mode Switch", ((int)advancedOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSCDUR", "Short Circuit Duration", ((int)advancedOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNESD", "Eletric Strike Delay", ((int)advancedOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNRESD", "Random Electric Strike Delay", ((int)advancedOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNESDUR", "Electric Strike Duration", ((int)advancedOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDRTOAR", "Disable RTO after ring", ((int)advancedOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNRTOT", "RTO timeout", ((int)advancedOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDRBSUP", "Doorbell suppression", ((int)advancedOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDRBSUPDUR", "Doorbell suppression duration", ((int)advancedOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSRING", "Sound Ring", ((int)advancedOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSOPN", "Sound Open", ((int)advancedOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSRTO", "Sound RTO", ((int)advancedOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSCM", "Sound CM", ((int)advancedOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSCFRM", "Sound confirmation", ((int)advancedOpenerConfigAclPrefs[14] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSLVL", "Sound level", ((int)advancedOpenerConfigAclPrefs[15] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSBPA", "Single button press action", ((int)advancedOpenerConfigAclPrefs[16] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDBPA", "Double button press action", ((int)advancedOpenerConfigAclPrefs[17] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNBATT", "Battery type", ((int)advancedOpenerConfigAclPrefs[18] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNABTD", "Automatic battery type detection", ((int)advancedOpenerConfigAclPrefs[19] == 1), "chk_config_opener"); + response.print("
"); + response.print("
"); } - _response.concat("
"); - _response.concat(""); - sendResponse(request); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildNukiConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildNukiConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
"); - _response.concat("

Basic Nuki Configuration

"); - _response.concat(""); - printCheckBox("LOCKENA", "Nuki Lock enabled", _preferences->getBool(preference_lock_enabled), ""); - if(_preferences->getBool(preference_lock_enabled)) printInputField("MQTTPATH", "MQTT Nuki Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180, ""); - printCheckBox("OPENA", "Nuki Opener enabled", _preferences->getBool(preference_opener_enabled), ""); - if(_preferences->getBool(preference_opener_enabled)) printInputField("MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180, ""); - _response.concat("

"); - _response.concat("

Advanced Nuki Configuration

"); - _response.concat(""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

Basic Nuki Configuration

"); + response.print("
"); + printCheckBox(&response, "LOCKENA", "Nuki Lock enabled", _preferences->getBool(preference_lock_enabled), ""); + if(_preferences->getBool(preference_lock_enabled)) printInputField(&response, "MQTTPATH", "MQTT Nuki Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180, ""); + printCheckBox(&response, "OPENA", "Nuki Opener enabled", _preferences->getBool(preference_opener_enabled), ""); + if(_preferences->getBool(preference_opener_enabled)) printInputField(&response, "MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180, ""); + response.print("

"); + response.print("

Advanced Nuki Configuration

"); + response.print(""); - printInputField("LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10, ""); - printInputField("CFGINT", "Query interval configuration (seconds)", _preferences->getInt(preference_query_interval_configuration), 10, ""); - printInputField("BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10, ""); + printInputField(&response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10, ""); + printInputField(&response, "CFGINT", "Query interval configuration (seconds)", _preferences->getInt(preference_query_interval_configuration), 10, ""); + printInputField(&response, "BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10, ""); if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad())) { - printInputField("KPINT", "Query interval keypad (seconds)", _preferences->getInt(preference_query_interval_keypad), 10, ""); + printInputField(&response, "KPINT", "Query interval keypad (seconds)", _preferences->getInt(preference_query_interval_keypad), 10, ""); } - printInputField("NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10, ""); - printInputField("TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10, ""); - if(_preferences->getBool(preference_lock_enabled, true)) printCheckBox("REGAPP", "Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), ""); - if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox("REGAPPOPN", "Opener: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_opener_as_app), ""); - printInputField("RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10, ""); - printInputField("TXPWR", "BLE transmit power in dB (minimum -12, maximum 9)", _preferences->getInt(preference_ble_tx_power, 9), 10, ""); + printInputField(&response, "NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10, ""); + printInputField(&response, "TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10, ""); + if(_preferences->getBool(preference_lock_enabled, true)) printCheckBox(&response, "REGAPP", "Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), ""); + if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox(&response, "REGAPPOPN", "Opener: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_opener_as_app), ""); + printInputField(&response, "RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10, ""); + printInputField(&response, "TXPWR", "BLE transmit power in dB (minimum -12, maximum 9)", _preferences->getInt(preference_ble_tx_power, 9), 10, ""); - _response.concat("
"); - _response.concat("
"); - _response.concat("
"); - _response.concat(""); - sendResponse(request); + response.print(""); + response.print("
"); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildGpioConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildGpioConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
"); - _response.concat("

GPIO Configuration

"); - _response.concat(""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

GPIO Configuration

"); + response.print("
"); std::vector> options; String gpiopreselects = "var gpio = []; "; @@ -3130,129 +3146,131 @@ void WebCfgServer::buildGpioConfigHtml(AsyncWebServerRequest *request) { String pinStr = String(pin); String pinDesc = "Gpio " + pinStr; - printDropDown(pinStr.c_str(), pinDesc.c_str(), "", options, "gpioselect"); + printDropDown(&response, pinStr.c_str(), pinDesc.c_str(), "", options, "gpioselect"); if(std::find(disabledPins.begin(), disabledPins.end(), pin) != disabledPins.end()) gpiopreselects.concat("gpio[" + pinStr + "] = '21';"); else gpiopreselects.concat("gpio[" + pinStr + "] = '" + getPreselectionForGpio(pin) + "';"); } - _response.concat("
"); - _response.concat("
"); - _response.concat("
"); + response.print(""); + response.print("
"); + response.print(""); options = getGpioOptions(); - _response.concat(""); - _response.concat(""); - sendResponse(request); + response.print("'; var gpioselects = document.getElementsByClassName('gpioselect'); for (let i = 0; i < gpioselects.length; i++) { gpioselects[i].options.length = 0; gpioselects[i].innerHTML = gpiooptions; gpioselects[i].value = gpio[gpioselects[i].name]; if(gpioselects[i].value == 21) { gpioselects[i].disabled = true; } }"); + response.print(""); + return response.endSend(); } #ifndef CONFIG_IDF_TARGET_ESP32H2 -void WebCfgServer::buildConfigureWifiHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildConfigureWifiHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("

Wi-Fi

"); - _response.concat("Click confirm to restart ESP into Wi-Fi configuration mode. After restart, connect to ESP access point to reconfigure Wi-Fi.

"); - buildNavigationButton("Confirm", "/wifimanager"); - _response.concat(""); - sendResponse(request); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print("

Wi-Fi

"); + response.print("Click confirm to restart ESP into Wi-Fi configuration mode. After restart, connect to ESP access point to reconfigure Wi-Fi.

"); + buildNavigationButton(&response, "Confirm", "/wifimanager"); + response.print(""); + return response.endSend(); } #endif -void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) { - _response = ""; uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); - buildHtmlHeader(); - _response.concat("

System Information

");
-    _response.concat("------------ NUKI HUB ------------");
-    _response.concat("\nVersion: ");
-    _response.concat(NUKI_HUB_VERSION);
-    _response.concat("\nBuild: ");
-    _response.concat(NUKI_HUB_BUILD);
+    PsychicStreamResponse response(request, "text/plain");
+    response.beginSend();
+    buildHtmlHeader(&response);
+    response.print("

System Information

");
+    response.print("------------ NUKI HUB ------------");
+    response.print("\nVersion: ");
+    response.print(NUKI_HUB_VERSION);
+    response.print("\nBuild: ");
+    response.print(NUKI_HUB_BUILD);
     #ifndef DEBUG_NUKIHUB
-    _response.concat("\nBuild type: Release");
+    response.print("\nBuild type: Release");
     #else
-    _response.concat("\nBuild type: Debug");
+    response.print("\nBuild type: Debug");
     #endif
-    _response.concat("\nBuild date: ");
-    _response.concat(NUKI_HUB_DATE);
-    _response.concat("\nUpdater version: ");
-    _response.concat(_preferences->getString(preference_updater_version, ""));
-    _response.concat("\nUpdater build: ");
-    _response.concat(_preferences->getString(preference_updater_build, ""));
-    _response.concat("\nUpdater build date: ");
-    _response.concat(_preferences->getString(preference_updater_date, ""));
-    _response.concat("\nUptime (min): ");
-    _response.concat(esp_timer_get_time() / 1000 / 1000 / 60);
-    _response.concat("\nConfig version: ");
-    _response.concat(_preferences->getInt(preference_config_version));
-    _response.concat("\nLast restart reason FW: ");
-    _response.concat(getRestartReason());
-    _response.concat("\nLast restart reason ESP: ");
-    _response.concat(getEspRestartReason());
-    _response.concat("\nFree heap: ");
-    _response.concat(esp_get_free_heap_size());
-    _response.concat("\nNetwork task stack high watermark: ");
-    _response.concat(uxTaskGetStackHighWaterMark(networkTaskHandle));
-    _response.concat("\nNuki task stack high watermark: ");
-    _response.concat(uxTaskGetStackHighWaterMark(nukiTaskHandle));
-    _response.concat("\n\n------------ GENERAL SETTINGS ------------");
-    _response.concat("\nNetwork task stack size: ");
-    _response.concat(_preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE));
-    _response.concat("\nNuki task stack size: ");
-    _response.concat(_preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE));
-    _response.concat("\nCheck for updates: ");
-    _response.concat(_preferences->getBool(preference_check_updates, false) ? "Yes" : "No");
-    _response.concat("\nLatest version: ");
-    _response.concat(_preferences->getString(preference_latest_version, ""));
-    _response.concat("\nAllow update from MQTT: ");
-    _response.concat(_preferences->getBool(preference_update_from_mqtt, false) ? "Yes" : "No");
-    _response.concat("\nWeb configurator username: ");
-    _response.concat(_preferences->getString(preference_cred_user, "").length() > 0 ? "***" : "Not set");
-    _response.concat("\nWeb configurator password: ");
-    _response.concat(_preferences->getString(preference_cred_password, "").length() > 0 ? "***" : "Not set");
-    _response.concat("\nWeb configurator enabled: ");
-    _response.concat(_preferences->getBool(preference_webserver_enabled, true) ? "Yes" : "No");
-    _response.concat("\nPublish debug information enabled: ");
-    _response.concat(_preferences->getBool(preference_publish_debug_info, false) ? "Yes" : "No");
-    _response.concat("\nMQTT log enabled: ");
-    _response.concat(_preferences->getBool(preference_mqtt_log_enabled, false) ? "Yes" : "No");
-    _response.concat("\nWebserial enabled: ");
-    _response.concat(_preferences->getBool(preference_webserial_enabled, false) ? "Yes" : "No");
-    _response.concat("\nBootloop protection enabled: ");
-    _response.concat(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Yes" : "No");
-    _response.concat("\n\n------------ NETWORK ------------");
-    _response.concat("\nNetwork device: ");
-    _response.concat(_network->networkDeviceName());
-    _response.concat("\nNetwork connected: ");
-    _response.concat(_network->isConnected() ? "Yes" : "No");
+    response.print("\nBuild date: ");
+    response.print(NUKI_HUB_DATE);
+    response.print("\nUpdater version: ");
+    response.print(_preferences->getString(preference_updater_version, ""));
+    response.print("\nUpdater build: ");
+    response.print(_preferences->getString(preference_updater_build, ""));
+    response.print("\nUpdater build date: ");
+    response.print(_preferences->getString(preference_updater_date, ""));
+    response.print("\nUptime (min): ");
+    response.print(esp_timer_get_time() / 1000 / 1000 / 60);
+    response.print("\nConfig version: ");
+    response.print(_preferences->getInt(preference_config_version));
+    response.print("\nLast restart reason FW: ");
+    response.print(getRestartReason());
+    response.print("\nLast restart reason ESP: ");
+    response.print(getEspRestartReason());
+    response.print("\nFree heap: ");
+    response.print(esp_get_free_heap_size());
+    response.print("\nNetwork task stack high watermark: ");
+    response.print(uxTaskGetStackHighWaterMark(networkTaskHandle));
+    response.print("\nNuki task stack high watermark: ");
+    response.print(uxTaskGetStackHighWaterMark(nukiTaskHandle));
+    response.print("\n\n------------ GENERAL SETTINGS ------------");
+    response.print("\nNetwork task stack size: ");
+    response.print(_preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE));
+    response.print("\nNuki task stack size: ");
+    response.print(_preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE));
+    response.print("\nCheck for updates: ");
+    response.print(_preferences->getBool(preference_check_updates, false) ? "Yes" : "No");
+    response.print("\nLatest version: ");
+    response.print(_preferences->getString(preference_latest_version, ""));
+    response.print("\nAllow update from MQTT: ");
+    response.print(_preferences->getBool(preference_update_from_mqtt, false) ? "Yes" : "No");
+    response.print("\nWeb configurator username: ");
+    response.print(_preferences->getString(preference_cred_user, "").length() > 0 ? "***" : "Not set");
+    response.print("\nWeb configurator password: ");
+    response.print(_preferences->getString(preference_cred_password, "").length() > 0 ? "***" : "Not set");
+    response.print("\nWeb configurator enabled: ");
+    response.print(_preferences->getBool(preference_webserver_enabled, true) ? "Yes" : "No");
+    response.print("\nPublish debug information enabled: ");
+    response.print(_preferences->getBool(preference_publish_debug_info, false) ? "Yes" : "No");
+    response.print("\nMQTT log enabled: ");
+    response.print(_preferences->getBool(preference_mqtt_log_enabled, false) ? "Yes" : "No");
+    response.print("\nWebserial enabled: ");
+    response.print(_preferences->getBool(preference_webserial_enabled, false) ? "Yes" : "No");
+    response.print("\nBootloop protection enabled: ");
+    response.print(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Yes" : "No");
+    response.print("\n\n------------ NETWORK ------------");
+    response.print("\nNetwork device: ");
+    response.print(_network->networkDeviceName());
+    response.print("\nNetwork connected: ");
+    response.print(_network->isConnected() ? "Yes" : "No");
     if(_network->isConnected())
     {
-        _response.concat("\nIP Address: ");
-        _response.concat(_network->localIP());
+        response.print("\nIP Address: ");
+        response.print(_network->localIP());
 
         if(_network->networkDeviceName() == "Built-in Wi-Fi")
         {
             #ifndef CONFIG_IDF_TARGET_ESP32H2
-            _response.concat("\nSSID: ");
-            _response.concat(WiFi.SSID());
-            _response.concat("\nBSSID of AP: ");
-            _response.concat(_network->networkBSSID());
-            _response.concat("\nESP32 MAC address: ");
-            _response.concat(WiFi.macAddress());
+            response.print("\nSSID: ");
+            response.print(WiFi.SSID());
+            response.print("\nBSSID of AP: ");
+            response.print(_network->networkBSSID());
+            response.print("\nESP32 MAC address: ");
+            response.print(WiFi.macAddress());
             #endif
         }
         else
@@ -3260,276 +3278,276 @@ void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request)
             //Ethernet info
         }
     }
-    _response.concat("\n\n------------ NETWORK SETTINGS ------------");
-    _response.concat("\nNuki Hub hostname: ");
-    _response.concat(_preferences->getString(preference_hostname, ""));
-    if(_preferences->getBool(preference_ip_dhcp_enabled, true)) _response.concat("\nDHCP enabled: Yes");
+    response.print("\n\n------------ NETWORK SETTINGS ------------");
+    response.print("\nNuki Hub hostname: ");
+    response.print(_preferences->getString(preference_hostname, ""));
+    if(_preferences->getBool(preference_ip_dhcp_enabled, true)) response.print("\nDHCP enabled: Yes");
     else
     {
-        _response.concat("\nDHCP enabled: No");
-        _response.concat("\nStatic IP address: ");
-        _response.concat(_preferences->getString(preference_ip_address, ""));
-        _response.concat("\nStatic IP subnet: ");
-        _response.concat(_preferences->getString(preference_ip_subnet, ""));
-        _response.concat("\nStatic IP gateway: ");
-        _response.concat(_preferences->getString(preference_ip_gateway, ""));
-        _response.concat("\nStatic IP DNS server: ");
-        _response.concat(_preferences->getString(preference_ip_dns_server, ""));
+        response.print("\nDHCP enabled: No");
+        response.print("\nStatic IP address: ");
+        response.print(_preferences->getString(preference_ip_address, ""));
+        response.print("\nStatic IP subnet: ");
+        response.print(_preferences->getString(preference_ip_subnet, ""));
+        response.print("\nStatic IP gateway: ");
+        response.print(_preferences->getString(preference_ip_gateway, ""));
+        response.print("\nStatic IP DNS server: ");
+        response.print(_preferences->getString(preference_ip_dns_server, ""));
     }
 
     #ifndef CONFIG_IDF_TARGET_ESP32H2
-    _response.concat("\nFallback to Wi-Fi / Wi-Fi config portal disabled: ");
-    _response.concat(_preferences->getBool(preference_network_wifi_fallback_disabled, false) ? "Yes" : "No");
+    response.print("\nFallback to Wi-Fi / Wi-Fi config portal disabled: ");
+    response.print(_preferences->getBool(preference_network_wifi_fallback_disabled, false) ? "Yes" : "No");
     if(_network->networkDeviceName() == "Built-in Wi-Fi")
     {
-        _response.concat("\nConnect to AP with the best signal enabled: ");
-        _response.concat(_preferences->getBool(preference_find_best_rssi, false) ? "Yes" : "No");
-        _response.concat("\nRSSI Publish interval (s): ");
+        response.print("\nConnect to AP with the best signal enabled: ");
+        response.print(_preferences->getBool(preference_find_best_rssi, false) ? "Yes" : "No");
+        response.print("\nRSSI Publish interval (s): ");
 
-        if(_preferences->getInt(preference_rssi_publish_interval, 60) < 0) _response.concat("Disabled");
-        else _response.concat(_preferences->getInt(preference_rssi_publish_interval, 60));
+        if(_preferences->getInt(preference_rssi_publish_interval, 60) < 0) response.print("Disabled");
+        else response.print(_preferences->getInt(preference_rssi_publish_interval, 60));
     }
     #endif
-    _response.concat("\nRestart ESP32 on network disconnect enabled: ");
-    _response.concat(_preferences->getBool(preference_restart_on_disconnect, false) ? "Yes" : "No");
-    _response.concat("\nReconnect network on MQTT connection failure enabled: ");
-    _response.concat(_preferences->getBool(preference_recon_netw_on_mqtt_discon, false) ? "Yes" : "No");
-    _response.concat("\nMQTT Timeout until restart (s): ");
-    if(_preferences->getInt(preference_network_timeout, 60) < 0) _response.concat("Disabled");
-    else _response.concat(_preferences->getInt(preference_network_timeout, 60));
-    _response.concat("\n\n------------ MQTT ------------");
-    _response.concat("\nMQTT connected: ");
-    _response.concat(_network->mqttConnectionState() > 0 ? "Yes" : "No");
-    _response.concat("\nMQTT broker address: ");
-    _response.concat(_preferences->getString(preference_mqtt_broker, ""));
-    _response.concat("\nMQTT broker port: ");
-    _response.concat(_preferences->getInt(preference_mqtt_broker_port, 1883));
-    _response.concat("\nMQTT username: ");
-    _response.concat(_preferences->getString(preference_mqtt_user, "").length() > 0 ? "***" : "Not set");
-    _response.concat("\nMQTT password: ");
-    _response.concat(_preferences->getString(preference_mqtt_password, "").length() > 0 ? "***" : "Not set");
+    response.print("\nRestart ESP32 on network disconnect enabled: ");
+    response.print(_preferences->getBool(preference_restart_on_disconnect, false) ? "Yes" : "No");
+    response.print("\nReconnect network on MQTT connection failure enabled: ");
+    response.print(_preferences->getBool(preference_recon_netw_on_mqtt_discon, false) ? "Yes" : "No");
+    response.print("\nMQTT Timeout until restart (s): ");
+    if(_preferences->getInt(preference_network_timeout, 60) < 0) response.print("Disabled");
+    else response.print(_preferences->getInt(preference_network_timeout, 60));
+    response.print("\n\n------------ MQTT ------------");
+    response.print("\nMQTT connected: ");
+    response.print(_network->mqttConnectionState() > 0 ? "Yes" : "No");
+    response.print("\nMQTT broker address: ");
+    response.print(_preferences->getString(preference_mqtt_broker, ""));
+    response.print("\nMQTT broker port: ");
+    response.print(_preferences->getInt(preference_mqtt_broker_port, 1883));
+    response.print("\nMQTT username: ");
+    response.print(_preferences->getString(preference_mqtt_user, "").length() > 0 ? "***" : "Not set");
+    response.print("\nMQTT password: ");
+    response.print(_preferences->getString(preference_mqtt_password, "").length() > 0 ? "***" : "Not set");
     if(_preferences->getBool(preference_lock_enabled, true))
     {
-        _response.concat("\nMQTT lock base topic: ");
-        _response.concat(_preferences->getString(preference_mqtt_lock_path, ""));
+        response.print("\nMQTT lock base topic: ");
+        response.print(_preferences->getString(preference_mqtt_lock_path, ""));
     }
     if(_preferences->getBool(preference_opener_enabled, false))
     {
-        _response.concat("\nMQTT opener base topic: ");
-        _response.concat(_preferences->getString(preference_mqtt_lock_path, ""));
+        response.print("\nMQTT opener base topic: ");
+        response.print(_preferences->getString(preference_mqtt_lock_path, ""));
     }
-    _response.concat("\nMQTT SSL CA: ");
-    _response.concat(_preferences->getString(preference_mqtt_ca, "").length() > 0 ? "***" : "Not set");
-    _response.concat("\nMQTT SSL CRT: ");
-    _response.concat(_preferences->getString(preference_mqtt_crt, "").length() > 0 ? "***" : "Not set");
-    _response.concat("\nMQTT SSL Key: ");
-    _response.concat(_preferences->getString(preference_mqtt_key, "").length() > 0 ? "***" : "Not set");
-    _response.concat("\n\n------------ BLUETOOTH ------------");
-    _response.concat("\nBluetooth TX power (dB): ");
-    _response.concat(_preferences->getInt(preference_ble_tx_power, 9));
-    _response.concat("\nBluetooth command nr of retries: ");
-    _response.concat(_preferences->getInt(preference_command_nr_of_retries, 3));
-    _response.concat("\nBluetooth command retry delay (ms): ");
-    _response.concat(_preferences->getInt(preference_command_retry_delay, 100));
-    _response.concat("\nSeconds until reboot when no BLE beacons recieved: ");
-    _response.concat(_preferences->getInt(preference_restart_ble_beacon_lost, 60));
-    _response.concat("\n\n------------ QUERY / PUBLISH SETTINGS ------------");
-    _response.concat("\nLock/Opener state query interval (s): ");
-    _response.concat(_preferences->getInt(preference_query_interval_lockstate, 1800));
-    _response.concat("\nPublish Nuki device authorization log: ");
-    _response.concat(_preferences->getBool(preference_publish_authdata, false) ? "Yes" : "No");
-    _response.concat("\nMax authorization log entries to retrieve: ");
-    _response.concat(_preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG));
-    _response.concat("\nBattery state query interval (s): ");
-    _response.concat(_preferences->getInt(preference_query_interval_battery, 1800));
-    _response.concat("\nMost non-JSON MQTT topics disabled: ");
-    _response.concat(_preferences->getBool(preference_disable_non_json, false) ? "Yes" : "No");
-    _response.concat("\nPublish Nuki device config: ");
-    _response.concat(_preferences->getBool(preference_conf_info_enabled, false) ? "Yes" : "No");
-    _response.concat("\nConfig query interval (s): ");
-    _response.concat(_preferences->getInt(preference_query_interval_configuration, 3600));
-    _response.concat("\nPublish Keypad info: ");
-    _response.concat(_preferences->getBool(preference_keypad_info_enabled, false) ? "Yes" : "No");
-    _response.concat("\nKeypad query interval (s): ");
-    _response.concat(_preferences->getInt(preference_query_interval_keypad, 1800));
-    _response.concat("\nEnable Keypad control: ");
-    _response.concat(_preferences->getBool(preference_keypad_control_enabled, false) ? "Yes" : "No");
-    _response.concat("\nPublish Keypad topic per entry: ");
-    _response.concat(_preferences->getBool(preference_keypad_topic_per_entry, false) ? "Yes" : "No");
-    _response.concat("\nPublish Keypad codes: ");
-    _response.concat(_preferences->getBool(preference_keypad_publish_code, false) ? "Yes" : "No");
-    _response.concat("\nMax keypad entries to retrieve: ");
-    _response.concat(_preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD));
-    _response.concat("\nPublish timecontrol info: ");
-    _response.concat(_preferences->getBool(preference_timecontrol_info_enabled, false) ? "Yes" : "No");
-    _response.concat("\nKeypad query interval (s): ");
-    _response.concat(_preferences->getInt(preference_query_interval_keypad, 1800));
-    _response.concat("\nEnable timecontrol control: ");
-    _response.concat(_preferences->getBool(preference_timecontrol_control_enabled, false) ? "Yes" : "No");
-    _response.concat("\nPublish timecontrol topic per entry: ");
-    _response.concat(_preferences->getBool(preference_timecontrol_topic_per_entry, false) ? "Yes" : "No");
-    _response.concat("\nMax timecontrol entries to retrieve: ");
-    _response.concat(_preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL));
-    _response.concat("\n\n------------ HOME ASSISTANT ------------");
-    _response.concat("\nHome Assistant auto discovery enabled: ");
+    response.print("\nMQTT SSL CA: ");
+    response.print(_preferences->getString(preference_mqtt_ca, "").length() > 0 ? "***" : "Not set");
+    response.print("\nMQTT SSL CRT: ");
+    response.print(_preferences->getString(preference_mqtt_crt, "").length() > 0 ? "***" : "Not set");
+    response.print("\nMQTT SSL Key: ");
+    response.print(_preferences->getString(preference_mqtt_key, "").length() > 0 ? "***" : "Not set");
+    response.print("\n\n------------ BLUETOOTH ------------");
+    response.print("\nBluetooth TX power (dB): ");
+    response.print(_preferences->getInt(preference_ble_tx_power, 9));
+    response.print("\nBluetooth command nr of retries: ");
+    response.print(_preferences->getInt(preference_command_nr_of_retries, 3));
+    response.print("\nBluetooth command retry delay (ms): ");
+    response.print(_preferences->getInt(preference_command_retry_delay, 100));
+    response.print("\nSeconds until reboot when no BLE beacons recieved: ");
+    response.print(_preferences->getInt(preference_restart_ble_beacon_lost, 60));
+    response.print("\n\n------------ QUERY / PUBLISH SETTINGS ------------");
+    response.print("\nLock/Opener state query interval (s): ");
+    response.print(_preferences->getInt(preference_query_interval_lockstate, 1800));
+    response.print("\nPublish Nuki device authorization log: ");
+    response.print(_preferences->getBool(preference_publish_authdata, false) ? "Yes" : "No");
+    response.print("\nMax authorization log entries to retrieve: ");
+    response.print(_preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG));
+    response.print("\nBattery state query interval (s): ");
+    response.print(_preferences->getInt(preference_query_interval_battery, 1800));
+    response.print("\nMost non-JSON MQTT topics disabled: ");
+    response.print(_preferences->getBool(preference_disable_non_json, false) ? "Yes" : "No");
+    response.print("\nPublish Nuki device config: ");
+    response.print(_preferences->getBool(preference_conf_info_enabled, false) ? "Yes" : "No");
+    response.print("\nConfig query interval (s): ");
+    response.print(_preferences->getInt(preference_query_interval_configuration, 3600));
+    response.print("\nPublish Keypad info: ");
+    response.print(_preferences->getBool(preference_keypad_info_enabled, false) ? "Yes" : "No");
+    response.print("\nKeypad query interval (s): ");
+    response.print(_preferences->getInt(preference_query_interval_keypad, 1800));
+    response.print("\nEnable Keypad control: ");
+    response.print(_preferences->getBool(preference_keypad_control_enabled, false) ? "Yes" : "No");
+    response.print("\nPublish Keypad topic per entry: ");
+    response.print(_preferences->getBool(preference_keypad_topic_per_entry, false) ? "Yes" : "No");
+    response.print("\nPublish Keypad codes: ");
+    response.print(_preferences->getBool(preference_keypad_publish_code, false) ? "Yes" : "No");
+    response.print("\nMax keypad entries to retrieve: ");
+    response.print(_preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD));
+    response.print("\nPublish timecontrol info: ");
+    response.print(_preferences->getBool(preference_timecontrol_info_enabled, false) ? "Yes" : "No");
+    response.print("\nKeypad query interval (s): ");
+    response.print(_preferences->getInt(preference_query_interval_keypad, 1800));
+    response.print("\nEnable timecontrol control: ");
+    response.print(_preferences->getBool(preference_timecontrol_control_enabled, false) ? "Yes" : "No");
+    response.print("\nPublish timecontrol topic per entry: ");
+    response.print(_preferences->getBool(preference_timecontrol_topic_per_entry, false) ? "Yes" : "No");
+    response.print("\nMax timecontrol entries to retrieve: ");
+    response.print(_preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL));
+    response.print("\n\n------------ HOME ASSISTANT ------------");
+    response.print("\nHome Assistant auto discovery enabled: ");
     if(_preferences->getString(preference_mqtt_hass_discovery, "").length() > 0)
     {
-        _response.concat("Yes");
-        _response.concat("\nHome Assistant auto discovery topic: ");
-        _response.concat(_preferences->getString(preference_mqtt_hass_discovery, "") + "/");
-        _response.concat("\nNuki Hub configuration URL for HA: ");
-        _response.concat(_preferences->getString(preference_mqtt_hass_cu_url, "").length() > 0 ? _preferences->getString(preference_mqtt_hass_cu_url, "") : "http://" + _network->localIP());
+        response.print("Yes");
+        response.print("\nHome Assistant auto discovery topic: ");
+        response.print(_preferences->getString(preference_mqtt_hass_discovery, "") + "/");
+        response.print("\nNuki Hub configuration URL for HA: ");
+        response.print(_preferences->getString(preference_mqtt_hass_cu_url, "").length() > 0 ? _preferences->getString(preference_mqtt_hass_cu_url, "") : "http://" + _network->localIP());
     }
-    else _response.concat("No");
-    _response.concat("\n\n------------ NUKI LOCK ------------");
-    if(_nuki == nullptr || !_preferences->getBool(preference_lock_enabled, true)) _response.concat("\nLock enabled: No");
+    else response.print("No");
+    response.print("\n\n------------ NUKI LOCK ------------");
+    if(_nuki == nullptr || !_preferences->getBool(preference_lock_enabled, true)) response.print("\nLock enabled: No");
     else
     {
-        _response.concat("\nLock enabled: Yes");
-        _response.concat("\nPaired: ");
-        _response.concat(_nuki->isPaired() ? "Yes" : "No");
-        _response.concat("\nNuki Hub device ID: ");
-        _response.concat(_preferences->getUInt(preference_device_id_lock, 0));
-        _response.concat("\nNuki device ID: ");
-        _response.concat(_preferences->getUInt(preference_nuki_id_lock, 0) > 0 ? "***" : "Not set");
-        _response.concat("\nFirmware version: ");
-        _response.concat(_nuki->firmwareVersion().c_str());
-        _response.concat("\nHardware version: ");
-        _response.concat(_nuki->hardwareVersion().c_str());
-        _response.concat("\nValid PIN set: ");
-        _response.concat(_nuki->isPaired() ? _nuki->isPinValid() ? "Yes" : "No" : "-");
-        _response.concat("\nHas door sensor: ");
-        _response.concat(_nuki->hasDoorSensor() ? "Yes" : "No");
-        _response.concat("\nHas keypad: ");
-        _response.concat(_nuki->hasKeypad() ? "Yes" : "No");
+        response.print("\nLock enabled: Yes");
+        response.print("\nPaired: ");
+        response.print(_nuki->isPaired() ? "Yes" : "No");
+        response.print("\nNuki Hub device ID: ");
+        response.print(_preferences->getUInt(preference_device_id_lock, 0));
+        response.print("\nNuki device ID: ");
+        response.print(_preferences->getUInt(preference_nuki_id_lock, 0) > 0 ? "***" : "Not set");
+        response.print("\nFirmware version: ");
+        response.print(_nuki->firmwareVersion().c_str());
+        response.print("\nHardware version: ");
+        response.print(_nuki->hardwareVersion().c_str());
+        response.print("\nValid PIN set: ");
+        response.print(_nuki->isPaired() ? _nuki->isPinValid() ? "Yes" : "No" : "-");
+        response.print("\nHas door sensor: ");
+        response.print(_nuki->hasDoorSensor() ? "Yes" : "No");
+        response.print("\nHas keypad: ");
+        response.print(_nuki->hasKeypad() ? "Yes" : "No");
         if(_nuki->hasKeypad())
         {
-            _response.concat("\nKeypad highest entries count: ");
-            _response.concat(_preferences->getInt(preference_lock_max_keypad_code_count, 0));
+            response.print("\nKeypad highest entries count: ");
+            response.print(_preferences->getInt(preference_lock_max_keypad_code_count, 0));
         }
-        _response.concat("\nTimecontrol highest entries count: ");
-        _response.concat(_preferences->getInt(preference_lock_max_timecontrol_entry_count, 0));
-        _response.concat("\nRegister as: ");
-        _response.concat(_preferences->getBool(preference_register_as_app, false) ? "App" : "Bridge");
-        _response.concat("\n\n------------ HYBRID MODE ------------");
-        if(!_preferences->getBool(preference_official_hybrid, false)) _response.concat("\nHybrid mode enabled: No");
+        response.print("\nTimecontrol highest entries count: ");
+        response.print(_preferences->getInt(preference_lock_max_timecontrol_entry_count, 0));
+        response.print("\nRegister as: ");
+        response.print(_preferences->getBool(preference_register_as_app, false) ? "App" : "Bridge");
+        response.print("\n\n------------ HYBRID MODE ------------");
+        if(!_preferences->getBool(preference_official_hybrid, false)) response.print("\nHybrid mode enabled: No");
         else
         {
-            _response.concat("\nHybrid mode enabled: Yes");
-            _response.concat("\nHybrid mode connected: ");
-            _response.concat(_nuki->offConnected() ? "Yes": "No");
-            _response.concat("\nSending actions through official MQTT enabled: ");
-            _response.concat(_preferences->getBool(preference_official_hybrid_actions, false) ? "Yes" : "No");
+            response.print("\nHybrid mode enabled: Yes");
+            response.print("\nHybrid mode connected: ");
+            response.print(_nuki->offConnected() ? "Yes": "No");
+            response.print("\nSending actions through official MQTT enabled: ");
+            response.print(_preferences->getBool(preference_official_hybrid_actions, false) ? "Yes" : "No");
             /* NOT IMPLEMENTED (YET?)
             if(_preferences->getBool(preference_official_hybrid_actions, false))
             {
-                _response.concat("\nRetry actions through BLE enabled: ");
-                _response.concat(_preferences->getBool(preference_official_hybrid_retry, false) ? "Yes" : "No");
+                response.print("\nRetry actions through BLE enabled: ");
+                response.print(_preferences->getBool(preference_official_hybrid_retry, false) ? "Yes" : "No");
             }
             */
-            _response.concat("\nTime between status updates when official MQTT is offline (s): ");
-            _response.concat(_preferences->getInt(preference_query_interval_hybrid_lockstate, 600));
+            response.print("\nTime between status updates when official MQTT is offline (s): ");
+            response.print(_preferences->getInt(preference_query_interval_hybrid_lockstate, 600));
         }
         uint32_t basicLockConfigAclPrefs[16];
         _preferences->getBytes(preference_conf_lock_basic_acl, &basicLockConfigAclPrefs, sizeof(basicLockConfigAclPrefs));
         uint32_t advancedLockConfigAclPrefs[22];
         _preferences->getBytes(preference_conf_lock_advanced_acl, &advancedLockConfigAclPrefs, sizeof(advancedLockConfigAclPrefs));
-        _response.concat("\n\n------------ NUKI LOCK ACL ------------");
-        _response.concat("\nLock: ");
-        _response.concat((int)aclPrefs[0] ? "Allowed" : "Disallowed");
-        _response.concat("\nUnlock: ");
-        _response.concat((int)aclPrefs[1] ? "Allowed" : "Disallowed");
-        _response.concat("\nUnlatch: ");
-        _response.concat((int)aclPrefs[2] ? "Allowed" : "Disallowed");
-        _response.concat("\nLock N Go: ");
-        _response.concat((int)aclPrefs[3] ? "Allowed" : "Disallowed");
-        _response.concat("\nLock N Go Unlatch: ");
-        _response.concat((int)aclPrefs[4] ? "Allowed" : "Disallowed");
-        _response.concat("\nFull Lock: ");
-        _response.concat((int)aclPrefs[5] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 1: ");
-        _response.concat((int)aclPrefs[6] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 2: ");
-        _response.concat((int)aclPrefs[7] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 3: ");
-        _response.concat((int)aclPrefs[8] ? "Allowed" : "Disallowed");
-        _response.concat("\n\n------------ NUKI LOCK CONFIG ACL ------------");
-        _response.concat("\nName: ");
-        _response.concat((int)basicLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
-        _response.concat("\nLatitude: ");
-        _response.concat((int)basicLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
-        _response.concat("\nLongitude: ");
-        _response.concat((int)basicLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
-        _response.concat("\nAuto Unlatch: ");
-        _response.concat((int)basicLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
-        _response.concat("\nPairing enabled: ");
-        _response.concat((int)basicLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
-        _response.concat("\nButton enabled: ");
-        _response.concat((int)basicLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
-        _response.concat("\nLED flash enabled: ");
-        _response.concat((int)basicLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
-        _response.concat("\nLED brightness: ");
-        _response.concat((int)basicLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
-        _response.concat("\nTimezone offset: ");
-        _response.concat((int)basicLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
-        _response.concat("\nDST mode: ");
-        _response.concat((int)basicLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 1: ");
-        _response.concat((int)basicLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 2: ");
-        _response.concat((int)basicLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 3: ");
-        _response.concat((int)basicLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
-        _response.concat("\nSingle Lock: ");
-        _response.concat((int)basicLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
-        _response.concat("\nAdvertising Mode: ");
-        _response.concat((int)basicLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
-        _response.concat("\nTimezone ID: ");
-        _response.concat((int)basicLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
-        _response.concat("\nUnlocked Position Offset Degrees: ");
-        _response.concat((int)advancedLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
-        _response.concat("\nLocked Position Offset Degrees: ");
-        _response.concat((int)advancedLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
-        _response.concat("\nSingle Locked Position Offset Degrees: ");
-        _response.concat((int)advancedLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
-        _response.concat("\nUnlocked To Locked Transition Offset Degrees: ");
-        _response.concat((int)advancedLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
-        _response.concat("\nLock n Go timeout: ");
-        _response.concat((int)advancedLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
-        _response.concat("\nSingle button press action: ");
-        _response.concat((int)advancedLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
-        _response.concat("\nDouble button press action: ");
-        _response.concat((int)advancedLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
-        _response.concat("\nDetached cylinder: ");
-        _response.concat((int)advancedLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
-        _response.concat("\nBattery type: ");
-        _response.concat((int)advancedLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
-        _response.concat("\nAutomatic battery type detection: ");
-        _response.concat((int)advancedLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
-        _response.concat("\nUnlatch duration: ");
-        _response.concat((int)advancedLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
-        _response.concat("\nAuto lock timeout: ");
-        _response.concat((int)advancedLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
-        _response.concat("\nAuto unlock disabled: ");
-        _response.concat((int)advancedLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
-        _response.concat("\nNightmode enabled: ");
-        _response.concat((int)advancedLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
-        _response.concat("\nNightmode start time: ");
-        _response.concat((int)advancedLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
-        _response.concat("\nNightmode end time: ");
-        _response.concat((int)advancedLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
-        _response.concat("\nNightmode auto lock enabled: ");
-        _response.concat((int)advancedLockConfigAclPrefs[16] ? "Allowed" : "Disallowed");
-        _response.concat("\nNightmode auto unlock disabled: ");
-        _response.concat((int)advancedLockConfigAclPrefs[17] ? "Allowed" : "Disallowed");
-        _response.concat("\nNightmode immediate lock on start: ");
-        _response.concat((int)advancedLockConfigAclPrefs[18] ? "Allowed" : "Disallowed");
-        _response.concat("\nAuto lock enabled: ");
-        _response.concat((int)advancedLockConfigAclPrefs[19] ? "Allowed" : "Disallowed");
-        _response.concat("\nImmediate auto lock enabled: ");
-        _response.concat((int)advancedLockConfigAclPrefs[20] ? "Allowed" : "Disallowed");
-        _response.concat("\nAuto update enabled: ");
-        _response.concat((int)advancedLockConfigAclPrefs[21] ? "Allowed" : "Disallowed");
+        response.print("\n\n------------ NUKI LOCK ACL ------------");
+        response.print("\nLock: ");
+        response.print((int)aclPrefs[0] ? "Allowed" : "Disallowed");
+        response.print("\nUnlock: ");
+        response.print((int)aclPrefs[1] ? "Allowed" : "Disallowed");
+        response.print("\nUnlatch: ");
+        response.print((int)aclPrefs[2] ? "Allowed" : "Disallowed");
+        response.print("\nLock N Go: ");
+        response.print((int)aclPrefs[3] ? "Allowed" : "Disallowed");
+        response.print("\nLock N Go Unlatch: ");
+        response.print((int)aclPrefs[4] ? "Allowed" : "Disallowed");
+        response.print("\nFull Lock: ");
+        response.print((int)aclPrefs[5] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 1: ");
+        response.print((int)aclPrefs[6] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 2: ");
+        response.print((int)aclPrefs[7] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 3: ");
+        response.print((int)aclPrefs[8] ? "Allowed" : "Disallowed");
+        response.print("\n\n------------ NUKI LOCK CONFIG ACL ------------");
+        response.print("\nName: ");
+        response.print((int)basicLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
+        response.print("\nLatitude: ");
+        response.print((int)basicLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
+        response.print("\nLongitude: ");
+        response.print((int)basicLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
+        response.print("\nAuto Unlatch: ");
+        response.print((int)basicLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
+        response.print("\nPairing enabled: ");
+        response.print((int)basicLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
+        response.print("\nButton enabled: ");
+        response.print((int)basicLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
+        response.print("\nLED flash enabled: ");
+        response.print((int)basicLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
+        response.print("\nLED brightness: ");
+        response.print((int)basicLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
+        response.print("\nTimezone offset: ");
+        response.print((int)basicLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
+        response.print("\nDST mode: ");
+        response.print((int)basicLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 1: ");
+        response.print((int)basicLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 2: ");
+        response.print((int)basicLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 3: ");
+        response.print((int)basicLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
+        response.print("\nSingle Lock: ");
+        response.print((int)basicLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
+        response.print("\nAdvertising Mode: ");
+        response.print((int)basicLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
+        response.print("\nTimezone ID: ");
+        response.print((int)basicLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
+        response.print("\nUnlocked Position Offset Degrees: ");
+        response.print((int)advancedLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
+        response.print("\nLocked Position Offset Degrees: ");
+        response.print((int)advancedLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
+        response.print("\nSingle Locked Position Offset Degrees: ");
+        response.print((int)advancedLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
+        response.print("\nUnlocked To Locked Transition Offset Degrees: ");
+        response.print((int)advancedLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
+        response.print("\nLock n Go timeout: ");
+        response.print((int)advancedLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
+        response.print("\nSingle button press action: ");
+        response.print((int)advancedLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
+        response.print("\nDouble button press action: ");
+        response.print((int)advancedLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
+        response.print("\nDetached cylinder: ");
+        response.print((int)advancedLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
+        response.print("\nBattery type: ");
+        response.print((int)advancedLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
+        response.print("\nAutomatic battery type detection: ");
+        response.print((int)advancedLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
+        response.print("\nUnlatch duration: ");
+        response.print((int)advancedLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
+        response.print("\nAuto lock timeout: ");
+        response.print((int)advancedLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
+        response.print("\nAuto unlock disabled: ");
+        response.print((int)advancedLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
+        response.print("\nNightmode enabled: ");
+        response.print((int)advancedLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
+        response.print("\nNightmode start time: ");
+        response.print((int)advancedLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
+        response.print("\nNightmode end time: ");
+        response.print((int)advancedLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
+        response.print("\nNightmode auto lock enabled: ");
+        response.print((int)advancedLockConfigAclPrefs[16] ? "Allowed" : "Disallowed");
+        response.print("\nNightmode auto unlock disabled: ");
+        response.print((int)advancedLockConfigAclPrefs[17] ? "Allowed" : "Disallowed");
+        response.print("\nNightmode immediate lock on start: ");
+        response.print((int)advancedLockConfigAclPrefs[18] ? "Allowed" : "Disallowed");
+        response.print("\nAuto lock enabled: ");
+        response.print((int)advancedLockConfigAclPrefs[19] ? "Allowed" : "Disallowed");
+        response.print("\nImmediate auto lock enabled: ");
+        response.print((int)advancedLockConfigAclPrefs[20] ? "Allowed" : "Disallowed");
+        response.print("\nAuto update enabled: ");
+        response.print((int)advancedLockConfigAclPrefs[21] ? "Allowed" : "Disallowed");
 
         if(_preferences->getBool(preference_show_secrets))
         {
@@ -3543,151 +3561,151 @@ void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request)
             nukiBlePref.getBytes("secretKeyK", secretKeyK, 32);
             nukiBlePref.getBytes("authorizationId", authorizationId, 4);
             nukiBlePref.end();
-            _response.concat("\n\n------------ NUKI LOCK PAIRING ------------");
-            _response.concat("\nBLE Address: ");
+            response.print("\n\n------------ NUKI LOCK PAIRING ------------");
+            response.print("\nBLE Address: ");
             for (int i = 0; i < 6; i++)
             {
                 sprintf(tmp, "%02x", currentBleAddress[i]);
-                _response.concat(tmp);
+                response.print(tmp);
             }
-            _response.concat("\nSecretKeyK: ");
+            response.print("\nSecretKeyK: ");
             for (int i = 0; i < 32; i++)
             {
                 sprintf(tmp, "%02x", secretKeyK[i]);
-                _response.concat(tmp);
+                response.print(tmp);
             }
-            _response.concat("\nAuthorizationId: ");
+            response.print("\nAuthorizationId: ");
             for (int i = 0; i < 4; i++)
             {
                 sprintf(tmp, "%02x", authorizationId[i]);
-                _response.concat(tmp);
+                response.print(tmp);
             }
             uint32_t authorizationIdInt = authorizationId[0] + 256U*authorizationId[1] + 65536U*authorizationId[2] + 16777216U*authorizationId[3];
-            _response.concat("\nAuthorizationId (UINT32_T): ");
-            _response.concat(authorizationIdInt);
+            response.print("\nAuthorizationId (UINT32_T): ");
+            response.print(authorizationIdInt);
         }
     }
 
-    _response.concat("\n\n------------ NUKI OPENER ------------");
-    if(_nukiOpener == nullptr || !_preferences->getBool(preference_opener_enabled, false)) _response.concat("\nOpener enabled: No");
+    response.print("\n\n------------ NUKI OPENER ------------");
+    if(_nukiOpener == nullptr || !_preferences->getBool(preference_opener_enabled, false)) response.print("\nOpener enabled: No");
     else
     {
-        _response.concat("\nOpener enabled: Yes");
-        _response.concat("\nPaired: ");
-        _response.concat(_nukiOpener->isPaired() ? "Yes" : "No");
-        _response.concat("\nNuki Hub device ID: ");
-        _response.concat(_preferences->getUInt(preference_device_id_opener, 0));
-        _response.concat("\nNuki device ID: ");
-        _response.concat(_preferences->getUInt(preference_nuki_id_opener, 0) > 0 ? "***" : "Not set");
-        _response.concat("\nFirmware version: ");
-        _response.concat(_nukiOpener->firmwareVersion().c_str());
-        _response.concat("\nHardware version: ");
-        _response.concat(_nukiOpener->hardwareVersion().c_str());
-        _response.concat("\nOpener valid PIN set: ");
-        _response.concat(_nukiOpener->isPaired() ? _nukiOpener->isPinValid() ? "Yes" : "No" : "-");
-        _response.concat("\nOpener has keypad: ");
-        _response.concat(_nukiOpener->hasKeypad() ? "Yes" : "No");
+        response.print("\nOpener enabled: Yes");
+        response.print("\nPaired: ");
+        response.print(_nukiOpener->isPaired() ? "Yes" : "No");
+        response.print("\nNuki Hub device ID: ");
+        response.print(_preferences->getUInt(preference_device_id_opener, 0));
+        response.print("\nNuki device ID: ");
+        response.print(_preferences->getUInt(preference_nuki_id_opener, 0) > 0 ? "***" : "Not set");
+        response.print("\nFirmware version: ");
+        response.print(_nukiOpener->firmwareVersion().c_str());
+        response.print("\nHardware version: ");
+        response.print(_nukiOpener->hardwareVersion().c_str());
+        response.print("\nOpener valid PIN set: ");
+        response.print(_nukiOpener->isPaired() ? _nukiOpener->isPinValid() ? "Yes" : "No" : "-");
+        response.print("\nOpener has keypad: ");
+        response.print(_nukiOpener->hasKeypad() ? "Yes" : "No");
         if(_nuki->hasKeypad())
         {
-            _response.concat("\nKeypad highest entries count: ");
-            _response.concat(_preferences->getInt(preference_opener_max_keypad_code_count, 0));
+            response.print("\nKeypad highest entries count: ");
+            response.print(_preferences->getInt(preference_opener_max_keypad_code_count, 0));
         }
-        _response.concat("\nTimecontrol highest entries count: ");
-        _response.concat(_preferences->getInt(preference_opener_max_timecontrol_entry_count, 0));
-        _response.concat("\nRegister as: ");
-        _response.concat(_preferences->getBool(preference_register_opener_as_app, false) ? "App" : "Bridge");
-        _response.concat("\nNuki Opener Lock/Unlock action set to Continuous mode in Home Assistant: ");
-        _response.concat(_preferences->getBool(preference_opener_continuous_mode, false) ? "Yes" : "No");
+        response.print("\nTimecontrol highest entries count: ");
+        response.print(_preferences->getInt(preference_opener_max_timecontrol_entry_count, 0));
+        response.print("\nRegister as: ");
+        response.print(_preferences->getBool(preference_register_opener_as_app, false) ? "App" : "Bridge");
+        response.print("\nNuki Opener Lock/Unlock action set to Continuous mode in Home Assistant: ");
+        response.print(_preferences->getBool(preference_opener_continuous_mode, false) ? "Yes" : "No");
         uint32_t basicOpenerConfigAclPrefs[14];
         _preferences->getBytes(preference_conf_opener_basic_acl, &basicOpenerConfigAclPrefs, sizeof(basicOpenerConfigAclPrefs));
         uint32_t advancedOpenerConfigAclPrefs[20];
         _preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs));
-        _response.concat("\n\n------------ NUKI OPENER ACL ------------");
-        _response.concat("\nActivate Ring-to-Open: ");
-        _response.concat((int)aclPrefs[9] ? "Allowed" : "Disallowed");
-        _response.concat("\nDeactivate Ring-to-Open: ");
-        _response.concat((int)aclPrefs[10] ? "Allowed" : "Disallowed");
-        _response.concat("\nElectric Strike Actuation: ");
-        _response.concat((int)aclPrefs[11] ? "Allowed" : "Disallowed");
-        _response.concat("\nActivate Continuous Mode: ");
-        _response.concat((int)aclPrefs[12] ? "Allowed" : "Disallowed");
-        _response.concat("\nDeactivate Continuous Mode: ");
-        _response.concat((int)aclPrefs[13] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 1: ");
-        _response.concat((int)aclPrefs[14] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 2: ");
-        _response.concat((int)aclPrefs[15] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 3: ");
-        _response.concat((int)aclPrefs[16] ? "Allowed" : "Disallowed");
-        _response.concat("\n\n------------ NUKI OPENER CONFIG ACL ------------");
-        _response.concat("\nName: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
-        _response.concat("\nLatitude: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
-        _response.concat("\nLongitude: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
-        _response.concat("\nPairing enabled: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
-        _response.concat("\nButton enabled: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
-        _response.concat("\nLED flash enabled: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
-        _response.concat("\nTimezone offset: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
-        _response.concat("\nDST mode: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 1: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 2: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
-        _response.concat("\nFob Action 3: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
-        _response.concat("\nOperating Mode: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
-        _response.concat("\nAdvertising Mode: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
-        _response.concat("\nTimezone ID: ");
-        _response.concat((int)basicOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
-        _response.concat("\nIntercom ID: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
-        _response.concat("\nBUS mode Switch: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
-        _response.concat("\nShort Circuit Duration: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
-        _response.concat("\nEletric Strike Delay: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
-        _response.concat("\nRandom Electric Strike Delay: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
-        _response.concat("\nElectric Strike Duration: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
-        _response.concat("\nDisable RTO after ring: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
-        _response.concat("\nRTO timeout: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
-        _response.concat("\nDoorbell suppression: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
-        _response.concat("\nDoorbell suppression duration: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
-        _response.concat("\nSound Ring: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
-        _response.concat("\nSound Open: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
-        _response.concat("\nSound RTO: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
-        _response.concat("\nSound CM: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
-        _response.concat("\nSound confirmation: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[14] ? "Allowed" : "Disallowed");
-        _response.concat("\nSound level: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[15] ? "Allowed" : "Disallowed");
-        _response.concat("\nSingle button press action: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[16] ? "Allowed" : "Disallowed");
-        _response.concat("\nDouble button press action: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[17] ? "Allowed" : "Disallowed");
-        _response.concat("\nBattery type: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[18] ? "Allowed" : "Disallowed");
-        _response.concat("\nAutomatic battery type detection: ");
-        _response.concat((int)advancedOpenerConfigAclPrefs[19] ? "Allowed" : "Disallowed");
+        response.print("\n\n------------ NUKI OPENER ACL ------------");
+        response.print("\nActivate Ring-to-Open: ");
+        response.print((int)aclPrefs[9] ? "Allowed" : "Disallowed");
+        response.print("\nDeactivate Ring-to-Open: ");
+        response.print((int)aclPrefs[10] ? "Allowed" : "Disallowed");
+        response.print("\nElectric Strike Actuation: ");
+        response.print((int)aclPrefs[11] ? "Allowed" : "Disallowed");
+        response.print("\nActivate Continuous Mode: ");
+        response.print((int)aclPrefs[12] ? "Allowed" : "Disallowed");
+        response.print("\nDeactivate Continuous Mode: ");
+        response.print((int)aclPrefs[13] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 1: ");
+        response.print((int)aclPrefs[14] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 2: ");
+        response.print((int)aclPrefs[15] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 3: ");
+        response.print((int)aclPrefs[16] ? "Allowed" : "Disallowed");
+        response.print("\n\n------------ NUKI OPENER CONFIG ACL ------------");
+        response.print("\nName: ");
+        response.print((int)basicOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
+        response.print("\nLatitude: ");
+        response.print((int)basicOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
+        response.print("\nLongitude: ");
+        response.print((int)basicOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
+        response.print("\nPairing enabled: ");
+        response.print((int)basicOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
+        response.print("\nButton enabled: ");
+        response.print((int)basicOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
+        response.print("\nLED flash enabled: ");
+        response.print((int)basicOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
+        response.print("\nTimezone offset: ");
+        response.print((int)basicOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
+        response.print("\nDST mode: ");
+        response.print((int)basicOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 1: ");
+        response.print((int)basicOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 2: ");
+        response.print((int)basicOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
+        response.print("\nFob Action 3: ");
+        response.print((int)basicOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
+        response.print("\nOperating Mode: ");
+        response.print((int)basicOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
+        response.print("\nAdvertising Mode: ");
+        response.print((int)basicOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
+        response.print("\nTimezone ID: ");
+        response.print((int)basicOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
+        response.print("\nIntercom ID: ");
+        response.print((int)advancedOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
+        response.print("\nBUS mode Switch: ");
+        response.print((int)advancedOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
+        response.print("\nShort Circuit Duration: ");
+        response.print((int)advancedOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
+        response.print("\nEletric Strike Delay: ");
+        response.print((int)advancedOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
+        response.print("\nRandom Electric Strike Delay: ");
+        response.print((int)advancedOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
+        response.print("\nElectric Strike Duration: ");
+        response.print((int)advancedOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
+        response.print("\nDisable RTO after ring: ");
+        response.print((int)advancedOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
+        response.print("\nRTO timeout: ");
+        response.print((int)advancedOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
+        response.print("\nDoorbell suppression: ");
+        response.print((int)advancedOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
+        response.print("\nDoorbell suppression duration: ");
+        response.print((int)advancedOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
+        response.print("\nSound Ring: ");
+        response.print((int)advancedOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
+        response.print("\nSound Open: ");
+        response.print((int)advancedOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
+        response.print("\nSound RTO: ");
+        response.print((int)advancedOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
+        response.print("\nSound CM: ");
+        response.print((int)advancedOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
+        response.print("\nSound confirmation: ");
+        response.print((int)advancedOpenerConfigAclPrefs[14] ? "Allowed" : "Disallowed");
+        response.print("\nSound level: ");
+        response.print((int)advancedOpenerConfigAclPrefs[15] ? "Allowed" : "Disallowed");
+        response.print("\nSingle button press action: ");
+        response.print((int)advancedOpenerConfigAclPrefs[16] ? "Allowed" : "Disallowed");
+        response.print("\nDouble button press action: ");
+        response.print((int)advancedOpenerConfigAclPrefs[17] ? "Allowed" : "Disallowed");
+        response.print("\nBattery type: ");
+        response.print((int)advancedOpenerConfigAclPrefs[18] ? "Allowed" : "Disallowed");
+        response.print("\nAutomatic battery type detection: ");
+        response.print((int)advancedOpenerConfigAclPrefs[19] ? "Allowed" : "Disallowed");
         if(_preferences->getBool(preference_show_secrets))
         {
             char tmp[16];
@@ -3700,52 +3718,51 @@ void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request)
             nukiBlePref.getBytes("secretKeyK", secretKeyKOpn, 32);
             nukiBlePref.getBytes("authorizationId", authorizationIdOpn, 4);
             nukiBlePref.end();
-            _response.concat("\n\n------------ NUKI OPENER PAIRING ------------");
-            _response.concat("\nBLE Address: ");
+            response.print("\n\n------------ NUKI OPENER PAIRING ------------");
+            response.print("\nBLE Address: ");
             for (int i = 0; i < 6; i++)
             {
                 sprintf(tmp, "%02x", currentBleAddressOpn[i]);
-                _response.concat(tmp);
+                response.print(tmp);
             }
-            _response.concat("\nSecretKeyK: ");
+            response.print("\nSecretKeyK: ");
             for (int i = 0; i < 32; i++)
             {
                 sprintf(tmp, "%02x", secretKeyKOpn[i]);
-                _response.concat(tmp);
+                response.print(tmp);
             }
-            _response.concat("\nAuthorizationId: ");
+            response.print("\nAuthorizationId: ");
             for (int i = 0; i < 4; i++)
             {
                 sprintf(tmp, "%02x", authorizationIdOpn[i]);
-                _response.concat(tmp);
+                response.print(tmp);
             }
         }
     }
 
-    _response.concat("\n\n------------ GPIO ------------\n");
+    response.print("\n\n------------ GPIO ------------\n");
     String gpioStr = "";
     _gpio->getConfigurationText(gpioStr, _gpio->pinConfiguration());
-    _response.concat(gpioStr);
-    _response.concat("
"); - sendResponse(request); + response.print(gpioStr); + response.print("
"); + return response.endSend(); } -void WebCfgServer::processUnpair(AsyncWebServerRequest *request, bool opener) +esp_err_t WebCfgServer::processUnpair(PsychicRequest *request, bool opener) { String value = ""; - if(request->hasParam("CONFIRMTOKEN", true)) + if(request->hasParam("CONFIRMTOKEN")) { - const AsyncWebParameter* p = request->getParam("CONFIRMTOKEN", true); + const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); if(p->value() != "") value = p->value(); } if(value != _confirmCode) { - buildConfirmHtml(request, "Confirm code is invalid.", 3, true); - return; + return buildConfirmHtml(request, "Confirm code is invalid.", 3, true); } - buildConfirmHtml(request, opener ? "Unpairing Nuki Opener and restarting." : "Unpairing Nuki Lock and restarting.", 3, true); + esp_err_t res = buildConfirmHtml(request, opener ? "Unpairing Nuki Opener and restarting." : "Unpairing Nuki Lock and restarting.", 3, true); if(!opener && _nuki != nullptr) { @@ -3759,34 +3776,35 @@ void WebCfgServer::processUnpair(AsyncWebServerRequest *request, bool opener) } waitAndProcess(false, 1000); restartEsp(RestartReason::DeviceUnpaired); + return res; } -void WebCfgServer::processUpdate(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::processUpdate(PsychicRequest *request) { + esp_err_t res; String value = ""; if(request->hasParam("token")) { - const AsyncWebParameter* p = request->getParam("token"); + const PsychicWebParameter* p = request->getParam("token"); if(p->value() != "") value = p->value(); } if(value != _confirmCode) { - buildConfirmHtml(request, "Confirm code is invalid.", 3, true); - return; + return buildConfirmHtml(request, "Confirm code is invalid.", 3, true); } if(request->hasParam("beta")) { if(request->hasParam("debug")) { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG BETA version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG BETA version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL_DBG); _preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL_DBG); } else { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest BETA version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest BETA version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL); } @@ -3795,13 +3813,13 @@ void WebCfgServer::processUpdate(AsyncWebServerRequest *request) { if(request->hasParam("debug")) { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG DEVELOPMENT version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG DEVELOPMENT version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL_DBG); _preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL_DBG); } else { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEVELOPMENT version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEVELOPMENT version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL); } @@ -3810,53 +3828,54 @@ void WebCfgServer::processUpdate(AsyncWebServerRequest *request) { if(request->hasParam("debug")) { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG RELEASE version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG RELEASE version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL_DBG); _preferences->putString(preference_ota_main_url, GITHUB_LATEST_UPDATER_BINARY_URL_DBG); } else { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest RELEASE version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest RELEASE version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL); } } waitAndProcess(true, 1000); restartEsp(RestartReason::OTAReboot); + return res; } -void WebCfgServer::processFactoryReset(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) { + esp_err_t res; String value = ""; - if(request->hasParam("CONFIRMTOKEN", true)) + if(request->hasParam("CONFIRMTOKEN")) { - const AsyncWebParameter* p = request->getParam("CONFIRMTOKEN", true); + const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); if(p->value() != "") value = p->value(); } bool resetWifi = false; if(value.length() == 0 || value != _confirmCode) { - buildConfirmHtml(request, "Confirm code is invalid.", 3, true); - return; + return buildConfirmHtml(request, "Confirm code is invalid.", 3, true); } else { String value2 = ""; - if(request->hasParam("WIFI", true)) + if(request->hasParam("WIFI")) { - const AsyncWebParameter* p = request->getParam("WIFI", true); + const PsychicWebParameter* p = request->getParam("WIFI"); if(p->value() != "") value = p->value(); } if(value2 == "1") { resetWifi = true; - buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener and resetting WiFi.", 3, true); + res = buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener and resetting WiFi.", 3, true); } else { - buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener.", 3, true); + res = buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener.", 3, true); } } @@ -3889,9 +3908,11 @@ void WebCfgServer::processFactoryReset(AsyncWebServerRequest *request) waitAndProcess(false, 3000); restartEsp(RestartReason::NukiHubReset); + return res; } -void WebCfgServer::printInputField(const char *token, +void WebCfgServer::printInputField(PsychicStreamResponse *response, + const char *token, const char *description, const char *value, const size_t& maxLength, @@ -3903,38 +3924,39 @@ void WebCfgServer::printInputField(const char *token, itoa(maxLength, maxLengthStr, 10); - _response.concat(""); - _response.concat(description); + response->print(""); + response->print(description); if(showLengthRestriction) { - _response.concat(" (Max. "); - _response.concat(maxLength); - _response.concat(" characters)"); + response->print(" (Max. "); + response->print(maxLength); + response->print(" characters)"); } - _response.concat(""); - _response.concat("print(""); + response->print("print(" "); + response->print(args); } if(strcmp(value, "") != 0) { - _response.concat(" value=\""); - _response.concat(value); + response->print(" value=\""); + response->print(value); } - _response.concat("\" name=\""); - _response.concat(token); - _response.concat("\" size=\"25\" maxlength=\""); - _response.concat(maxLengthStr); - _response.concat("\"/>"); - _response.concat(""); + response->print("\" name=\""); + response->print(token); + response->print("\" size=\"25\" maxlength=\""); + response->print(maxLengthStr); + response->print("\"/>"); + response->print(""); } -void WebCfgServer::printInputField(const char *token, +void WebCfgServer::printInputField(PsychicStreamResponse *response, + const char *token, const char *description, const int value, size_t maxLength, @@ -3942,32 +3964,33 @@ void WebCfgServer::printInputField(const char *token, { char valueStr[20]; itoa(value, valueStr, 10); - printInputField(token, description, valueStr, maxLength, args); + printInputField(response, token, description, valueStr, maxLength, args); } -void WebCfgServer::printCheckBox(const char *token, const char *description, const bool value, const char *htmlClass) +void WebCfgServer::printCheckBox(PsychicStreamResponse *response, const char *token, const char *description, const bool value, const char *htmlClass) { - _response.concat(""); - _response.concat(description); - _response.concat(""); + response->print(""); + response->print(description); + response->print(""); - _response.concat(""); + response->print("print(token); + response->print("\" value=\"0\""); + response->print("/>"); - _response.concat("print("print(token); - _response.concat("\" class=\""); - _response.concat(htmlClass); + response->print("\" class=\""); + response->print(htmlClass); - _response.concat("\" value=\"1\""); - _response.concat(value ? " checked=\"checked\"" : ""); - _response.concat("/>"); + response->print("\" value=\"1\""); + response->print(value ? " checked=\"checked\"" : ""); + response->print("/>"); } -void WebCfgServer::printTextarea(const char *token, +void WebCfgServer::printTextarea(PsychicStreamResponse *response, + const char *token, const char *description, const char *value, const size_t& maxLength, @@ -3978,106 +4001,106 @@ void WebCfgServer::printTextarea(const char *token, itoa(maxLength, maxLengthStr, 10); - _response.concat(""); - _response.concat(description); + response->print(""); + response->print(description); if(showLengthRestriction) { - _response.concat(" (Max. "); - _response.concat(maxLength); - _response.concat(" characters)"); + response->print(" (Max. "); + response->print(maxLength); + response->print(" characters)"); } - _response.concat(""); - _response.concat(" "); - _response.concat(""); + response->print(" name=\""); + response->print(token); + response->print("\" maxlength=\""); + response->print(maxLengthStr); + response->print("\">"); + response->print(value); + response->print(""); + response->print(""); } -void WebCfgServer::printDropDown(const char *token, const char *description, const String preselectedValue, const std::vector> options, const String className) +void WebCfgServer::printDropDown(PsychicStreamResponse *response, const char *token, const char *description, const String preselectedValue, const std::vector> options, const String className) { - _response.concat(""); - _response.concat(description); - _response.concat(""); + response->print(""); + response->print(description); + response->print(""); - if(className.length() > 0) _response.concat("print(""); - _response.concat(""); + response->print(""); + response->print(""); } -void WebCfgServer::buildNavigationButton(const char *caption, const char *targetPath, const char* labelText) +void WebCfgServer::buildNavigationButton(PsychicStreamResponse *response, const char *caption, const char *targetPath, const char* labelText) { - _response.concat("
"); - _response.concat(" "); - _response.concat(labelText); - _response.concat("
"); + response->print("
print(targetPath); + response->print("\">"); + response->print(" "); + response->print(labelText); + response->print("
"); } -void WebCfgServer::buildNavigationMenuEntry(const char *title, const char *targetPath, const char* warningMessage) +void WebCfgServer::buildNavigationMenuEntry(PsychicStreamResponse *response, const char *title, const char *targetPath, const char* warningMessage) { - _response.concat(""); - _response.concat("
  • "); - _response.concat(title); + response->print("print(targetPath); + response->print("\">"); + response->print("
  • "); + response->print(title); if(strcmp(warningMessage, "") != 0){ - _response.concat(""); - _response.concat(warningMessage); - _response.concat(""); + response->print(""); + response->print(warningMessage); + response->print(""); } - _response.concat("
  • "); + response->print(""); } -void WebCfgServer::printParameter(const char *description, const char *value, const char *link, const char *id) +void WebCfgServer::printParameter(PsychicStreamResponse *response, const char *description, const char *value, const char *link, const char *id) { - _response.concat(""); - _response.concat(""); - _response.concat(description); - _response.concat(""); - if(strcmp(id, "") == 0) _response.concat(""); + response->print(""); + response->print(""); + response->print(description); + response->print(""); + if(strcmp(id, "") == 0) response->print(""); else { - _response.concat(""); + response->print("print(id); + response->print("\">"); } - if(strcmp(link, "") == 0) _response.concat(value); + if(strcmp(link, "") == 0) response->print(value); else { - _response.concat(" "); - _response.concat(value); - _response.concat(""); + response->print("print(link); + response->print("\"> "); + response->print(value); + response->print(""); } - _response.concat(""); - _response.concat(""); + response->print(""); + response->print(""); } diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 583175d..4eb30a8 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -1,9 +1,8 @@ #pragma once #include -#include -#include -#include +#include +#include #include "esp_ota_ops.h" #include "Config.h" @@ -37,9 +36,9 @@ class WebCfgServer { public: #ifndef NUKI_HUB_UPDATER - WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer); + WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer); #else - WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer); + WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer); #endif ~WebCfgServer() = default; @@ -47,34 +46,34 @@ public: private: #ifndef NUKI_HUB_UPDATER - void sendSettings(AsyncWebServerRequest *request); - bool processArgs(AsyncWebServerRequest *request, String& message); - bool processImport(AsyncWebServerRequest *request, String& message); - void processGpioArgs(AsyncWebServerRequest *request); - void buildHtml(AsyncWebServerRequest *request); - void buildAccLvlHtml(AsyncWebServerRequest *request); - void buildCredHtml(AsyncWebServerRequest *request); - void buildImportExportHtml(AsyncWebServerRequest *request); - void buildMqttConfigHtml(AsyncWebServerRequest *request); - void buildStatusHtml(AsyncWebServerRequest *request); - void buildAdvancedConfigHtml(AsyncWebServerRequest *request); - void buildNukiConfigHtml(AsyncWebServerRequest *request); - void buildGpioConfigHtml(AsyncWebServerRequest *request); + esp_err_t sendSettings(PsychicRequest *request); + bool processArgs(PsychicRequest *request, String& message); + bool processImport(PsychicRequest *request, String& message); + void processGpioArgs(PsychicRequest *request); + esp_err_t buildHtml(PsychicRequest *request); + esp_err_t buildAccLvlHtml(PsychicRequest *request); + esp_err_t buildCredHtml(PsychicRequest *request); + esp_err_t buildImportExportHtml(PsychicRequest *request); + esp_err_t buildMqttConfigHtml(PsychicRequest *request); + esp_err_t buildStatusHtml(PsychicRequest *request); + esp_err_t buildAdvancedConfigHtml(PsychicRequest *request); + esp_err_t buildNukiConfigHtml(PsychicRequest *request); + esp_err_t buildGpioConfigHtml(PsychicRequest *request); #ifndef CONFIG_IDF_TARGET_ESP32H2 - void buildConfigureWifiHtml(AsyncWebServerRequest *request); + esp_err_t buildConfigureWifiHtml(PsychicRequest *request); #endif - void buildInfoHtml(AsyncWebServerRequest *request); - void buildCustomNetworkConfigHtml(AsyncWebServerRequest *request); - void processUnpair(AsyncWebServerRequest *request, bool opener); - void processUpdate(AsyncWebServerRequest *request); - void processFactoryReset(AsyncWebServerRequest *request); - void printInputField(const char* token, const char* description, const char* value, const size_t& maxLength, const char* args, const bool& isPassword = false, const bool& showLengthRestriction = false); - void printInputField(const char* token, const char* description, const int value, size_t maxLength, const char* args); - void printCheckBox(const char* token, const char* description, const bool value, const char* htmlClass); - void printTextarea(const char *token, const char *description, const char *value, const size_t& maxLength, const bool& enabled = true, const bool& showLengthRestriction = false); - void printDropDown(const char *token, const char *description, const String preselectedValue, std::vector> options, const String className); - void buildNavigationButton(const char* caption, const char* targetPath, const char* labelText = ""); - void buildNavigationMenuEntry(const char *title, const char *targetPath, const char* warningMessage = ""); + esp_err_t buildInfoHtml(PsychicRequest *request); + esp_err_t buildCustomNetworkConfigHtml(PsychicRequest *request); + esp_err_t processUnpair(PsychicRequest *request, bool opener); + esp_err_t processUpdate(PsychicRequest *request); + esp_err_t processFactoryReset(PsychicRequest *request); + void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const char* value, const size_t& maxLength, const char* args, const bool& isPassword = false, const bool& showLengthRestriction = false); + void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const int value, size_t maxLength, const char* args); + void printCheckBox(PsychicStreamResponse *response, const char* token, const char* description, const bool value, const char* htmlClass); + void printTextarea(PsychicStreamResponse *response, const char *token, const char *description, const char *value, const size_t& maxLength, const bool& enabled = true, const bool& showLengthRestriction = false); + void printDropDown(PsychicStreamResponse *response, const char *token, const char *description, const String preselectedValue, std::vector> options, const String className); + void buildNavigationButton(PsychicStreamResponse *response, const char* caption, const char* targetPath, const char* labelText = ""); + void buildNavigationMenuEntry(PsychicStreamResponse *response, const char *title, const char *targetPath, const char* warningMessage = ""); const std::vector> getNetworkDetectionOptions() const; const std::vector> getGpioOptions() const; @@ -86,8 +85,8 @@ private: String getPreselectionForGpio(const uint8_t& pin); String pinStateToString(uint8_t value); - void printParameter(const char* description, const char* value, const char *link = "", const char *id = ""); - + void printParameter(PsychicStreamResponse *response, const char* description, const char* value, const char *link = "", const char *id = ""); + NukiWrapper* _nuki = nullptr; NukiOpenerWrapper* _nukiOpener = nullptr; Gpio* _gpio = nullptr; @@ -96,21 +95,19 @@ private: bool _rebootRequired = false; #endif - String _response; String generateConfirmCode(); String _confirmCode = "----"; - void buildConfirmHtml(AsyncWebServerRequest *request, const String &message, uint32_t redirectDelay = 5, bool redirect = false); - void buildOtaHtml(AsyncWebServerRequest *request, bool debug = false); - void buildOtaCompletedHtml(AsyncWebServerRequest *request); - void sendCss(AsyncWebServerRequest *request); - void sendFavicon(AsyncWebServerRequest *request); - void buildHtmlHeader(String additionalHeader = ""); + esp_err_t buildConfirmHtml(PsychicRequest *request, const String &message, uint32_t redirectDelay = 5, bool redirect = false); + esp_err_t buildOtaHtml(PsychicRequest *request, bool debug = false); + esp_err_t buildOtaCompletedHtml(PsychicRequest *request); + esp_err_t sendCss(PsychicRequest *request); + esp_err_t sendFavicon(PsychicRequest *request); + void buildHtmlHeader(PsychicStreamResponse *response, String additionalHeader = ""); void waitAndProcess(const bool blocking, const uint32_t duration); - void handleOtaUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); + void handleOtaUpload(PsychicRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); void printProgress(size_t prg, size_t sz); - void sendResponse(AsyncWebServerRequest *request); - - AsyncWebServer* _asyncServer = nullptr; + + PsychicHttpServer* _psychicServer = nullptr; NukiNetwork* _network = nullptr; Preferences* _preferences = nullptr; diff --git a/src/main.cpp b/src/main.cpp index e1ae161..2cd32ca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,11 +19,10 @@ #include "Logger.h" #include "PreferencesKeys.h" #include "RestartReason.h" -#include -#include -#include +#ifdef DEBUG_NUKIHUB #include #include +#endif char log_print_buffer[1024]; @@ -54,7 +53,7 @@ int64_t restartTs = 10 * 1000 * 60000; #endif -AsyncWebServer* asyncServer = nullptr; +PsychicHttpServer* psychicServer = nullptr; NukiNetwork* network = nullptr; WebCfgServer* webCfgServer = nullptr; Preferences* preferences = nullptr; @@ -418,11 +417,11 @@ void setup() if(!doOta) { - asyncServer = new AsyncWebServer(80); - webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, asyncServer); + psychicServer = new PsychicHttpServer; + webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); - asyncServer->onNotFound([](AsyncWebServerRequest* request) { request->redirect("/"); }); - asyncServer->begin(); + psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); + psychicServer->listen(80); } #else Log->print(F("Nuki Hub version ")); @@ -490,24 +489,26 @@ void setup() { if(!doOta) { - asyncServer = new AsyncWebServer(80); + psychicServer = new PsychicHttpServer; if(forceEnableWebServer || preferences->getBool(preference_webserver_enabled, true)) { - webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, asyncServer); + webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); - asyncServer->onNotFound([](AsyncWebServerRequest* request) { request->redirect("/"); }); + psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); } - else asyncServer->onNotFound([](AsyncWebServerRequest* request) { request->redirect("/webserial"); }); + #ifdef DEBUG_NUKIHUB + else psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/webserial"); }); if(preferences->getBool(preference_webserial_enabled, false)) { - WebSerial.setAuthentication(preferences->getString(preference_cred_user), preferences->getString(preference_cred_password)); - WebSerial.begin(asyncServer); - WebSerial.setBuffer(1024); + //WebSerial.setAuthentication(preferences->getString(preference_cred_user), preferences->getString(preference_cred_password)); + //WebSerial.begin(asyncServer); + //WebSerial.setBuffer(1024); } + #endif - asyncServer->begin(); + psychicServer->listen(80); } } #endif diff --git a/updater/platformio.ini b/updater/platformio.ini index 7bf7ec3..8a0d352 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -53,8 +53,7 @@ lib_ignore = SimpleBLE WiFiProv lib_deps = - AsyncTCP=symlink://../lib/AsyncTCP - ESPAsyncWebServer=symlink://../lib/ESPAsyncWebServer + PsychicHttp=symlink://../lib/PsychicHttp WiFiManager=symlink://../lib/WiFiManager monitor_speed = 115200 @@ -86,8 +85,7 @@ board = esp32-h2-devkitm-1 board_build.cmake_extra_args = -DNUKI_TARGET_H2=y lib_deps = - AsyncTCP=symlink://../lib/AsyncTCP - ESPAsyncWebServer=symlink://../lib/ESPAsyncWebServer + PsychicHttp=symlink://../lib/PsychicHttp [env:updater_esp32-solo1] extends = env:updater_esp32 diff --git a/updater/sdkconfig.defaults b/updater/sdkconfig.defaults index e60db16..14d722b 100644 --- a/updater/sdkconfig.defaults +++ b/updater/sdkconfig.defaults @@ -22,4 +22,13 @@ CONFIG_ETH_ENABLED=y CONFIG_ETH_USE_SPI_ETHERNET=y CONFIG_ETH_SPI_ETHERNET_W5500=y CONFIG_ETH_SPI_ETHERNET_DM9051=y -CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y \ No newline at end of file +CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +CONFIG_HTTPD_WS_SUPPORT=y +CONFIG_ESP_HTTPS_SERVER_ENABLE=y \ No newline at end of file From 5e20c89f537f6967c9eb3d98fc5ec1e572906fa7 Mon Sep 17 00:00:00 2001 From: iranl Date: Mon, 26 Aug 2024 23:41:54 +0200 Subject: [PATCH 03/30] PsychicHTTP --- lib/MqttLogger/src/MqttLogger.cpp | 4 +- lib/MqttLogger/src/MqttLogger.h | 2 +- lib/PsychicHttp/src/PsychicHttpServer.cpp | 8 + lib/PsychicHttp/src/PsychicRequest.cpp | 17 + lib/PsychicHttp/src/PsychicRequest.h | 4 +- lib/WiFiManager/WiFiManager.cpp | 225 +- lib/WiFiManager/WiFiManager.h | 44 +- sdkconfig.defaults | 11 +- src/Config.h | 2 +- src/NukiNetwork.h | 2 - src/NukiNetworkLock.h | 2 - src/WebCfgServer.cpp | 2726 +++++++++++---------- src/WebCfgServer.h | 85 +- src/main.cpp | 34 +- updater/platformio.ini | 6 +- updater/sdkconfig.defaults | 11 +- 16 files changed, 1653 insertions(+), 1530 deletions(-) diff --git a/lib/MqttLogger/src/MqttLogger.cpp b/lib/MqttLogger/src/MqttLogger.cpp index d1b1c71..d8d08f7 100644 --- a/lib/MqttLogger/src/MqttLogger.cpp +++ b/lib/MqttLogger/src/MqttLogger.cpp @@ -90,8 +90,8 @@ void MqttLogger::sendBuffer() } if (doWebSerial) { - WebSerial.write(this->buffer, this->bufferCnt); - WebSerial.println(); + //WebSerial.write(this->buffer, this->bufferCnt); + //WebSerial.println(); } this->bufferCnt=0; } diff --git a/lib/MqttLogger/src/MqttLogger.h b/lib/MqttLogger/src/MqttLogger.h index 6f30a6f..d82e62a 100644 --- a/lib/MqttLogger/src/MqttLogger.h +++ b/lib/MqttLogger/src/MqttLogger.h @@ -12,7 +12,7 @@ #include #include #include -#include "MycilaWebSerial.h" +//#include "MycilaWebSerial.h" #define MQTT_MAX_PACKET_SIZE 1024 diff --git a/lib/PsychicHttp/src/PsychicHttpServer.cpp b/lib/PsychicHttp/src/PsychicHttpServer.cpp index 628f38b..f6cbb7f 100644 --- a/lib/PsychicHttp/src/PsychicHttpServer.cpp +++ b/lib/PsychicHttp/src/PsychicHttpServer.cpp @@ -325,11 +325,19 @@ const std::list& PsychicHttpServer::getClientList() { } bool ON_STA_FILTER(PsychicRequest *request) { + #ifndef CONFIG_IDF_TARGET_ESP32H2 return WiFi.localIP() == request->client()->localIP(); + #else + return false; + #endif } bool ON_AP_FILTER(PsychicRequest *request) { + #ifndef CONFIG_IDF_TARGET_ESP32H2 return WiFi.softAPIP() == request->client()->localIP(); + #else + return false; + #endif } String urlDecode(const char* encoded) diff --git a/lib/PsychicHttp/src/PsychicRequest.cpp b/lib/PsychicHttp/src/PsychicRequest.cpp index 4244358..2005d91 100644 --- a/lib/PsychicHttp/src/PsychicRequest.cpp +++ b/lib/PsychicHttp/src/PsychicRequest.cpp @@ -318,6 +318,11 @@ PsychicWebParameter * PsychicRequest::addParam(PsychicWebParameter *param) { return param; } +int PsychicRequest::params() +{ + return _params.size(); +} + bool PsychicRequest::hasParam(const char *key) { return getParam(key) != NULL; @@ -332,6 +337,18 @@ PsychicWebParameter * PsychicRequest::getParam(const char *key) return NULL; } +PsychicWebParameter * PsychicRequest::getParam(int index) +{ + if (_params.size() > index){ + std::list::iterator it = _params.begin(); + for(int i=0; i_session->find(key) != this->_session->end(); diff --git a/lib/PsychicHttp/src/PsychicRequest.h b/lib/PsychicHttp/src/PsychicRequest.h index fe48a1b..5b54d6e 100644 --- a/lib/PsychicHttp/src/PsychicRequest.h +++ b/lib/PsychicHttp/src/PsychicRequest.h @@ -81,9 +81,11 @@ class PsychicRequest { void loadParams(); PsychicWebParameter * addParam(PsychicWebParameter *param); PsychicWebParameter * addParam(const String &name, const String &value, bool decode = true, bool post = false); + int params(); bool hasParam(const char *key); PsychicWebParameter * getParam(const char *name); - + PsychicWebParameter * getParam(int index); + const String getFilename(); bool authenticate(const char * username, const char * password); diff --git a/lib/WiFiManager/WiFiManager.cpp b/lib/WiFiManager/WiFiManager.cpp index d7aa82c..34c6868 100644 --- a/lib/WiFiManager/WiFiManager.cpp +++ b/lib/WiFiManager/WiFiManager.cpp @@ -627,30 +627,28 @@ boolean WiFiManager::configPortalHasTimeout(){ } void WiFiManager::setupHTTPServer(){ - - server.reset(new WM_WebServer(_httpPort)); + server->listen(_httpPort); /* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */ - server->on(String(FPSTR(R_wifi)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,true)); - server->on(String(FPSTR(R_wifinoscan)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,false)); - server->on(String(FPSTR(R_erase)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleErase, this,std::placeholders::_1,false)); + server->on(String(FPSTR(R_wifi)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,true)); + server->on(String(FPSTR(R_wifinoscan)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,false)); + server->on(String(FPSTR(R_erase)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleErase, this,std::placeholders::_1,false)); { using namespace std::placeholders; - server->on(String(FPSTR(R_root)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleRoot, this,_1)); - server->on(String(FPSTR(R_wifisave)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWifiSave, this,_1)); - server->on(String(FPSTR(R_info)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleInfo, this,_1)); - server->on(String(FPSTR(R_param)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleParam, this,_1)); - server->on(String(FPSTR(R_paramsave)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleParamSave, this,_1)); - server->on(String(FPSTR(R_restart)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleReset, this,_1)); - server->on(String(FPSTR(R_exit)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleExit, this,_1)); - server->on(String(FPSTR(R_close)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleClose, this,_1)); - server->on(String(FPSTR(R_status)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleWiFiStatus, this,_1)); - server->onNotFound (std::bind(&WiFiManager::handleNotFound, this, _1)); + server->on(String(FPSTR(R_root)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleRoot, this,_1)); + server->on(String(FPSTR(R_wifisave)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifiSave, this,_1)); + server->on(String(FPSTR(R_info)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleInfo, this,_1)); + server->on(String(FPSTR(R_param)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParam, this,_1)); + server->on(String(FPSTR(R_paramsave)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParamSave, this,_1)); + server->on(String(FPSTR(R_restart)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleReset, this,_1)); + server->on(String(FPSTR(R_exit)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleExit, this,_1)); + server->on(String(FPSTR(R_close)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleClose, this,_1)); + server->on(String(FPSTR(R_status)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWiFiStatus, this,_1)); + server->onNotFound ((PsychicHttpRequestCallback)std::bind(&WiFiManager::handleNotFound, this, _1)); - server->on(String(FPSTR(R_update)).c_str(), HTTP_ANY, std::bind(&WiFiManager::handleUpdate, this, _1)); - server->on(String(FPSTR(R_updatedone)).c_str(), HTTP_POST,std::bind(&WiFiManager::handleUpdateDone, this, _1), std::bind(&WiFiManager::handleUpdating, this, _1,_2,_3,_4,_5,_6)); + server->on(String(FPSTR(R_update)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleUpdate, this, _1)); + //server->on(String(FPSTR(R_updatedone)).c_str(), HTTP_POST, std::bind(&WiFiManager::handleUpdateDone, this, _1), std::bind(&WiFiManager::handleUpdating, this, _1,_2,_3,_4,_5,_6)); } - server->begin(); // Web server start } void WiFiManager::teardownHTTPServer(){ @@ -979,7 +977,7 @@ bool WiFiManager::shutdownConfigPortal(){ // @todo what is the proper way to shutdown and free the server up // debug - many open issues aobut port not clearing for use with other servers #ifdef WM_ASYNCWEBSERVER - server->end(); + server->stop(); #else server->stop(); #endif @@ -1423,10 +1421,13 @@ String WiFiManager::getHTTPHead(String title){ return page; } -void WiFiManager::HTTPSend(AsyncWebServerRequest *request, String page){ - AsyncWebServerResponse *response = request->beginResponse(200,FPSTR(HTTP_HEAD_CT), page); - response->addHeader(FPSTR(HTTP_HEAD_CL), String(page.length())); - request->send(response); +esp_err_t WiFiManager::HTTPSend(PsychicRequest *request, String page){ + PsychicResponse response(request); + response.addHeader(HTTP_HEAD_CL, ((String)page.length()).c_str()); + response.setCode(200); + response.setContentType(HTTP_HEAD_CT); + response.setContent(page.c_str()); + return response.send(); } /** @@ -1446,12 +1447,12 @@ void WiFiManager::handleRequest() { /** * HTTPD CALLBACK root or redirect to captive portal */ -void WiFiManager::handleRoot(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleRoot(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Root")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page handleRequest(); String page = getHTTPHead(_title); // @token options @todo replace options with title String str = FPSTR(HTTP_ROOT_MAIN); // @todo custom title @@ -1463,28 +1464,29 @@ void WiFiManager::handleRoot(AsyncWebServerRequest *request) { reportStatus(page); page += FPSTR(HTTP_END); - HTTPSend(request,page); if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime,true); // preload wifiscan throttled, async // @todo buggy, captive portals make a query on every page load, causing this to run every time in addition to the real page load // I dont understand why, when you are already in the captive portal, I guess they want to know that its still up and not done or gone // if we can detect these and ignore them that would be great, since they come from the captive portal redirect maybe there is a refferer + + return HTTPSend(request,page); } /** * HTTPD CALLBACK Wifi config page handler */ -void WiFiManager::handleWifi(AsyncWebServerRequest *request,bool scan = true) { +esp_err_t WiFiManager::handleWifi(PsychicRequest *request,bool scan = true) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Wifi")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titlewifi)); // @token titlewifi if (scan) { #ifdef WM_DEBUG_LEVEL // DEBUG_WM(WM_DEBUG_DEV,"refresh flag:",request->hasArg(F("refresh"))); #endif - WiFi_scanNetworks(request->hasArg(F("refresh")),true); //wifiscan, force if arg refresh + WiFi_scanNetworks(request->hasParam("refresh")); //wifiscan, force if arg refresh page += getScanItemOut(); } String pitem = ""; @@ -1520,21 +1522,21 @@ void WiFiManager::handleWifi(AsyncWebServerRequest *request,bool scan = true) { reportStatus(page); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent config page")); #endif + + return HTTPSend(request,page); } /** * HTTPD CALLBACK Wifi param page handler */ -void WiFiManager::handleParam(AsyncWebServerRequest *request){ +esp_err_t WiFiManager::handleParam(PsychicRequest *request){ #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Param")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titleparam)); // @token titlewifi @@ -1550,11 +1552,11 @@ void WiFiManager::handleParam(AsyncWebServerRequest *request){ reportStatus(page); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent param page")); #endif + + return HTTPSend(request,page); } @@ -1914,34 +1916,35 @@ String WiFiManager::getParamOut(){ return page; } -void WiFiManager::handleWiFiStatus(AsyncWebServerRequest *request){ +esp_err_t WiFiManager::handleWiFiStatus(PsychicRequest *request){ #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi status ")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page; // String page = "{\"result\":true,\"count\":1}"; #ifdef WM_JSTEST page = FPSTR(HTTP_JS); #endif - HTTPSend(request,page); + + return HTTPSend(request,page); } /** * HTTPD CALLBACK save form and redirect to WLAN config page again */ -void WiFiManager::handleWifiSave(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleWifiSave(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi save ")); DEBUG_WM(WM_DEBUG_DEV,F("Method:"),request->method() == HTTP_GET ? (String)FPSTR(S_GET) : (String)FPSTR(S_POST)); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); //SAVE/connect here - _ssid = request->arg(F("s")).c_str(); - _pass = request->arg(F("p")).c_str(); + _ssid = request->getParam("s")->value(); + _pass = request->getParam("p")->value(); if(_ssid == "" && _pass != ""){ _ssid = WiFi_SSID(true); // password change, placeholder ssid, @todo compare pass to old?, confirm ssid is clean @@ -1957,40 +1960,40 @@ void WiFiManager::handleWifiSave(AsyncWebServerRequest *request) { requestinfo += "\nMethod: "; requestinfo += (request->method() == HTTP_GET) ? "GET" : "POST"; requestinfo += "\nArguments: "; - requestinfo += request->args(); + requestinfo += request->params(); requestinfo += "\n"; - for (uint8_t i = 0; i < request->args(); i++) { - requestinfo += " " + request->argName(i) + ": " + request->arg(i) + "\n"; + for (uint8_t i = 0; i < request->params(); i++) { + requestinfo += " " + request->getParam(i)->name() + ": " + request->getParam(i)->value() + "\n"; } DEBUG_WM(WM_DEBUG_MAX,requestinfo); #endif // set static ips from server args - if (request->arg(FPSTR(S_ip)) != "") { + if (request->getParam(S_ip)->value() != "") { //_sta_static_ip.fromString(request->arg(FPSTR(S_ip)); - String ip = request->arg(FPSTR(S_ip)); + String ip = request->getParam(S_ip)->value(); optionalIPFromString(&_sta_static_ip, ip.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static ip:"),ip); #endif } - if (request->arg(FPSTR(S_gw)) != "") { - String gw = request->arg(FPSTR(S_gw)); + if (request->getParam(S_gw)->value() != "") { + String gw = request->getParam(S_gw)->value(); optionalIPFromString(&_sta_static_gw, gw.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static gateway:"),gw); #endif } - if (request->arg(FPSTR(S_sn)) != "") { - String sn = request->arg(FPSTR(S_sn)); + if (request->getParam(S_sn)->value() != "") { + String sn = request->getParam(S_sn)->value(); optionalIPFromString(&_sta_static_sn, sn.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static netmask:"),sn); #endif } - if (request->arg(FPSTR(S_dns)) != "") { - String dns = request->arg(FPSTR(S_dns)); + if (request->getParam(S_dns)->value() != "") { + String dns = request->getParam(S_dns)->value(); optionalIPFromString(&_sta_static_dns, dns.c_str()); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("static DNS:"),dns); @@ -2018,16 +2021,17 @@ void WiFiManager::handleWifiSave(AsyncWebServerRequest *request) { page += FPSTR(HTTP_END); //server->sendHeader(FPSTR(HTTP_HEAD_CORS), FPSTR(HTTP_HEAD_CORS_ALLOW_ALL)); // @HTTPHEAD send cors - HTTPSend(request,page); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent wifi save page")); #endif connect = true; //signal ready to connect/reset process in processConfigPortal + + return HTTPSend(request,page); } -void WiFiManager::handleParamSave(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleParamSave(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Param save ")); @@ -2035,7 +2039,7 @@ void WiFiManager::handleParamSave(AsyncWebServerRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Method:"),request->method() == HTTP_GET ? (String)FPSTR(S_GET) : (String)FPSTR(S_POST)); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); doParamSave(request); @@ -2045,14 +2049,14 @@ void WiFiManager::handleParamSave(AsyncWebServerRequest *request) { if(_showBack) page += FPSTR(HTTP_BACKBTN); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent param save page")); #endif + + return HTTPSend(request,page); } -void WiFiManager::doParamSave(AsyncWebServerRequest *request){ +void WiFiManager::doParamSave(PsychicRequest *request){ // @todo use new callback for before paramsaves, is this really needed? if ( _presaveparamscallback != NULL) { _presaveparamscallback(); // @CALLBACK @@ -2075,10 +2079,10 @@ void WiFiManager::doParamSave(AsyncWebServerRequest *request){ //read parameter from server String name = (String)FPSTR(S_parampre)+(String)i; String value; - if(request->hasArg(name.c_str())) { - value = request->arg(name); + if(request->hasParam(name.c_str())) { + value = request->getParam(name.c_str())->value(); } else { - value = request->arg(_params[i]->getID()); + value = request->getParam(_params[i]->getID())->value(); } //store it in params array @@ -2101,11 +2105,11 @@ void WiFiManager::doParamSave(AsyncWebServerRequest *request){ /** * HTTPD CALLBACK info page */ -void WiFiManager::handleInfo(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleInfo(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Info")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); //handleRequest(); String page = getHTTPHead(FPSTR(S_titleinfo)); // @token titleinfo reportStatus(page); @@ -2203,11 +2207,11 @@ void WiFiManager::handleInfo(AsyncWebServerRequest *request) { page += FPSTR(HTTP_HELP); page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_DEV,F("Sent info page")); #endif + + return HTTPSend(request,page); } String WiFiManager::getInfoData(String id){ @@ -2447,41 +2451,45 @@ String WiFiManager::getInfoData(String id){ /** * HTTPD CALLBACK exit, closes configportal if blocking, if non blocking undefined */ -void WiFiManager::handleExit(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleExit(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Exit")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titleexit)); // @token titleexit page += FPSTR(S_exiting); // @token exiting // ('Logout', 401, {'WWW-Authenticate': 'Basic realm="Login required"'}) - AsyncWebServerResponse *response = request->beginResponse(200,FPSTR(HTTP_HEAD_CT), page); - response->addHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); - request->send(response); delay(2000); abort = true; + + PsychicResponse response(request); + response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setCode(200); + response.setContentType(HTTP_HEAD_CT); + response.setContent(page.c_str()); + return response.send(); } /** * HTTPD CALLBACK reset page */ -void WiFiManager::handleReset(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleReset(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Reset")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titlereset)); //@token titlereset page += FPSTR(S_resetting); //@token resetting page += FPSTR(HTTP_END); - HTTPSend(request,page); - #ifdef WM_DEBUG_LEVEL DEBUG_WM(F("RESETTING ESP")); #endif _rebootNeeded = true; + + return HTTPSend(request,page); } /** @@ -2491,11 +2499,11 @@ void WiFiManager::handleReset(AsyncWebServerRequest *request) { // void WiFiManager::handleErase() { // handleErase(false); // } -void WiFiManager::handleErase(AsyncWebServerRequest *request,bool opt = false) { +esp_err_t WiFiManager::handleErase(PsychicRequest *request,bool opt = false) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_NOTIFY,F("<- HTTP Erase")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); handleRequest(); String page = getHTTPHead(FPSTR(S_titleerase)); // @token titleerase @@ -2510,18 +2518,19 @@ void WiFiManager::handleErase(AsyncWebServerRequest *request,bool opt = false) { } page += FPSTR(HTTP_END); - HTTPSend(request,page); if(ret){ _rebootNeeded = true; } + + return HTTPSend(request,page); } /** * HTTPD CALLBACK 404 */ -void WiFiManager::handleNotFound(AsyncWebServerRequest *request) { - if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page +esp_err_t WiFiManager::handleNotFound(PsychicRequest *request) { + if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page handleRequest(); String message = FPSTR(S_notfound); // @token notfound @@ -2532,18 +2541,22 @@ void WiFiManager::handleNotFound(AsyncWebServerRequest *request) { message += FPSTR(S_method); // @token method message += ( request->method() == HTTP_GET ) ? FPSTR(S_GET) : FPSTR(S_POST); message += FPSTR(S_args); // @token args - message += request->args(); + message += request->params(); message += F("\n"); - for ( uint8_t i = 0; i < request->args(); i++ ) { - message += " " + request->argName ( i ) + ": " + request->arg ( i ) + "\n"; + for ( uint8_t i = 0; i < request->params(); i++ ) { + message += " " + request->getParam(i)->name() + ": " + request->getParam(i)->value() + "\n"; } } - AsyncWebServerResponse *response = request->beginResponse(404,FPSTR(HTTP_HEAD_CT2), message); - response->addHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); - response->addHeader(F("Pragma"), F("no-cache")); - response->addHeader(F("Expires"), F("-1")); - request->send(response); + + PsychicResponse response(request); + response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.addHeader("Pragma", "no-cache"); + response.addHeader("Expires", "-1"); + response.setCode(404); + response.setContentType(HTTP_HEAD_CT2); + response.setContent(message.c_str()); + return response.send(); } /** @@ -2551,7 +2564,7 @@ void WiFiManager::handleNotFound(AsyncWebServerRequest *request) { * Redirect to captive portal if we got a request for another domain. * Return true in that case so the page handler do not try to handle the request again. */ -boolean WiFiManager::captivePortal(AsyncWebServerRequest *request) { +boolean WiFiManager::captivePortal(PsychicRequest *request) { if(!_enableCaptivePortal || !configPortalActive) return false; // skip redirections if cp not enabled or not in ap mode @@ -2578,9 +2591,11 @@ boolean WiFiManager::captivePortal(AsyncWebServerRequest *request) { DEBUG_WM(WM_DEBUG_VERBOSE,F("<- Request redirected to captive portal")); DEBUG_WM(WM_DEBUG_DEV,"serverLoc " + serverLoc); #endif - AsyncWebServerResponse *response = request->beginResponse(302,FPSTR(HTTP_HEAD_CT2), ""); - response->addHeader(F("Location"), (String)F("http://") + serverLoc); - request->send(response); + PsychicResponse response(request); + response.addHeader("Location", ((String)("http://") + serverLoc).c_str()); + response.setCode(302); + response.setContentType(HTTP_HEAD_CT2); + response.send(); return true; } return false; @@ -2592,9 +2607,9 @@ void WiFiManager::stopCaptivePortal(){ } // HTTPD CALLBACK, handle close, stop captive portal, if not enabled undefined -void WiFiManager::handleClose(AsyncWebServerRequest *request){ +esp_err_t WiFiManager::handleClose(PsychicRequest *request){ DEBUG_WM(WM_DEBUG_VERBOSE,F("Disabling Captive Portal")); - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); stopCaptivePortal(); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP close")); @@ -2602,7 +2617,7 @@ void WiFiManager::handleClose(AsyncWebServerRequest *request){ handleRequest(); String page = getHTTPHead(FPSTR(S_titleclose)); // @token titleclose page += FPSTR(S_closing); // @token closing - HTTPSend(request,page); + return HTTPSend(request,page); } void WiFiManager::reportStatus(String &page){ @@ -4011,12 +4026,12 @@ void WiFiManager::WiFi_autoReconnect(){ } // Called when /update is requested -void WiFiManager::handleUpdate(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleUpdate(PsychicRequest *request) { #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("<- Handle update")); #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page String page = getHTTPHead(_title); // @token options String str = FPSTR(HTTP_ROOT_MAIN); str.replace(FPSTR(T_t), _title); @@ -4026,12 +4041,12 @@ void WiFiManager::handleUpdate(AsyncWebServerRequest *request) { page += FPSTR(HTTP_UPDATE); page += FPSTR(HTTP_END); - HTTPSend(request,page); + return HTTPSend(request,page); } // upload via /u POST -void WiFiManager::handleUpdating(AsyncWebServerRequest *request,String filename, size_t index, uint8_t *data, size_t len, bool final){ +void WiFiManager::handleUpdating(String filename, size_t index, uint8_t *data, size_t len, bool final){ // @todo // cannot upload files in captive portal, file select is not allowed, show message with link or hide // cannot upload if softreset after upload, maybe check for hard reset at least for dev, ERROR[11]: Invalid bootstrapping state, reset ESP8266 before updating @@ -4123,10 +4138,10 @@ void WiFiManager::handleUpdating(AsyncWebServerRequest *request,String filename, } // upload and ota done, show status -void WiFiManager::handleUpdateDone(AsyncWebServerRequest *request) { +esp_err_t WiFiManager::handleUpdateDone(PsychicRequest *request) { DEBUG_WM(WM_DEBUG_VERBOSE, F("<- Handle update done")); // if (captivePortal(request)) return; // If captive portal redirect instead of displaying the page - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); String page = getHTTPHead(FPSTR(S_options)); // @token options String str = FPSTR(HTTP_ROOT_MAIN); str.replace(FPSTR(T_t),_title); @@ -4148,13 +4163,13 @@ void WiFiManager::handleUpdateDone(AsyncWebServerRequest *request) { } page += FPSTR(HTTP_END); - HTTPSend(request,page); - // delay(1000); // send page if (!Update.hasError()) { //ESP.restart(); _rebootNeeded = true; } + + return HTTPSend(request,page); } #endif diff --git a/lib/WiFiManager/WiFiManager.h b/lib/WiFiManager/WiFiManager.h index 9f0eac5..38f9bfb 100644 --- a/lib/WiFiManager/WiFiManager.h +++ b/lib/WiFiManager/WiFiManager.h @@ -97,8 +97,8 @@ #define WM_WIFIOPEN WIFI_AUTH_OPEN #ifdef WM_ASYNCWEBSERVER - #include - #include + #include + #include #else #ifndef WEBSERVER_H #ifdef WM_WEBSERVERSHIM @@ -521,7 +521,7 @@ class WiFiManager #if defined(ESP32) && defined(WM_WEBSERVERSHIM) #ifdef WM_ASYNCWEBSERVER - using WM_WebServer = AsyncWebServer; + using WM_WebServer = PsychicHttpServer; #else using WM_WebServer = WebServer; #endif @@ -694,32 +694,32 @@ public: bool WiFi_scanNetworks(bool force,bool async); protected: // webserver handlers - void handleRoot(AsyncWebServerRequest *request); - void handleWifi(AsyncWebServerRequest *request,bool scan); - void handleWifiSave(AsyncWebServerRequest *request); - void handleInfo(AsyncWebServerRequest *request); - void handleReset(AsyncWebServerRequest *request); - void handleNotFound(AsyncWebServerRequest *request); - void handleExit(AsyncWebServerRequest *request); - void handleClose(AsyncWebServerRequest *request); - // void handleErase(AsyncWebServerRequest *request); - void handleErase(AsyncWebServerRequest *request, bool opt); - void handleParam(AsyncWebServerRequest *request); - void handleWiFiStatus(AsyncWebServerRequest *request); - void handleParamSave(AsyncWebServerRequest *request); - void doParamSave(AsyncWebServerRequest *request); + esp_err_t handleRoot(PsychicRequest *request); + esp_err_t handleWifi(PsychicRequest *request,bool scan); + esp_err_t handleWifiSave(PsychicRequest *request); + esp_err_t handleInfo(PsychicRequest *request); + esp_err_t handleReset(PsychicRequest *request); + esp_err_t handleNotFound(PsychicRequest *request); + esp_err_t handleExit(PsychicRequest *request); + esp_err_t handleClose(PsychicRequest *request); + // esp_err_t handleErase(PsychicRequest *request); + esp_err_t handleErase(PsychicRequest *request, bool opt); + esp_err_t handleParam(PsychicRequest *request); + esp_err_t handleWiFiStatus(PsychicRequest *request); + esp_err_t handleParamSave(PsychicRequest *request); + void doParamSave(PsychicRequest *request); void handleRequest(); - void HTTPSend(AsyncWebServerRequest *request, String page); + esp_err_t HTTPSend(PsychicRequest *request, String page); - boolean captivePortal(AsyncWebServerRequest *request); + boolean captivePortal(PsychicRequest *request); boolean configPortalHasTimeout(); uint8_t processConfigPortal(); void stopCaptivePortal(); // OTA Update handler - void handleUpdate(AsyncWebServerRequest *request); - void handleUpdating(AsyncWebServerRequest *request,String filename, size_t index, uint8_t *data, size_t len, bool final); - void handleUpdateDone(AsyncWebServerRequest *request); + esp_err_t handleUpdate(PsychicRequest *request); + void handleUpdating(String filename, size_t index, uint8_t *data, size_t len, bool final); + esp_err_t handleUpdateDone(PsychicRequest *request); // wifi platform abstractions bool WiFi_Mode(WiFiMode_t m); diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 780d167..89c8474 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -82,4 +82,13 @@ CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE=y CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y -CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/github_root_ca.pem" \ No newline at end of file +CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/github_root_ca.pem" +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +CONFIG_HTTPD_WS_SUPPORT=y +CONFIG_ESP_HTTPS_SERVER_ENABLE=y \ No newline at end of file diff --git a/src/Config.h b/src/Config.h index d621c64..70cfcf7 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.01" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-08-18" +#define NUKI_HUB_DATE "2024-08-27" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index 65d2921..afc5ca8 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -16,8 +16,6 @@ #include "NukiConstants.h" #endif -#define JSON_BUFFER_SIZE 1024 - class NukiNetwork { public: diff --git a/src/NukiNetworkLock.h b/src/NukiNetworkLock.h index 21f921c..d069ccf 100644 --- a/src/NukiNetworkLock.h +++ b/src/NukiNetworkLock.h @@ -13,8 +13,6 @@ #include "QueryCommand.h" #include "LockActionResult.h" -#define LOCK_LOG_JSON_BUFFER_SIZE 2048 - class NukiNetworkLock : public MqttReceiver { public: diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index a595508..86f62e3 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -17,7 +17,7 @@ extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_ #include #include "ArduinoJson.h" -WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer) +WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) : _nuki(nuki), _nukiOpener(nukiOpener), _network(network), @@ -25,14 +25,14 @@ WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Nuk _preferences(preferences), _allowRestartToPortal(allowRestartToPortal), _partitionType(partitionType), - _asyncServer(asyncServer) + _psychicServer(psychicServer) #else -WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer) +WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) : _network(network), _preferences(preferences), _allowRestartToPortal(allowRestartToPortal), _partitionType(partitionType), - _asyncServer(asyncServer) + _psychicServer(psychicServer) #endif { _hostname = _preferences->getString(preference_hostname, ""); @@ -72,206 +72,224 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool void WebCfgServer::initialize() { - _response.reserve(8192); - - _asyncServer->on("/", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); #ifndef NUKI_HUB_UPDATER - buildHtml(request); + return buildHtml(request); #else - buildOtaHtml(request); + return buildOtaHtml(request); #endif }); - _asyncServer->on("/style.css", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - sendCss(request); + _psychicServer->on("/style.css", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return sendCss(request); }); - _asyncServer->on("/favicon.ico", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - sendFavicon(request); + _psychicServer->on("/favicon.ico", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return sendFavicon(request); }); #ifndef NUKI_HUB_UPDATER - _asyncServer->on("/import", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/import", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); String message = ""; bool restart = processImport(request, message); - buildConfirmHtml(request, message, 3, true); + return buildConfirmHtml(request, message, 3, true); }); - _asyncServer->on("/export", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - sendSettings(request); + _psychicServer->on("/export", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return sendSettings(request); }); - _asyncServer->on("/impexpcfg", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildImportExportHtml(request); + _psychicServer->on("/impexpcfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildImportExportHtml(request); }); - _asyncServer->on("/status", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildStatusHtml(request); + _psychicServer->on("/status", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildStatusHtml(request); }); - _asyncServer->on("/acclvl", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildAccLvlHtml(request); + _psychicServer->on("/acclvl", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildAccLvlHtml(request); }); - _asyncServer->on("/custntw", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildCustomNetworkConfigHtml(request); + _psychicServer->on("/custntw", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildCustomNetworkConfigHtml(request); }); - _asyncServer->on("/advanced", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildAdvancedConfigHtml(request); + _psychicServer->on("/advanced", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildAdvancedConfigHtml(request); }); - _asyncServer->on("/cred", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildCredHtml(request); + _psychicServer->on("/cred", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildCredHtml(request); }); - _asyncServer->on("/mqttconfig", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildMqttConfigHtml(request); + _psychicServer->on("/mqttconfig", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildMqttConfigHtml(request); }); - _asyncServer->on("/nukicfg", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildNukiConfigHtml(request); + _psychicServer->on("/nukicfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildNukiConfigHtml(request); }); - _asyncServer->on("/gpiocfg", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildGpioConfigHtml(request); + _psychicServer->on("/gpiocfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildGpioConfigHtml(request); }); #ifndef CONFIG_IDF_TARGET_ESP32H2 - _asyncServer->on("/wifi", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildConfigureWifiHtml(request); + _psychicServer->on("/wifi", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildConfigureWifiHtml(request); }); - _asyncServer->on("/wifimanager", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/wifimanager", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); if(_allowRestartToPortal) { - buildConfirmHtml(request, "Restarting. Connect to ESP access point to reconfigure Wi-Fi.", 0); + esp_err_t res = buildConfirmHtml(request, "Restarting. Connect to ESP access point to reconfigure Wi-Fi.", 0); waitAndProcess(false, 1000); _network->reconfigureDevice(); + return res; } + return(ESP_OK); }); #endif - _asyncServer->on("/unpairlock", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - processUnpair(request, false); + _psychicServer->on("/unpairlock", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processUnpair(request, false); }); - _asyncServer->on("/unpairopener", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - processUnpair(request, true); + _psychicServer->on("/unpairopener", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processUnpair(request, true); }); - _asyncServer->on("/factoryreset", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - processFactoryReset(request); + _psychicServer->on("/factoryreset", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processFactoryReset(request); }); - _asyncServer->on("/info", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildInfoHtml(request); + _psychicServer->on("/info", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildInfoHtml(request); }); - _asyncServer->on("/debugon", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/debugon", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); _preferences->putBool(preference_publish_debug_info, true); - buildConfirmHtml(request, "Debug On", 3, true); + return buildConfirmHtml(request, "Debug On", 3, true); }); - _asyncServer->on("/debugoff", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/debugoff", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); _preferences->putBool(preference_publish_debug_info, false); - buildConfirmHtml(request, "Debug Off", 3, true); + return buildConfirmHtml(request, "Debug Off", 3, true); }); - _asyncServer->on("/savecfg", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/savecfg", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); String message = ""; bool restart = processArgs(request, message); - buildConfirmHtml(request, message, 3, true); + return buildConfirmHtml(request, message, 3, true); }); - _asyncServer->on("/savegpiocfg", HTTP_POST, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/savegpiocfg", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); processGpioArgs(request); - buildConfirmHtml(request, "Saving GPIO configuration. Restarting.", 3, true); + esp_err_t res = buildConfirmHtml(request, "Saving GPIO configuration. Restarting.", 3, true); Log->println(F("Restarting")); waitAndProcess(true, 1000); restartEsp(RestartReason::GpioConfigurationUpdated); + return res; }); #endif - _asyncServer->on("/ota", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildOtaHtml(request); + _psychicServer->on("/ota", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildOtaHtml(request); }); - _asyncServer->on("/otadebug", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildOtaHtml(request, true); + _psychicServer->on("/otadebug", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildOtaHtml(request, true); }); - _asyncServer->on("/reboottoota", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildConfirmHtml(request, "Rebooting to other partition", 2, true); + _psychicServer->on("/reboottoota", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + esp_err_t res = buildConfirmHtml(request, "Rebooting to other partition", 2, true); waitAndProcess(true, 1000); esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)); restartEsp(RestartReason::OTAReboot); + return res; }); - _asyncServer->on("/reboot", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - buildConfirmHtml(request, "Rebooting", 2, true); + _psychicServer->on("/reboot", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + esp_err_t res = buildConfirmHtml(request, "Rebooting", 2, true); waitAndProcess(true, 1000); restartEsp(RestartReason::RequestedViaWebServer); + return res; }); - _asyncServer->on("/autoupdate", HTTP_GET, [&](AsyncWebServerRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); + _psychicServer->on("/autoupdate", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); #ifndef NUKI_HUB_UPDATER - processUpdate(request); + return processUpdate(request); #else - request->redirect("/"); + return request->redirect("/"); #endif }); - _asyncServer->on("/uploadota", HTTP_POST, - [&](AsyncWebServerRequest *request) {}, - [&](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) - { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(); - handleOtaUpload(request, filename, index, data, len, final); - } + + PsychicUploadHandler *updateHandler = new PsychicUploadHandler(); + updateHandler->onUpload([&](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return handleOtaUpload(request, filename, index, data, len, final); + } ); + + updateHandler->onRequest([&](PsychicRequest *request) { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + + String result; + if (!Update.hasError()) + { + Log->print("Update code or data OK Update.errorString() "); + Log->println(Update.errorString()); + result = "Update OK."; + esp_err_t res = request->reply(200,"text/html",result.c_str()); + restartEsp(RestartReason::OTACompleted); + return res; + } + else { + result = " Update.errorString() " + String(Update.errorString()); + Log->print("ERROR : error "); + Log->println(result.c_str()); + esp_err_t res = request->reply(500, "text/html", result.c_str()); + restartEsp(RestartReason::OTAAborted); + return res; + } + }); + + _psychicServer->on("/uploadota", HTTP_POST, updateHandler); //Update.onProgress(printProgress); } -void WebCfgServer::sendResponse(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) { - AsyncWebServerResponse *response = request->beginChunkedResponse("text/html", - [&](uint8_t *buffer, size_t maxlen, size_t index) -> size_t { - size_t len = min(maxlen, _response.length() - index); - memcpy(buffer, _response.c_str() + index, len); - return len; - }); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); - request->send(response); -} - -void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) -{ - _response = ""; - buildHtmlHeader(); + buildHtmlHeader(&response); bool errored = false; if(request->hasParam("errored")) { - const AsyncWebParameter* p = request->getParam("errored"); + const PsychicWebParameter* p = request->getParam("errored"); if(p->value() != "") errored = true; } - if(errored) _response.concat("
    Over-the-air update errored. Please check the logs for more info

    "); + if(errored) response.print("
    Over-the-air update errored. Please check the logs for more info

    "); if(_partitionType == 0) { - _response.concat("

    You are currently running Nuki Hub with an outdated partition scheme. Because of this you cannot use OTA to update to 9.00 or higher. Please check GitHub for instructions on how to update to 9.00 and the new partition scheme

    "); - _response.concat(""); - return; + response.print("

    You are currently running Nuki Hub with an outdated partition scheme. Because of this you cannot use OTA to update to 9.00 or higher. Please check GitHub for instructions on how to update to 9.00 and the new partition scheme

    "); + response.print(""); + return response.endSend(); } - _response.concat("
    Initiating Over-the-air update. This will take about two minutes, please be patient.
    You will be forwarded automatically when the update is complete.
    "); - _response.concat("

    Update Nuki Hub

    "); - _response.concat("Click on the button to reboot and automatically update Nuki Hub and the Nuki Hub updater to the latest versions from GitHub"); - _response.concat("
    "); + response.print("
    Initiating Over-the-air update. This will take about two minutes, please be patient.
    You will be forwarded automatically when the update is complete.
    "); + response.print("

    Update Nuki Hub

    "); + response.print("Click on the button to reboot and automatically update Nuki Hub and the Nuki Hub updater to the latest versions from GitHub"); + response.print("
    "); String release_type; @@ -283,81 +301,81 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) #else String build_type = "debug"; #endif - _response.concat("

    "); - _response.concat("

    "); - _response.concat("

    "); - _response.concat("

    "); + response.print("

    "); + response.print("

    "); + response.print("

    "); + response.print("

    "); - _response.concat("Current version: "); - _response.concat(NUKI_HUB_VERSION); - _response.concat(" ("); - _response.concat(NUKI_HUB_BUILD); - _response.concat("), "); - _response.concat(NUKI_HUB_DATE); - _response.concat("
    "); + response.print("Current version: "); + response.print(NUKI_HUB_VERSION); + response.print(" ("); + response.print(NUKI_HUB_BUILD); + response.print("), "); + response.print(NUKI_HUB_DATE); + response.print("
    "); #ifndef NUKI_HUB_UPDATER bool manifestSuccess = false; JsonDocument doc; - NetworkClientSecure *client = new NetworkClientSecure; - if (client) { - client->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); + NetworkClientSecure *clientOTAUpdate = new NetworkClientSecure; + if (clientOTAUpdate) { + clientOTAUpdate->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); { - HTTPClient https; - https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - https.setTimeout(2500); - https.useHTTP10(true); + HTTPClient httpsOTAClient; + httpsOTAClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + httpsOTAClient.setTimeout(2500); + httpsOTAClient.useHTTP10(true); - if (https.begin(*client, GITHUB_OTA_MANIFEST_URL)) { - int http_responseCode = https.GET(); + if (httpsOTAClient.begin(*clientOTAUpdate, GITHUB_OTA_MANIFEST_URL)) { + int httpResponseCodeOTA = httpsOTAClient.GET(); - if (http_responseCode == HTTP_CODE_OK || http_responseCode == HTTP_CODE_MOVED_PERMANENTLY) + if (httpResponseCodeOTA == HTTP_CODE_OK || httpResponseCodeOTA == HTTP_CODE_MOVED_PERMANENTLY) { - DeserializationError jsonError = deserializeJson(doc, https.getStream()); + DeserializationError jsonError = deserializeJson(doc, httpsOTAClient.getStream()); if (!jsonError) { manifestSuccess = true; } } - https.end(); + httpsOTAClient.end(); } } - delete client; + delete clientOTAUpdate; } if(!manifestSuccess) { - _response.concat("currentverlatestverdevverbetaver"); + response.print("currentverlatestverdevverbetaver"); } else { - _response.concat("Latest release version: "); - _response.concat(doc["release"]["fullversion"].as()); - _response.concat(" ("); - _response.concat(doc["release"]["build"].as()); - _response.concat("), "); - _response.concat(doc["release"]["time"].as()); - _response.concat("
    "); - _response.concat("Latest beta version: "); + response.print("Latest release version: "); + response.print(doc["release"]["fullversion"].as()); + response.print(" ("); + response.print(doc["release"]["build"].as()); + response.print("), "); + response.print(doc["release"]["time"].as()); + response.print("
    "); + response.print("Latest beta version: "); if(doc["beta"]["fullversion"] != "No beta available") { - _response.concat(doc["beta"]["fullversion"].as()); - _response.concat(" ("); - _response.concat(doc["beta"]["build"].as()); - _response.concat("), "); - _response.concat(doc["beta"]["time"].as()); + response.print(doc["beta"]["fullversion"].as()); + response.print(" ("); + response.print(doc["beta"]["build"].as()); + response.print(")
    , "); + response.print(doc["beta"]["time"].as()); } else { - _response.concat(doc["beta"]["fullversion"].as()); - _response.concat(""); + response.print(doc["beta"]["fullversion"].as()); + response.print(""); } - _response.concat("
    "); - _response.concat("Latest development version: "); - _response.concat(doc["master"]["fullversion"].as()); - _response.concat(" ("); - _response.concat(doc["master"]["build"].as()); - _response.concat("), "); - _response.concat(doc["master"]["time"].as()); - _response.concat("
    "); + response.print("
    "); + response.print("Latest development version: "); + response.print(doc["master"]["fullversion"].as()); + response.print(" ("); + response.print(doc["master"]["build"].as()); + response.print("), "); + response.print(doc["master"]["time"].as()); + response.print("
    "); String currentVersion = NUKI_HUB_VERSION; const char* latestVersion; @@ -370,87 +388,88 @@ void WebCfgServer::buildOtaHtml(AsyncWebServerRequest *request, bool debug) if(strcmp(latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) _preferences->putString(preference_latest_version, latestVersion); } #endif - _response.concat("
    "); + response.print("
    "); if(_partitionType == 1) { - _response.concat("

    Manually update Nuki Hub

    "); - _response.concat("

    Reboot to Nuki Hub Updater

    "); - _response.concat("Click on the button to reboot to the Nuki Hub updater, where you can select the latest Nuki Hub binary to update"); - _response.concat("



    "); - _response.concat("

    Update Nuki Hub Updater

    "); - _response.concat("Select the latest Nuki Hub updater binary to update the Nuki Hub updater"); - _response.concat("
    Choose the nuki_hub_updater.bin file to upload:
    "); + response.print("

    Manually update Nuki Hub

    "); + response.print("

    Reboot to Nuki Hub Updater

    "); + response.print("Click on the button to reboot to the Nuki Hub updater, where you can select the latest Nuki Hub binary to update"); + response.print("


    "); + response.print("

    Update Nuki Hub Updater

    "); + response.print("Select the latest Nuki Hub updater binary to update the Nuki Hub updater"); + response.print("
    Choose the nuki_hub_updater.bin file to upload:
    "); } else { - _response.concat("
    "); - _response.concat("

    Reboot to Nuki Hub

    "); - _response.concat("Click on the button to reboot to Nuki Hub"); - _response.concat("


    "); - _response.concat("

    Update Nuki Hub

    "); - _response.concat("Select the latest Nuki Hub binary to update Nuki Hub"); - _response.concat("
    Choose the nuki_hub.bin file to upload:
    "); + response.print("
    "); + response.print("

    Reboot to Nuki Hub

    "); + response.print("Click on the button to reboot to Nuki Hub"); + response.print("


    "); + response.print("

    Update Nuki Hub

    "); + response.print("Select the latest Nuki Hub binary to update Nuki Hub"); + response.print("
    Choose the nuki_hub.bin file to upload:
    "); } - _response.concat("


    "); - _response.concat("
    "); - _response.concat("

    GitHub


    "); - _response.concat(""); - _response.concat("

    "); - _response.concat("

    "); - _response.concat(""); - _response.concat(""); - sendResponse(request); + response.print("


    "); + response.print("
    "); + response.print("

    GitHub


    "); + response.print(""); + response.print("

    "); + response.print("

    "); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildOtaCompletedHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildOtaCompletedHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); - _response.concat("
    Over-the-air update completed.
    You will be forwarded automatically.
    "); - _response.concat(""); - _response.concat(""); - sendResponse(request); + response.print("
    Over-the-air update completed.
    You will be forwarded automatically.
    "); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildHtmlHeader(String additionalHeader) +void WebCfgServer::buildHtmlHeader(PsychicStreamResponse *response, String additionalHeader) { - _response.concat(""); - _response.concat(""); - if(strcmp(additionalHeader.c_str(), "") != 0) _response.concat(additionalHeader); - _response.concat(""); - _response.concat("Nuki Hub"); + response->print(""); + response->print(""); + if(strcmp(additionalHeader.c_str(), "") != 0) response->print(additionalHeader); + response->print(""); + response->print("Nuki Hub"); } void WebCfgServer::waitAndProcess(const bool blocking, const uint32_t duration) @@ -473,91 +492,114 @@ void WebCfgServer::printProgress(size_t prg, size_t sz) { Log->printf("Progress: %d%%\n", (prg*100)/_otaContentLen); } -void WebCfgServer::handleOtaUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final) { - if(!request->url().endsWith("/uploadota")) return; + if(!request->url().endsWith("/uploadota")) return(ESP_FAIL); if(filename == "") { Log->println("Invalid file for OTA upload"); - return; + return(ESP_FAIL); } - if (!index) + if (!Update.hasError()) { + if (!index){ + Update.clearError(); + + Log->println("Starting manual OTA update"); + _otaContentLen = request->contentLength(); + + if(_partitionType == 1 && _otaContentLen > 1600000) + { + Log->println("Uploaded OTA file too large, are you trying to upload a Nuki Hub binary instead of a Nuki Hub updater binary?"); + return(ESP_FAIL); + } + else if(_partitionType == 2 && _otaContentLen < 1600000) + { + Log->println("Uploaded OTA file is too small, are you trying to upload a Nuki Hub updater binary instead of a Nuki Hub binary?"); + return(ESP_FAIL); + } + + _otaStartTs = esp_timer_get_time() / 1000; + esp_task_wdt_config_t twdt_config = { + .timeout_ms = 30000, + .idle_core_mask = 0, + .trigger_panic = false, + }; + esp_task_wdt_reconfigure(&twdt_config); + + #ifndef NUKI_HUB_UPDATER + _network->disableAutoRestarts(); + _network->disableMqtt(); + if(_nuki != nullptr) + { + _nuki->disableWatchdog(); + } + if(_nukiOpener != nullptr) + { + _nukiOpener->disableWatchdog(); + } + #endif + Log->print("handleFileUpload Name: "); + Log->println(filename); + + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { + if (!Update.hasError()) + { + Update.abort(); + } + Log->print("ERROR : update.begin error Update.errorString() "); + Log->println(Update.errorString()); + return(ESP_FAIL); + } + } + + if ((len) && (!Update.hasError())) { + if (Update.write(data, len) != len) { + if (!Update.hasError()) + { + Update.abort(); + } + Log->print("ERROR : update.write error Update.errorString() "); + Log->println(Update.errorString()); + return(ESP_FAIL); + } + } + + if ((final) && (!Update.hasError())) { + if (Update.end(true)) { + Log->print("Update Success: "); + Log->print(index+len); + Log->println(" written"); + } + else { + if (!Update.hasError()) + { + Update.abort(); + } + Log->print("ERROR : update end error Update.errorString() "); + Log->println(Update.errorString()); + return(ESP_FAIL); + } + } + Log->print(F("Progress: 100%")); + Log->println(); + Log->print("handleFileUpload Total Size: "); + Log->println(index+len); + Log->println("Update complete"); + Log->flush(); + return(ESP_OK); + } + else { - Log->println("Starting manual OTA update"); - _otaContentLen = request->contentLength(); - - if(_partitionType == 1 && _otaContentLen > 1600000) - { - Log->println("Uploaded OTA file too large, are you trying to upload a Nuki Hub binary instead of a Nuki Hub updater binary?"); - return; - } - else if(_partitionType == 2 && _otaContentLen < 1600000) - { - Log->println("Uploaded OTA file is too small, are you trying to upload a Nuki Hub updater binary instead of a Nuki Hub binary?"); - return; - } - - int cmd = U_FLASH; - if (!Update.begin(UPDATE_SIZE_UNKNOWN, cmd)) { - Update.printError(Serial); - } - - _otaStartTs = esp_timer_get_time() / 1000; - esp_task_wdt_config_t twdt_config = { - .timeout_ms = 30000, - .idle_core_mask = 0, - .trigger_panic = false, - }; - esp_task_wdt_reconfigure(&twdt_config); - - #ifndef NUKI_HUB_UPDATER - _network->disableAutoRestarts(); - _network->disableMqtt(); - if(_nuki != nullptr) - { - _nuki->disableWatchdog(); - } - if(_nukiOpener != nullptr) - { - _nukiOpener->disableWatchdog(); - } - #endif - Log->print("handleFileUpload Name: "); - Log->println(filename); - } - - if (_otaContentLen == 0) return; - - if (Update.write(data, len) != len) { - Update.printError(Serial); - restartEsp(RestartReason::OTAAborted); - } - - if (final) { - AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots"); - response->addHeader("Refresh", "20"); - response->addHeader("Location", "/"); - request->send(response); - if (!Update.end(true)){ - Update.printError(Serial); - restartEsp(RestartReason::OTAAborted); - } else { - Log->print(F("Progress: 100%")); - Log->println(); - Log->print("handleFileUpload Total Size: "); - Log->println(index+len); - Log->println("Update complete"); - Log->flush(); - restartEsp(RestartReason::OTACompleted); - } + return(ESP_FAIL); } } -void WebCfgServer::buildConfirmHtml(AsyncWebServerRequest *request, const String &message, uint32_t redirectDelay, bool redirect) +esp_err_t WebCfgServer::buildConfirmHtml(PsychicRequest *request, const String &message, uint32_t redirectDelay, bool redirect) { - _response = ""; + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); String header; if(!redirect) @@ -570,25 +612,31 @@ void WebCfgServer::buildConfirmHtml(AsyncWebServerRequest *request, const String String delay(redirectDelay * 1000); header = ""; } - buildHtmlHeader(header); - _response.concat(message); - _response.concat(""); - sendResponse(request); + buildHtmlHeader(&response, header); + response.print(message); + response.print(""); + return response.endSend(); } -void WebCfgServer::sendCss(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::sendCss(PsychicRequest *request) { // escaped by https://www.cescaper.com/ - AsyncWebServerResponse *asyncResponse = request->beginResponse(200, "text/css", (const uint8_t*)stylecss, sizeof(stylecss)); - asyncResponse ->addHeader("Cache-Control", "public, max-age=3600"); - request->send(asyncResponse); + PsychicResponse response(request); + response.addHeader("Cache-Control", "public, max-age=3600"); + response.setCode(200); + response.setContentType("text/css"); + response.setContent(stylecss); + return response.send(); } -void WebCfgServer::sendFavicon(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::sendFavicon(PsychicRequest *request) { - AsyncWebServerResponse *asyncResponse = request->beginResponse(200, "image/png", (const uint8_t*)favicon_32x32, sizeof(favicon_32x32)); - asyncResponse->addHeader("Cache-Control", "public, max-age=604800"); - request->send(asyncResponse); + PsychicResponse response(request); + response.addHeader("Cache-Control", "public, max-age=604800"); + response.setCode(200); + response.setContentType("image/png"); + response.setContent((const char*)favicon_32x32); + return response.send(); } String WebCfgServer::generateConfirmCode() @@ -598,19 +646,19 @@ String WebCfgServer::generateConfirmCode() } #ifndef NUKI_HUB_UPDATER -void WebCfgServer::sendSettings(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) { bool redacted = false; bool pairing = false; if(request->hasParam("redacted")) { - const AsyncWebParameter* p = request->getParam("redacted"); + const PsychicWebParameter* p = request->getParam("redacted"); if(p->value() == "1") redacted = true; } if(request->hasParam("pairing")) { - const AsyncWebParameter* p = request->getParam("pairing"); + const PsychicWebParameter* p = request->getParam("pairing"); if(p->value() == "1") pairing = true; } @@ -769,17 +817,10 @@ void WebCfgServer::sendSettings(AsyncWebServerRequest *request) serializeJsonPretty(json, jsonPretty); - AsyncWebServerResponse *response = request->beginChunkedResponse("application/json", - [&](uint8_t *buffer, size_t maxlen, size_t index) -> size_t { - size_t len = min(maxlen, jsonPretty.length() - index); - memcpy(buffer, jsonPretty.c_str() + index, len); - return len; - }); - - request->send(response); + return request->reply(200, "application/json", jsonPretty.c_str()); } -bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) +bool WebCfgServer::processArgs(PsychicRequest *request, String& message) { bool configChanged = false; bool aclLvlChanged = false; @@ -809,13 +850,13 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) for(int index = 0; index < params; index++) { - const AsyncWebParameter* p = request->getParam(index); + const PsychicWebParameter* p = request->getParam(index); String key = p->name(); String value = p->value(); if(index < params -1) { - const AsyncWebParameter* next = request->getParam(index+1); + const PsychicWebParameter* next = request->getParam(index+1); if(key == next->name()) continue; } @@ -2323,7 +2364,7 @@ bool WebCfgServer::processArgs(AsyncWebServerRequest *request, String& message) return configChanged; } -bool WebCfgServer::processImport(AsyncWebServerRequest *request, String& message) +bool WebCfgServer::processImport(PsychicRequest *request, String& message) { bool configChanged = false; unsigned char currentBleAddress[6]; @@ -2337,7 +2378,7 @@ bool WebCfgServer::processImport(AsyncWebServerRequest *request, String& message for(int index = 0; index < params; index++) { - const AsyncWebParameter* p = request->getParam(index); + const PsychicWebParameter* p = request->getParam(index); if(p->name() == "importjson") { JsonDocument doc; @@ -2481,14 +2522,14 @@ bool WebCfgServer::processImport(AsyncWebServerRequest *request, String& message return configChanged; } -void WebCfgServer::processGpioArgs(AsyncWebServerRequest *request) +void WebCfgServer::processGpioArgs(PsychicRequest *request) { int params = request->params(); std::vector pinConfiguration; for(int index = 0; index < params; index++) { - const AsyncWebParameter* p = request->getParam(index); + const PsychicWebParameter* p = request->getParam(index); PinRole role = (PinRole)p->value().toInt(); if(role != PinRole::Disabled) { @@ -2502,87 +2543,90 @@ void WebCfgServer::processGpioArgs(AsyncWebServerRequest *request) _gpio->savePinConfiguration(pinConfiguration); } -void WebCfgServer::buildImportExportHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildImportExportHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - - _response.concat("

    Import configuration

    "); - _response.concat("

    "); - _response.concat("


    "); - _response.concat("
    "); - _response.concat("

    Export configuration


    "); - _response.concat(""); - _response.concat("

    "); - _response.concat("

    "); - _response.concat("
    "); - sendResponse(request); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print("

    Import configuration

    "); + response.print("

    "); + response.print("


    "); + response.print("
    "); + response.print("

    Export configuration


    "); + response.print(""); + response.print("

    "); + response.print("

    "); + response.print("
    "); + return response.endSend(); } -void WebCfgServer::buildCustomNetworkConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildCustomNetworkConfigHtml(PsychicRequest *request) { String header = ""; - _response = ""; - buildHtmlHeader(header); - _response.concat("
    "); - _response.concat("

    Custom Ethernet Configuration

    "); - _response.concat(""); - printDropDown("NWCUSTPHY", "PHY", String(_preferences->getInt(preference_network_custom_phy)), getNetworkCustomPHYOptions(), ""); - printInputField("NWCUSTADDR", "ADDR", _preferences->getInt(preference_network_custom_addr, 1), 6, ""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response, header); + response.print(""); + response.print("

    Custom Ethernet Configuration

    "); + response.print("
    "); + printDropDown(&response, "NWCUSTPHY", "PHY", String(_preferences->getInt(preference_network_custom_phy)), getNetworkCustomPHYOptions(), ""); + printInputField(&response, "NWCUSTADDR", "ADDR", _preferences->getInt(preference_network_custom_addr, 1), 6, ""); #if defined(CONFIG_IDF_TARGET_ESP32) - printDropDown("NWCUSTCLK", "CLK", String(_preferences->getInt(preference_network_custom_clk, 0)), getNetworkCustomCLKOptions(), "internalopt"); - printInputField("NWCUSTPWR", "PWR", _preferences->getInt(preference_network_custom_pwr, 12), 6, "class=\"internalopt\""); - printInputField("NWCUSTMDIO", "MDIO", _preferences->getInt(preference_network_custom_mdio), 6, "class=\"internalopt\""); - printInputField("NWCUSTMDC", "MDC", _preferences->getInt(preference_network_custom_mdc), 6, "class=\"internalopt\""); + printDropDown(&response, "NWCUSTCLK", "CLK", String(_preferences->getInt(preference_network_custom_clk, 0)), getNetworkCustomCLKOptions(), "internalopt"); + printInputField(&response, "NWCUSTPWR", "PWR", _preferences->getInt(preference_network_custom_pwr, 12), 6, "class=\"internalopt\""); + printInputField(&response, "NWCUSTMDIO", "MDIO", _preferences->getInt(preference_network_custom_mdio), 6, "class=\"internalopt\""); + printInputField(&response, "NWCUSTMDC", "MDC", _preferences->getInt(preference_network_custom_mdc), 6, "class=\"internalopt\""); #endif - printInputField("NWCUSTIRQ", "IRQ", _preferences->getInt(preference_network_custom_irq, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTRST", "RST", _preferences->getInt(preference_network_custom_rst, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTCS", "CS", _preferences->getInt(preference_network_custom_cs, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTSCK", "SCK", _preferences->getInt(preference_network_custom_sck, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTMISO", "MISO", _preferences->getInt(preference_network_custom_miso, -1), 6, "class=\"externalopt\""); - printInputField("NWCUSTMOSI", "MOSI", _preferences->getInt(preference_network_custom_mosi, -1), 6, "class=\"externalopt\""); - - _response.concat("
    "); - - _response.concat("
    "); - _response.concat("
    "); - _response.concat(""); - sendResponse(request); + printInputField(&response, "NWCUSTIRQ", "IRQ", _preferences->getInt(preference_network_custom_irq, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTRST", "RST", _preferences->getInt(preference_network_custom_rst, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTCS", "CS", _preferences->getInt(preference_network_custom_cs, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTSCK", "SCK", _preferences->getInt(preference_network_custom_sck, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTMISO", "MISO", _preferences->getInt(preference_network_custom_miso, -1), 6, "class=\"externalopt\""); + printInputField(&response, "NWCUSTMOSI", "MOSI", _preferences->getInt(preference_network_custom_mosi, -1), 6, "class=\"externalopt\""); + response.print(""); + response.print("
    "); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildHtml(PsychicRequest *request) { String header = ""; - _response = ""; - buildHtmlHeader(header); - - if(_rebootRequired) _response.concat("
    REBOOT REQUIRED TO APPLY SETTINGS
    "); - if(_preferences->getBool(preference_webserial_enabled, false)) _response.concat("
    WEBSERIAL IS ENABLED, ONLY ENABLE WHEN DEBUGGING AND DISABLE ASAP
    "); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response, header); + if(_rebootRequired) + { + response.print("
    REBOOT REQUIRED TO APPLY SETTINGS
    "); + } + if(_preferences->getBool(preference_webserial_enabled, false)) + { + response.print("
    WEBSERIAL IS ENABLED, ONLY ENABLE WHEN DEBUGGING AND DISABLE ASAP
    "); + } #ifdef DEBUG_NUKIHUB - _response.concat("
    RUNNING DEBUG BUILD, SWITCH TO RELEASE BUILD ASAP
    "); + response.print("
    RUNNING DEBUG BUILD, SWITCH TO RELEASE BUILD ASAP
    "); #endif - - _response.concat("

    Info


    "); - _response.concat(""); - - printParameter("Hostname", _hostname.c_str(), "", "hostname"); - printParameter("MQTT Connected", _network->mqttConnectionState() > 0 ? "Yes" : "No", "", "mqttState"); + response.print("

    Info


    "); + response.print("
    "); + printParameter(&response, "Hostname", _hostname.c_str(), "", "hostname"); + printParameter(&response, "MQTT Connected", _network->mqttConnectionState() > 0 ? "Yes" : "No", "", "mqttState"); if(_nuki != nullptr) { char lockStateArr[20]; NukiLock::lockstateToString(_nuki->keyTurnerState().lockState, lockStateArr); - printParameter("Nuki Lock paired", _nuki->isPaired() ? ("Yes (BLE Address " + _nuki->getBleAddress().toString() + ")").c_str() : "No", "", "lockPaired"); - printParameter("Nuki Lock state", lockStateArr, "", "lockState"); + printParameter(&response, "Nuki Lock paired", _nuki->isPaired() ? ("Yes (BLE Address " + _nuki->getBleAddress().toString() + ")").c_str() : "No", "", "lockPaired"); + printParameter(&response, "Nuki Lock state", lockStateArr, "", "lockState"); if(_nuki->isPaired()) { String lockState = pinStateToString(_preferences->getInt(preference_lock_pin_status, 4)); - printParameter("Nuki Lock PIN status", lockState.c_str(), "", "lockPin"); + printParameter(&response, "Nuki Lock PIN status", lockState.c_str(), "", "lockPin"); if(_preferences->getBool(preference_official_hybrid, false)) { String offConnected = _nuki->offConnected() ? "Yes": "No"; - printParameter("Nuki Lock hybrid mode connected", offConnected.c_str(), "", "lockHybrid"); + printParameter(&response, "Nuki Lock hybrid mode connected", offConnected.c_str(), "", "lockHybrid"); } } } @@ -2590,234 +2634,247 @@ void WebCfgServer::buildHtml(AsyncWebServerRequest *request) { char openerStateArr[20]; NukiOpener::lockstateToString(_nukiOpener->keyTurnerState().lockState, openerStateArr); - printParameter("Nuki Opener paired", _nukiOpener->isPaired() ? ("Yes (BLE Address " + _nukiOpener->getBleAddress().toString() + ")").c_str() : "No", "", "openerPaired"); - - if(_nukiOpener->keyTurnerState().nukiState == NukiOpener::State::ContinuousMode) printParameter("Nuki Opener state", "Open (Continuous Mode)", "", "openerState"); - else printParameter("Nuki Opener state", openerStateArr, "", "openerState"); + printParameter(&response, "Nuki Opener paired", _nukiOpener->isPaired() ? ("Yes (BLE Address " + _nukiOpener->getBleAddress().toString() + ")").c_str() : "No", "", "openerPaired"); + if(_nukiOpener->keyTurnerState().nukiState == NukiOpener::State::ContinuousMode) + { + printParameter(&response, "Nuki Opener state", "Open (Continuous Mode)", "", "openerState"); + } + else + { + printParameter(&response, "Nuki Opener state", openerStateArr, "", "openerState"); + } if(_nukiOpener->isPaired()) { String openerState = pinStateToString(_preferences->getInt(preference_opener_pin_status, 4)); - printParameter("Nuki Opener PIN status", openerState.c_str(), "", "openerPin"); + printParameter(&response, "Nuki Opener PIN status", openerState.c_str(), "", "openerPin"); } } - printParameter("Firmware", NUKI_HUB_VERSION, "/info", "firmware"); - if(_preferences->getBool(preference_check_updates)) printParameter("Latest Firmware", _preferences->getString(preference_latest_version).c_str(), "/ota", "ota"); - _response.concat("

    "); - _response.concat("
      "); - buildNavigationMenuEntry("MQTT and Network Configuration", "/mqttconfig", _brokerConfigured ? "" : "Please configure MQTT broker"); - buildNavigationMenuEntry("Nuki Configuration", "/nukicfg"); - buildNavigationMenuEntry("Access Level Configuration", "/acclvl"); - buildNavigationMenuEntry("Credentials", "/cred", _pinsConfigured ? "" : "Please configure PIN"); - buildNavigationMenuEntry("GPIO Configuration", "/gpiocfg"); - buildNavigationMenuEntry("Firmware update", "/ota"); - buildNavigationMenuEntry("Import/Export Configuration", "/impexpcfg"); + printParameter(&response, "Firmware", NUKI_HUB_VERSION, "/info", "firmware"); + if(_preferences->getBool(preference_check_updates)) + { + printParameter(&response, "Latest Firmware", _preferences->getString(preference_latest_version).c_str(), "/ota", "ota"); + } + response.print("
      "); + response.print("
        "); + buildNavigationMenuEntry(&response, "MQTT and Network Configuration", "/mqttconfig", _brokerConfigured ? "" : "Please configure MQTT broker"); + buildNavigationMenuEntry(&response, "Nuki Configuration", "/nukicfg"); + buildNavigationMenuEntry(&response, "Access Level Configuration", "/acclvl"); + buildNavigationMenuEntry(&response, "Credentials", "/cred", _pinsConfigured ? "" : "Please configure PIN"); + buildNavigationMenuEntry(&response, "GPIO Configuration", "/gpiocfg"); + buildNavigationMenuEntry(&response, "Firmware update", "/ota"); + buildNavigationMenuEntry(&response, "Import/Export Configuration", "/impexpcfg"); if(_preferences->getInt(preference_network_hardware, 0) == 11) { - buildNavigationMenuEntry("Custom Ethernet Configuration", "/custntw"); + buildNavigationMenuEntry(&response, "Custom Ethernet Configuration", "/custntw"); } if (_preferences->getBool(preference_publish_debug_info, false)) { - buildNavigationMenuEntry("Advanced Configuration", "/advanced"); + buildNavigationMenuEntry(&response, "Advanced Configuration", "/advanced"); } if(_preferences->getBool(preference_webserial_enabled, false)) { - buildNavigationMenuEntry("Open Webserial", "/webserial"); + buildNavigationMenuEntry(&response, "Open Webserial", "/webserial"); } #ifndef CONFIG_IDF_TARGET_ESP32H2 - if(_allowRestartToPortal) buildNavigationMenuEntry("Configure Wi-Fi", "/wifi"); + if(_allowRestartToPortal) + { + buildNavigationMenuEntry(&response, "Configure Wi-Fi", "/wifi"); + } #endif - buildNavigationMenuEntry("Reboot Nuki Hub", "/reboot"); - _response.concat("
      "); - sendResponse(request); + buildNavigationMenuEntry(&response, "Reboot Nuki Hub", "/reboot"); + response.print("
    "); + return response.endSend(); } -void WebCfgServer::buildCredHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
    "); - _response.concat("

    Credentials

    "); - _response.concat(""); - printInputField("CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, "", false, true); - printInputField("CREDPASS", "Password", "*", 30, "", true, true); - printInputField("CREDPASSRE", "Retype password", "*", 30, "", true); - _response.concat("
    "); - _response.concat("
    "); - _response.concat("
    "); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print("
    "); + response.print("

    Credentials

    "); + response.print(""); + printInputField(&response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, "", false, true); + printInputField(&response, "CREDPASS", "Password", "*", 30, "", true, true); + printInputField(&response, "CREDPASSRE", "Retype password", "*", 30, "", true); + response.print("
    "); + response.print("
    "); + response.print("
    "); if(_nuki != nullptr) { - _response.concat("

    "); - _response.concat("

    Nuki Lock PIN

    "); - _response.concat(""); - printInputField("NUKIPIN", "PIN Code (# to clear)", "*", 20, "", true); - _response.concat("
    "); - _response.concat("
    "); - _response.concat("
    "); + response.print("

    "); + response.print("

    Nuki Lock PIN

    "); + response.print(""); + printInputField(&response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, "", true); + response.print("
    "); + response.print("
    "); + response.print("
    "); } if(_nukiOpener != nullptr) { - _response.concat("

    "); - _response.concat("

    Nuki Opener PIN

    "); - _response.concat(""); - printInputField("NUKIOPPIN", "PIN Code (# to clear)", "*", 20, "", true); - _response.concat("
    "); - _response.concat("
    "); - _response.concat("
    "); + response.print("

    "); + response.print("

    Nuki Opener PIN

    "); + response.print(""); + printInputField(&response, "NUKIOPPIN", "PIN Code (# to clear)", "*", 20, "", true); + response.print("
    "); + response.print("
    "); + response.print("
    "); } if(_nuki != nullptr) { - _response.concat("

    Unpair Nuki Lock

    "); - _response.concat("
    "); - _response.concat(""); + response.print("

    Unpair Nuki Lock

    "); + response.print(""); + response.print("
    "); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm unpair"); - printInputField("CONFIRMTOKEN", message.c_str(), "", 10, ""); - _response.concat("
    "); - _response.concat("
    "); + printInputField(&response, "CONFIRMTOKEN", message.c_str(), "", 10, ""); + response.print(""); + response.print("
    "); } if(_nukiOpener != nullptr) { - _response.concat("

    Unpair Nuki Opener

    "); - _response.concat("
    "); - _response.concat(""); + response.print("

    Unpair Nuki Opener

    "); + response.print(""); + response.print("
    "); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm unpair"); - printInputField("CONFIRMTOKEN", message.c_str(), "", 10, ""); - _response.concat("
    "); - _response.concat("
    "); + printInputField(&response, "CONFIRMTOKEN", message.c_str(), "", 10, ""); + response.print(""); + response.print("
    "); } - _response.concat("

    Factory reset Nuki Hub

    "); - _response.concat("

    This will reset all settings to default and unpair Nuki Lock and/or Opener."); + response.print("

    Factory reset Nuki Hub

    "); + response.print("

    This will reset all settings to default and unpair Nuki Lock and/or Opener."); #ifndef CONFIG_IDF_TARGET_ESP32H2 - _response.concat("Optionally will also reset WiFi settings and reopen WiFi manager portal."); + response.print("Optionally will also reset WiFi settings and reopen WiFi manager portal."); #endif - _response.concat("

    "); - _response.concat("
    "); - _response.concat(""); + response.print(""); + response.print(""); + response.print("
    "); String message = "Type "; message.concat(_confirmCode); message.concat(" to confirm factory reset"); - printInputField("CONFIRMTOKEN", message.c_str(), "", 10, ""); + printInputField(&response, "CONFIRMTOKEN", message.c_str(), "", 10, ""); #ifndef CONFIG_IDF_TARGET_ESP32H2 - printCheckBox("WIFI", "Also reset WiFi settings", false, ""); + printCheckBox(&response, "WIFI", "Also reset WiFi settings", false, ""); #endif - _response.concat("
    "); - _response.concat("
    "); - _response.concat(""); - sendResponse(request); + response.print(""); + response.print("
    "); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildMqttConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildMqttConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
    "); - _response.concat("

    Basic MQTT and Network Configuration

    "); - _response.concat(""); - printInputField("HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100, ""); - printInputField("MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100, ""); - printInputField("MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5, ""); - printInputField("MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30, "", false, true); - printInputField("MQTTPASS", "MQTT Password", "*", 30, "", true, true); - _response.concat("

    "); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

    Basic MQTT and Network Configuration

    "); + response.print(""); + printInputField(&response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100, ""); + printInputField(&response, "MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100, ""); + printInputField(&response, "MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5, ""); + printInputField(&response, "MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30, "", false, true); + printInputField(&response, "MQTTPASS", "MQTT Password", "*", 30, "", true, true); + response.print("

    "); - _response.concat("

    Advanced MQTT and Network Configuration

    "); - _response.concat(""); - printInputField("HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30, ""); - printInputField("HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261, ""); - if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox("OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), ""); - printTextarea("MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported(), true); - printTextarea("MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true); - printTextarea("MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported(), true); - printDropDown("NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions(), ""); + response.print("

    Advanced MQTT and Network Configuration

    "); + response.print("
    "); + printInputField(&response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30, ""); + printInputField(&response, "HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261, ""); + if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox(&response, "OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), ""); + printTextarea(&response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported(), true); + printTextarea(&response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true); + printTextarea(&response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported(), true); + printDropDown(&response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions(), ""); #ifndef CONFIG_IDF_TARGET_ESP32H2 - printCheckBox("NWHWWIFIFB", "Disable fallback to Wi-Fi / Wi-Fi config portal", _preferences->getBool(preference_network_wifi_fallback_disabled), ""); - printCheckBox("BESTRSSI", "Connect to AP with the best signal in an environment with multiple APs with the same SSID", _preferences->getBool(preference_find_best_rssi), ""); - printInputField("RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6, ""); + printCheckBox(&response, "NWHWWIFIFB", "Disable fallback to Wi-Fi / Wi-Fi config portal", _preferences->getBool(preference_network_wifi_fallback_disabled), ""); + printCheckBox(&response, "BESTRSSI", "Connect to AP with the best signal in an environment with multiple APs with the same SSID", _preferences->getBool(preference_find_best_rssi), ""); + printInputField(&response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6, ""); #endif - printInputField("NETTIMEOUT", "MQTT Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5, ""); - printCheckBox("RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect), ""); - printCheckBox("RECNWTMQTTDIS", "Reconnect network on MQTT connection failure", _preferences->getBool(preference_recon_netw_on_mqtt_discon), ""); - printCheckBox("MQTTLOG", "Enable MQTT logging", _preferences->getBool(preference_mqtt_log_enabled), ""); - printCheckBox("CHECKUPDATE", "Check for Firmware Updates every 24h", _preferences->getBool(preference_check_updates), ""); - printCheckBox("UPDATEMQTT", "Allow updating using MQTT", _preferences->getBool(preference_update_from_mqtt), ""); - printCheckBox("DISNONJSON", "Disable some extraneous non-JSON topics", _preferences->getBool(preference_disable_non_json), ""); - printCheckBox("OFFHYBRID", "Enable hybrid official MQTT and Nuki Hub setup", _preferences->getBool(preference_official_hybrid), ""); - printCheckBox("HYBRIDACT", "Enable sending actions through official MQTT", _preferences->getBool(preference_official_hybrid_actions), ""); - printInputField("HYBRIDTIMER", "Time between status updates when official MQTT is offline (seconds)", _preferences->getInt(preference_query_interval_hybrid_lockstate), 5, ""); - // printCheckBox("HYBRIDRETRY", "Retry command sent using official MQTT over BLE if failed", _preferences->getBool(preference_official_hybrid_retry), ""); // NOT IMPLEMENTED (YET?) - _response.concat("
    "); - _response.concat("* If no encryption is configured for the MQTT broker, leave empty.

    "); + printInputField(&response, "NETTIMEOUT", "MQTT Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5, ""); + printCheckBox(&response, "RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect), ""); + printCheckBox(&response, "RECNWTMQTTDIS", "Reconnect network on MQTT connection failure", _preferences->getBool(preference_recon_netw_on_mqtt_discon), ""); + printCheckBox(&response, "MQTTLOG", "Enable MQTT logging", _preferences->getBool(preference_mqtt_log_enabled), ""); + printCheckBox(&response, "CHECKUPDATE", "Check for Firmware Updates every 24h", _preferences->getBool(preference_check_updates), ""); + printCheckBox(&response, "UPDATEMQTT", "Allow updating using MQTT", _preferences->getBool(preference_update_from_mqtt), ""); + printCheckBox(&response, "DISNONJSON", "Disable some extraneous non-JSON topics", _preferences->getBool(preference_disable_non_json), ""); + printCheckBox(&response, "OFFHYBRID", "Enable hybrid official MQTT and Nuki Hub setup", _preferences->getBool(preference_official_hybrid), ""); + printCheckBox(&response, "HYBRIDACT", "Enable sending actions through official MQTT", _preferences->getBool(preference_official_hybrid_actions), ""); + printInputField(&response, "HYBRIDTIMER", "Time between status updates when official MQTT is offline (seconds)", _preferences->getInt(preference_query_interval_hybrid_lockstate), 5, ""); + // printCheckBox(&response, "HYBRIDRETRY", "Retry command sent using official MQTT over BLE if failed", _preferences->getBool(preference_official_hybrid_retry), ""); // NOT IMPLEMENTED (YET?) + response.print(""); + response.print("* If no encryption is configured for the MQTT broker, leave empty.

    "); - _response.concat("

    IP Address assignment

    "); - _response.concat(""); - printCheckBox("DHCPENA", "Enable DHCP", _preferences->getBool(preference_ip_dhcp_enabled), ""); - printInputField("IPADDR", "Static IP address", _preferences->getString(preference_ip_address).c_str(), 15, ""); - printInputField("IPSUB", "Subnet", _preferences->getString(preference_ip_subnet).c_str(), 15, ""); - printInputField("IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15, ""); - printInputField("DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15, ""); - _response.concat("
    "); - _response.concat("
    "); - _response.concat("
    "); - _response.concat(""); - sendResponse(request); + response.print("

    IP Address assignment

    "); + response.print(""); + printCheckBox(&response, "DHCPENA", "Enable DHCP", _preferences->getBool(preference_ip_dhcp_enabled), ""); + printInputField(&response, "IPADDR", "Static IP address", _preferences->getString(preference_ip_address).c_str(), 15, ""); + printInputField(&response, "IPSUB", "Subnet", _preferences->getString(preference_ip_subnet).c_str(), 15, ""); + printInputField(&response, "IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15, ""); + printInputField(&response, "DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15, ""); + response.print("
    "); + response.print("
    "); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildAdvancedConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildAdvancedConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
    "); - _response.concat("

    Advanced Configuration

    "); - _response.concat("

    Warning: Changing these settings can lead to bootloops that might require you to erase the ESP32 and reflash nukihub using USB/serial

    "); - _response.concat(""); - _response.concat(""); - printCheckBox("WEBLOG", "Enable WebSerial logging", _preferences->getBool(preference_webserial_enabled), ""); - printCheckBox("BTLPRST", "Enable Bootloop prevention (Try to reset these settings to default on bootloop)", true, ""); - printInputField("BUFFSIZE", "Char buffer size (min 4096, max 32768)", _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE), 6, ""); - _response.concat(""); - printInputField("TSKNTWK", "Task size Network (min 12288, max 32768)", _preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), 6, ""); - _response.concat(""); - printInputField("TSKNUKI", "Task size Nuki (min 8192, max 32768)", _preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), 6, ""); - printInputField("ALMAX", "Max auth log entries (min 1, max 50)", _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 3, "id=\"inputmaxauthlog\""); - printInputField("KPMAX", "Max keypad entries (min 1, max 100)", _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD), 3, "id=\"inputmaxkeypad\""); - printInputField("TCMAX", "Max timecontrol entries (min 1, max 50)", _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL), 3, "id=\"inputmaxtimecontrol\""); - printInputField("AUTHMAX", "Max authorization entries (min 1, max 50)", _preferences->getInt(preference_auth_max_entries, MAX_AUTH), 3, "id=\"inputmaxauth\""); - printCheckBox("SHOWSECRETS", "Show Pairing secrets on Info page", _preferences->getBool(preference_show_secrets), ""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

    Advanced Configuration

    "); + response.print("

    Warning: Changing these settings can lead to bootloops that might require you to erase the ESP32 and reflash nukihub using USB/serial

    "); + response.print("
    Current bootloop prevention state"); - _response.concat(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Enabled" : "Disabled"); - _response.concat("
    Advised minimum char buffer size based on current settings
    Advised minimum network task size based on current settings
    "); + response.print(""); + printCheckBox(&response, "WEBLOG", "Enable WebSerial logging", _preferences->getBool(preference_webserial_enabled), ""); + printCheckBox(&response, "BTLPRST", "Enable Bootloop prevention (Try to reset these settings to default on bootloop)", true, ""); + printInputField(&response, "BUFFSIZE", "Char buffer size (min 4096, max 32768)", _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE), 6, ""); + response.print(""); + printInputField(&response, "TSKNTWK", "Task size Network (min 12288, max 32768)", _preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), 6, ""); + response.print(""); + printInputField(&response, "TSKNUKI", "Task size Nuki (min 8192, max 32768)", _preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), 6, ""); + printInputField(&response, "ALMAX", "Max auth log entries (min 1, max 50)", _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 3, "id=\"inputmaxauthlog\""); + printInputField(&response, "KPMAX", "Max keypad entries (min 1, max 100)", _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD), 3, "id=\"inputmaxkeypad\""); + printInputField(&response, "TCMAX", "Max timecontrol entries (min 1, max 50)", _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL), 3, "id=\"inputmaxtimecontrol\""); + printInputField(&response, "AUTHMAX", "Max authorization entries (min 1, max 50)", _preferences->getInt(preference_auth_max_entries, MAX_AUTH), 3, "id=\"inputmaxauth\""); + printCheckBox(&response, "SHOWSECRETS", "Show Pairing secrets on Info page", _preferences->getBool(preference_show_secrets), ""); if(_preferences->getBool(preference_lock_enabled, true)) { - printCheckBox("LCKMANPAIR", "Manually set lock pairing data (enable to save values below)", false, ""); - printInputField("LCKBLEADDR", "currentBleAddress", "", 12, ""); - printInputField("LCKSECRETK", "secretKeyK", "", 64, ""); - printInputField("LCKAUTHID", "authorizationId", "", 8, ""); + printCheckBox(&response, "LCKMANPAIR", "Manually set lock pairing data (enable to save values below)", false, ""); + printInputField(&response, "LCKBLEADDR", "currentBleAddress", "", 12, ""); + printInputField(&response, "LCKSECRETK", "secretKeyK", "", 64, ""); + printInputField(&response, "LCKAUTHID", "authorizationId", "", 8, ""); } if(_preferences->getBool(preference_opener_enabled, false)) { - printCheckBox("OPNMANPAIR", "Manually set opener pairing data (enable to save values below)", false, ""); - printInputField("OPNBLEADDR", "currentBleAddress", "", 12, ""); - printInputField("OPNSECRETK", "secretKeyK", "", 64, ""); - printInputField("OPNAUTHID", "authorizationId", "", 8, ""); + printCheckBox(&response, "OPNMANPAIR", "Manually set opener pairing data (enable to save values below)", false, ""); + printInputField(&response, "OPNBLEADDR", "currentBleAddress", "", 12, ""); + printInputField(&response, "OPNSECRETK", "secretKeyK", "", 64, ""); + printInputField(&response, "OPNAUTHID", "authorizationId", "", 8, ""); } - printInputField("OTAUPD", "Custom URL to update Nuki Hub updater", "", 255, ""); - printInputField("OTAMAIN", "Custom URL to update Nuki Hub", "", 255, ""); - _response.concat("
    Current bootloop prevention state"); + response.print(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Enabled" : "Disabled"); + response.print("
    Advised minimum char buffer size based on current settings
    Advised minimum network task size based on current settings
    "); + printInputField(&response, "OTAUPD", "Custom URL to update Nuki Hub updater", "", 255, ""); + printInputField(&response, "OTAMAIN", "Custom URL to update Nuki Hub", "", 255, ""); + response.print(""); - _response.concat("
    "); - _response.concat("
    "); - _response.concat(""); - sendResponse(request); + response.print("
    "); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildStatusHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildStatusHtml(PsychicRequest *request) { - _response = ""; JsonDocument json; - char _resbuf[2048]; + String jsonStr; bool mqttDone = false; bool lockDone = false; bool openerDone = false; @@ -2878,9 +2935,8 @@ void WebCfgServer::buildStatusHtml(AsyncWebServerRequest *request) if(mqttDone && lockDone && openerDone && latestDone) json["stop"] = 1; - serializeJson(json, _resbuf, sizeof(_resbuf)); - _response.concat(_resbuf); - sendResponse(request); + serializeJson(json, jsonStr); + return request->reply(200, "application/json", jsonStr.c_str()); } String WebCfgServer::pinStateToString(uint8_t value) { @@ -2897,36 +2953,37 @@ String WebCfgServer::pinStateToString(uint8_t value) { } } -void WebCfgServer::buildAccLvlHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildAccLvlHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); - _response.concat("
    "); - _response.concat(""); - _response.concat("

    Nuki General Access Control

    "); - _response.concat(""); - printCheckBox("CONFPUB", "Publish Nuki configuration information", _preferences->getBool(preference_conf_info_enabled, true), ""); + response.print(""); + response.print(""); + response.print("

    Nuki General Access Control

    "); + response.print("
    SettingEnabled
    "); + printCheckBox(&response, "CONFPUB", "Publish Nuki configuration information", _preferences->getBool(preference_conf_info_enabled, true), ""); if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad())) { - printCheckBox("KPPUB", "Publish keypad entries information", _preferences->getBool(preference_keypad_info_enabled), ""); - printCheckBox("KPPER", "Publish a topic per keypad entry and create HA sensor", _preferences->getBool(preference_keypad_topic_per_entry), ""); - printCheckBox("KPCODE", "Also publish keypad codes (Disadvised for security reasons)", _preferences->getBool(preference_keypad_publish_code, false), ""); - printCheckBox("KPENA", "Add, modify and delete keypad codes", _preferences->getBool(preference_keypad_control_enabled), ""); + printCheckBox(&response, "KPPUB", "Publish keypad entries information", _preferences->getBool(preference_keypad_info_enabled), ""); + printCheckBox(&response, "KPPER", "Publish a topic per keypad entry and create HA sensor", _preferences->getBool(preference_keypad_topic_per_entry), ""); + printCheckBox(&response, "KPCODE", "Also publish keypad codes (Disadvised for security reasons)", _preferences->getBool(preference_keypad_publish_code, false), ""); + printCheckBox(&response, "KPENA", "Add, modify and delete keypad codes", _preferences->getBool(preference_keypad_control_enabled), ""); } - printCheckBox("TCPUB", "Publish time control entries information", _preferences->getBool(preference_timecontrol_info_enabled), ""); - printCheckBox("TCPER", "Publish a topic per time control entry and create HA sensor", _preferences->getBool(preference_timecontrol_topic_per_entry), ""); - printCheckBox("TCENA", "Add, modify and delete time control entries", _preferences->getBool(preference_timecontrol_control_enabled), ""); - printCheckBox("AUTHPUB", "Publish authorization entries information", _preferences->getBool(preference_auth_info_enabled), ""); - printCheckBox("AUTHPER", "Publish a topic per authorization entry and create HA sensor", _preferences->getBool(preference_auth_topic_per_entry), ""); - printCheckBox("AUTHENA", "Modify and delete authorization entries", _preferences->getBool(preference_auth_control_enabled), ""); - printCheckBox("PUBAUTH", "Publish authorization log", _preferences->getBool(preference_publish_authdata), ""); - _response.concat("
    SettingEnabled

    "); - _response.concat("
    "); + printCheckBox(&response, "TCPUB", "Publish time control entries information", _preferences->getBool(preference_timecontrol_info_enabled), ""); + printCheckBox(&response, "TCPER", "Publish a topic per time control entry and create HA sensor", _preferences->getBool(preference_timecontrol_topic_per_entry), ""); + printCheckBox(&response, "TCENA", "Add, modify and delete time control entries", _preferences->getBool(preference_timecontrol_control_enabled), ""); + printCheckBox(&response, "AUTHPUB", "Publish authorization entries information", _preferences->getBool(preference_auth_info_enabled), ""); + printCheckBox(&response, "AUTHPER", "Publish a topic per authorization entry and create HA sensor", _preferences->getBool(preference_auth_topic_per_entry), ""); + printCheckBox(&response, "AUTHENA", "Modify and delete authorization entries", _preferences->getBool(preference_auth_control_enabled), ""); + printCheckBox(&response, "PUBAUTH", "Publish authorization log", _preferences->getBool(preference_publish_authdata), ""); + response.print("
    "); + response.print("
    "); if(_nuki != nullptr) { @@ -2935,72 +2992,72 @@ void WebCfgServer::buildAccLvlHtml(AsyncWebServerRequest *request) uint32_t advancedLockConfigAclPrefs[22]; _preferences->getBytes(preference_conf_lock_advanced_acl, &advancedLockConfigAclPrefs, sizeof(advancedLockConfigAclPrefs)); - _response.concat("

    Nuki Lock Access Control

    "); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

    Nuki Lock Access Control

    "); + response.print(""); + response.print(""); + response.print("
    ActionAllowed
    "); - printCheckBox("ACLLCKLCK", "Lock", ((int)aclPrefs[0] == 1), "chk_access_lock"); - printCheckBox("ACLLCKUNLCK", "Unlock", ((int)aclPrefs[1] == 1), "chk_access_lock"); - printCheckBox("ACLLCKUNLTCH", "Unlatch", ((int)aclPrefs[2] == 1), "chk_access_lock"); - printCheckBox("ACLLCKLNG", "Lock N Go", ((int)aclPrefs[3] == 1), "chk_access_lock"); - printCheckBox("ACLLCKLNGU", "Lock N Go Unlatch", ((int)aclPrefs[4] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFLLCK", "Full Lock", ((int)aclPrefs[5] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFOB1", "Fob Action 1", ((int)aclPrefs[6] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFOB2", "Fob Action 2", ((int)aclPrefs[7] == 1), "chk_access_lock"); - printCheckBox("ACLLCKFOB3", "Fob Action 3", ((int)aclPrefs[8] == 1), "chk_access_lock"); - _response.concat("
    ActionAllowed

    "); + printCheckBox(&response, "ACLLCKLCK", "Lock", ((int)aclPrefs[0] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKUNLCK", "Unlock", ((int)aclPrefs[1] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKUNLTCH", "Unlatch", ((int)aclPrefs[2] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKLNG", "Lock N Go", ((int)aclPrefs[3] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKLNGU", "Lock N Go Unlatch", ((int)aclPrefs[4] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFLLCK", "Full Lock", ((int)aclPrefs[5] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFOB1", "Fob Action 1", ((int)aclPrefs[6] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFOB2", "Fob Action 2", ((int)aclPrefs[7] == 1), "chk_access_lock"); + printCheckBox(&response, "ACLLCKFOB3", "Fob Action 3", ((int)aclPrefs[8] == 1), "chk_access_lock"); + response.print("
    "); - _response.concat("

    Nuki Lock Config Control (Requires PIN to be set)

    "); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

    Nuki Lock Config Control (Requires PIN to be set)

    "); + response.print(""); + response.print(""); + response.print("
    ChangeAllowed
    "); - printCheckBox("CONFLCKNAME", "Name", ((int)basicLockConfigAclPrefs[0] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLAT", "Latitude", ((int)basicLockConfigAclPrefs[1] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLONG", "Longitude", ((int)basicLockConfigAclPrefs[2] == 1), "chk_config_lock"); - printCheckBox("CONFLCKAUNL", "Auto unlatch", ((int)basicLockConfigAclPrefs[3] == 1), "chk_config_lock"); - printCheckBox("CONFLCKPRENA", "Pairing enabled", ((int)basicLockConfigAclPrefs[4] == 1), "chk_config_lock"); - printCheckBox("CONFLCKBTENA", "Button enabled", ((int)basicLockConfigAclPrefs[5] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLEDENA", "LED flash enabled", ((int)basicLockConfigAclPrefs[6] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLEDBR", "LED brightness", ((int)basicLockConfigAclPrefs[7] == 1), "chk_config_lock"); - printCheckBox("CONFLCKTZOFF", "Timezone offset", ((int)basicLockConfigAclPrefs[8] == 1), "chk_config_lock"); - printCheckBox("CONFLCKDSTM", "DST mode", ((int)basicLockConfigAclPrefs[9] == 1), "chk_config_lock"); - printCheckBox("CONFLCKFOB1", "Fob Action 1", ((int)basicLockConfigAclPrefs[10] == 1), "chk_config_lock"); - printCheckBox("CONFLCKFOB2", "Fob Action 2", ((int)basicLockConfigAclPrefs[11] == 1), "chk_config_lock"); - printCheckBox("CONFLCKFOB3", "Fob Action 3", ((int)basicLockConfigAclPrefs[12] == 1), "chk_config_lock"); - printCheckBox("CONFLCKSGLLCK", "Single Lock", ((int)basicLockConfigAclPrefs[13] == 1), "chk_config_lock"); - printCheckBox("CONFLCKADVM", "Advertising Mode", ((int)basicLockConfigAclPrefs[14] == 1), "chk_config_lock"); - printCheckBox("CONFLCKTZID", "Timezone ID", ((int)basicLockConfigAclPrefs[15] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNAME", "Name", ((int)basicLockConfigAclPrefs[0] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLAT", "Latitude", ((int)basicLockConfigAclPrefs[1] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLONG", "Longitude", ((int)basicLockConfigAclPrefs[2] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKAUNL", "Auto unlatch", ((int)basicLockConfigAclPrefs[3] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKPRENA", "Pairing enabled", ((int)basicLockConfigAclPrefs[4] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKBTENA", "Button enabled", ((int)basicLockConfigAclPrefs[5] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLEDENA", "LED flash enabled", ((int)basicLockConfigAclPrefs[6] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLEDBR", "LED brightness", ((int)basicLockConfigAclPrefs[7] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKTZOFF", "Timezone offset", ((int)basicLockConfigAclPrefs[8] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKDSTM", "DST mode", ((int)basicLockConfigAclPrefs[9] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKFOB1", "Fob Action 1", ((int)basicLockConfigAclPrefs[10] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKFOB2", "Fob Action 2", ((int)basicLockConfigAclPrefs[11] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKFOB3", "Fob Action 3", ((int)basicLockConfigAclPrefs[12] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKSGLLCK", "Single Lock", ((int)basicLockConfigAclPrefs[13] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKADVM", "Advertising Mode", ((int)basicLockConfigAclPrefs[14] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKTZID", "Timezone ID", ((int)basicLockConfigAclPrefs[15] == 1), "chk_config_lock"); - printCheckBox("CONFLCKUPOD", "Unlocked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[0] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLPOD", "Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[1] == 1), "chk_config_lock"); - printCheckBox("CONFLCKSLPOD", "Single Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[2] == 1), "chk_config_lock"); - printCheckBox("CONFLCKUTLTOD", "Unlocked To Locked Transition Offset Degrees", ((int)advancedLockConfigAclPrefs[3] == 1), "chk_config_lock"); - printCheckBox("CONFLCKLNGT", "Lock n Go timeout", ((int)advancedLockConfigAclPrefs[4] == 1), "chk_config_lock"); - printCheckBox("CONFLCKSBPA", "Single button press action", ((int)advancedLockConfigAclPrefs[5] == 1), "chk_config_lock"); - printCheckBox("CONFLCKDBPA", "Double button press action", ((int)advancedLockConfigAclPrefs[6] == 1), "chk_config_lock"); - printCheckBox("CONFLCKDC", "Detached cylinder", ((int)advancedLockConfigAclPrefs[7] == 1), "chk_config_lock"); - printCheckBox("CONFLCKBATT", "Battery type", ((int)advancedLockConfigAclPrefs[8] == 1), "chk_config_lock"); - printCheckBox("CONFLCKABTD", "Automatic battery type detection", ((int)advancedLockConfigAclPrefs[9] == 1), "chk_config_lock"); - printCheckBox("CONFLCKUNLD", "Unlatch duration", ((int)advancedLockConfigAclPrefs[10] == 1), "chk_config_lock"); - printCheckBox("CONFLCKALT", "Auto lock timeout", ((int)advancedLockConfigAclPrefs[11] == 1), "chk_config_lock"); - printCheckBox("CONFLCKAUNLD", "Auto unlock disabled", ((int)advancedLockConfigAclPrefs[12] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMENA", "Nightmode enabled", ((int)advancedLockConfigAclPrefs[13] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMST", "Nightmode start time", ((int)advancedLockConfigAclPrefs[14] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMET", "Nightmode end time", ((int)advancedLockConfigAclPrefs[15] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMALENA", "Nightmode auto lock enabled", ((int)advancedLockConfigAclPrefs[16] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMAULD", "Nightmode auto unlock disabled", ((int)advancedLockConfigAclPrefs[17] == 1), "chk_config_lock"); - printCheckBox("CONFLCKNMLOS", "Nightmode immediate lock on start", ((int)advancedLockConfigAclPrefs[18] == 1), "chk_config_lock"); - printCheckBox("CONFLCKALENA", "Auto lock enabled", ((int)advancedLockConfigAclPrefs[19] == 1), "chk_config_lock"); - printCheckBox("CONFLCKIALENA", "Immediate auto lock enabled", ((int)advancedLockConfigAclPrefs[20] == 1), "chk_config_lock"); - printCheckBox("CONFLCKAUENA", "Auto update enabled", ((int)advancedLockConfigAclPrefs[21] == 1), "chk_config_lock"); - _response.concat("
    ChangeAllowed

    "); - _response.concat("
    "); + printCheckBox(&response, "CONFLCKUPOD", "Unlocked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[0] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLPOD", "Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[1] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKSLPOD", "Single Locked Position Offset Degrees", ((int)advancedLockConfigAclPrefs[2] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKUTLTOD", "Unlocked To Locked Transition Offset Degrees", ((int)advancedLockConfigAclPrefs[3] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKLNGT", "Lock n Go timeout", ((int)advancedLockConfigAclPrefs[4] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKSBPA", "Single button press action", ((int)advancedLockConfigAclPrefs[5] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKDBPA", "Double button press action", ((int)advancedLockConfigAclPrefs[6] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKDC", "Detached cylinder", ((int)advancedLockConfigAclPrefs[7] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKBATT", "Battery type", ((int)advancedLockConfigAclPrefs[8] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKABTD", "Automatic battery type detection", ((int)advancedLockConfigAclPrefs[9] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKUNLD", "Unlatch duration", ((int)advancedLockConfigAclPrefs[10] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKALT", "Auto lock timeout", ((int)advancedLockConfigAclPrefs[11] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKAUNLD", "Auto unlock disabled", ((int)advancedLockConfigAclPrefs[12] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMENA", "Nightmode enabled", ((int)advancedLockConfigAclPrefs[13] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMST", "Nightmode start time", ((int)advancedLockConfigAclPrefs[14] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMET", "Nightmode end time", ((int)advancedLockConfigAclPrefs[15] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMALENA", "Nightmode auto lock enabled", ((int)advancedLockConfigAclPrefs[16] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMAULD", "Nightmode auto unlock disabled", ((int)advancedLockConfigAclPrefs[17] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKNMLOS", "Nightmode immediate lock on start", ((int)advancedLockConfigAclPrefs[18] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKALENA", "Auto lock enabled", ((int)advancedLockConfigAclPrefs[19] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKIALENA", "Immediate auto lock enabled", ((int)advancedLockConfigAclPrefs[20] == 1), "chk_config_lock"); + printCheckBox(&response, "CONFLCKAUENA", "Auto update enabled", ((int)advancedLockConfigAclPrefs[21] == 1), "chk_config_lock"); + response.print("
    "); + response.print("
    "); } if(_nukiOpener != nullptr) { @@ -3009,117 +3066,119 @@ void WebCfgServer::buildAccLvlHtml(AsyncWebServerRequest *request) uint32_t advancedOpenerConfigAclPrefs[20]; _preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs)); - _response.concat("

    Nuki Opener Access Control

    "); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

    Nuki Opener Access Control

    "); + response.print(""); + response.print(""); + response.print("
    ActionAllowed
    "); - printCheckBox("ACLOPNUNLCK", "Activate Ring-to-Open", ((int)aclPrefs[9] == 1), "chk_access_opener"); - printCheckBox("ACLOPNLCK", "Deactivate Ring-to-Open", ((int)aclPrefs[10] == 1), "chk_access_opener"); - printCheckBox("ACLOPNUNLTCH", "Electric Strike Actuation", ((int)aclPrefs[11] == 1), "chk_access_opener"); - printCheckBox("ACLOPNUNLCKCM", "Activate Continuous Mode", ((int)aclPrefs[12] == 1), "chk_access_opener"); - printCheckBox("ACLOPNLCKCM", "Deactivate Continuous Mode", ((int)aclPrefs[13] == 1), "chk_access_opener"); - printCheckBox("ACLOPNFOB1", "Fob Action 1", ((int)aclPrefs[14] == 1), "chk_access_opener"); - printCheckBox("ACLOPNFOB2", "Fob Action 2", ((int)aclPrefs[15] == 1), "chk_access_opener"); - printCheckBox("ACLOPNFOB3", "Fob Action 3", ((int)aclPrefs[16] == 1), "chk_access_opener"); - _response.concat("
    ActionAllowed

    "); + printCheckBox(&response, "ACLOPNUNLCK", "Activate Ring-to-Open", ((int)aclPrefs[9] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNLCK", "Deactivate Ring-to-Open", ((int)aclPrefs[10] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNUNLTCH", "Electric Strike Actuation", ((int)aclPrefs[11] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNUNLCKCM", "Activate Continuous Mode", ((int)aclPrefs[12] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNLCKCM", "Deactivate Continuous Mode", ((int)aclPrefs[13] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNFOB1", "Fob Action 1", ((int)aclPrefs[14] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNFOB2", "Fob Action 2", ((int)aclPrefs[15] == 1), "chk_access_opener"); + printCheckBox(&response, "ACLOPNFOB3", "Fob Action 3", ((int)aclPrefs[16] == 1), "chk_access_opener"); + response.print("
    "); - _response.concat("

    Nuki Opener Config Control (Requires PIN to be set)

    "); - _response.concat(""); - _response.concat(""); - _response.concat(""); + response.print("

    Nuki Opener Config Control (Requires PIN to be set)

    "); + response.print(""); + response.print(""); + response.print("
    ChangeAllowed
    "); - printCheckBox("CONFOPNNAME", "Name", ((int)basicOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); - printCheckBox("CONFOPNLAT", "Latitude", ((int)basicOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); - printCheckBox("CONFOPNLONG", "Longitude", ((int)basicOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); - printCheckBox("CONFOPNPRENA", "Pairing enabled", ((int)basicOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); - printCheckBox("CONFOPNBTENA", "Button enabled", ((int)basicOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); - printCheckBox("CONFOPNLEDENA", "LED flash enabled", ((int)basicOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); - printCheckBox("CONFOPNTZOFF", "Timezone offset", ((int)basicOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDSTM", "DST mode", ((int)basicOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); - printCheckBox("CONFOPNFOB1", "Fob Action 1", ((int)basicOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); - printCheckBox("CONFOPNFOB2", "Fob Action 2", ((int)basicOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); - printCheckBox("CONFOPNFOB3", "Fob Action 3", ((int)basicOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); - printCheckBox("CONFOPNOPM", "Operating Mode", ((int)basicOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); - printCheckBox("CONFOPNADVM", "Advertising Mode", ((int)basicOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); - printCheckBox("CONFOPNTZID", "Timezone ID", ((int)basicOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNNAME", "Name", ((int)basicOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNLAT", "Latitude", ((int)basicOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNLONG", "Longitude", ((int)basicOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNPRENA", "Pairing enabled", ((int)basicOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNBTENA", "Button enabled", ((int)basicOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNLEDENA", "LED flash enabled", ((int)basicOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNTZOFF", "Timezone offset", ((int)basicOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDSTM", "DST mode", ((int)basicOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNFOB1", "Fob Action 1", ((int)basicOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNFOB2", "Fob Action 2", ((int)basicOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNFOB3", "Fob Action 3", ((int)basicOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNOPM", "Operating Mode", ((int)basicOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNADVM", "Advertising Mode", ((int)basicOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNTZID", "Timezone ID", ((int)basicOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); - printCheckBox("CONFOPNICID", "Intercom ID", ((int)advancedOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); - printCheckBox("CONFOPNBUSMS", "BUS mode Switch", ((int)advancedOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSCDUR", "Short Circuit Duration", ((int)advancedOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); - printCheckBox("CONFOPNESD", "Eletric Strike Delay", ((int)advancedOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); - printCheckBox("CONFOPNRESD", "Random Electric Strike Delay", ((int)advancedOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); - printCheckBox("CONFOPNESDUR", "Electric Strike Duration", ((int)advancedOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDRTOAR", "Disable RTO after ring", ((int)advancedOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); - printCheckBox("CONFOPNRTOT", "RTO timeout", ((int)advancedOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDRBSUP", "Doorbell suppression", ((int)advancedOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDRBSUPDUR", "Doorbell suppression duration", ((int)advancedOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSRING", "Sound Ring", ((int)advancedOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSOPN", "Sound Open", ((int)advancedOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSRTO", "Sound RTO", ((int)advancedOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSCM", "Sound CM", ((int)advancedOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSCFRM", "Sound confirmation", ((int)advancedOpenerConfigAclPrefs[14] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSLVL", "Sound level", ((int)advancedOpenerConfigAclPrefs[15] == 1), "chk_config_opener"); - printCheckBox("CONFOPNSBPA", "Single button press action", ((int)advancedOpenerConfigAclPrefs[16] == 1), "chk_config_opener"); - printCheckBox("CONFOPNDBPA", "Double button press action", ((int)advancedOpenerConfigAclPrefs[17] == 1), "chk_config_opener"); - printCheckBox("CONFOPNBATT", "Battery type", ((int)advancedOpenerConfigAclPrefs[18] == 1), "chk_config_opener"); - printCheckBox("CONFOPNABTD", "Automatic battery type detection", ((int)advancedOpenerConfigAclPrefs[19] == 1), "chk_config_opener"); - _response.concat("
    ChangeAllowed

    "); - _response.concat("
    "); + printCheckBox(&response, "CONFOPNICID", "Intercom ID", ((int)advancedOpenerConfigAclPrefs[0] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNBUSMS", "BUS mode Switch", ((int)advancedOpenerConfigAclPrefs[1] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSCDUR", "Short Circuit Duration", ((int)advancedOpenerConfigAclPrefs[2] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNESD", "Eletric Strike Delay", ((int)advancedOpenerConfigAclPrefs[3] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNRESD", "Random Electric Strike Delay", ((int)advancedOpenerConfigAclPrefs[4] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNESDUR", "Electric Strike Duration", ((int)advancedOpenerConfigAclPrefs[5] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDRTOAR", "Disable RTO after ring", ((int)advancedOpenerConfigAclPrefs[6] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNRTOT", "RTO timeout", ((int)advancedOpenerConfigAclPrefs[7] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDRBSUP", "Doorbell suppression", ((int)advancedOpenerConfigAclPrefs[8] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDRBSUPDUR", "Doorbell suppression duration", ((int)advancedOpenerConfigAclPrefs[9] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSRING", "Sound Ring", ((int)advancedOpenerConfigAclPrefs[10] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSOPN", "Sound Open", ((int)advancedOpenerConfigAclPrefs[11] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSRTO", "Sound RTO", ((int)advancedOpenerConfigAclPrefs[12] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSCM", "Sound CM", ((int)advancedOpenerConfigAclPrefs[13] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSCFRM", "Sound confirmation", ((int)advancedOpenerConfigAclPrefs[14] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSLVL", "Sound level", ((int)advancedOpenerConfigAclPrefs[15] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNSBPA", "Single button press action", ((int)advancedOpenerConfigAclPrefs[16] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNDBPA", "Double button press action", ((int)advancedOpenerConfigAclPrefs[17] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNBATT", "Battery type", ((int)advancedOpenerConfigAclPrefs[18] == 1), "chk_config_opener"); + printCheckBox(&response, "CONFOPNABTD", "Automatic battery type detection", ((int)advancedOpenerConfigAclPrefs[19] == 1), "chk_config_opener"); + response.print("
    "); + response.print("
    "); } - _response.concat("
    "); - _response.concat(""); - sendResponse(request); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildNukiConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildNukiConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
    "); - _response.concat("

    Basic Nuki Configuration

    "); - _response.concat(""); - printCheckBox("LOCKENA", "Nuki Lock enabled", _preferences->getBool(preference_lock_enabled), ""); - if(_preferences->getBool(preference_lock_enabled)) printInputField("MQTTPATH", "MQTT Nuki Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180, ""); - printCheckBox("OPENA", "Nuki Opener enabled", _preferences->getBool(preference_opener_enabled), ""); - if(_preferences->getBool(preference_opener_enabled)) printInputField("MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180, ""); - _response.concat("

    "); - _response.concat("

    Advanced Nuki Configuration

    "); - _response.concat(""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

    Basic Nuki Configuration

    "); + response.print("
    "); + printCheckBox(&response, "LOCKENA", "Nuki Lock enabled", _preferences->getBool(preference_lock_enabled), ""); + if(_preferences->getBool(preference_lock_enabled)) printInputField(&response, "MQTTPATH", "MQTT Nuki Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180, ""); + printCheckBox(&response, "OPENA", "Nuki Opener enabled", _preferences->getBool(preference_opener_enabled), ""); + if(_preferences->getBool(preference_opener_enabled)) printInputField(&response, "MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180, ""); + response.print("

    "); + response.print("

    Advanced Nuki Configuration

    "); + response.print(""); - printInputField("LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10, ""); - printInputField("CFGINT", "Query interval configuration (seconds)", _preferences->getInt(preference_query_interval_configuration), 10, ""); - printInputField("BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10, ""); + printInputField(&response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10, ""); + printInputField(&response, "CFGINT", "Query interval configuration (seconds)", _preferences->getInt(preference_query_interval_configuration), 10, ""); + printInputField(&response, "BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10, ""); if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad())) { - printInputField("KPINT", "Query interval keypad (seconds)", _preferences->getInt(preference_query_interval_keypad), 10, ""); + printInputField(&response, "KPINT", "Query interval keypad (seconds)", _preferences->getInt(preference_query_interval_keypad), 10, ""); } - printInputField("NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10, ""); - printInputField("TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10, ""); - if(_preferences->getBool(preference_lock_enabled, true)) printCheckBox("REGAPP", "Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), ""); - if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox("REGAPPOPN", "Opener: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_opener_as_app), ""); - printInputField("RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10, ""); - printInputField("TXPWR", "BLE transmit power in dB (minimum -12, maximum 9)", _preferences->getInt(preference_ble_tx_power, 9), 10, ""); + printInputField(&response, "NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10, ""); + printInputField(&response, "TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10, ""); + if(_preferences->getBool(preference_lock_enabled, true)) printCheckBox(&response, "REGAPP", "Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), ""); + if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox(&response, "REGAPPOPN", "Opener: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_opener_as_app), ""); + printInputField(&response, "RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10, ""); + printInputField(&response, "TXPWR", "BLE transmit power in dB (minimum -12, maximum 9)", _preferences->getInt(preference_ble_tx_power, 9), 10, ""); - _response.concat("
    "); - _response.concat("
    "); - _response.concat("
    "); - _response.concat(""); - sendResponse(request); + response.print(""); + response.print("
    "); + response.print(""); + response.print(""); + return response.endSend(); } -void WebCfgServer::buildGpioConfigHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildGpioConfigHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("
    "); - _response.concat("

    GPIO Configuration

    "); - _response.concat(""); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print(""); + response.print("

    GPIO Configuration

    "); + response.print("
    "); std::vector> options; String gpiopreselects = "var gpio = []; "; @@ -3130,129 +3189,131 @@ void WebCfgServer::buildGpioConfigHtml(AsyncWebServerRequest *request) { String pinStr = String(pin); String pinDesc = "Gpio " + pinStr; - printDropDown(pinStr.c_str(), pinDesc.c_str(), "", options, "gpioselect"); + printDropDown(&response, pinStr.c_str(), pinDesc.c_str(), "", options, "gpioselect"); if(std::find(disabledPins.begin(), disabledPins.end(), pin) != disabledPins.end()) gpiopreselects.concat("gpio[" + pinStr + "] = '21';"); else gpiopreselects.concat("gpio[" + pinStr + "] = '" + getPreselectionForGpio(pin) + "';"); } - _response.concat("
    "); - _response.concat("
    "); - _response.concat("
    "); + response.print(""); + response.print("
    "); + response.print(""); options = getGpioOptions(); - _response.concat(""); - _response.concat(""); - sendResponse(request); + response.print("'; var gpioselects = document.getElementsByClassName('gpioselect'); for (let i = 0; i < gpioselects.length; i++) { gpioselects[i].options.length = 0; gpioselects[i].innerHTML = gpiooptions; gpioselects[i].value = gpio[gpioselects[i].name]; if(gpioselects[i].value == 21) { gpioselects[i].disabled = true; } }"); + response.print(""); + return response.endSend(); } #ifndef CONFIG_IDF_TARGET_ESP32H2 -void WebCfgServer::buildConfigureWifiHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildConfigureWifiHtml(PsychicRequest *request) { - _response = ""; - buildHtmlHeader(); - _response.concat("

    Wi-Fi

    "); - _response.concat("Click confirm to restart ESP into Wi-Fi configuration mode. After restart, connect to ESP access point to reconfigure Wi-Fi.

    "); - buildNavigationButton("Confirm", "/wifimanager"); - _response.concat(""); - sendResponse(request); + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response); + response.print("

    Wi-Fi

    "); + response.print("Click confirm to restart ESP into Wi-Fi configuration mode. After restart, connect to ESP access point to reconfigure Wi-Fi.

    "); + buildNavigationButton(&response, "Confirm", "/wifimanager"); + response.print(""); + return response.endSend(); } #endif -void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) { - _response = ""; uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); - buildHtmlHeader(); - _response.concat("

    System Information

    ");
    -    _response.concat("------------ NUKI HUB ------------");
    -    _response.concat("\nVersion: ");
    -    _response.concat(NUKI_HUB_VERSION);
    -    _response.concat("\nBuild: ");
    -    _response.concat(NUKI_HUB_BUILD);
    +    PsychicStreamResponse response(request, "text/plain");
    +    response.beginSend();
    +    buildHtmlHeader(&response);
    +    response.print("

    System Information

    ");
    +    response.print("------------ NUKI HUB ------------");
    +    response.print("\nVersion: ");
    +    response.print(NUKI_HUB_VERSION);
    +    response.print("\nBuild: ");
    +    response.print(NUKI_HUB_BUILD);
         #ifndef DEBUG_NUKIHUB
    -    _response.concat("\nBuild type: Release");
    +    response.print("\nBuild type: Release");
         #else
    -    _response.concat("\nBuild type: Debug");
    +    response.print("\nBuild type: Debug");
         #endif
    -    _response.concat("\nBuild date: ");
    -    _response.concat(NUKI_HUB_DATE);
    -    _response.concat("\nUpdater version: ");
    -    _response.concat(_preferences->getString(preference_updater_version, ""));
    -    _response.concat("\nUpdater build: ");
    -    _response.concat(_preferences->getString(preference_updater_build, ""));
    -    _response.concat("\nUpdater build date: ");
    -    _response.concat(_preferences->getString(preference_updater_date, ""));
    -    _response.concat("\nUptime (min): ");
    -    _response.concat(esp_timer_get_time() / 1000 / 1000 / 60);
    -    _response.concat("\nConfig version: ");
    -    _response.concat(_preferences->getInt(preference_config_version));
    -    _response.concat("\nLast restart reason FW: ");
    -    _response.concat(getRestartReason());
    -    _response.concat("\nLast restart reason ESP: ");
    -    _response.concat(getEspRestartReason());
    -    _response.concat("\nFree heap: ");
    -    _response.concat(esp_get_free_heap_size());
    -    _response.concat("\nNetwork task stack high watermark: ");
    -    _response.concat(uxTaskGetStackHighWaterMark(networkTaskHandle));
    -    _response.concat("\nNuki task stack high watermark: ");
    -    _response.concat(uxTaskGetStackHighWaterMark(nukiTaskHandle));
    -    _response.concat("\n\n------------ GENERAL SETTINGS ------------");
    -    _response.concat("\nNetwork task stack size: ");
    -    _response.concat(_preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE));
    -    _response.concat("\nNuki task stack size: ");
    -    _response.concat(_preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE));
    -    _response.concat("\nCheck for updates: ");
    -    _response.concat(_preferences->getBool(preference_check_updates, false) ? "Yes" : "No");
    -    _response.concat("\nLatest version: ");
    -    _response.concat(_preferences->getString(preference_latest_version, ""));
    -    _response.concat("\nAllow update from MQTT: ");
    -    _response.concat(_preferences->getBool(preference_update_from_mqtt, false) ? "Yes" : "No");
    -    _response.concat("\nWeb configurator username: ");
    -    _response.concat(_preferences->getString(preference_cred_user, "").length() > 0 ? "***" : "Not set");
    -    _response.concat("\nWeb configurator password: ");
    -    _response.concat(_preferences->getString(preference_cred_password, "").length() > 0 ? "***" : "Not set");
    -    _response.concat("\nWeb configurator enabled: ");
    -    _response.concat(_preferences->getBool(preference_webserver_enabled, true) ? "Yes" : "No");
    -    _response.concat("\nPublish debug information enabled: ");
    -    _response.concat(_preferences->getBool(preference_publish_debug_info, false) ? "Yes" : "No");
    -    _response.concat("\nMQTT log enabled: ");
    -    _response.concat(_preferences->getBool(preference_mqtt_log_enabled, false) ? "Yes" : "No");
    -    _response.concat("\nWebserial enabled: ");
    -    _response.concat(_preferences->getBool(preference_webserial_enabled, false) ? "Yes" : "No");
    -    _response.concat("\nBootloop protection enabled: ");
    -    _response.concat(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Yes" : "No");
    -    _response.concat("\n\n------------ NETWORK ------------");
    -    _response.concat("\nNetwork device: ");
    -    _response.concat(_network->networkDeviceName());
    -    _response.concat("\nNetwork connected: ");
    -    _response.concat(_network->isConnected() ? "Yes" : "No");
    +    response.print("\nBuild date: ");
    +    response.print(NUKI_HUB_DATE);
    +    response.print("\nUpdater version: ");
    +    response.print(_preferences->getString(preference_updater_version, ""));
    +    response.print("\nUpdater build: ");
    +    response.print(_preferences->getString(preference_updater_build, ""));
    +    response.print("\nUpdater build date: ");
    +    response.print(_preferences->getString(preference_updater_date, ""));
    +    response.print("\nUptime (min): ");
    +    response.print(esp_timer_get_time() / 1000 / 1000 / 60);
    +    response.print("\nConfig version: ");
    +    response.print(_preferences->getInt(preference_config_version));
    +    response.print("\nLast restart reason FW: ");
    +    response.print(getRestartReason());
    +    response.print("\nLast restart reason ESP: ");
    +    response.print(getEspRestartReason());
    +    response.print("\nFree heap: ");
    +    response.print(esp_get_free_heap_size());
    +    response.print("\nNetwork task stack high watermark: ");
    +    response.print(uxTaskGetStackHighWaterMark(networkTaskHandle));
    +    response.print("\nNuki task stack high watermark: ");
    +    response.print(uxTaskGetStackHighWaterMark(nukiTaskHandle));
    +    response.print("\n\n------------ GENERAL SETTINGS ------------");
    +    response.print("\nNetwork task stack size: ");
    +    response.print(_preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE));
    +    response.print("\nNuki task stack size: ");
    +    response.print(_preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE));
    +    response.print("\nCheck for updates: ");
    +    response.print(_preferences->getBool(preference_check_updates, false) ? "Yes" : "No");
    +    response.print("\nLatest version: ");
    +    response.print(_preferences->getString(preference_latest_version, ""));
    +    response.print("\nAllow update from MQTT: ");
    +    response.print(_preferences->getBool(preference_update_from_mqtt, false) ? "Yes" : "No");
    +    response.print("\nWeb configurator username: ");
    +    response.print(_preferences->getString(preference_cred_user, "").length() > 0 ? "***" : "Not set");
    +    response.print("\nWeb configurator password: ");
    +    response.print(_preferences->getString(preference_cred_password, "").length() > 0 ? "***" : "Not set");
    +    response.print("\nWeb configurator enabled: ");
    +    response.print(_preferences->getBool(preference_webserver_enabled, true) ? "Yes" : "No");
    +    response.print("\nPublish debug information enabled: ");
    +    response.print(_preferences->getBool(preference_publish_debug_info, false) ? "Yes" : "No");
    +    response.print("\nMQTT log enabled: ");
    +    response.print(_preferences->getBool(preference_mqtt_log_enabled, false) ? "Yes" : "No");
    +    response.print("\nWebserial enabled: ");
    +    response.print(_preferences->getBool(preference_webserial_enabled, false) ? "Yes" : "No");
    +    response.print("\nBootloop protection enabled: ");
    +    response.print(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Yes" : "No");
    +    response.print("\n\n------------ NETWORK ------------");
    +    response.print("\nNetwork device: ");
    +    response.print(_network->networkDeviceName());
    +    response.print("\nNetwork connected: ");
    +    response.print(_network->isConnected() ? "Yes" : "No");
         if(_network->isConnected())
         {
    -        _response.concat("\nIP Address: ");
    -        _response.concat(_network->localIP());
    +        response.print("\nIP Address: ");
    +        response.print(_network->localIP());
     
             if(_network->networkDeviceName() == "Built-in Wi-Fi")
             {
                 #ifndef CONFIG_IDF_TARGET_ESP32H2
    -            _response.concat("\nSSID: ");
    -            _response.concat(WiFi.SSID());
    -            _response.concat("\nBSSID of AP: ");
    -            _response.concat(_network->networkBSSID());
    -            _response.concat("\nESP32 MAC address: ");
    -            _response.concat(WiFi.macAddress());
    +            response.print("\nSSID: ");
    +            response.print(WiFi.SSID());
    +            response.print("\nBSSID of AP: ");
    +            response.print(_network->networkBSSID());
    +            response.print("\nESP32 MAC address: ");
    +            response.print(WiFi.macAddress());
                 #endif
             }
             else
    @@ -3260,276 +3321,276 @@ void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request)
                 //Ethernet info
             }
         }
    -    _response.concat("\n\n------------ NETWORK SETTINGS ------------");
    -    _response.concat("\nNuki Hub hostname: ");
    -    _response.concat(_preferences->getString(preference_hostname, ""));
    -    if(_preferences->getBool(preference_ip_dhcp_enabled, true)) _response.concat("\nDHCP enabled: Yes");
    +    response.print("\n\n------------ NETWORK SETTINGS ------------");
    +    response.print("\nNuki Hub hostname: ");
    +    response.print(_preferences->getString(preference_hostname, ""));
    +    if(_preferences->getBool(preference_ip_dhcp_enabled, true)) response.print("\nDHCP enabled: Yes");
         else
         {
    -        _response.concat("\nDHCP enabled: No");
    -        _response.concat("\nStatic IP address: ");
    -        _response.concat(_preferences->getString(preference_ip_address, ""));
    -        _response.concat("\nStatic IP subnet: ");
    -        _response.concat(_preferences->getString(preference_ip_subnet, ""));
    -        _response.concat("\nStatic IP gateway: ");
    -        _response.concat(_preferences->getString(preference_ip_gateway, ""));
    -        _response.concat("\nStatic IP DNS server: ");
    -        _response.concat(_preferences->getString(preference_ip_dns_server, ""));
    +        response.print("\nDHCP enabled: No");
    +        response.print("\nStatic IP address: ");
    +        response.print(_preferences->getString(preference_ip_address, ""));
    +        response.print("\nStatic IP subnet: ");
    +        response.print(_preferences->getString(preference_ip_subnet, ""));
    +        response.print("\nStatic IP gateway: ");
    +        response.print(_preferences->getString(preference_ip_gateway, ""));
    +        response.print("\nStatic IP DNS server: ");
    +        response.print(_preferences->getString(preference_ip_dns_server, ""));
         }
     
         #ifndef CONFIG_IDF_TARGET_ESP32H2
    -    _response.concat("\nFallback to Wi-Fi / Wi-Fi config portal disabled: ");
    -    _response.concat(_preferences->getBool(preference_network_wifi_fallback_disabled, false) ? "Yes" : "No");
    +    response.print("\nFallback to Wi-Fi / Wi-Fi config portal disabled: ");
    +    response.print(_preferences->getBool(preference_network_wifi_fallback_disabled, false) ? "Yes" : "No");
         if(_network->networkDeviceName() == "Built-in Wi-Fi")
         {
    -        _response.concat("\nConnect to AP with the best signal enabled: ");
    -        _response.concat(_preferences->getBool(preference_find_best_rssi, false) ? "Yes" : "No");
    -        _response.concat("\nRSSI Publish interval (s): ");
    +        response.print("\nConnect to AP with the best signal enabled: ");
    +        response.print(_preferences->getBool(preference_find_best_rssi, false) ? "Yes" : "No");
    +        response.print("\nRSSI Publish interval (s): ");
     
    -        if(_preferences->getInt(preference_rssi_publish_interval, 60) < 0) _response.concat("Disabled");
    -        else _response.concat(_preferences->getInt(preference_rssi_publish_interval, 60));
    +        if(_preferences->getInt(preference_rssi_publish_interval, 60) < 0) response.print("Disabled");
    +        else response.print(_preferences->getInt(preference_rssi_publish_interval, 60));
         }
         #endif
    -    _response.concat("\nRestart ESP32 on network disconnect enabled: ");
    -    _response.concat(_preferences->getBool(preference_restart_on_disconnect, false) ? "Yes" : "No");
    -    _response.concat("\nReconnect network on MQTT connection failure enabled: ");
    -    _response.concat(_preferences->getBool(preference_recon_netw_on_mqtt_discon, false) ? "Yes" : "No");
    -    _response.concat("\nMQTT Timeout until restart (s): ");
    -    if(_preferences->getInt(preference_network_timeout, 60) < 0) _response.concat("Disabled");
    -    else _response.concat(_preferences->getInt(preference_network_timeout, 60));
    -    _response.concat("\n\n------------ MQTT ------------");
    -    _response.concat("\nMQTT connected: ");
    -    _response.concat(_network->mqttConnectionState() > 0 ? "Yes" : "No");
    -    _response.concat("\nMQTT broker address: ");
    -    _response.concat(_preferences->getString(preference_mqtt_broker, ""));
    -    _response.concat("\nMQTT broker port: ");
    -    _response.concat(_preferences->getInt(preference_mqtt_broker_port, 1883));
    -    _response.concat("\nMQTT username: ");
    -    _response.concat(_preferences->getString(preference_mqtt_user, "").length() > 0 ? "***" : "Not set");
    -    _response.concat("\nMQTT password: ");
    -    _response.concat(_preferences->getString(preference_mqtt_password, "").length() > 0 ? "***" : "Not set");
    +    response.print("\nRestart ESP32 on network disconnect enabled: ");
    +    response.print(_preferences->getBool(preference_restart_on_disconnect, false) ? "Yes" : "No");
    +    response.print("\nReconnect network on MQTT connection failure enabled: ");
    +    response.print(_preferences->getBool(preference_recon_netw_on_mqtt_discon, false) ? "Yes" : "No");
    +    response.print("\nMQTT Timeout until restart (s): ");
    +    if(_preferences->getInt(preference_network_timeout, 60) < 0) response.print("Disabled");
    +    else response.print(_preferences->getInt(preference_network_timeout, 60));
    +    response.print("\n\n------------ MQTT ------------");
    +    response.print("\nMQTT connected: ");
    +    response.print(_network->mqttConnectionState() > 0 ? "Yes" : "No");
    +    response.print("\nMQTT broker address: ");
    +    response.print(_preferences->getString(preference_mqtt_broker, ""));
    +    response.print("\nMQTT broker port: ");
    +    response.print(_preferences->getInt(preference_mqtt_broker_port, 1883));
    +    response.print("\nMQTT username: ");
    +    response.print(_preferences->getString(preference_mqtt_user, "").length() > 0 ? "***" : "Not set");
    +    response.print("\nMQTT password: ");
    +    response.print(_preferences->getString(preference_mqtt_password, "").length() > 0 ? "***" : "Not set");
         if(_preferences->getBool(preference_lock_enabled, true))
         {
    -        _response.concat("\nMQTT lock base topic: ");
    -        _response.concat(_preferences->getString(preference_mqtt_lock_path, ""));
    +        response.print("\nMQTT lock base topic: ");
    +        response.print(_preferences->getString(preference_mqtt_lock_path, ""));
         }
         if(_preferences->getBool(preference_opener_enabled, false))
         {
    -        _response.concat("\nMQTT opener base topic: ");
    -        _response.concat(_preferences->getString(preference_mqtt_lock_path, ""));
    +        response.print("\nMQTT opener base topic: ");
    +        response.print(_preferences->getString(preference_mqtt_lock_path, ""));
         }
    -    _response.concat("\nMQTT SSL CA: ");
    -    _response.concat(_preferences->getString(preference_mqtt_ca, "").length() > 0 ? "***" : "Not set");
    -    _response.concat("\nMQTT SSL CRT: ");
    -    _response.concat(_preferences->getString(preference_mqtt_crt, "").length() > 0 ? "***" : "Not set");
    -    _response.concat("\nMQTT SSL Key: ");
    -    _response.concat(_preferences->getString(preference_mqtt_key, "").length() > 0 ? "***" : "Not set");
    -    _response.concat("\n\n------------ BLUETOOTH ------------");
    -    _response.concat("\nBluetooth TX power (dB): ");
    -    _response.concat(_preferences->getInt(preference_ble_tx_power, 9));
    -    _response.concat("\nBluetooth command nr of retries: ");
    -    _response.concat(_preferences->getInt(preference_command_nr_of_retries, 3));
    -    _response.concat("\nBluetooth command retry delay (ms): ");
    -    _response.concat(_preferences->getInt(preference_command_retry_delay, 100));
    -    _response.concat("\nSeconds until reboot when no BLE beacons recieved: ");
    -    _response.concat(_preferences->getInt(preference_restart_ble_beacon_lost, 60));
    -    _response.concat("\n\n------------ QUERY / PUBLISH SETTINGS ------------");
    -    _response.concat("\nLock/Opener state query interval (s): ");
    -    _response.concat(_preferences->getInt(preference_query_interval_lockstate, 1800));
    -    _response.concat("\nPublish Nuki device authorization log: ");
    -    _response.concat(_preferences->getBool(preference_publish_authdata, false) ? "Yes" : "No");
    -    _response.concat("\nMax authorization log entries to retrieve: ");
    -    _response.concat(_preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG));
    -    _response.concat("\nBattery state query interval (s): ");
    -    _response.concat(_preferences->getInt(preference_query_interval_battery, 1800));
    -    _response.concat("\nMost non-JSON MQTT topics disabled: ");
    -    _response.concat(_preferences->getBool(preference_disable_non_json, false) ? "Yes" : "No");
    -    _response.concat("\nPublish Nuki device config: ");
    -    _response.concat(_preferences->getBool(preference_conf_info_enabled, false) ? "Yes" : "No");
    -    _response.concat("\nConfig query interval (s): ");
    -    _response.concat(_preferences->getInt(preference_query_interval_configuration, 3600));
    -    _response.concat("\nPublish Keypad info: ");
    -    _response.concat(_preferences->getBool(preference_keypad_info_enabled, false) ? "Yes" : "No");
    -    _response.concat("\nKeypad query interval (s): ");
    -    _response.concat(_preferences->getInt(preference_query_interval_keypad, 1800));
    -    _response.concat("\nEnable Keypad control: ");
    -    _response.concat(_preferences->getBool(preference_keypad_control_enabled, false) ? "Yes" : "No");
    -    _response.concat("\nPublish Keypad topic per entry: ");
    -    _response.concat(_preferences->getBool(preference_keypad_topic_per_entry, false) ? "Yes" : "No");
    -    _response.concat("\nPublish Keypad codes: ");
    -    _response.concat(_preferences->getBool(preference_keypad_publish_code, false) ? "Yes" : "No");
    -    _response.concat("\nMax keypad entries to retrieve: ");
    -    _response.concat(_preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD));
    -    _response.concat("\nPublish timecontrol info: ");
    -    _response.concat(_preferences->getBool(preference_timecontrol_info_enabled, false) ? "Yes" : "No");
    -    _response.concat("\nKeypad query interval (s): ");
    -    _response.concat(_preferences->getInt(preference_query_interval_keypad, 1800));
    -    _response.concat("\nEnable timecontrol control: ");
    -    _response.concat(_preferences->getBool(preference_timecontrol_control_enabled, false) ? "Yes" : "No");
    -    _response.concat("\nPublish timecontrol topic per entry: ");
    -    _response.concat(_preferences->getBool(preference_timecontrol_topic_per_entry, false) ? "Yes" : "No");
    -    _response.concat("\nMax timecontrol entries to retrieve: ");
    -    _response.concat(_preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL));
    -    _response.concat("\n\n------------ HOME ASSISTANT ------------");
    -    _response.concat("\nHome Assistant auto discovery enabled: ");
    +    response.print("\nMQTT SSL CA: ");
    +    response.print(_preferences->getString(preference_mqtt_ca, "").length() > 0 ? "***" : "Not set");
    +    response.print("\nMQTT SSL CRT: ");
    +    response.print(_preferences->getString(preference_mqtt_crt, "").length() > 0 ? "***" : "Not set");
    +    response.print("\nMQTT SSL Key: ");
    +    response.print(_preferences->getString(preference_mqtt_key, "").length() > 0 ? "***" : "Not set");
    +    response.print("\n\n------------ BLUETOOTH ------------");
    +    response.print("\nBluetooth TX power (dB): ");
    +    response.print(_preferences->getInt(preference_ble_tx_power, 9));
    +    response.print("\nBluetooth command nr of retries: ");
    +    response.print(_preferences->getInt(preference_command_nr_of_retries, 3));
    +    response.print("\nBluetooth command retry delay (ms): ");
    +    response.print(_preferences->getInt(preference_command_retry_delay, 100));
    +    response.print("\nSeconds until reboot when no BLE beacons recieved: ");
    +    response.print(_preferences->getInt(preference_restart_ble_beacon_lost, 60));
    +    response.print("\n\n------------ QUERY / PUBLISH SETTINGS ------------");
    +    response.print("\nLock/Opener state query interval (s): ");
    +    response.print(_preferences->getInt(preference_query_interval_lockstate, 1800));
    +    response.print("\nPublish Nuki device authorization log: ");
    +    response.print(_preferences->getBool(preference_publish_authdata, false) ? "Yes" : "No");
    +    response.print("\nMax authorization log entries to retrieve: ");
    +    response.print(_preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG));
    +    response.print("\nBattery state query interval (s): ");
    +    response.print(_preferences->getInt(preference_query_interval_battery, 1800));
    +    response.print("\nMost non-JSON MQTT topics disabled: ");
    +    response.print(_preferences->getBool(preference_disable_non_json, false) ? "Yes" : "No");
    +    response.print("\nPublish Nuki device config: ");
    +    response.print(_preferences->getBool(preference_conf_info_enabled, false) ? "Yes" : "No");
    +    response.print("\nConfig query interval (s): ");
    +    response.print(_preferences->getInt(preference_query_interval_configuration, 3600));
    +    response.print("\nPublish Keypad info: ");
    +    response.print(_preferences->getBool(preference_keypad_info_enabled, false) ? "Yes" : "No");
    +    response.print("\nKeypad query interval (s): ");
    +    response.print(_preferences->getInt(preference_query_interval_keypad, 1800));
    +    response.print("\nEnable Keypad control: ");
    +    response.print(_preferences->getBool(preference_keypad_control_enabled, false) ? "Yes" : "No");
    +    response.print("\nPublish Keypad topic per entry: ");
    +    response.print(_preferences->getBool(preference_keypad_topic_per_entry, false) ? "Yes" : "No");
    +    response.print("\nPublish Keypad codes: ");
    +    response.print(_preferences->getBool(preference_keypad_publish_code, false) ? "Yes" : "No");
    +    response.print("\nMax keypad entries to retrieve: ");
    +    response.print(_preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD));
    +    response.print("\nPublish timecontrol info: ");
    +    response.print(_preferences->getBool(preference_timecontrol_info_enabled, false) ? "Yes" : "No");
    +    response.print("\nKeypad query interval (s): ");
    +    response.print(_preferences->getInt(preference_query_interval_keypad, 1800));
    +    response.print("\nEnable timecontrol control: ");
    +    response.print(_preferences->getBool(preference_timecontrol_control_enabled, false) ? "Yes" : "No");
    +    response.print("\nPublish timecontrol topic per entry: ");
    +    response.print(_preferences->getBool(preference_timecontrol_topic_per_entry, false) ? "Yes" : "No");
    +    response.print("\nMax timecontrol entries to retrieve: ");
    +    response.print(_preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL));
    +    response.print("\n\n------------ HOME ASSISTANT ------------");
    +    response.print("\nHome Assistant auto discovery enabled: ");
         if(_preferences->getString(preference_mqtt_hass_discovery, "").length() > 0)
         {
    -        _response.concat("Yes");
    -        _response.concat("\nHome Assistant auto discovery topic: ");
    -        _response.concat(_preferences->getString(preference_mqtt_hass_discovery, "") + "/");
    -        _response.concat("\nNuki Hub configuration URL for HA: ");
    -        _response.concat(_preferences->getString(preference_mqtt_hass_cu_url, "").length() > 0 ? _preferences->getString(preference_mqtt_hass_cu_url, "") : "http://" + _network->localIP());
    +        response.print("Yes");
    +        response.print("\nHome Assistant auto discovery topic: ");
    +        response.print(_preferences->getString(preference_mqtt_hass_discovery, "") + "/");
    +        response.print("\nNuki Hub configuration URL for HA: ");
    +        response.print(_preferences->getString(preference_mqtt_hass_cu_url, "").length() > 0 ? _preferences->getString(preference_mqtt_hass_cu_url, "") : "http://" + _network->localIP());
         }
    -    else _response.concat("No");
    -    _response.concat("\n\n------------ NUKI LOCK ------------");
    -    if(_nuki == nullptr || !_preferences->getBool(preference_lock_enabled, true)) _response.concat("\nLock enabled: No");
    +    else response.print("No");
    +    response.print("\n\n------------ NUKI LOCK ------------");
    +    if(_nuki == nullptr || !_preferences->getBool(preference_lock_enabled, true)) response.print("\nLock enabled: No");
         else
         {
    -        _response.concat("\nLock enabled: Yes");
    -        _response.concat("\nPaired: ");
    -        _response.concat(_nuki->isPaired() ? "Yes" : "No");
    -        _response.concat("\nNuki Hub device ID: ");
    -        _response.concat(_preferences->getUInt(preference_device_id_lock, 0));
    -        _response.concat("\nNuki device ID: ");
    -        _response.concat(_preferences->getUInt(preference_nuki_id_lock, 0) > 0 ? "***" : "Not set");
    -        _response.concat("\nFirmware version: ");
    -        _response.concat(_nuki->firmwareVersion().c_str());
    -        _response.concat("\nHardware version: ");
    -        _response.concat(_nuki->hardwareVersion().c_str());
    -        _response.concat("\nValid PIN set: ");
    -        _response.concat(_nuki->isPaired() ? _nuki->isPinValid() ? "Yes" : "No" : "-");
    -        _response.concat("\nHas door sensor: ");
    -        _response.concat(_nuki->hasDoorSensor() ? "Yes" : "No");
    -        _response.concat("\nHas keypad: ");
    -        _response.concat(_nuki->hasKeypad() ? "Yes" : "No");
    +        response.print("\nLock enabled: Yes");
    +        response.print("\nPaired: ");
    +        response.print(_nuki->isPaired() ? "Yes" : "No");
    +        response.print("\nNuki Hub device ID: ");
    +        response.print(_preferences->getUInt(preference_device_id_lock, 0));
    +        response.print("\nNuki device ID: ");
    +        response.print(_preferences->getUInt(preference_nuki_id_lock, 0) > 0 ? "***" : "Not set");
    +        response.print("\nFirmware version: ");
    +        response.print(_nuki->firmwareVersion().c_str());
    +        response.print("\nHardware version: ");
    +        response.print(_nuki->hardwareVersion().c_str());
    +        response.print("\nValid PIN set: ");
    +        response.print(_nuki->isPaired() ? _nuki->isPinValid() ? "Yes" : "No" : "-");
    +        response.print("\nHas door sensor: ");
    +        response.print(_nuki->hasDoorSensor() ? "Yes" : "No");
    +        response.print("\nHas keypad: ");
    +        response.print(_nuki->hasKeypad() ? "Yes" : "No");
             if(_nuki->hasKeypad())
             {
    -            _response.concat("\nKeypad highest entries count: ");
    -            _response.concat(_preferences->getInt(preference_lock_max_keypad_code_count, 0));
    +            response.print("\nKeypad highest entries count: ");
    +            response.print(_preferences->getInt(preference_lock_max_keypad_code_count, 0));
             }
    -        _response.concat("\nTimecontrol highest entries count: ");
    -        _response.concat(_preferences->getInt(preference_lock_max_timecontrol_entry_count, 0));
    -        _response.concat("\nRegister as: ");
    -        _response.concat(_preferences->getBool(preference_register_as_app, false) ? "App" : "Bridge");
    -        _response.concat("\n\n------------ HYBRID MODE ------------");
    -        if(!_preferences->getBool(preference_official_hybrid, false)) _response.concat("\nHybrid mode enabled: No");
    +        response.print("\nTimecontrol highest entries count: ");
    +        response.print(_preferences->getInt(preference_lock_max_timecontrol_entry_count, 0));
    +        response.print("\nRegister as: ");
    +        response.print(_preferences->getBool(preference_register_as_app, false) ? "App" : "Bridge");
    +        response.print("\n\n------------ HYBRID MODE ------------");
    +        if(!_preferences->getBool(preference_official_hybrid, false)) response.print("\nHybrid mode enabled: No");
             else
             {
    -            _response.concat("\nHybrid mode enabled: Yes");
    -            _response.concat("\nHybrid mode connected: ");
    -            _response.concat(_nuki->offConnected() ? "Yes": "No");
    -            _response.concat("\nSending actions through official MQTT enabled: ");
    -            _response.concat(_preferences->getBool(preference_official_hybrid_actions, false) ? "Yes" : "No");
    +            response.print("\nHybrid mode enabled: Yes");
    +            response.print("\nHybrid mode connected: ");
    +            response.print(_nuki->offConnected() ? "Yes": "No");
    +            response.print("\nSending actions through official MQTT enabled: ");
    +            response.print(_preferences->getBool(preference_official_hybrid_actions, false) ? "Yes" : "No");
                 /* NOT IMPLEMENTED (YET?)
                 if(_preferences->getBool(preference_official_hybrid_actions, false))
                 {
    -                _response.concat("\nRetry actions through BLE enabled: ");
    -                _response.concat(_preferences->getBool(preference_official_hybrid_retry, false) ? "Yes" : "No");
    +                response.print("\nRetry actions through BLE enabled: ");
    +                response.print(_preferences->getBool(preference_official_hybrid_retry, false) ? "Yes" : "No");
                 }
                 */
    -            _response.concat("\nTime between status updates when official MQTT is offline (s): ");
    -            _response.concat(_preferences->getInt(preference_query_interval_hybrid_lockstate, 600));
    +            response.print("\nTime between status updates when official MQTT is offline (s): ");
    +            response.print(_preferences->getInt(preference_query_interval_hybrid_lockstate, 600));
             }
             uint32_t basicLockConfigAclPrefs[16];
             _preferences->getBytes(preference_conf_lock_basic_acl, &basicLockConfigAclPrefs, sizeof(basicLockConfigAclPrefs));
             uint32_t advancedLockConfigAclPrefs[22];
             _preferences->getBytes(preference_conf_lock_advanced_acl, &advancedLockConfigAclPrefs, sizeof(advancedLockConfigAclPrefs));
    -        _response.concat("\n\n------------ NUKI LOCK ACL ------------");
    -        _response.concat("\nLock: ");
    -        _response.concat((int)aclPrefs[0] ? "Allowed" : "Disallowed");
    -        _response.concat("\nUnlock: ");
    -        _response.concat((int)aclPrefs[1] ? "Allowed" : "Disallowed");
    -        _response.concat("\nUnlatch: ");
    -        _response.concat((int)aclPrefs[2] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLock N Go: ");
    -        _response.concat((int)aclPrefs[3] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLock N Go Unlatch: ");
    -        _response.concat((int)aclPrefs[4] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFull Lock: ");
    -        _response.concat((int)aclPrefs[5] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 1: ");
    -        _response.concat((int)aclPrefs[6] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 2: ");
    -        _response.concat((int)aclPrefs[7] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 3: ");
    -        _response.concat((int)aclPrefs[8] ? "Allowed" : "Disallowed");
    -        _response.concat("\n\n------------ NUKI LOCK CONFIG ACL ------------");
    -        _response.concat("\nName: ");
    -        _response.concat((int)basicLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLatitude: ");
    -        _response.concat((int)basicLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLongitude: ");
    -        _response.concat((int)basicLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAuto Unlatch: ");
    -        _response.concat((int)basicLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    -        _response.concat("\nPairing enabled: ");
    -        _response.concat((int)basicLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    -        _response.concat("\nButton enabled: ");
    -        _response.concat((int)basicLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLED flash enabled: ");
    -        _response.concat((int)basicLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLED brightness: ");
    -        _response.concat((int)basicLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    -        _response.concat("\nTimezone offset: ");
    -        _response.concat((int)basicLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDST mode: ");
    -        _response.concat((int)basicLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 1: ");
    -        _response.concat((int)basicLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 2: ");
    -        _response.concat((int)basicLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 3: ");
    -        _response.concat((int)basicLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSingle Lock: ");
    -        _response.concat((int)basicLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAdvertising Mode: ");
    -        _response.concat((int)basicLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
    -        _response.concat("\nTimezone ID: ");
    -        _response.concat((int)basicLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
    -        _response.concat("\nUnlocked Position Offset Degrees: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLocked Position Offset Degrees: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSingle Locked Position Offset Degrees: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    -        _response.concat("\nUnlocked To Locked Transition Offset Degrees: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLock n Go timeout: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSingle button press action: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDouble button press action: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDetached cylinder: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    -        _response.concat("\nBattery type: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAutomatic battery type detection: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    -        _response.concat("\nUnlatch duration: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAuto lock timeout: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAuto unlock disabled: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    -        _response.concat("\nNightmode enabled: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    -        _response.concat("\nNightmode start time: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
    -        _response.concat("\nNightmode end time: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
    -        _response.concat("\nNightmode auto lock enabled: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[16] ? "Allowed" : "Disallowed");
    -        _response.concat("\nNightmode auto unlock disabled: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[17] ? "Allowed" : "Disallowed");
    -        _response.concat("\nNightmode immediate lock on start: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[18] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAuto lock enabled: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[19] ? "Allowed" : "Disallowed");
    -        _response.concat("\nImmediate auto lock enabled: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[20] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAuto update enabled: ");
    -        _response.concat((int)advancedLockConfigAclPrefs[21] ? "Allowed" : "Disallowed");
    +        response.print("\n\n------------ NUKI LOCK ACL ------------");
    +        response.print("\nLock: ");
    +        response.print((int)aclPrefs[0] ? "Allowed" : "Disallowed");
    +        response.print("\nUnlock: ");
    +        response.print((int)aclPrefs[1] ? "Allowed" : "Disallowed");
    +        response.print("\nUnlatch: ");
    +        response.print((int)aclPrefs[2] ? "Allowed" : "Disallowed");
    +        response.print("\nLock N Go: ");
    +        response.print((int)aclPrefs[3] ? "Allowed" : "Disallowed");
    +        response.print("\nLock N Go Unlatch: ");
    +        response.print((int)aclPrefs[4] ? "Allowed" : "Disallowed");
    +        response.print("\nFull Lock: ");
    +        response.print((int)aclPrefs[5] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 1: ");
    +        response.print((int)aclPrefs[6] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 2: ");
    +        response.print((int)aclPrefs[7] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 3: ");
    +        response.print((int)aclPrefs[8] ? "Allowed" : "Disallowed");
    +        response.print("\n\n------------ NUKI LOCK CONFIG ACL ------------");
    +        response.print("\nName: ");
    +        response.print((int)basicLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    +        response.print("\nLatitude: ");
    +        response.print((int)basicLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    +        response.print("\nLongitude: ");
    +        response.print((int)basicLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    +        response.print("\nAuto Unlatch: ");
    +        response.print((int)basicLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    +        response.print("\nPairing enabled: ");
    +        response.print((int)basicLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    +        response.print("\nButton enabled: ");
    +        response.print((int)basicLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    +        response.print("\nLED flash enabled: ");
    +        response.print((int)basicLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    +        response.print("\nLED brightness: ");
    +        response.print((int)basicLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    +        response.print("\nTimezone offset: ");
    +        response.print((int)basicLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    +        response.print("\nDST mode: ");
    +        response.print((int)basicLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 1: ");
    +        response.print((int)basicLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 2: ");
    +        response.print((int)basicLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 3: ");
    +        response.print((int)basicLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    +        response.print("\nSingle Lock: ");
    +        response.print((int)basicLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    +        response.print("\nAdvertising Mode: ");
    +        response.print((int)basicLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
    +        response.print("\nTimezone ID: ");
    +        response.print((int)basicLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
    +        response.print("\nUnlocked Position Offset Degrees: ");
    +        response.print((int)advancedLockConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    +        response.print("\nLocked Position Offset Degrees: ");
    +        response.print((int)advancedLockConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    +        response.print("\nSingle Locked Position Offset Degrees: ");
    +        response.print((int)advancedLockConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    +        response.print("\nUnlocked To Locked Transition Offset Degrees: ");
    +        response.print((int)advancedLockConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    +        response.print("\nLock n Go timeout: ");
    +        response.print((int)advancedLockConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    +        response.print("\nSingle button press action: ");
    +        response.print((int)advancedLockConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    +        response.print("\nDouble button press action: ");
    +        response.print((int)advancedLockConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    +        response.print("\nDetached cylinder: ");
    +        response.print((int)advancedLockConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    +        response.print("\nBattery type: ");
    +        response.print((int)advancedLockConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    +        response.print("\nAutomatic battery type detection: ");
    +        response.print((int)advancedLockConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    +        response.print("\nUnlatch duration: ");
    +        response.print((int)advancedLockConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    +        response.print("\nAuto lock timeout: ");
    +        response.print((int)advancedLockConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    +        response.print("\nAuto unlock disabled: ");
    +        response.print((int)advancedLockConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    +        response.print("\nNightmode enabled: ");
    +        response.print((int)advancedLockConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    +        response.print("\nNightmode start time: ");
    +        response.print((int)advancedLockConfigAclPrefs[14] ? "Allowed" : "Disallowed");
    +        response.print("\nNightmode end time: ");
    +        response.print((int)advancedLockConfigAclPrefs[15] ? "Allowed" : "Disallowed");
    +        response.print("\nNightmode auto lock enabled: ");
    +        response.print((int)advancedLockConfigAclPrefs[16] ? "Allowed" : "Disallowed");
    +        response.print("\nNightmode auto unlock disabled: ");
    +        response.print((int)advancedLockConfigAclPrefs[17] ? "Allowed" : "Disallowed");
    +        response.print("\nNightmode immediate lock on start: ");
    +        response.print((int)advancedLockConfigAclPrefs[18] ? "Allowed" : "Disallowed");
    +        response.print("\nAuto lock enabled: ");
    +        response.print((int)advancedLockConfigAclPrefs[19] ? "Allowed" : "Disallowed");
    +        response.print("\nImmediate auto lock enabled: ");
    +        response.print((int)advancedLockConfigAclPrefs[20] ? "Allowed" : "Disallowed");
    +        response.print("\nAuto update enabled: ");
    +        response.print((int)advancedLockConfigAclPrefs[21] ? "Allowed" : "Disallowed");
     
             if(_preferences->getBool(preference_show_secrets))
             {
    @@ -3543,151 +3604,151 @@ void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request)
                 nukiBlePref.getBytes("secretKeyK", secretKeyK, 32);
                 nukiBlePref.getBytes("authorizationId", authorizationId, 4);
                 nukiBlePref.end();
    -            _response.concat("\n\n------------ NUKI LOCK PAIRING ------------");
    -            _response.concat("\nBLE Address: ");
    +            response.print("\n\n------------ NUKI LOCK PAIRING ------------");
    +            response.print("\nBLE Address: ");
                 for (int i = 0; i < 6; i++)
                 {
                     sprintf(tmp, "%02x", currentBleAddress[i]);
    -                _response.concat(tmp);
    +                response.print(tmp);
                 }
    -            _response.concat("\nSecretKeyK: ");
    +            response.print("\nSecretKeyK: ");
                 for (int i = 0; i < 32; i++)
                 {
                     sprintf(tmp, "%02x", secretKeyK[i]);
    -                _response.concat(tmp);
    +                response.print(tmp);
                 }
    -            _response.concat("\nAuthorizationId: ");
    +            response.print("\nAuthorizationId: ");
                 for (int i = 0; i < 4; i++)
                 {
                     sprintf(tmp, "%02x", authorizationId[i]);
    -                _response.concat(tmp);
    +                response.print(tmp);
                 }
                 uint32_t authorizationIdInt = authorizationId[0] + 256U*authorizationId[1] + 65536U*authorizationId[2] + 16777216U*authorizationId[3];
    -            _response.concat("\nAuthorizationId (UINT32_T): ");
    -            _response.concat(authorizationIdInt);
    +            response.print("\nAuthorizationId (UINT32_T): ");
    +            response.print(authorizationIdInt);
             }
         }
     
    -    _response.concat("\n\n------------ NUKI OPENER ------------");
    -    if(_nukiOpener == nullptr || !_preferences->getBool(preference_opener_enabled, false)) _response.concat("\nOpener enabled: No");
    +    response.print("\n\n------------ NUKI OPENER ------------");
    +    if(_nukiOpener == nullptr || !_preferences->getBool(preference_opener_enabled, false)) response.print("\nOpener enabled: No");
         else
         {
    -        _response.concat("\nOpener enabled: Yes");
    -        _response.concat("\nPaired: ");
    -        _response.concat(_nukiOpener->isPaired() ? "Yes" : "No");
    -        _response.concat("\nNuki Hub device ID: ");
    -        _response.concat(_preferences->getUInt(preference_device_id_opener, 0));
    -        _response.concat("\nNuki device ID: ");
    -        _response.concat(_preferences->getUInt(preference_nuki_id_opener, 0) > 0 ? "***" : "Not set");
    -        _response.concat("\nFirmware version: ");
    -        _response.concat(_nukiOpener->firmwareVersion().c_str());
    -        _response.concat("\nHardware version: ");
    -        _response.concat(_nukiOpener->hardwareVersion().c_str());
    -        _response.concat("\nOpener valid PIN set: ");
    -        _response.concat(_nukiOpener->isPaired() ? _nukiOpener->isPinValid() ? "Yes" : "No" : "-");
    -        _response.concat("\nOpener has keypad: ");
    -        _response.concat(_nukiOpener->hasKeypad() ? "Yes" : "No");
    +        response.print("\nOpener enabled: Yes");
    +        response.print("\nPaired: ");
    +        response.print(_nukiOpener->isPaired() ? "Yes" : "No");
    +        response.print("\nNuki Hub device ID: ");
    +        response.print(_preferences->getUInt(preference_device_id_opener, 0));
    +        response.print("\nNuki device ID: ");
    +        response.print(_preferences->getUInt(preference_nuki_id_opener, 0) > 0 ? "***" : "Not set");
    +        response.print("\nFirmware version: ");
    +        response.print(_nukiOpener->firmwareVersion().c_str());
    +        response.print("\nHardware version: ");
    +        response.print(_nukiOpener->hardwareVersion().c_str());
    +        response.print("\nOpener valid PIN set: ");
    +        response.print(_nukiOpener->isPaired() ? _nukiOpener->isPinValid() ? "Yes" : "No" : "-");
    +        response.print("\nOpener has keypad: ");
    +        response.print(_nukiOpener->hasKeypad() ? "Yes" : "No");
             if(_nuki->hasKeypad())
             {
    -            _response.concat("\nKeypad highest entries count: ");
    -            _response.concat(_preferences->getInt(preference_opener_max_keypad_code_count, 0));
    +            response.print("\nKeypad highest entries count: ");
    +            response.print(_preferences->getInt(preference_opener_max_keypad_code_count, 0));
             }
    -        _response.concat("\nTimecontrol highest entries count: ");
    -        _response.concat(_preferences->getInt(preference_opener_max_timecontrol_entry_count, 0));
    -        _response.concat("\nRegister as: ");
    -        _response.concat(_preferences->getBool(preference_register_opener_as_app, false) ? "App" : "Bridge");
    -        _response.concat("\nNuki Opener Lock/Unlock action set to Continuous mode in Home Assistant: ");
    -        _response.concat(_preferences->getBool(preference_opener_continuous_mode, false) ? "Yes" : "No");
    +        response.print("\nTimecontrol highest entries count: ");
    +        response.print(_preferences->getInt(preference_opener_max_timecontrol_entry_count, 0));
    +        response.print("\nRegister as: ");
    +        response.print(_preferences->getBool(preference_register_opener_as_app, false) ? "App" : "Bridge");
    +        response.print("\nNuki Opener Lock/Unlock action set to Continuous mode in Home Assistant: ");
    +        response.print(_preferences->getBool(preference_opener_continuous_mode, false) ? "Yes" : "No");
             uint32_t basicOpenerConfigAclPrefs[14];
             _preferences->getBytes(preference_conf_opener_basic_acl, &basicOpenerConfigAclPrefs, sizeof(basicOpenerConfigAclPrefs));
             uint32_t advancedOpenerConfigAclPrefs[20];
             _preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs));
    -        _response.concat("\n\n------------ NUKI OPENER ACL ------------");
    -        _response.concat("\nActivate Ring-to-Open: ");
    -        _response.concat((int)aclPrefs[9] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDeactivate Ring-to-Open: ");
    -        _response.concat((int)aclPrefs[10] ? "Allowed" : "Disallowed");
    -        _response.concat("\nElectric Strike Actuation: ");
    -        _response.concat((int)aclPrefs[11] ? "Allowed" : "Disallowed");
    -        _response.concat("\nActivate Continuous Mode: ");
    -        _response.concat((int)aclPrefs[12] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDeactivate Continuous Mode: ");
    -        _response.concat((int)aclPrefs[13] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 1: ");
    -        _response.concat((int)aclPrefs[14] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 2: ");
    -        _response.concat((int)aclPrefs[15] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 3: ");
    -        _response.concat((int)aclPrefs[16] ? "Allowed" : "Disallowed");
    -        _response.concat("\n\n------------ NUKI OPENER CONFIG ACL ------------");
    -        _response.concat("\nName: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLatitude: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLongitude: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    -        _response.concat("\nPairing enabled: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    -        _response.concat("\nButton enabled: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    -        _response.concat("\nLED flash enabled: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    -        _response.concat("\nTimezone offset: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDST mode: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 1: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 2: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    -        _response.concat("\nFob Action 3: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    -        _response.concat("\nOperating Mode: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAdvertising Mode: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    -        _response.concat("\nTimezone ID: ");
    -        _response.concat((int)basicOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    -        _response.concat("\nIntercom ID: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    -        _response.concat("\nBUS mode Switch: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    -        _response.concat("\nShort Circuit Duration: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    -        _response.concat("\nEletric Strike Delay: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    -        _response.concat("\nRandom Electric Strike Delay: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    -        _response.concat("\nElectric Strike Duration: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDisable RTO after ring: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    -        _response.concat("\nRTO timeout: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDoorbell suppression: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDoorbell suppression duration: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSound Ring: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSound Open: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSound RTO: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSound CM: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSound confirmation: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[14] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSound level: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[15] ? "Allowed" : "Disallowed");
    -        _response.concat("\nSingle button press action: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[16] ? "Allowed" : "Disallowed");
    -        _response.concat("\nDouble button press action: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[17] ? "Allowed" : "Disallowed");
    -        _response.concat("\nBattery type: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[18] ? "Allowed" : "Disallowed");
    -        _response.concat("\nAutomatic battery type detection: ");
    -        _response.concat((int)advancedOpenerConfigAclPrefs[19] ? "Allowed" : "Disallowed");
    +        response.print("\n\n------------ NUKI OPENER ACL ------------");
    +        response.print("\nActivate Ring-to-Open: ");
    +        response.print((int)aclPrefs[9] ? "Allowed" : "Disallowed");
    +        response.print("\nDeactivate Ring-to-Open: ");
    +        response.print((int)aclPrefs[10] ? "Allowed" : "Disallowed");
    +        response.print("\nElectric Strike Actuation: ");
    +        response.print((int)aclPrefs[11] ? "Allowed" : "Disallowed");
    +        response.print("\nActivate Continuous Mode: ");
    +        response.print((int)aclPrefs[12] ? "Allowed" : "Disallowed");
    +        response.print("\nDeactivate Continuous Mode: ");
    +        response.print((int)aclPrefs[13] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 1: ");
    +        response.print((int)aclPrefs[14] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 2: ");
    +        response.print((int)aclPrefs[15] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 3: ");
    +        response.print((int)aclPrefs[16] ? "Allowed" : "Disallowed");
    +        response.print("\n\n------------ NUKI OPENER CONFIG ACL ------------");
    +        response.print("\nName: ");
    +        response.print((int)basicOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    +        response.print("\nLatitude: ");
    +        response.print((int)basicOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    +        response.print("\nLongitude: ");
    +        response.print((int)basicOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    +        response.print("\nPairing enabled: ");
    +        response.print((int)basicOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    +        response.print("\nButton enabled: ");
    +        response.print((int)basicOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    +        response.print("\nLED flash enabled: ");
    +        response.print((int)basicOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    +        response.print("\nTimezone offset: ");
    +        response.print((int)basicOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    +        response.print("\nDST mode: ");
    +        response.print((int)basicOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 1: ");
    +        response.print((int)basicOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 2: ");
    +        response.print((int)basicOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    +        response.print("\nFob Action 3: ");
    +        response.print((int)basicOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    +        response.print("\nOperating Mode: ");
    +        response.print((int)basicOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    +        response.print("\nAdvertising Mode: ");
    +        response.print((int)basicOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    +        response.print("\nTimezone ID: ");
    +        response.print((int)basicOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    +        response.print("\nIntercom ID: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[0] ? "Allowed" : "Disallowed");
    +        response.print("\nBUS mode Switch: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[1] ? "Allowed" : "Disallowed");
    +        response.print("\nShort Circuit Duration: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[2] ? "Allowed" : "Disallowed");
    +        response.print("\nEletric Strike Delay: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[3] ? "Allowed" : "Disallowed");
    +        response.print("\nRandom Electric Strike Delay: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[4] ? "Allowed" : "Disallowed");
    +        response.print("\nElectric Strike Duration: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[5] ? "Allowed" : "Disallowed");
    +        response.print("\nDisable RTO after ring: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[6] ? "Allowed" : "Disallowed");
    +        response.print("\nRTO timeout: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[7] ? "Allowed" : "Disallowed");
    +        response.print("\nDoorbell suppression: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[8] ? "Allowed" : "Disallowed");
    +        response.print("\nDoorbell suppression duration: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[9] ? "Allowed" : "Disallowed");
    +        response.print("\nSound Ring: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[10] ? "Allowed" : "Disallowed");
    +        response.print("\nSound Open: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[11] ? "Allowed" : "Disallowed");
    +        response.print("\nSound RTO: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[12] ? "Allowed" : "Disallowed");
    +        response.print("\nSound CM: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[13] ? "Allowed" : "Disallowed");
    +        response.print("\nSound confirmation: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[14] ? "Allowed" : "Disallowed");
    +        response.print("\nSound level: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[15] ? "Allowed" : "Disallowed");
    +        response.print("\nSingle button press action: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[16] ? "Allowed" : "Disallowed");
    +        response.print("\nDouble button press action: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[17] ? "Allowed" : "Disallowed");
    +        response.print("\nBattery type: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[18] ? "Allowed" : "Disallowed");
    +        response.print("\nAutomatic battery type detection: ");
    +        response.print((int)advancedOpenerConfigAclPrefs[19] ? "Allowed" : "Disallowed");
             if(_preferences->getBool(preference_show_secrets))
             {
                 char tmp[16];
    @@ -3700,52 +3761,51 @@ void WebCfgServer::buildInfoHtml(AsyncWebServerRequest *request)
                 nukiBlePref.getBytes("secretKeyK", secretKeyKOpn, 32);
                 nukiBlePref.getBytes("authorizationId", authorizationIdOpn, 4);
                 nukiBlePref.end();
    -            _response.concat("\n\n------------ NUKI OPENER PAIRING ------------");
    -            _response.concat("\nBLE Address: ");
    +            response.print("\n\n------------ NUKI OPENER PAIRING ------------");
    +            response.print("\nBLE Address: ");
                 for (int i = 0; i < 6; i++)
                 {
                     sprintf(tmp, "%02x", currentBleAddressOpn[i]);
    -                _response.concat(tmp);
    +                response.print(tmp);
                 }
    -            _response.concat("\nSecretKeyK: ");
    +            response.print("\nSecretKeyK: ");
                 for (int i = 0; i < 32; i++)
                 {
                     sprintf(tmp, "%02x", secretKeyKOpn[i]);
    -                _response.concat(tmp);
    +                response.print(tmp);
                 }
    -            _response.concat("\nAuthorizationId: ");
    +            response.print("\nAuthorizationId: ");
                 for (int i = 0; i < 4; i++)
                 {
                     sprintf(tmp, "%02x", authorizationIdOpn[i]);
    -                _response.concat(tmp);
    +                response.print(tmp);
                 }
             }
         }
     
    -    _response.concat("\n\n------------ GPIO ------------\n");
    +    response.print("\n\n------------ GPIO ------------\n");
         String gpioStr = "";
         _gpio->getConfigurationText(gpioStr, _gpio->pinConfiguration());
    -    _response.concat(gpioStr);
    -    _response.concat("
    "); - sendResponse(request); + response.print(gpioStr); + response.print("
    "); + return response.endSend(); } -void WebCfgServer::processUnpair(AsyncWebServerRequest *request, bool opener) +esp_err_t WebCfgServer::processUnpair(PsychicRequest *request, bool opener) { String value = ""; - if(request->hasParam("CONFIRMTOKEN", true)) + if(request->hasParam("CONFIRMTOKEN")) { - const AsyncWebParameter* p = request->getParam("CONFIRMTOKEN", true); + const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); if(p->value() != "") value = p->value(); } if(value != _confirmCode) { - buildConfirmHtml(request, "Confirm code is invalid.", 3, true); - return; + return buildConfirmHtml(request, "Confirm code is invalid.", 3, true); } - buildConfirmHtml(request, opener ? "Unpairing Nuki Opener and restarting." : "Unpairing Nuki Lock and restarting.", 3, true); + esp_err_t res = buildConfirmHtml(request, opener ? "Unpairing Nuki Opener and restarting." : "Unpairing Nuki Lock and restarting.", 3, true); if(!opener && _nuki != nullptr) { @@ -3759,34 +3819,35 @@ void WebCfgServer::processUnpair(AsyncWebServerRequest *request, bool opener) } waitAndProcess(false, 1000); restartEsp(RestartReason::DeviceUnpaired); + return res; } -void WebCfgServer::processUpdate(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::processUpdate(PsychicRequest *request) { + esp_err_t res; String value = ""; if(request->hasParam("token")) { - const AsyncWebParameter* p = request->getParam("token"); + const PsychicWebParameter* p = request->getParam("token"); if(p->value() != "") value = p->value(); } if(value != _confirmCode) { - buildConfirmHtml(request, "Confirm code is invalid.", 3, true); - return; + return buildConfirmHtml(request, "Confirm code is invalid.", 3, true); } if(request->hasParam("beta")) { if(request->hasParam("debug")) { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEBUG BETA version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEBUG BETA version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL_DBG); _preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL_DBG); } else { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest BETA version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest BETA version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL); } @@ -3795,13 +3856,13 @@ void WebCfgServer::processUpdate(AsyncWebServerRequest *request) { if(request->hasParam("debug")) { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEBUG DEVELOPMENT version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEBUG DEVELOPMENT version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL_DBG); _preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL_DBG); } else { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEVELOPMENT version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEVELOPMENT version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL); } @@ -3810,53 +3871,54 @@ void WebCfgServer::processUpdate(AsyncWebServerRequest *request) { if(request->hasParam("debug")) { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEBUG RELEASE version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest DEBUG RELEASE version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL_DBG); _preferences->putString(preference_ota_main_url, GITHUB_LATEST_UPDATER_BINARY_URL_DBG); } else { - buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest RELEASE version", 2, true); + res = buildConfirmHtml(request, "Rebooting to update Nuki Hub and Nuki Hub updater
    Updating to latest RELEASE version", 2, true); _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL); } } waitAndProcess(true, 1000); restartEsp(RestartReason::OTAReboot); + return res; } -void WebCfgServer::processFactoryReset(AsyncWebServerRequest *request) +esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) { + esp_err_t res; String value = ""; - if(request->hasParam("CONFIRMTOKEN", true)) + if(request->hasParam("CONFIRMTOKEN")) { - const AsyncWebParameter* p = request->getParam("CONFIRMTOKEN", true); + const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); if(p->value() != "") value = p->value(); } bool resetWifi = false; if(value.length() == 0 || value != _confirmCode) { - buildConfirmHtml(request, "Confirm code is invalid.", 3, true); - return; + return buildConfirmHtml(request, "Confirm code is invalid.", 3, true); } else { String value2 = ""; - if(request->hasParam("WIFI", true)) + if(request->hasParam("WIFI")) { - const AsyncWebParameter* p = request->getParam("WIFI", true); + const PsychicWebParameter* p = request->getParam("WIFI"); if(p->value() != "") value = p->value(); } if(value2 == "1") { resetWifi = true; - buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener and resetting WiFi.", 3, true); + res = buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener and resetting WiFi.", 3, true); } else { - buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener.", 3, true); + res = buildConfirmHtml(request, "Factory resetting Nuki Hub, unpairing Nuki Lock and Nuki Opener.", 3, true); } } @@ -3889,9 +3951,11 @@ void WebCfgServer::processFactoryReset(AsyncWebServerRequest *request) waitAndProcess(false, 3000); restartEsp(RestartReason::NukiHubReset); + return res; } -void WebCfgServer::printInputField(const char *token, +void WebCfgServer::printInputField(PsychicStreamResponse *response, + const char *token, const char *description, const char *value, const size_t& maxLength, @@ -3903,38 +3967,39 @@ void WebCfgServer::printInputField(const char *token, itoa(maxLength, maxLengthStr, 10); - _response.concat(""); - _response.concat(description); + response->print(""); + response->print(description); if(showLengthRestriction) { - _response.concat(" (Max. "); - _response.concat(maxLength); - _response.concat(" characters)"); + response->print(" (Max. "); + response->print(maxLength); + response->print(" characters)"); } - _response.concat(""); - _response.concat("print(""); + response->print("print(" "); + response->print(args); } if(strcmp(value, "") != 0) { - _response.concat(" value=\""); - _response.concat(value); + response->print(" value=\""); + response->print(value); } - _response.concat("\" name=\""); - _response.concat(token); - _response.concat("\" size=\"25\" maxlength=\""); - _response.concat(maxLengthStr); - _response.concat("\"/>"); - _response.concat(""); + response->print("\" name=\""); + response->print(token); + response->print("\" size=\"25\" maxlength=\""); + response->print(maxLengthStr); + response->print("\"/>"); + response->print(""); } -void WebCfgServer::printInputField(const char *token, +void WebCfgServer::printInputField(PsychicStreamResponse *response, + const char *token, const char *description, const int value, size_t maxLength, @@ -3942,32 +4007,33 @@ void WebCfgServer::printInputField(const char *token, { char valueStr[20]; itoa(value, valueStr, 10); - printInputField(token, description, valueStr, maxLength, args); + printInputField(response, token, description, valueStr, maxLength, args); } -void WebCfgServer::printCheckBox(const char *token, const char *description, const bool value, const char *htmlClass) +void WebCfgServer::printCheckBox(PsychicStreamResponse *response, const char *token, const char *description, const bool value, const char *htmlClass) { - _response.concat(""); - _response.concat(description); - _response.concat(""); + response->print(""); + response->print(description); + response->print(""); - _response.concat(""); + response->print("print(token); + response->print("\" value=\"0\""); + response->print("/>"); - _response.concat("print("print(token); - _response.concat("\" class=\""); - _response.concat(htmlClass); + response->print("\" class=\""); + response->print(htmlClass); - _response.concat("\" value=\"1\""); - _response.concat(value ? " checked=\"checked\"" : ""); - _response.concat("/>"); + response->print("\" value=\"1\""); + response->print(value ? " checked=\"checked\"" : ""); + response->print("/>"); } -void WebCfgServer::printTextarea(const char *token, +void WebCfgServer::printTextarea(PsychicStreamResponse *response, + const char *token, const char *description, const char *value, const size_t& maxLength, @@ -3978,106 +4044,106 @@ void WebCfgServer::printTextarea(const char *token, itoa(maxLength, maxLengthStr, 10); - _response.concat(""); - _response.concat(description); + response->print(""); + response->print(description); if(showLengthRestriction) { - _response.concat(" (Max. "); - _response.concat(maxLength); - _response.concat(" characters)"); + response->print(" (Max. "); + response->print(maxLength); + response->print(" characters)"); } - _response.concat(""); - _response.concat(" "); - _response.concat(""); + response->print(" name=\""); + response->print(token); + response->print("\" maxlength=\""); + response->print(maxLengthStr); + response->print("\">"); + response->print(value); + response->print(""); + response->print(""); } -void WebCfgServer::printDropDown(const char *token, const char *description, const String preselectedValue, const std::vector> options, const String className) +void WebCfgServer::printDropDown(PsychicStreamResponse *response, const char *token, const char *description, const String preselectedValue, const std::vector> options, const String className) { - _response.concat(""); - _response.concat(description); - _response.concat(""); + response->print(""); + response->print(description); + response->print(""); - if(className.length() > 0) _response.concat("print(""); - _response.concat(""); + response->print(""); + response->print(""); } -void WebCfgServer::buildNavigationButton(const char *caption, const char *targetPath, const char* labelText) +void WebCfgServer::buildNavigationButton(PsychicStreamResponse *response, const char *caption, const char *targetPath, const char* labelText) { - _response.concat("
    "); - _response.concat(" "); - _response.concat(labelText); - _response.concat("
    "); + response->print("
    print(targetPath); + response->print("\">"); + response->print(" "); + response->print(labelText); + response->print("
    "); } -void WebCfgServer::buildNavigationMenuEntry(const char *title, const char *targetPath, const char* warningMessage) +void WebCfgServer::buildNavigationMenuEntry(PsychicStreamResponse *response, const char *title, const char *targetPath, const char* warningMessage) { - _response.concat(""); - _response.concat("
  • "); - _response.concat(title); + response->print("print(targetPath); + response->print("\">"); + response->print("
  • "); + response->print(title); if(strcmp(warningMessage, "") != 0){ - _response.concat(""); - _response.concat(warningMessage); - _response.concat(""); + response->print(""); + response->print(warningMessage); + response->print(""); } - _response.concat("
  • "); + response->print(""); } -void WebCfgServer::printParameter(const char *description, const char *value, const char *link, const char *id) +void WebCfgServer::printParameter(PsychicStreamResponse *response, const char *description, const char *value, const char *link, const char *id) { - _response.concat(""); - _response.concat(""); - _response.concat(description); - _response.concat(""); - if(strcmp(id, "") == 0) _response.concat(""); + response->print(""); + response->print(""); + response->print(description); + response->print(""); + if(strcmp(id, "") == 0) response->print(""); else { - _response.concat(""); + response->print("print(id); + response->print("\">"); } - if(strcmp(link, "") == 0) _response.concat(value); + if(strcmp(link, "") == 0) response->print(value); else { - _response.concat(" "); - _response.concat(value); - _response.concat(""); + response->print("print(link); + response->print("\"> "); + response->print(value); + response->print(""); } - _response.concat(""); - _response.concat(""); + response->print(""); + response->print(""); } diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 583175d..f9fe598 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -1,9 +1,8 @@ #pragma once #include -#include -#include -#include +#include +#include #include "esp_ota_ops.h" #include "Config.h" @@ -37,9 +36,9 @@ class WebCfgServer { public: #ifndef NUKI_HUB_UPDATER - WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer); + WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer); #else - WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, AsyncWebServer* asyncServer); + WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer); #endif ~WebCfgServer() = default; @@ -47,34 +46,34 @@ public: private: #ifndef NUKI_HUB_UPDATER - void sendSettings(AsyncWebServerRequest *request); - bool processArgs(AsyncWebServerRequest *request, String& message); - bool processImport(AsyncWebServerRequest *request, String& message); - void processGpioArgs(AsyncWebServerRequest *request); - void buildHtml(AsyncWebServerRequest *request); - void buildAccLvlHtml(AsyncWebServerRequest *request); - void buildCredHtml(AsyncWebServerRequest *request); - void buildImportExportHtml(AsyncWebServerRequest *request); - void buildMqttConfigHtml(AsyncWebServerRequest *request); - void buildStatusHtml(AsyncWebServerRequest *request); - void buildAdvancedConfigHtml(AsyncWebServerRequest *request); - void buildNukiConfigHtml(AsyncWebServerRequest *request); - void buildGpioConfigHtml(AsyncWebServerRequest *request); + esp_err_t sendSettings(PsychicRequest *request); + bool processArgs(PsychicRequest *request, String& message); + bool processImport(PsychicRequest *request, String& message); + void processGpioArgs(PsychicRequest *request); + esp_err_t buildHtml(PsychicRequest *request); + esp_err_t buildAccLvlHtml(PsychicRequest *request); + esp_err_t buildCredHtml(PsychicRequest *request); + esp_err_t buildImportExportHtml(PsychicRequest *request); + esp_err_t buildMqttConfigHtml(PsychicRequest *request); + esp_err_t buildStatusHtml(PsychicRequest *request); + esp_err_t buildAdvancedConfigHtml(PsychicRequest *request); + esp_err_t buildNukiConfigHtml(PsychicRequest *request); + esp_err_t buildGpioConfigHtml(PsychicRequest *request); #ifndef CONFIG_IDF_TARGET_ESP32H2 - void buildConfigureWifiHtml(AsyncWebServerRequest *request); + esp_err_t buildConfigureWifiHtml(PsychicRequest *request); #endif - void buildInfoHtml(AsyncWebServerRequest *request); - void buildCustomNetworkConfigHtml(AsyncWebServerRequest *request); - void processUnpair(AsyncWebServerRequest *request, bool opener); - void processUpdate(AsyncWebServerRequest *request); - void processFactoryReset(AsyncWebServerRequest *request); - void printInputField(const char* token, const char* description, const char* value, const size_t& maxLength, const char* args, const bool& isPassword = false, const bool& showLengthRestriction = false); - void printInputField(const char* token, const char* description, const int value, size_t maxLength, const char* args); - void printCheckBox(const char* token, const char* description, const bool value, const char* htmlClass); - void printTextarea(const char *token, const char *description, const char *value, const size_t& maxLength, const bool& enabled = true, const bool& showLengthRestriction = false); - void printDropDown(const char *token, const char *description, const String preselectedValue, std::vector> options, const String className); - void buildNavigationButton(const char* caption, const char* targetPath, const char* labelText = ""); - void buildNavigationMenuEntry(const char *title, const char *targetPath, const char* warningMessage = ""); + esp_err_t buildInfoHtml(PsychicRequest *request); + esp_err_t buildCustomNetworkConfigHtml(PsychicRequest *request); + esp_err_t processUnpair(PsychicRequest *request, bool opener); + esp_err_t processUpdate(PsychicRequest *request); + esp_err_t processFactoryReset(PsychicRequest *request); + void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const char* value, const size_t& maxLength, const char* args, const bool& isPassword = false, const bool& showLengthRestriction = false); + void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const int value, size_t maxLength, const char* args); + void printCheckBox(PsychicStreamResponse *response, const char* token, const char* description, const bool value, const char* htmlClass); + void printTextarea(PsychicStreamResponse *response, const char *token, const char *description, const char *value, const size_t& maxLength, const bool& enabled = true, const bool& showLengthRestriction = false); + void printDropDown(PsychicStreamResponse *response, const char *token, const char *description, const String preselectedValue, std::vector> options, const String className); + void buildNavigationButton(PsychicStreamResponse *response, const char* caption, const char* targetPath, const char* labelText = ""); + void buildNavigationMenuEntry(PsychicStreamResponse *response, const char *title, const char *targetPath, const char* warningMessage = ""); const std::vector> getNetworkDetectionOptions() const; const std::vector> getGpioOptions() const; @@ -86,8 +85,8 @@ private: String getPreselectionForGpio(const uint8_t& pin); String pinStateToString(uint8_t value); - void printParameter(const char* description, const char* value, const char *link = "", const char *id = ""); - + void printParameter(PsychicStreamResponse *response, const char* description, const char* value, const char *link = "", const char *id = ""); + NukiWrapper* _nuki = nullptr; NukiOpenerWrapper* _nukiOpener = nullptr; Gpio* _gpio = nullptr; @@ -96,21 +95,19 @@ private: bool _rebootRequired = false; #endif - String _response; String generateConfirmCode(); String _confirmCode = "----"; - void buildConfirmHtml(AsyncWebServerRequest *request, const String &message, uint32_t redirectDelay = 5, bool redirect = false); - void buildOtaHtml(AsyncWebServerRequest *request, bool debug = false); - void buildOtaCompletedHtml(AsyncWebServerRequest *request); - void sendCss(AsyncWebServerRequest *request); - void sendFavicon(AsyncWebServerRequest *request); - void buildHtmlHeader(String additionalHeader = ""); + esp_err_t buildConfirmHtml(PsychicRequest *request, const String &message, uint32_t redirectDelay = 5, bool redirect = false); + esp_err_t buildOtaHtml(PsychicRequest *request, bool debug = false); + esp_err_t buildOtaCompletedHtml(PsychicRequest *request); + esp_err_t sendCss(PsychicRequest *request); + esp_err_t sendFavicon(PsychicRequest *request); + void buildHtmlHeader(PsychicStreamResponse *response, String additionalHeader = ""); void waitAndProcess(const bool blocking, const uint32_t duration); - void handleOtaUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); + esp_err_t handleOtaUpload(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final); void printProgress(size_t prg, size_t sz); - void sendResponse(AsyncWebServerRequest *request); - - AsyncWebServer* _asyncServer = nullptr; + + PsychicHttpServer* _psychicServer = nullptr; NukiNetwork* _network = nullptr; Preferences* _preferences = nullptr; diff --git a/src/main.cpp b/src/main.cpp index e1ae161..ee565d9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,11 +19,12 @@ #include "Logger.h" #include "PreferencesKeys.h" #include "RestartReason.h" -#include -#include -#include +/* +#ifdef DEBUG_NUKIHUB #include #include +#endif +*/ char log_print_buffer[1024]; @@ -54,7 +55,7 @@ int64_t restartTs = 10 * 1000 * 60000; #endif -AsyncWebServer* asyncServer = nullptr; +PsychicHttpServer* psychicServer = nullptr; NukiNetwork* network = nullptr; WebCfgServer* webCfgServer = nullptr; Preferences* preferences = nullptr; @@ -418,11 +419,11 @@ void setup() if(!doOta) { - asyncServer = new AsyncWebServer(80); - webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, asyncServer); + psychicServer = new PsychicHttpServer; + webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); - asyncServer->onNotFound([](AsyncWebServerRequest* request) { request->redirect("/"); }); - asyncServer->begin(); + psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); + psychicServer->listen(80); } #else Log->print(F("Nuki Hub version ")); @@ -490,15 +491,20 @@ void setup() { if(!doOta) { - asyncServer = new AsyncWebServer(80); + psychicServer = new PsychicHttpServer; + psychicServer->config.max_uri_handlers = 40; + psychicServer->config.stack_size = 8192; + psychicServer->listen(80); if(forceEnableWebServer || preferences->getBool(preference_webserver_enabled, true)) { - webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, asyncServer); + webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); - asyncServer->onNotFound([](AsyncWebServerRequest* request) { request->redirect("/"); }); + psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); } - else asyncServer->onNotFound([](AsyncWebServerRequest* request) { request->redirect("/webserial"); }); + /* + #ifdef DEBUG_NUKIHUB + else psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/webserial"); }); if(preferences->getBool(preference_webserial_enabled, false)) { @@ -506,8 +512,8 @@ void setup() WebSerial.begin(asyncServer); WebSerial.setBuffer(1024); } - - asyncServer->begin(); + #endif + */ } } #endif diff --git a/updater/platformio.ini b/updater/platformio.ini index 7bf7ec3..8a0d352 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -53,8 +53,7 @@ lib_ignore = SimpleBLE WiFiProv lib_deps = - AsyncTCP=symlink://../lib/AsyncTCP - ESPAsyncWebServer=symlink://../lib/ESPAsyncWebServer + PsychicHttp=symlink://../lib/PsychicHttp WiFiManager=symlink://../lib/WiFiManager monitor_speed = 115200 @@ -86,8 +85,7 @@ board = esp32-h2-devkitm-1 board_build.cmake_extra_args = -DNUKI_TARGET_H2=y lib_deps = - AsyncTCP=symlink://../lib/AsyncTCP - ESPAsyncWebServer=symlink://../lib/ESPAsyncWebServer + PsychicHttp=symlink://../lib/PsychicHttp [env:updater_esp32-solo1] extends = env:updater_esp32 diff --git a/updater/sdkconfig.defaults b/updater/sdkconfig.defaults index e60db16..14d722b 100644 --- a/updater/sdkconfig.defaults +++ b/updater/sdkconfig.defaults @@ -22,4 +22,13 @@ CONFIG_ETH_ENABLED=y CONFIG_ETH_USE_SPI_ETHERNET=y CONFIG_ETH_SPI_ETHERNET_W5500=y CONFIG_ETH_SPI_ETHERNET_DM9051=y -CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y \ No newline at end of file +CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +CONFIG_HTTPD_WS_SUPPORT=y +CONFIG_ESP_HTTPS_SERVER_ENABLE=y \ No newline at end of file From 52bd4f74c1280edb1cf0b4cb12b07dd42ee01647 Mon Sep 17 00:00:00 2001 From: iranl Date: Wed, 28 Aug 2024 23:59:36 +0200 Subject: [PATCH 04/30] PsychicHTTP --- lib/PsychicHttp/src/PsychicHttpsServer.h | 7 +- lib/WiFiManager/WiFiManager.cpp | 82 ++++++++++++++---------- lib/WiFiManager/WiFiManager.h | 21 +----- platformio.ini | 6 ++ src/Config.h | 2 +- src/WebCfgServer.cpp | 5 +- src/main.cpp | 2 +- 7 files changed, 64 insertions(+), 61 deletions(-) diff --git a/lib/PsychicHttp/src/PsychicHttpsServer.h b/lib/PsychicHttp/src/PsychicHttpsServer.h index 51f8210..c60b25b 100644 --- a/lib/PsychicHttp/src/PsychicHttpsServer.h +++ b/lib/PsychicHttp/src/PsychicHttpsServer.h @@ -31,9 +31,8 @@ class PsychicHttpsServer : public PsychicHttpServer virtual esp_err_t _startServer() override final; virtual void stop() override final; }; - -#endif // PsychicHttpsServer_h - #else #warning ESP-IDF https server support not enabled. -#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE \ No newline at end of file +#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE + +#endif // PsychicHttpsServer_h \ No newline at end of file diff --git a/lib/WiFiManager/WiFiManager.cpp b/lib/WiFiManager/WiFiManager.cpp index 34c6868..aede273 100644 --- a/lib/WiFiManager/WiFiManager.cpp +++ b/lib/WiFiManager/WiFiManager.cpp @@ -627,6 +627,7 @@ boolean WiFiManager::configPortalHasTimeout(){ } void WiFiManager::setupHTTPServer(){ + server = new PsychicHttpServer; server->listen(_httpPort); /* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */ @@ -636,7 +637,7 @@ void WiFiManager::setupHTTPServer(){ { using namespace std::placeholders; server->on(String(FPSTR(R_root)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleRoot, this,_1)); - server->on(String(FPSTR(R_wifisave)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifiSave, this,_1)); + server->on(String(FPSTR(R_wifisave)).c_str(), HTTP_POST, (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifiSave, this,_1)); server->on(String(FPSTR(R_info)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleInfo, this,_1)); server->on(String(FPSTR(R_param)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParam, this,_1)); server->on(String(FPSTR(R_paramsave)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParamSave, this,_1)); @@ -899,6 +900,7 @@ uint8_t WiFiManager::processConfigPortal(){ DEBUG_WM(F("Connect to new AP [SUCCESS]")); DEBUG_WM(F("Got IP Address:")); DEBUG_WM(WiFi.localIP()); + reboot(); } #endif @@ -981,7 +983,6 @@ bool WiFiManager::shutdownConfigPortal(){ #else server->stop(); #endif - server.reset(); dnsServer->stop(); // free heap ? dnsServer.reset(); @@ -1128,7 +1129,7 @@ bool WiFiManager::wifiConnectNew(String ssid, String pass,bool connect){ DEBUG_WM(F("find best RSSI: TRUE")); #endif if (!_numNetworks) - WiFi_scanNetworks(false, false); // scan in case this gets called before any scans + WiFi_scanNetworks(false, _enableCaptivePortal); // scan in case this gets called before any scans int n = _numNetworks; if (n == 0) { @@ -1217,7 +1218,7 @@ bool WiFiManager::wifiConnectDefault(){ DEBUG_WM(F("find best RSSI: TRUE")); #endif if (!_numNetworks) - WiFi_scanNetworks(false, false); // scan in case this gets called before any scans + WiFi_scanNetworks(false, _enableCaptivePortal); // scan in case this gets called before any scans int n = _numNetworks; if (n == 0) { @@ -1464,7 +1465,7 @@ esp_err_t WiFiManager::handleRoot(PsychicRequest *request) { reportStatus(page); page += FPSTR(HTTP_END); - if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime,true); // preload wifiscan throttled, async + //if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime,true); // preload wifiscan throttled, async // @todo buggy, captive portals make a query on every page load, causing this to run every time in addition to the real page load // I dont understand why, when you are already in the captive portal, I guess they want to know that its still up and not done or gone // if we can detect these and ignore them that would be great, since they come from the captive portal redirect maybe there is a refferer @@ -1486,7 +1487,7 @@ esp_err_t WiFiManager::handleWifi(PsychicRequest *request,bool scan = true) { #ifdef WM_DEBUG_LEVEL // DEBUG_WM(WM_DEBUG_DEV,"refresh flag:",request->hasArg(F("refresh"))); #endif - WiFi_scanNetworks(request->hasParam("refresh")); //wifiscan, force if arg refresh + WiFi_scanNetworks(request->hasParam("refresh"), true); //wifiscan, force if arg refresh page += getScanItemOut(); } String pitem = ""; @@ -1684,7 +1685,7 @@ void WiFiManager::resetScan(){ String WiFiManager::WiFiManager::getScanItemOut(){ String page; - if(!_numNetworks) WiFi_scanNetworks(); // scan in case this gets called before any scans + if(!_numNetworks) WiFi_scanNetworks(true, true); // scan in case this gets called before any scans int n = _numNetworks; if (n == 0) { @@ -1943,8 +1944,12 @@ esp_err_t WiFiManager::handleWifiSave(PsychicRequest *request) { handleRequest(); //SAVE/connect here - _ssid = request->getParam("s")->value(); - _pass = request->getParam("p")->value(); + if(request->hasParam("s")){ + _ssid = request->getParam("s")->value(); + } + if(request->hasParam("p")){ + _pass = request->getParam("p")->value(); + } if(_ssid == "" && _pass != ""){ _ssid = WiFi_SSID(true); // password change, placeholder ssid, @todo compare pass to old?, confirm ssid is clean @@ -1970,36 +1975,43 @@ esp_err_t WiFiManager::handleWifiSave(PsychicRequest *request) { #endif // set static ips from server args - if (request->getParam(S_ip)->value() != "") { - //_sta_static_ip.fromString(request->arg(FPSTR(S_ip)); - String ip = request->getParam(S_ip)->value(); - optionalIPFromString(&_sta_static_ip, ip.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static ip:"),ip); - #endif + if(request->hasParam(S_ip)){ + if (request->getParam(S_ip)->value() != "") { + //_sta_static_ip.fromString(request->arg(FPSTR(S_ip)); + String ip = request->getParam(S_ip)->value(); + optionalIPFromString(&_sta_static_ip, ip.c_str()); + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_DEV,F("static ip:"),ip); + #endif + } } - if (request->getParam(S_gw)->value() != "") { - String gw = request->getParam(S_gw)->value(); - optionalIPFromString(&_sta_static_gw, gw.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static gateway:"),gw); - #endif + if(request->hasParam(S_gw)){ + if (request->getParam(S_gw)->value() != "") { + String gw = request->getParam(S_gw)->value(); + optionalIPFromString(&_sta_static_gw, gw.c_str()); + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_DEV,F("static gateway:"),gw); + #endif + } } - if (request->getParam(S_sn)->value() != "") { - String sn = request->getParam(S_sn)->value(); - optionalIPFromString(&_sta_static_sn, sn.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static netmask:"),sn); - #endif + if(request->hasParam(S_sn)){ + if (request->getParam(S_sn)->value() != "") { + String sn = request->getParam(S_sn)->value(); + optionalIPFromString(&_sta_static_sn, sn.c_str()); + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_DEV,F("static netmask:"),sn); + #endif + } } - if (request->getParam(S_dns)->value() != "") { - String dns = request->getParam(S_dns)->value(); - optionalIPFromString(&_sta_static_dns, dns.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static DNS:"),dns); - #endif + if(request->hasParam(S_dns)){ + if (request->getParam(S_dns)->value() != "") { + String dns = request->getParam(S_dns)->value(); + optionalIPFromString(&_sta_static_dns, dns.c_str()); + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_DEV,F("static DNS:"),dns); + #endif + } } - if (_presavewificallback != NULL) { _presavewificallback(); // @CALLBACK } diff --git a/lib/WiFiManager/WiFiManager.h b/lib/WiFiManager/WiFiManager.h index 38f9bfb..c48ec7f 100644 --- a/lib/WiFiManager/WiFiManager.h +++ b/lib/WiFiManager/WiFiManager.h @@ -518,22 +518,7 @@ class WiFiManager std::unique_ptr dnsServer; - - #if defined(ESP32) && defined(WM_WEBSERVERSHIM) - #ifdef WM_ASYNCWEBSERVER - using WM_WebServer = PsychicHttpServer; - #else - using WM_WebServer = WebServer; - #endif - #else - #ifdef WM_ASYNCWEBSERVER - using WM_WebServer = AsyncWebServer; - #else - using WM_WebServer = ESP8266WebServer; - #endif - #endif - - std::unique_ptr server; + PsychicHttpServer* server; protected: // vars @@ -651,9 +636,9 @@ class WiFiManager // preload scanning causes AP to delay showing for users, but also caches and lets the cp load faster once its open // my scan takes 7-10 seconds public: - boolean _preloadwifiscan = false; // preload wifiscan if true + boolean _preloadwifiscan = true; // preload wifiscan if true unsigned int _scancachetime = 30000; // ms cache time for preload scans - boolean _asyncScan = false; // perform wifi network scan async + boolean _asyncScan = true; // perform wifi network scan async bool wifiConnectDefault(); protected: diff --git a/platformio.ini b/platformio.ini index 21d694f..a0f7376 100644 --- a/platformio.ini +++ b/platformio.ini @@ -131,6 +131,7 @@ build_flags = -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 -DDEBUG_NUKIHUB + -DWM_DEBUG_LEVEL=4 -DDEBUG_SENSE_NUKI -DDEBUG_NUKI_COMMAND -DDEBUG_NUKI_CONNECT @@ -152,6 +153,7 @@ build_flags = -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 -DDEBUG_NUKIHUB + -DWM_DEBUG_LEVEL=4 -DDEBUG_SENSE_NUKI -DDEBUG_NUKI_COMMAND -DDEBUG_NUKI_CONNECT @@ -173,6 +175,7 @@ build_flags = -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 -DDEBUG_NUKIHUB + -DWM_DEBUG_LEVEL=4 -DDEBUG_SENSE_NUKI -DDEBUG_NUKI_COMMAND -DDEBUG_NUKI_CONNECT @@ -195,6 +198,7 @@ build_flags = -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 -DDEBUG_NUKIHUB + -DWM_DEBUG_LEVEL=4 -DDEBUG_SENSE_NUKI -DDEBUG_NUKI_COMMAND -DDEBUG_NUKI_CONNECT @@ -216,6 +220,7 @@ build_flags = -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 -DDEBUG_NUKIHUB + -DWM_DEBUG_LEVEL=4 -DDEBUG_SENSE_NUKI -DDEBUG_NUKI_COMMAND -DDEBUG_NUKI_CONNECT @@ -237,6 +242,7 @@ build_flags = -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 -DDEBUG_NUKIHUB + -DWM_DEBUG_LEVEL=4 -DDEBUG_SENSE_NUKI -DDEBUG_NUKI_COMMAND -DDEBUG_NUKI_CONNECT diff --git a/src/Config.h b/src/Config.h index 70cfcf7..d3a61df 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.01" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-08-27" +#define NUKI_HUB_DATE "2024-08-28" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 86f62e3..90f40cf 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -164,7 +164,7 @@ void WebCfgServer::initialize() if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); return processFactoryReset(request); }); - _psychicServer->on("/info", HTTP_GET, [&](PsychicRequest *request){ + _psychicServer->on("/infopg", HTTP_GET, [&](PsychicRequest *request){ if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); return buildInfoHtml(request); }); @@ -2650,7 +2650,7 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request) printParameter(&response, "Nuki Opener PIN status", openerState.c_str(), "", "openerPin"); } } - printParameter(&response, "Firmware", NUKI_HUB_VERSION, "/info", "firmware"); + printParameter(&response, "Firmware", NUKI_HUB_VERSION, "/infopg", "firmware"); if(_preferences->getBool(preference_check_updates)) { printParameter(&response, "Latest Firmware", _preferences->getString(preference_latest_version).c_str(), "/ota", "ota"); @@ -3232,6 +3232,7 @@ esp_err_t WebCfgServer::buildConfigureWifiHtml(PsychicRequest *request) esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) { + Log->println("INFO HTML"); uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); PsychicStreamResponse response(request, "text/plain"); diff --git a/src/main.cpp b/src/main.cpp index ee565d9..edefe53 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -422,8 +422,8 @@ void setup() psychicServer = new PsychicHttpServer; webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); - psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); psychicServer->listen(80); + psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); } #else Log->print(F("Nuki Hub version ")); From 25076e6e7e844e3aeb33cd5ec2c750b3a5f90f83 Mon Sep 17 00:00:00 2001 From: iranl Date: Thu, 29 Aug 2024 23:27:56 +0200 Subject: [PATCH 05/30] ESP-MQTT --- lib/AsyncTCP/CMakeLists.txt | 15 - lib/AsyncTCP/Kconfig.projbuild | 30 - lib/AsyncTCP/LICENSE | 165 -- lib/AsyncTCP/README.md | 56 - lib/AsyncTCP/arduino-cli-dev.yaml | 25 - lib/AsyncTCP/arduino-cli.yaml | 25 - lib/AsyncTCP/component.mk | 3 - .../examples/ClientServer/Client/Client.ino | 42 - .../examples/ClientServer/Client/config.h | 23 - lib/AsyncTCP/library.json | 38 - lib/AsyncTCP/library.properties | 9 - lib/AsyncTCP/platformio.ini | 48 - lib/AsyncTCP/src/AsyncTCP.cpp | 1557 ----------------- lib/AsyncTCP/src/AsyncTCP.h | 279 --- lib/MqttLogger/src/MqttLogger.cpp | 19 +- lib/MqttLogger/src/MqttLogger.h | 8 +- lib/espMqttClient/CMakeLists.txt | 17 - lib/espMqttClient/LICENSE | 21 - lib/espMqttClient/README.md | 61 - lib/espMqttClient/component.mk | 3 - lib/espMqttClient/docs/_config.yml | 6 - lib/espMqttClient/docs/index.md | 587 ------- lib/espMqttClient/docs/mqtt-v3.1.1.pdf | Bin 1506688 -> 0 bytes .../largepayload-esp8266.ino | 106 -- .../examples/notask-esp32/notask-esp32.ino | 148 -- .../examples/ota-esp8266/ota-esp8266.ino | 159 -- .../examples/simple-esp32-idf/CMakeLists.txt | 8 - .../examples/simple-esp32-idf/README.md | 3 - .../simple-esp32-idf/main/CMakeLists.txt | 3 - .../examples/simple-esp32-idf/main/main.cpp | 142 -- .../simple-esp32-idf/sdkconfig.defaults | 39 - .../examples/simple-esp32/simple-esp32.ino | 141 -- .../simple-esp8266/simple-esp8266.ino | 139 -- .../examples/simple-linux/main.cpp | 89 - .../examples/simple-linux/platformio.ini | 29 - .../simpleAsync-esp32/simpleAsync-esp32.ino | 141 -- .../simpleAsync-esp8266.ino | 138 -- .../examples/tls-esp32/tls-esp32.ino | 170 -- .../examples/tls-esp8266/tls-esp8266.ino | 144 -- lib/espMqttClient/keywords.txt | 61 - lib/espMqttClient/library.json | 25 - lib/espMqttClient/library.properties | 9 - lib/espMqttClient/platformio.ini | 43 - lib/espMqttClient/scripts/get-fingerprint.py | 30 - lib/espMqttClient/src/Config.h | 75 - lib/espMqttClient/src/Helpers.h | 49 - lib/espMqttClient/src/Logging.h | 43 - lib/espMqttClient/src/MemoryPool/LICENSE | 21 - lib/espMqttClient/src/MemoryPool/README.md | 105 -- lib/espMqttClient/src/MemoryPool/keywords.txt | 16 - lib/espMqttClient/src/MemoryPool/library.json | 21 - .../src/MemoryPool/library.properties | 10 - lib/espMqttClient/src/MemoryPool/src/Fixed.h | 119 -- .../src/MemoryPool/src/MemoryPool.h | 12 - .../src/MemoryPool/src/Variable.h | 242 --- lib/espMqttClient/src/MqttClient.cpp | 746 -------- lib/espMqttClient/src/MqttClient.h | 201 --- lib/espMqttClient/src/MqttClientSetup.h | 245 --- lib/espMqttClient/src/Outbox.h | 255 --- lib/espMqttClient/src/Packets/Constants.h | 77 - lib/espMqttClient/src/Packets/Packet.cpp | 454 ----- lib/espMqttClient/src/Packets/Packet.h | 163 -- lib/espMqttClient/src/Packets/Parser.cpp | 316 ---- lib/espMqttClient/src/Packets/Parser.h | 100 -- .../src/Packets/RemainingLength.cpp | 57 - .../src/Packets/RemainingLength.h | 32 - lib/espMqttClient/src/Packets/StringUtil.cpp | 26 - lib/espMqttClient/src/Packets/StringUtil.h | 22 - .../src/Transport/ClientAsync.cpp | 58 - lib/espMqttClient/src/Transport/ClientAsync.h | 44 - .../src/Transport/ClientPosix.cpp | 130 -- lib/espMqttClient/src/Transport/ClientPosix.h | 54 - .../src/Transport/ClientPosixIPAddress.cpp | 40 - .../src/Transport/ClientPosixIPAddress.h | 30 - .../src/Transport/ClientSecureSync.cpp | 71 - .../src/Transport/ClientSecureSync.h | 34 - .../src/Transport/ClientSync.cpp | 71 - lib/espMqttClient/src/Transport/ClientSync.h | 34 - lib/espMqttClient/src/Transport/Transport.h | 28 - lib/espMqttClient/src/TypeDefs.cpp | 51 - lib/espMqttClient/src/TypeDefs.h | 73 - lib/espMqttClient/src/espMqttClient.cpp | 113 -- lib/espMqttClient/src/espMqttClient.h | 80 - lib/espMqttClient/src/espMqttClientAsync.cpp | 61 - lib/espMqttClient/src/espMqttClientAsync.h | 36 - lib/espMqttClient/test-coverage.py | 22 - .../test_client_native/test_client_native.cpp | 405 ----- .../test/test_outbox/test_outbox.cpp | 171 -- .../test/test_packets/test_packets.cpp | 714 -------- .../test/test_parser/test_parser.cpp | 355 ---- .../test_remainingLength.cpp | 63 - .../test/test_string/test_string.cpp | 64 - src/Config.h | 2 +- src/NukiNetwork.cpp | 462 +++-- src/NukiNetwork.h | 22 +- src/WebCfgServer.cpp | 6 +- src/networkDevices/EthernetDevice.cpp | 61 - src/networkDevices/EthernetDevice.h | 12 - src/networkDevices/NetworkDevice.cpp | 175 +- src/networkDevices/NetworkDevice.h | 41 +- src/networkDevices/WifiDevice.cpp | 56 +- src/networkDevices/WifiDevice.h | 11 - 102 files changed, 262 insertions(+), 11599 deletions(-) delete mode 100644 lib/AsyncTCP/CMakeLists.txt delete mode 100644 lib/AsyncTCP/Kconfig.projbuild delete mode 100644 lib/AsyncTCP/LICENSE delete mode 100644 lib/AsyncTCP/README.md delete mode 100644 lib/AsyncTCP/arduino-cli-dev.yaml delete mode 100644 lib/AsyncTCP/arduino-cli.yaml delete mode 100644 lib/AsyncTCP/component.mk delete mode 100644 lib/AsyncTCP/examples/ClientServer/Client/Client.ino delete mode 100644 lib/AsyncTCP/examples/ClientServer/Client/config.h delete mode 100644 lib/AsyncTCP/library.json delete mode 100644 lib/AsyncTCP/library.properties delete mode 100644 lib/AsyncTCP/platformio.ini delete mode 100644 lib/AsyncTCP/src/AsyncTCP.cpp delete mode 100644 lib/AsyncTCP/src/AsyncTCP.h delete mode 100644 lib/espMqttClient/CMakeLists.txt delete mode 100644 lib/espMqttClient/LICENSE delete mode 100644 lib/espMqttClient/README.md delete mode 100644 lib/espMqttClient/component.mk delete mode 100644 lib/espMqttClient/docs/_config.yml delete mode 100644 lib/espMqttClient/docs/index.md delete mode 100644 lib/espMqttClient/docs/mqtt-v3.1.1.pdf delete mode 100644 lib/espMqttClient/examples/largepayload-esp8266/largepayload-esp8266.ino delete mode 100644 lib/espMqttClient/examples/notask-esp32/notask-esp32.ino delete mode 100644 lib/espMqttClient/examples/ota-esp8266/ota-esp8266.ino delete mode 100644 lib/espMqttClient/examples/simple-esp32-idf/CMakeLists.txt delete mode 100644 lib/espMqttClient/examples/simple-esp32-idf/README.md delete mode 100644 lib/espMqttClient/examples/simple-esp32-idf/main/CMakeLists.txt delete mode 100644 lib/espMqttClient/examples/simple-esp32-idf/main/main.cpp delete mode 100644 lib/espMqttClient/examples/simple-esp32-idf/sdkconfig.defaults delete mode 100644 lib/espMqttClient/examples/simple-esp32/simple-esp32.ino delete mode 100644 lib/espMqttClient/examples/simple-esp8266/simple-esp8266.ino delete mode 100644 lib/espMqttClient/examples/simple-linux/main.cpp delete mode 100644 lib/espMqttClient/examples/simple-linux/platformio.ini delete mode 100644 lib/espMqttClient/examples/simpleAsync-esp32/simpleAsync-esp32.ino delete mode 100644 lib/espMqttClient/examples/simpleAsync-esp8266/simpleAsync-esp8266.ino delete mode 100644 lib/espMqttClient/examples/tls-esp32/tls-esp32.ino delete mode 100644 lib/espMqttClient/examples/tls-esp8266/tls-esp8266.ino delete mode 100644 lib/espMqttClient/keywords.txt delete mode 100644 lib/espMqttClient/library.json delete mode 100644 lib/espMqttClient/library.properties delete mode 100644 lib/espMqttClient/platformio.ini delete mode 100644 lib/espMqttClient/scripts/get-fingerprint.py delete mode 100644 lib/espMqttClient/src/Config.h delete mode 100644 lib/espMqttClient/src/Helpers.h delete mode 100644 lib/espMqttClient/src/Logging.h delete mode 100644 lib/espMqttClient/src/MemoryPool/LICENSE delete mode 100644 lib/espMqttClient/src/MemoryPool/README.md delete mode 100644 lib/espMqttClient/src/MemoryPool/keywords.txt delete mode 100644 lib/espMqttClient/src/MemoryPool/library.json delete mode 100644 lib/espMqttClient/src/MemoryPool/library.properties delete mode 100644 lib/espMqttClient/src/MemoryPool/src/Fixed.h delete mode 100644 lib/espMqttClient/src/MemoryPool/src/MemoryPool.h delete mode 100644 lib/espMqttClient/src/MemoryPool/src/Variable.h delete mode 100644 lib/espMqttClient/src/MqttClient.cpp delete mode 100644 lib/espMqttClient/src/MqttClient.h delete mode 100644 lib/espMqttClient/src/MqttClientSetup.h delete mode 100644 lib/espMqttClient/src/Outbox.h delete mode 100644 lib/espMqttClient/src/Packets/Constants.h delete mode 100644 lib/espMqttClient/src/Packets/Packet.cpp delete mode 100644 lib/espMqttClient/src/Packets/Packet.h delete mode 100644 lib/espMqttClient/src/Packets/Parser.cpp delete mode 100644 lib/espMqttClient/src/Packets/Parser.h delete mode 100644 lib/espMqttClient/src/Packets/RemainingLength.cpp delete mode 100644 lib/espMqttClient/src/Packets/RemainingLength.h delete mode 100644 lib/espMqttClient/src/Packets/StringUtil.cpp delete mode 100644 lib/espMqttClient/src/Packets/StringUtil.h delete mode 100644 lib/espMqttClient/src/Transport/ClientAsync.cpp delete mode 100644 lib/espMqttClient/src/Transport/ClientAsync.h delete mode 100644 lib/espMqttClient/src/Transport/ClientPosix.cpp delete mode 100644 lib/espMqttClient/src/Transport/ClientPosix.h delete mode 100644 lib/espMqttClient/src/Transport/ClientPosixIPAddress.cpp delete mode 100644 lib/espMqttClient/src/Transport/ClientPosixIPAddress.h delete mode 100644 lib/espMqttClient/src/Transport/ClientSecureSync.cpp delete mode 100644 lib/espMqttClient/src/Transport/ClientSecureSync.h delete mode 100644 lib/espMqttClient/src/Transport/ClientSync.cpp delete mode 100644 lib/espMqttClient/src/Transport/ClientSync.h delete mode 100644 lib/espMqttClient/src/Transport/Transport.h delete mode 100644 lib/espMqttClient/src/TypeDefs.cpp delete mode 100644 lib/espMqttClient/src/TypeDefs.h delete mode 100644 lib/espMqttClient/src/espMqttClient.cpp delete mode 100644 lib/espMqttClient/src/espMqttClient.h delete mode 100644 lib/espMqttClient/src/espMqttClientAsync.cpp delete mode 100644 lib/espMqttClient/src/espMqttClientAsync.h delete mode 100644 lib/espMqttClient/test-coverage.py delete mode 100644 lib/espMqttClient/test/test_client_native/test_client_native.cpp delete mode 100644 lib/espMqttClient/test/test_outbox/test_outbox.cpp delete mode 100644 lib/espMqttClient/test/test_packets/test_packets.cpp delete mode 100644 lib/espMqttClient/test/test_parser/test_parser.cpp delete mode 100644 lib/espMqttClient/test/test_remainingLength/test_remainingLength.cpp delete mode 100644 lib/espMqttClient/test/test_string/test_string.cpp diff --git a/lib/AsyncTCP/CMakeLists.txt b/lib/AsyncTCP/CMakeLists.txt deleted file mode 100644 index f52e1c9..0000000 --- a/lib/AsyncTCP/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -set(COMPONENT_SRCDIRS - "src" -) - -set(COMPONENT_ADD_INCLUDEDIRS - "src" -) - -set(COMPONENT_REQUIRES - "arduino-esp32" -) - -register_component() - -target_compile_options(${COMPONENT_TARGET} PRIVATE -fno-rtti) diff --git a/lib/AsyncTCP/Kconfig.projbuild b/lib/AsyncTCP/Kconfig.projbuild deleted file mode 100644 index 1774926..0000000 --- a/lib/AsyncTCP/Kconfig.projbuild +++ /dev/null @@ -1,30 +0,0 @@ -menu "AsyncTCP Configuration" - -choice ASYNC_TCP_RUNNING_CORE - bool "Core on which AsyncTCP's thread is running" - default ASYNC_TCP_RUN_CORE1 - help - Select on which core AsyncTCP is running - - config ASYNC_TCP_RUN_CORE0 - bool "CORE 0" - config ASYNC_TCP_RUN_CORE1 - bool "CORE 1" - config ASYNC_TCP_RUN_NO_AFFINITY - bool "BOTH" - -endchoice - -config ASYNC_TCP_RUNNING_CORE - int - default 0 if ASYNC_TCP_RUN_CORE0 - default 1 if ASYNC_TCP_RUN_CORE1 - default -1 if ASYNC_TCP_RUN_NO_AFFINITY - -config ASYNC_TCP_USE_WDT - bool "Enable WDT for the AsyncTCP task" - default "y" - help - Enable WDT for the AsyncTCP task, so it will trigger if a handler is locking the thread. - -endmenu diff --git a/lib/AsyncTCP/LICENSE b/lib/AsyncTCP/LICENSE deleted file mode 100644 index 65c5ca8..0000000 --- a/lib/AsyncTCP/LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/lib/AsyncTCP/README.md b/lib/AsyncTCP/README.md deleted file mode 100644 index 61ccd09..0000000 --- a/lib/AsyncTCP/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# AsyncTCP - -[![License: LGPL 3.0](https://img.shields.io/badge/License-LGPL%203.0-yellow.svg)](https://opensource.org/license/lgpl-3-0/) -[![Continuous Integration](https://github.com/mathieucarbou/AsyncTCP/actions/workflows/ci.yml/badge.svg)](https://github.com/mathieucarbou/AsyncTCP/actions/workflows/ci.yml) -[![PlatformIO Registry](https://badges.registry.platformio.org/packages/mathieucarbou/library/AsyncTCP.svg)](https://registry.platformio.org/libraries/mathieucarbou/AsyncTCP) - -A fork of the [AsyncTCP](https://github.com/me-no-dev/AsyncTCP) library by [@me-no-dev](https://github.com/me-no-dev) for [ESPHome](https://esphome.io). - -### Async TCP Library for ESP32 Arduino - -This is a fully asynchronous TCP library, aimed at enabling trouble-free, multi-connection network environment for Espressif's ESP32 MCUs. - -This library is the base for [ESPAsyncWebServer](https://github.com/mathieucarbou/ESPAsyncWebServer) - -## AsyncClient and AsyncServer - -The base classes on which everything else is built. They expose all possible scenarios, but are really raw and require more skills to use. - -## Changes in this fork - -- All improvements from [ESPHome fork](https://github.com/esphome/AsyncTCP) -- Reverted back `library.properties` for Arduino IDE users -- Arduino 3 / ESP-IDF 5 compatibility -- IPv6 support - -## Coordinates - -``` -mathieucarbou/AsyncTCP @ ^3.2.4 -``` - -## Important recommendations - -Most of the crashes are caused by improper configuration of the library for the project. -Here are some recommendations to avoid them. - -1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` -2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`. - The default value of `16384` might be too much for your project. - You can look at the [MycilaTaskMonitor](https://oss.carbou.me/MycilaTaskMonitor) project to monitor the stack usage. -3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`. - Default is `10`. -4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`. - Default is `64`. -5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`. - Default is `5000`. - -I personally use the following configuration in my projects: - -```c++ - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 -``` diff --git a/lib/AsyncTCP/arduino-cli-dev.yaml b/lib/AsyncTCP/arduino-cli-dev.yaml deleted file mode 100644 index 174df7a..0000000 --- a/lib/AsyncTCP/arduino-cli-dev.yaml +++ /dev/null @@ -1,25 +0,0 @@ -board_manager: - additional_urls: - - https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json -directories: - builtin.libraries: ./src/ -build_cache: - compilations_before_purge: 10 - ttl: 720h0m0s -daemon: - port: "50051" -library: - enable_unsafe_install: false -logging: - file: "" - format: text - level: info -metrics: - addr: :9090 - enabled: true -output: - no_color: false -sketch: - always_export_binaries: false -updater: - enable_notification: true diff --git a/lib/AsyncTCP/arduino-cli.yaml b/lib/AsyncTCP/arduino-cli.yaml deleted file mode 100644 index 42365f4..0000000 --- a/lib/AsyncTCP/arduino-cli.yaml +++ /dev/null @@ -1,25 +0,0 @@ -board_manager: - additional_urls: - - https://espressif.github.io/arduino-esp32/package_esp32_index.json -directories: - builtin.libraries: ./src/ -build_cache: - compilations_before_purge: 10 - ttl: 720h0m0s -daemon: - port: "50051" -library: - enable_unsafe_install: false -logging: - file: "" - format: text - level: info -metrics: - addr: :9090 - enabled: true -output: - no_color: false -sketch: - always_export_binaries: false -updater: - enable_notification: true diff --git a/lib/AsyncTCP/component.mk b/lib/AsyncTCP/component.mk deleted file mode 100644 index bb5bb16..0000000 --- a/lib/AsyncTCP/component.mk +++ /dev/null @@ -1,3 +0,0 @@ -COMPONENT_ADD_INCLUDEDIRS := src -COMPONENT_SRCDIRS := src -CXXFLAGS += -fno-rtti diff --git a/lib/AsyncTCP/examples/ClientServer/Client/Client.ino b/lib/AsyncTCP/examples/ClientServer/Client/Client.ino deleted file mode 100644 index 47d8bc7..0000000 --- a/lib/AsyncTCP/examples/ClientServer/Client/Client.ino +++ /dev/null @@ -1,42 +0,0 @@ -#include - -#include "config.h" - -static void replyToServer(void* arg) { - AsyncClient* client = reinterpret_cast(arg); - - // send reply - if (client->space() > 32 && client->canSend()) { - char message[32]; - client->add(message, strlen(message)); - client->send(); - } -} - -/* event callbacks */ -static void handleData(void* arg, AsyncClient* client, void *data, size_t len) { - Serial.printf("\n data received from %s \n", client->remoteIP().toString().c_str()); - Serial.write((uint8_t*)data, len); - -} - -void onConnect(void* arg, AsyncClient* client) { - Serial.printf("\n client has been connected to %s on port %d \n", SERVER_HOST_NAME, TCP_PORT); - replyToServer(client); -} - - -void setup() { - Serial.begin(115200); - delay(20); - - AsyncClient* client = new AsyncClient; - client->onData(&handleData, client); - client->onConnect(&onConnect, client); - client->connect(SERVER_HOST_NAME, TCP_PORT); - -} - -void loop() { - -} diff --git a/lib/AsyncTCP/examples/ClientServer/Client/config.h b/lib/AsyncTCP/examples/ClientServer/Client/config.h deleted file mode 100644 index cf51e91..0000000 --- a/lib/AsyncTCP/examples/ClientServer/Client/config.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H - -/* - * This example demonstrate how to use asynchronous client & server APIs - * in order to establish tcp socket connections in client server manner. - * server is running (on port 7050) on one ESP, acts as AP, and other clients running on - * remaining ESPs acts as STAs. after connection establishment between server and clients - * there is a simple message transfer in every 2s. clients connect to server via it's host name - * (in this case 'esp_server') with help of DNS service running on server side. - * - * Note: default MSS for ESPAsyncTCP is 536 byte and defualt ACK timeout is 5s. -*/ - -#define SSID "ESP-TEST" -#define PASSWORD "123456789" - -#define SERVER_HOST_NAME "esp_server" - -#define TCP_PORT 7050 -#define DNS_PORT 53 - -#endif // CONFIG_H diff --git a/lib/AsyncTCP/library.json b/lib/AsyncTCP/library.json deleted file mode 100644 index c449f2a..0000000 --- a/lib/AsyncTCP/library.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "AsyncTCP", - "version": "3.2.4", - "description": "Asynchronous TCP Library for ESP32", - "keywords": "async,tcp", - "repository": { - "type": "git", - "url": "https://github.com/mathieucarbou/AsyncTCP.git" - }, - "authors": [ - { - "name": "Hristo Gochkov" - }, - { - "name": "Mathieu Carbou", - "maintainer": true - } - ], - "license": "LGPL-3.0", - "frameworks": "arduino", - "platforms": [ - "espressif32", - "libretiny" - ], - "build": { - "libCompatMode": 2 - }, - "export": { - "include": [ - "examples", - "src", - "library.json", - "library.properties", - "LICENSE", - "README.md" - ] - } -} \ No newline at end of file diff --git a/lib/AsyncTCP/library.properties b/lib/AsyncTCP/library.properties deleted file mode 100644 index 12a504c..0000000 --- a/lib/AsyncTCP/library.properties +++ /dev/null @@ -1,9 +0,0 @@ -name=AsyncTCP -version=3.2.4 -author=Me-No-Dev -maintainer=Mathieu Carbou -sentence=Async TCP Library for ESP32 -paragraph=Async TCP Library for ESP32 -category=Other -url=https://github.com/mathieucarbou/AsyncTCP.git -architectures=* diff --git a/lib/AsyncTCP/platformio.ini b/lib/AsyncTCP/platformio.ini deleted file mode 100644 index 4cfad22..0000000 --- a/lib/AsyncTCP/platformio.ini +++ /dev/null @@ -1,48 +0,0 @@ -[env] -framework = arduino -build_flags = - -Wall -Wextra - -D CONFIG_ARDUHAL_LOG_COLORS - -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG -upload_protocol = esptool -monitor_speed = 115200 -monitor_filters = esp32_exception_decoder, log2file - -[platformio] -lib_dir = . -src_dir = examples/ClientServer/Client - -[env:arduino] -platform = espressif32 -board = esp32dev - -[env:arduino-2] -platform = espressif32@6.7.0 -board = esp32dev - -[env:arduino-3] -platform = espressif32 -platform_packages= - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.3 - platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.3/esp32-arduino-libs-3.0.3.zip -board = esp32dev - -[env:pioarduino-esp32dev] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.03/platform-espressif32.zip -board = esp32dev - -[env:pioarduino-esp32-s2] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.03/platform-espressif32.zip -board = esp32-s2-saola-1 - -[env:pioarduino-esp32-s3] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.03/platform-espressif32.zip -board = esp32-s3-devkitc-1 - -[env:pioarduino-esp32-c3] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.03/platform-espressif32.zip -board = esp32-c3-devkitc-02 - -[env:pioarduino-esp32-c6] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.03/platform-espressif32.zip -board = esp32-c6-devkitc-1 diff --git a/lib/AsyncTCP/src/AsyncTCP.cpp b/lib/AsyncTCP/src/AsyncTCP.cpp deleted file mode 100644 index 2dae2cb..0000000 --- a/lib/AsyncTCP/src/AsyncTCP.cpp +++ /dev/null @@ -1,1557 +0,0 @@ -/* - Asynchronous TCP library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "Arduino.h" - -#include "AsyncTCP.h" -extern "C"{ -#include "lwip/opt.h" -#include "lwip/tcp.h" -#include "lwip/inet.h" -#include "lwip/dns.h" -#include "lwip/err.h" -} -#if CONFIG_ASYNC_TCP_USE_WDT -#include "esp_task_wdt.h" -#endif - -// Required for: -// https://github.com/espressif/arduino-esp32/blob/3.0.3/libraries/Network/src/NetworkInterface.cpp#L37-L47 -#if ESP_IDF_VERSION_MAJOR >= 5 -#include -#endif - -/* - * TCP/IP Event Task - * */ - -typedef enum { - LWIP_TCP_SENT, LWIP_TCP_RECV, LWIP_TCP_FIN, LWIP_TCP_ERROR, LWIP_TCP_POLL, LWIP_TCP_CLEAR, LWIP_TCP_ACCEPT, LWIP_TCP_CONNECTED, LWIP_TCP_DNS -} lwip_event_t; - -typedef struct { - lwip_event_t event; - void *arg; - union { - struct { - tcp_pcb * pcb; - int8_t err; - } connected; - struct { - int8_t err; - } error; - struct { - tcp_pcb * pcb; - uint16_t len; - } sent; - struct { - tcp_pcb * pcb; - pbuf * pb; - int8_t err; - } recv; - struct { - tcp_pcb * pcb; - int8_t err; - } fin; - struct { - tcp_pcb * pcb; - } poll; - struct { - AsyncClient * client; - } accept; - struct { - const char * name; - ip_addr_t addr; - } dns; - }; -} lwip_event_packet_t; - -static QueueHandle_t _async_queue; -static TaskHandle_t _async_service_task_handle = NULL; - - -SemaphoreHandle_t _slots_lock; -const int _number_of_closed_slots = CONFIG_LWIP_MAX_ACTIVE_TCP; -static uint32_t _closed_slots[_number_of_closed_slots]; -static uint32_t _closed_index = []() { - _slots_lock = xSemaphoreCreateBinary(); - xSemaphoreGive(_slots_lock); - for (int i = 0; i < _number_of_closed_slots; ++ i) { - _closed_slots[i] = 1; - } - return 1; -}(); - - -static inline bool _init_async_event_queue(){ - if(!_async_queue){ - _async_queue = xQueueCreate(CONFIG_ASYNC_TCP_QUEUE_SIZE, sizeof(lwip_event_packet_t *)); - if(!_async_queue){ - return false; - } - } - return true; -} - -static inline bool _send_async_event(lwip_event_packet_t ** e){ - return _async_queue && xQueueSend(_async_queue, e, portMAX_DELAY) == pdPASS; -} - -static inline bool _prepend_async_event(lwip_event_packet_t ** e){ - return _async_queue && xQueueSendToFront(_async_queue, e, portMAX_DELAY) == pdPASS; -} - -static inline bool _get_async_event(lwip_event_packet_t ** e){ - return _async_queue && xQueueReceive(_async_queue, e, portMAX_DELAY) == pdPASS; -} - -static bool _remove_events_with_arg(void * arg){ - lwip_event_packet_t * first_packet = NULL; - lwip_event_packet_t * packet = NULL; - - if(!_async_queue){ - return false; - } - //figure out which is the first packet so we can keep the order - while(!first_packet){ - if(xQueueReceive(_async_queue, &first_packet, 0) != pdPASS){ - return false; - } - //discard packet if matching - if((int)first_packet->arg == (int)arg){ - free(first_packet); - first_packet = NULL; - //return first packet to the back of the queue - } else if(xQueueSend(_async_queue, &first_packet, portMAX_DELAY) != pdPASS){ - return false; - } - } - - while(xQueuePeek(_async_queue, &packet, 0) == pdPASS && packet != first_packet){ - if(xQueueReceive(_async_queue, &packet, 0) != pdPASS){ - return false; - } - if((int)packet->arg == (int)arg){ - free(packet); - packet = NULL; - } else if(xQueueSend(_async_queue, &packet, portMAX_DELAY) != pdPASS){ - return false; - } - } - return true; -} - -static void _handle_async_event(lwip_event_packet_t * e){ - if(e->arg == NULL){ - // do nothing when arg is NULL - //ets_printf("event arg == NULL: 0x%08x\n", e->recv.pcb); - } else if(e->event == LWIP_TCP_CLEAR){ - _remove_events_with_arg(e->arg); - } else if(e->event == LWIP_TCP_RECV){ - //ets_printf("-R: 0x%08x\n", e->recv.pcb); - AsyncClient::_s_recv(e->arg, e->recv.pcb, e->recv.pb, e->recv.err); - } else if(e->event == LWIP_TCP_FIN){ - //ets_printf("-F: 0x%08x\n", e->fin.pcb); - AsyncClient::_s_fin(e->arg, e->fin.pcb, e->fin.err); - } else if(e->event == LWIP_TCP_SENT){ - //ets_printf("-S: 0x%08x\n", e->sent.pcb); - AsyncClient::_s_sent(e->arg, e->sent.pcb, e->sent.len); - } else if(e->event == LWIP_TCP_POLL){ - //ets_printf("-P: 0x%08x\n", e->poll.pcb); - AsyncClient::_s_poll(e->arg, e->poll.pcb); - } else if(e->event == LWIP_TCP_ERROR){ - //ets_printf("-E: 0x%08x %d\n", e->arg, e->error.err); - AsyncClient::_s_error(e->arg, e->error.err); - } else if(e->event == LWIP_TCP_CONNECTED){ - //ets_printf("C: 0x%08x 0x%08x %d\n", e->arg, e->connected.pcb, e->connected.err); - AsyncClient::_s_connected(e->arg, e->connected.pcb, e->connected.err); - } else if(e->event == LWIP_TCP_ACCEPT){ - //ets_printf("A: 0x%08x 0x%08x\n", e->arg, e->accept.client); - AsyncServer::_s_accepted(e->arg, e->accept.client); - } else if(e->event == LWIP_TCP_DNS){ - //ets_printf("D: 0x%08x %s = %s\n", e->arg, e->dns.name, ipaddr_ntoa(&e->dns.addr)); - AsyncClient::_s_dns_found(e->dns.name, &e->dns.addr, e->arg); - } - free((void*)(e)); -} - -static void _async_service_task(void *pvParameters){ - lwip_event_packet_t * packet = NULL; - for (;;) { - if(_get_async_event(&packet)){ -#if CONFIG_ASYNC_TCP_USE_WDT - if(esp_task_wdt_add(NULL) != ESP_OK){ - log_e("Failed to add async task to WDT"); - } -#endif - _handle_async_event(packet); -#if CONFIG_ASYNC_TCP_USE_WDT - if(esp_task_wdt_delete(NULL) != ESP_OK){ - log_e("Failed to remove loop task from WDT"); - } -#endif - } - } - vTaskDelete(NULL); - _async_service_task_handle = NULL; -} -/* -static void _stop_async_task(){ - if(_async_service_task_handle){ - vTaskDelete(_async_service_task_handle); - _async_service_task_handle = NULL; - } -} -*/ - -static bool customTaskCreateUniversal( - TaskFunction_t pxTaskCode, - const char * const pcName, - const uint32_t usStackDepth, - void * const pvParameters, - UBaseType_t uxPriority, - TaskHandle_t * const pxCreatedTask, - const BaseType_t xCoreID) { -#ifndef CONFIG_FREERTOS_UNICORE - if(xCoreID >= 0 && xCoreID < 2) { - return xTaskCreatePinnedToCore(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask, xCoreID); - } else { -#endif - return xTaskCreate(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask); -#ifndef CONFIG_FREERTOS_UNICORE - } -#endif -} - -static bool _start_async_task(){ - if(!_init_async_event_queue()){ - return false; - } - if(!_async_service_task_handle){ - customTaskCreateUniversal(_async_service_task, "async_tcp", CONFIG_ASYNC_TCP_STACK_SIZE, NULL, CONFIG_ASYNC_TCP_PRIORITY, &_async_service_task_handle, CONFIG_ASYNC_TCP_RUNNING_CORE); - if(!_async_service_task_handle){ - return false; - } - } - return true; -} - -/* - * LwIP Callbacks - * */ - -static int8_t _tcp_clear_events(void * arg) { - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - e->event = LWIP_TCP_CLEAR; - e->arg = arg; - if (!_prepend_async_event(&e)) { - free((void*)(e)); - } - return ERR_OK; -} - -static int8_t _tcp_connected(void * arg, tcp_pcb * pcb, int8_t err) { - //ets_printf("+C: 0x%08x\n", pcb); - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - e->event = LWIP_TCP_CONNECTED; - e->arg = arg; - e->connected.pcb = pcb; - e->connected.err = err; - if (!_prepend_async_event(&e)) { - free((void*)(e)); - } - return ERR_OK; -} - -static int8_t _tcp_poll(void * arg, struct tcp_pcb * pcb) { - //ets_printf("+P: 0x%08x\n", pcb); - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - e->event = LWIP_TCP_POLL; - e->arg = arg; - e->poll.pcb = pcb; - if (!_send_async_event(&e)) { - free((void*)(e)); - } - return ERR_OK; -} - -static int8_t _tcp_recv(void * arg, struct tcp_pcb * pcb, struct pbuf *pb, int8_t err) { - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - e->arg = arg; - if(pb){ - //ets_printf("+R: 0x%08x\n", pcb); - e->event = LWIP_TCP_RECV; - e->recv.pcb = pcb; - e->recv.pb = pb; - e->recv.err = err; - } else { - //ets_printf("+F: 0x%08x\n", pcb); - e->event = LWIP_TCP_FIN; - e->fin.pcb = pcb; - e->fin.err = err; - //close the PCB in LwIP thread - AsyncClient::_s_lwip_fin(e->arg, e->fin.pcb, e->fin.err); - } - if (!_send_async_event(&e)) { - free((void*)(e)); - } - return ERR_OK; -} - -static int8_t _tcp_sent(void * arg, struct tcp_pcb * pcb, uint16_t len) { - //ets_printf("+S: 0x%08x\n", pcb); - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - e->event = LWIP_TCP_SENT; - e->arg = arg; - e->sent.pcb = pcb; - e->sent.len = len; - if (!_send_async_event(&e)) { - free((void*)(e)); - } - return ERR_OK; -} - -static void _tcp_error(void * arg, int8_t err) { - //ets_printf("+E: 0x%08x\n", arg); - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - e->event = LWIP_TCP_ERROR; - e->arg = arg; - e->error.err = err; - if (!_send_async_event(&e)) { - free((void*)(e)); - } -} - -static void _tcp_dns_found(const char * name, struct ip_addr * ipaddr, void * arg) { - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - //ets_printf("+DNS: name=%s ipaddr=0x%08x arg=%x\n", name, ipaddr, arg); - e->event = LWIP_TCP_DNS; - e->arg = arg; - e->dns.name = name; - if (ipaddr) { - memcpy(&e->dns.addr, ipaddr, sizeof(struct ip_addr)); - } else { - memset(&e->dns.addr, 0, sizeof(e->dns.addr)); - } - if (!_send_async_event(&e)) { - free((void*)(e)); - } -} - -//Used to switch out from LwIP thread -static int8_t _tcp_accept(void * arg, AsyncClient * client) { - lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); - e->event = LWIP_TCP_ACCEPT; - e->arg = arg; - e->accept.client = client; - if (!_prepend_async_event(&e)) { - free((void*)(e)); - } - return ERR_OK; -} - -/* - * TCP/IP API Calls - * */ - -#include "lwip/priv/tcpip_priv.h" - -typedef struct { - struct tcpip_api_call_data call; - tcp_pcb * pcb; - int8_t closed_slot; - int8_t err; - union { - struct { - const char* data; - size_t size; - uint8_t apiflags; - } write; - size_t received; - struct { - ip_addr_t * addr; - uint16_t port; - tcp_connected_fn cb; - } connect; - struct { - ip_addr_t * addr; - uint16_t port; - } bind; - uint8_t backlog; - }; -} tcp_api_call_t; - -static err_t _tcp_output_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = ERR_CONN; - if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { - msg->err = tcp_output(msg->pcb); - } - return msg->err; -} - -static esp_err_t _tcp_output(tcp_pcb * pcb, int8_t closed_slot) { - if(!pcb){ - return ERR_CONN; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = closed_slot; - tcpip_api_call(_tcp_output_api, (struct tcpip_api_call_data*)&msg); - return msg.err; -} - -static err_t _tcp_write_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = ERR_CONN; - if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { - msg->err = tcp_write(msg->pcb, msg->write.data, msg->write.size, msg->write.apiflags); - } - return msg->err; -} - -static esp_err_t _tcp_write(tcp_pcb * pcb, int8_t closed_slot, const char* data, size_t size, uint8_t apiflags) { - if(!pcb){ - return ERR_CONN; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = closed_slot; - msg.write.data = data; - msg.write.size = size; - msg.write.apiflags = apiflags; - tcpip_api_call(_tcp_write_api, (struct tcpip_api_call_data*)&msg); - return msg.err; -} - -static err_t _tcp_recved_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = ERR_CONN; - if(msg->closed_slot != -1 && !_closed_slots[msg->closed_slot]) { - msg->err = 0; - tcp_recved(msg->pcb, msg->received); - } - return msg->err; -} - -static esp_err_t _tcp_recved(tcp_pcb * pcb, int8_t closed_slot, size_t len) { - if(!pcb){ - return ERR_CONN; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = closed_slot; - msg.received = len; - tcpip_api_call(_tcp_recved_api, (struct tcpip_api_call_data*)&msg); - return msg.err; -} - -static err_t _tcp_close_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = ERR_CONN; - if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { - msg->err = tcp_close(msg->pcb); - } - return msg->err; -} - -static esp_err_t _tcp_close(tcp_pcb * pcb, int8_t closed_slot) { - if(!pcb){ - return ERR_CONN; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = closed_slot; - tcpip_api_call(_tcp_close_api, (struct tcpip_api_call_data*)&msg); - return msg.err; -} - -static err_t _tcp_abort_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = ERR_CONN; - if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { - tcp_abort(msg->pcb); - } - return msg->err; -} - -static esp_err_t _tcp_abort(tcp_pcb * pcb, int8_t closed_slot) { - if(!pcb){ - return ERR_CONN; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = closed_slot; - tcpip_api_call(_tcp_abort_api, (struct tcpip_api_call_data*)&msg); - return msg.err; -} - -static err_t _tcp_connect_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = tcp_connect(msg->pcb, msg->connect.addr, msg->connect.port, msg->connect.cb); - return msg->err; -} - -static esp_err_t _tcp_connect(tcp_pcb * pcb, int8_t closed_slot, ip_addr_t * addr, uint16_t port, tcp_connected_fn cb) { - if(!pcb){ - return ESP_FAIL; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = closed_slot; - msg.connect.addr = addr; - msg.connect.port = port; - msg.connect.cb = cb; - tcpip_api_call(_tcp_connect_api, (struct tcpip_api_call_data*)&msg); - return msg.err; -} - -static err_t _tcp_bind_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = tcp_bind(msg->pcb, msg->bind.addr, msg->bind.port); - return msg->err; -} - -static esp_err_t _tcp_bind(tcp_pcb * pcb, ip_addr_t * addr, uint16_t port) { - if(!pcb){ - return ESP_FAIL; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = -1; - msg.bind.addr = addr; - msg.bind.port = port; - tcpip_api_call(_tcp_bind_api, (struct tcpip_api_call_data*)&msg); - return msg.err; -} - -static err_t _tcp_listen_api(struct tcpip_api_call_data *api_call_msg){ - tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; - msg->err = 0; - msg->pcb = tcp_listen_with_backlog(msg->pcb, msg->backlog); - return msg->err; -} - -static tcp_pcb * _tcp_listen_with_backlog(tcp_pcb * pcb, uint8_t backlog) { - if(!pcb){ - return NULL; - } - tcp_api_call_t msg; - msg.pcb = pcb; - msg.closed_slot = -1; - msg.backlog = backlog?backlog:0xFF; - tcpip_api_call(_tcp_listen_api, (struct tcpip_api_call_data*)&msg); - return msg.pcb; -} - - - -/* - Async TCP Client - */ - -AsyncClient::AsyncClient(tcp_pcb* pcb) -: _connect_cb(0) -, _connect_cb_arg(0) -, _discard_cb(0) -, _discard_cb_arg(0) -, _sent_cb(0) -, _sent_cb_arg(0) -, _error_cb(0) -, _error_cb_arg(0) -, _recv_cb(0) -, _recv_cb_arg(0) -, _pb_cb(0) -, _pb_cb_arg(0) -, _timeout_cb(0) -, _timeout_cb_arg(0) -, _ack_pcb(true) -, _tx_last_packet(0) -, _rx_timeout(0) -, _rx_last_ack(0) -, _ack_timeout(CONFIG_ASYNC_TCP_MAX_ACK_TIME) -, _connect_port(0) -, prev(NULL) -, next(NULL) -{ - _pcb = pcb; - _closed_slot = -1; - if(_pcb){ - _rx_last_packet = millis(); - tcp_arg(_pcb, this); - tcp_recv(_pcb, &_tcp_recv); - tcp_sent(_pcb, &_tcp_sent); - tcp_err(_pcb, &_tcp_error); - tcp_poll(_pcb, &_tcp_poll, 1); - if(!_allocate_closed_slot()) { - _close(); - } - } -} - -AsyncClient::~AsyncClient(){ - if(_pcb) { - _close(); - } - _free_closed_slot(); -} - -/* - * Operators - * */ - -AsyncClient& AsyncClient::operator=(const AsyncClient& other){ - if (_pcb) { - _close(); - } - - _pcb = other._pcb; - _closed_slot = other._closed_slot; - if (_pcb) { - _rx_last_packet = millis(); - tcp_arg(_pcb, this); - tcp_recv(_pcb, &_tcp_recv); - tcp_sent(_pcb, &_tcp_sent); - tcp_err(_pcb, &_tcp_error); - tcp_poll(_pcb, &_tcp_poll, 1); - } - return *this; -} - -bool AsyncClient::operator==(const AsyncClient &other) { - return _pcb == other._pcb; -} - -AsyncClient & AsyncClient::operator+=(const AsyncClient &other) { - if(next == NULL){ - next = (AsyncClient*)(&other); - next->prev = this; - } else { - AsyncClient *c = next; - while(c->next != NULL) { - c = c->next; - } - c->next =(AsyncClient*)(&other); - c->next->prev = c; - } - return *this; -} - -/* - * Callback Setters - * */ - -void AsyncClient::onConnect(AcConnectHandler cb, void* arg){ - _connect_cb = cb; - _connect_cb_arg = arg; -} - -void AsyncClient::onDisconnect(AcConnectHandler cb, void* arg){ - _discard_cb = cb; - _discard_cb_arg = arg; -} - -void AsyncClient::onAck(AcAckHandler cb, void* arg){ - _sent_cb = cb; - _sent_cb_arg = arg; -} - -void AsyncClient::onError(AcErrorHandler cb, void* arg){ - _error_cb = cb; - _error_cb_arg = arg; -} - -void AsyncClient::onData(AcDataHandler cb, void* arg){ - _recv_cb = cb; - _recv_cb_arg = arg; -} - -void AsyncClient::onPacket(AcPacketHandler cb, void* arg){ - _pb_cb = cb; - _pb_cb_arg = arg; -} - -void AsyncClient::onTimeout(AcTimeoutHandler cb, void* arg){ - _timeout_cb = cb; - _timeout_cb_arg = arg; -} - -void AsyncClient::onPoll(AcConnectHandler cb, void* arg){ - _poll_cb = cb; - _poll_cb_arg = arg; -} - -/* - * Main Public Methods - * */ - -bool AsyncClient::_connect(ip_addr_t addr, uint16_t port){ - if (_pcb){ - log_d("already connected, state %d", _pcb->state); - return false; - } - if(!_start_async_task()){ - log_e("failed to start task"); - return false; - } - - if(!_allocate_closed_slot()) { - log_e("failed to allocate: closed slot full"); - return false; - } - - tcp_pcb* pcb = tcp_new_ip_type(addr.type); - if (!pcb){ - log_e("pcb == NULL"); - return false; - } - - tcp_arg(pcb, this); - tcp_err(pcb, &_tcp_error); - tcp_recv(pcb, &_tcp_recv); - tcp_sent(pcb, &_tcp_sent); - tcp_poll(pcb, &_tcp_poll, 1); - esp_err_t err =_tcp_connect(pcb, _closed_slot, &addr, port,(tcp_connected_fn)&_tcp_connected); - return err == ESP_OK; -} - -bool AsyncClient::connect(const IPAddress& ip, uint16_t port){ - ip_addr_t addr; -#if ESP_IDF_VERSION_MAJOR < 5 - addr.u_addr.ip4.addr = ip; - addr.type = IPADDR_TYPE_V4; -#else - ip.to_ip_addr_t(&addr); -#endif - - return _connect(addr, port); -} - -#if LWIP_IPV6 && ESP_IDF_VERSION_MAJOR < 5 -bool AsyncClient::connect(const IPv6Address& ip, uint16_t port){ - ip_addr_t addr; - addr.type = IPADDR_TYPE_V6; - memcpy(addr.u_addr.ip6.addr, static_cast(ip), sizeof(uint32_t) * 4); - - return _connect(addr, port); -} -#endif - -bool AsyncClient::connect(const char* host, uint16_t port){ - ip_addr_t addr; - - if(!_start_async_task()){ - log_e("failed to start task"); - return false; - } - - err_t err = dns_gethostbyname(host, &addr, (dns_found_callback)&_tcp_dns_found, this); - if(err == ERR_OK) { -#if ESP_IDF_VERSION_MAJOR < 5 -#if LWIP_IPV6 - if(addr.type == IPADDR_TYPE_V6) { - return connect(IPv6Address(addr.u_addr.ip6.addr), port); - } - return connect(IPAddress(addr.u_addr.ip4.addr), port); -#else - return connect(IPAddress(addr.addr), port); -#endif -#else - return _connect(addr, port); -#endif - } else if(err == ERR_INPROGRESS) { - _connect_port = port; - return true; - } - log_d("error: %d", err); - return false; -} - -void AsyncClient::close(bool now){ - if(_pcb){ - _tcp_recved(_pcb, _closed_slot, _rx_ack_len); - } - _close(); -} - -int8_t AsyncClient::abort(){ - if(_pcb) { - _tcp_abort(_pcb, _closed_slot ); - _pcb = NULL; - } - return ERR_ABRT; -} - -size_t AsyncClient::space(){ - if((_pcb != NULL) && (_pcb->state == 4)){ - return tcp_sndbuf(_pcb); - } - return 0; -} - -size_t AsyncClient::add(const char* data, size_t size, uint8_t apiflags) { - if(!_pcb || size == 0 || data == NULL) { - return 0; - } - size_t room = space(); - if(!room) { - return 0; - } - size_t will_send = (room < size) ? room : size; - int8_t err = ERR_OK; - err = _tcp_write(_pcb, _closed_slot, data, will_send, apiflags); - if(err != ERR_OK) { - return 0; - } - return will_send; -} - -bool AsyncClient::send(){ - auto backup = _tx_last_packet; - _tx_last_packet = millis(); - if (_tcp_output(_pcb, _closed_slot) == ERR_OK) { - return true; - } - _tx_last_packet = backup; - return false; -} - -size_t AsyncClient::ack(size_t len){ - if(len > _rx_ack_len) - len = _rx_ack_len; - if(len){ - _tcp_recved(_pcb, _closed_slot, len); - } - _rx_ack_len -= len; - return len; -} - -void AsyncClient::ackPacket(struct pbuf * pb){ - if(!pb){ - return; - } - _tcp_recved(_pcb, _closed_slot, pb->len); - pbuf_free(pb); -} - -/* - * Main Private Methods - * */ - -int8_t AsyncClient::_close(){ - //ets_printf("X: 0x%08x\n", (uint32_t)this); - int8_t err = ERR_OK; - if(_pcb) { - tcp_arg(_pcb, NULL); - tcp_sent(_pcb, NULL); - tcp_recv(_pcb, NULL); - tcp_err(_pcb, NULL); - tcp_poll(_pcb, NULL, 0); - _tcp_clear_events(this); - err = _tcp_close(_pcb, _closed_slot); - if(err != ERR_OK) { - err = abort(); - } - _free_closed_slot(); - _pcb = NULL; - if(_discard_cb) { - _discard_cb(_discard_cb_arg, this); - } - } - return err; -} - -bool AsyncClient::_allocate_closed_slot(){ - if (_closed_slot != -1) { - return true; - } - xSemaphoreTake(_slots_lock, portMAX_DELAY); - uint32_t closed_slot_min_index = 0; - for (int i = 0; i < _number_of_closed_slots; ++ i) { - if ((_closed_slot == -1 || _closed_slots[i] <= closed_slot_min_index) && _closed_slots[i] != 0) { - closed_slot_min_index = _closed_slots[i]; - _closed_slot = i; - } - } - if (_closed_slot != -1) { - _closed_slots[_closed_slot] = 0; - } - xSemaphoreGive(_slots_lock); - return (_closed_slot != -1); -} - -void AsyncClient::_free_closed_slot(){ - xSemaphoreTake(_slots_lock, portMAX_DELAY); - if (_closed_slot != -1) { - _closed_slots[_closed_slot] = _closed_index; - _closed_slot = -1; - ++ _closed_index; - } - xSemaphoreGive(_slots_lock); -} - -/* - * Private Callbacks - * */ - -int8_t AsyncClient::_connected(tcp_pcb* pcb, int8_t err){ - _pcb = reinterpret_cast(pcb); - if(_pcb){ - _rx_last_packet = millis(); - } - if(_connect_cb) { - _connect_cb(_connect_cb_arg, this); - } - return ERR_OK; -} - -void AsyncClient::_error(int8_t err) { - if(_pcb){ - tcp_arg(_pcb, NULL); - if(_pcb->state == LISTEN) { - tcp_sent(_pcb, NULL); - tcp_recv(_pcb, NULL); - tcp_err(_pcb, NULL); - tcp_poll(_pcb, NULL, 0); - } - _free_closed_slot(); - _pcb = NULL; - } - if(_error_cb) { - _error_cb(_error_cb_arg, this, err); - } - if(_discard_cb) { - _discard_cb(_discard_cb_arg, this); - } -} - -//In LwIP Thread -int8_t AsyncClient::_lwip_fin(tcp_pcb* pcb, int8_t err) { - if(!_pcb || pcb != _pcb){ - log_d("0x%08x != 0x%08x", (uint32_t)pcb, (uint32_t)_pcb); - return ERR_OK; - } - tcp_arg(_pcb, NULL); - if(_pcb->state == LISTEN) { - tcp_sent(_pcb, NULL); - tcp_recv(_pcb, NULL); - tcp_err(_pcb, NULL); - tcp_poll(_pcb, NULL, 0); - } - if(tcp_close(_pcb) != ERR_OK) { - tcp_abort(_pcb); - } - _free_closed_slot(); - _pcb = NULL; - return ERR_OK; -} - -//In Async Thread -int8_t AsyncClient::_fin(tcp_pcb* pcb, int8_t err) { - _tcp_clear_events(this); - if(_discard_cb) { - _discard_cb(_discard_cb_arg, this); - } - return ERR_OK; -} - -int8_t AsyncClient::_sent(tcp_pcb* pcb, uint16_t len) { - _rx_last_ack = _rx_last_packet = millis(); - if(_sent_cb) { - _sent_cb(_sent_cb_arg, this, len, (_rx_last_packet - _tx_last_packet)); - } - return ERR_OK; -} - -int8_t AsyncClient::_recv(tcp_pcb* pcb, pbuf* pb, int8_t err) { - if(!_pcb || pcb != _pcb){ - log_d("0x%08x != 0x%08x", (uint32_t)pcb, (uint32_t)_pcb); - return ERR_OK; - } - size_t total = 0; - while((pb != NULL) && (ERR_OK == err)) { - _rx_last_packet = millis(); - //we should not ack before we assimilate the data - _ack_pcb = true; - pbuf *b = pb; - pb = b->next; - b->next = NULL; - total += b->len; - if(_pb_cb){ - _pb_cb(_pb_cb_arg, this, b); - } else { - if(_recv_cb) { - _recv_cb(_recv_cb_arg, this, b->payload, b->len); - } - if(!_ack_pcb) { - _rx_ack_len += b->len; - } - } - pbuf_free(b); - } - return _tcp_recved(pcb, _closed_slot, total); -} - -int8_t AsyncClient::_poll(tcp_pcb* pcb){ - if(!_pcb){ - // log_d("pcb is NULL"); - return ERR_OK; - } - if(pcb != _pcb){ - log_d("0x%08x != 0x%08x", (uint32_t)pcb, (uint32_t)_pcb); - return ERR_OK; - } - - uint32_t now = millis(); - - // ACK Timeout - if(_ack_timeout){ - const uint32_t one_day = 86400000; - bool last_tx_is_after_last_ack = (_rx_last_ack - _tx_last_packet + one_day) < one_day; - if(last_tx_is_after_last_ack && (now - _tx_last_packet) >= _ack_timeout) { - log_d("ack timeout %d", pcb->state); - if(_timeout_cb) - _timeout_cb(_timeout_cb_arg, this, (now - _tx_last_packet)); - return ERR_OK; - } - } - // RX Timeout - if(_rx_timeout && (now - _rx_last_packet) >= (_rx_timeout * 1000)) { - log_d("rx timeout %d", pcb->state); - _close(); - return ERR_OK; - } - // Everything is fine - if(_poll_cb) { - _poll_cb(_poll_cb_arg, this); - } - return ERR_OK; -} - -void AsyncClient::_dns_found(struct ip_addr *ipaddr){ -#if ESP_IDF_VERSION_MAJOR < 5 - if(ipaddr && IP_IS_V4(ipaddr)){ - connect(IPAddress(ip_addr_get_ip4_u32(ipaddr)), _connect_port); -#if LWIP_IPV6 - } else if(ipaddr && ipaddr->u_addr.ip6.addr){ - connect(IPv6Address(ipaddr->u_addr.ip6.addr), _connect_port); -#endif -#else - if(ipaddr) { - IPAddress ip; - ip.from_ip_addr_t(ipaddr); - connect(ip, _connect_port); -#endif - } else { - if(_error_cb) { - _error_cb(_error_cb_arg, this, -55); - } - if(_discard_cb) { - _discard_cb(_discard_cb_arg, this); - } - } -} - -/* - * Public Helper Methods - * */ - -void AsyncClient::stop() { - close(false); -} - -bool AsyncClient::free(){ - if(!_pcb) { - return true; - } - if(_pcb->state == 0 || _pcb->state > 4) { - return true; - } - return false; -} - -size_t AsyncClient::write(const char* data) { - if(data == NULL) { - return 0; - } - return write(data, strlen(data)); -} - -size_t AsyncClient::write(const char* data, size_t size, uint8_t apiflags) { - size_t will_send = add(data, size, apiflags); - if(!will_send || !send()) { - return 0; - } - return will_send; -} - -void AsyncClient::setRxTimeout(uint32_t timeout){ - _rx_timeout = timeout; -} - -uint32_t AsyncClient::getRxTimeout(){ - return _rx_timeout; -} - -uint32_t AsyncClient::getAckTimeout(){ - return _ack_timeout; -} - -void AsyncClient::setAckTimeout(uint32_t timeout){ - _ack_timeout = timeout; -} - -void AsyncClient::setNoDelay(bool nodelay){ - if(!_pcb) { - return; - } - if(nodelay) { - tcp_nagle_disable(_pcb); - } else { - tcp_nagle_enable(_pcb); - } -} - -bool AsyncClient::getNoDelay(){ - if(!_pcb) { - return false; - } - return tcp_nagle_disabled(_pcb); -} - -void AsyncClient::setKeepAlive(uint32_t ms, uint8_t cnt){ - if(ms!=0) { - _pcb->so_options |= SOF_KEEPALIVE; //Turn on TCP Keepalive for the given pcb - // Set the time between keepalive messages in milli-seconds - _pcb->keep_idle = ms; - _pcb->keep_intvl = ms; - _pcb->keep_cnt = cnt; //The number of unanswered probes required to force closure of the socket - } else { - _pcb->so_options &= ~SOF_KEEPALIVE; //Turn off TCP Keepalive for the given pcb - } -} - -uint16_t AsyncClient::getMss(){ - if(!_pcb) { - return 0; - } - return tcp_mss(_pcb); -} - -uint32_t AsyncClient::getRemoteAddress() { - if(!_pcb) { - return 0; - } -#if LWIP_IPV4 && LWIP_IPV6 - return _pcb->remote_ip.u_addr.ip4.addr; -#else - return _pcb->remote_ip.addr; -#endif -} - -#if LWIP_IPV6 -ip6_addr_t AsyncClient::getRemoteAddress6() { - if(!_pcb) { - ip6_addr_t nulladdr; - ip6_addr_set_zero(&nulladdr); - return nulladdr; - } - return _pcb->remote_ip.u_addr.ip6; -} - -ip6_addr_t AsyncClient::getLocalAddress6() { - if(!_pcb) { - ip6_addr_t nulladdr; - ip6_addr_set_zero(&nulladdr); - return nulladdr; - } - return _pcb->local_ip.u_addr.ip6; -} -#if ESP_IDF_VERSION_MAJOR < 5 -IPv6Address AsyncClient::remoteIP6() { - return IPv6Address(getRemoteAddress6().addr); -} - -IPv6Address AsyncClient::localIP6() { - return IPv6Address(getLocalAddress6().addr); -} -#else -IPAddress AsyncClient::remoteIP6() { - if (!_pcb) { - return IPAddress(IPType::IPv6); - } - IPAddress ip; - ip.from_ip_addr_t(&(_pcb->remote_ip)); - return ip; -} - -IPAddress AsyncClient::localIP6() { - if (!_pcb) { - return IPAddress(IPType::IPv6); - } - IPAddress ip; - ip.from_ip_addr_t(&(_pcb->local_ip)); - return ip; -} -#endif -#endif - -uint16_t AsyncClient::getRemotePort() { - if(!_pcb) { - return 0; - } - return _pcb->remote_port; -} - -uint32_t AsyncClient::getLocalAddress() { - if(!_pcb) { - return 0; - } -#if LWIP_IPV4 && LWIP_IPV6 - return _pcb->local_ip.u_addr.ip4.addr; -#else - return _pcb->local_ip.addr; -#endif -} - -uint16_t AsyncClient::getLocalPort() { - if(!_pcb) { - return 0; - } - return _pcb->local_port; -} - -IPAddress AsyncClient::remoteIP() { -#if ESP_IDF_VERSION_MAJOR < 5 - return IPAddress(getRemoteAddress()); -#else - if (!_pcb) { - return IPAddress(); - } - IPAddress ip; - ip.from_ip_addr_t(&(_pcb->remote_ip)); - return ip; -#endif -} - -uint16_t AsyncClient::remotePort() { - return getRemotePort(); -} - -IPAddress AsyncClient::localIP() { -#if ESP_IDF_VERSION_MAJOR < 5 - return IPAddress(getLocalAddress()); -#else - if (!_pcb) { - return IPAddress(); - } - IPAddress ip; - ip.from_ip_addr_t(&(_pcb->local_ip)); - return ip; -#endif -} - - -uint16_t AsyncClient::localPort() { - return getLocalPort(); -} - -uint8_t AsyncClient::state() { - if(!_pcb) { - return 0; - } - return _pcb->state; -} - -bool AsyncClient::connected(){ - if (!_pcb) { - return false; - } - return _pcb->state == 4; -} - -bool AsyncClient::connecting(){ - if (!_pcb) { - return false; - } - return _pcb->state > 0 && _pcb->state < 4; -} - -bool AsyncClient::disconnecting(){ - if (!_pcb) { - return false; - } - return _pcb->state > 4 && _pcb->state < 10; -} - -bool AsyncClient::disconnected(){ - if (!_pcb) { - return true; - } - return _pcb->state == 0 || _pcb->state == 10; -} - -bool AsyncClient::freeable(){ - if (!_pcb) { - return true; - } - return _pcb->state == 0 || _pcb->state > 4; -} - -bool AsyncClient::canSend(){ - return space() > 0; -} - -const char * AsyncClient::errorToString(int8_t error){ - switch(error){ - case ERR_OK: return "OK"; - case ERR_MEM: return "Out of memory error"; - case ERR_BUF: return "Buffer error"; - case ERR_TIMEOUT: return "Timeout"; - case ERR_RTE: return "Routing problem"; - case ERR_INPROGRESS: return "Operation in progress"; - case ERR_VAL: return "Illegal value"; - case ERR_WOULDBLOCK: return "Operation would block"; - case ERR_USE: return "Address in use"; - case ERR_ALREADY: return "Already connected"; - case ERR_CONN: return "Not connected"; - case ERR_IF: return "Low-level netif error"; - case ERR_ABRT: return "Connection aborted"; - case ERR_RST: return "Connection reset"; - case ERR_CLSD: return "Connection closed"; - case ERR_ARG: return "Illegal argument"; - case -55: return "DNS failed"; - default: return "UNKNOWN"; - } -} - -const char * AsyncClient::stateToString(){ - switch(state()){ - case 0: return "Closed"; - case 1: return "Listen"; - case 2: return "SYN Sent"; - case 3: return "SYN Received"; - case 4: return "Established"; - case 5: return "FIN Wait 1"; - case 6: return "FIN Wait 2"; - case 7: return "Close Wait"; - case 8: return "Closing"; - case 9: return "Last ACK"; - case 10: return "Time Wait"; - default: return "UNKNOWN"; - } -} - -/* - * Static Callbacks (LwIP C2C++ interconnect) - * */ - -void AsyncClient::_s_dns_found(const char * name, struct ip_addr * ipaddr, void * arg){ - reinterpret_cast(arg)->_dns_found(ipaddr); -} - -int8_t AsyncClient::_s_poll(void * arg, struct tcp_pcb * pcb) { - return reinterpret_cast(arg)->_poll(pcb); -} - -int8_t AsyncClient::_s_recv(void * arg, struct tcp_pcb * pcb, struct pbuf *pb, int8_t err) { - return reinterpret_cast(arg)->_recv(pcb, pb, err); -} - -int8_t AsyncClient::_s_fin(void * arg, struct tcp_pcb * pcb, int8_t err) { - return reinterpret_cast(arg)->_fin(pcb, err); -} - -int8_t AsyncClient::_s_lwip_fin(void * arg, struct tcp_pcb * pcb, int8_t err) { - return reinterpret_cast(arg)->_lwip_fin(pcb, err); -} - -int8_t AsyncClient::_s_sent(void * arg, struct tcp_pcb * pcb, uint16_t len) { - return reinterpret_cast(arg)->_sent(pcb, len); -} - -void AsyncClient::_s_error(void * arg, int8_t err) { - reinterpret_cast(arg)->_error(err); -} - -int8_t AsyncClient::_s_connected(void * arg, struct tcp_pcb * pcb, int8_t err){ - return reinterpret_cast(arg)->_connected(pcb, err); -} - -/* - Async TCP Server - */ - -AsyncServer::AsyncServer(IPAddress addr, uint16_t port) -: _port(port) -#if ESP_IDF_VERSION_MAJOR < 5 -, _bind4(true) -, _bind6(false) -#else -, _bind4(addr.type() != IPType::IPv6) -, _bind6(addr.type() == IPType::IPv6) -#endif -, _addr(addr) -, _noDelay(false) -, _pcb(0) -, _connect_cb(0) -, _connect_cb_arg(0) -{} - -#if ESP_IDF_VERSION_MAJOR < 5 -AsyncServer::AsyncServer(IPv6Address addr, uint16_t port) -: _port(port) -, _bind4(false) -, _bind6(true) -, _addr6(addr) -, _noDelay(false) -, _pcb(0) -, _connect_cb(0) -, _connect_cb_arg(0) -{} -#endif - -AsyncServer::AsyncServer(uint16_t port) -: _port(port) -, _bind4(true) -, _bind6(false) -, _addr((uint32_t) IPADDR_ANY) -#if ESP_IDF_VERSION_MAJOR < 5 -, _addr6() -#endif -, _noDelay(false) -, _pcb(0) -, _connect_cb(0) -, _connect_cb_arg(0) -{} - -AsyncServer::~AsyncServer(){ - end(); -} - -void AsyncServer::onClient(AcConnectHandler cb, void* arg){ - _connect_cb = cb; - _connect_cb_arg = arg; -} - -void AsyncServer::begin(){ - if(_pcb) { - return; - } - - if(!_start_async_task()){ - log_e("failed to start task"); - return; - } - int8_t err; - _pcb = tcp_new_ip_type(_bind4 && _bind6 ? IPADDR_TYPE_ANY : (_bind6 ? IPADDR_TYPE_V6 : IPADDR_TYPE_V4)); - if (!_pcb){ - log_e("_pcb == NULL"); - return; - } - - ip_addr_t local_addr; -#if ESP_IDF_VERSION_MAJOR < 5 - if (_bind6) { // _bind6 && _bind4 both at the same time is not supported on Arduino 2 in this lib API - local_addr.type = IPADDR_TYPE_V6; - memcpy(local_addr.u_addr.ip6.addr, static_cast(_addr6), sizeof(uint32_t) * 4); - } else { - local_addr.type = IPADDR_TYPE_V4; - local_addr.u_addr.ip4.addr = _addr; - } -#else - _addr.to_ip_addr_t(&local_addr); -#endif - err = _tcp_bind(_pcb, &local_addr, _port); - - if (err != ERR_OK) { - _tcp_close(_pcb, -1); - log_e("bind error: %d", err); - return; - } - - static uint8_t backlog = 5; - _pcb = _tcp_listen_with_backlog(_pcb, backlog); - if (!_pcb) { - log_e("listen_pcb == NULL"); - return; - } - tcp_arg(_pcb, (void*) this); - tcp_accept(_pcb, &_s_accept); -} - -void AsyncServer::end(){ - if(_pcb){ - tcp_arg(_pcb, NULL); - tcp_accept(_pcb, NULL); - if(tcp_close(_pcb) != ERR_OK){ - _tcp_abort(_pcb, -1); - } - _pcb = NULL; - } -} - -//runs on LwIP thread -int8_t AsyncServer::_accept(tcp_pcb* pcb, int8_t err){ - //ets_printf("+A: 0x%08x\n", pcb); - if(_connect_cb){ - AsyncClient *c = new AsyncClient(pcb); - if(c){ - c->setNoDelay(_noDelay); - return _tcp_accept(this, c); - } - } - if(tcp_close(pcb) != ERR_OK){ - tcp_abort(pcb); - } - log_d("FAIL"); - return ERR_OK; -} - -int8_t AsyncServer::_accepted(AsyncClient* client){ - if(_connect_cb){ - _connect_cb(_connect_cb_arg, client); - } - return ERR_OK; -} - -void AsyncServer::setNoDelay(bool nodelay){ - _noDelay = nodelay; -} - -bool AsyncServer::getNoDelay(){ - return _noDelay; -} - -uint8_t AsyncServer::status(){ - if (!_pcb) { - return 0; - } - return _pcb->state; -} - -int8_t AsyncServer::_s_accept(void * arg, tcp_pcb * pcb, int8_t err){ - return reinterpret_cast(arg)->_accept(pcb, err); -} - -int8_t AsyncServer::_s_accepted(void *arg, AsyncClient* client){ - return reinterpret_cast(arg)->_accepted(client); -} diff --git a/lib/AsyncTCP/src/AsyncTCP.h b/lib/AsyncTCP/src/AsyncTCP.h deleted file mode 100644 index feac4d2..0000000 --- a/lib/AsyncTCP/src/AsyncTCP.h +++ /dev/null @@ -1,279 +0,0 @@ -/* - Asynchronous TCP library for Espressif MCUs - - Copyright (c) 2016 Hristo Gochkov. All rights reserved. - This file is part of the esp8266 core for Arduino environment. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#ifndef ASYNCTCP_H_ -#define ASYNCTCP_H_ - -#define ASYNCTCP_VERSION "3.2.4" -#define ASYNCTCP_VERSION_MAJOR 3 -#define ASYNCTCP_VERSION_MINOR 2 -#define ASYNCTCP_VERSION_REVISION 4 -#define ASYNCTCP_FORK_mathieucarbou - -#include "IPAddress.h" -#if ESP_IDF_VERSION_MAJOR < 5 -#include "IPv6Address.h" -#endif -#include -#include "lwip/ip_addr.h" -#include "lwip/ip6_addr.h" - -#ifndef LIBRETINY -#include "sdkconfig.h" -extern "C" { - #include "freertos/semphr.h" - #include "lwip/pbuf.h" -} -#else -extern "C" { - #include - #include -} -#define CONFIG_ASYNC_TCP_RUNNING_CORE -1 //any available core -#define CONFIG_ASYNC_TCP_USE_WDT 0 -#endif - -//If core is not defined, then we are running in Arduino or PIO -#ifndef CONFIG_ASYNC_TCP_RUNNING_CORE -#define CONFIG_ASYNC_TCP_RUNNING_CORE -1 //any available core -#define CONFIG_ASYNC_TCP_USE_WDT 1 //if enabled, adds between 33us and 200us per event -#endif - -#ifndef CONFIG_ASYNC_TCP_STACK_SIZE -#define CONFIG_ASYNC_TCP_STACK_SIZE 8192 * 2 -#endif - -#ifndef CONFIG_ASYNC_TCP_PRIORITY -#define CONFIG_ASYNC_TCP_PRIORITY 10 -#endif - -#ifndef CONFIG_ASYNC_TCP_QUEUE_SIZE -#define CONFIG_ASYNC_TCP_QUEUE_SIZE 64 -#endif - -#ifndef CONFIG_ASYNC_TCP_MAX_ACK_TIME -#define CONFIG_ASYNC_TCP_MAX_ACK_TIME 5000 -#endif - -class AsyncClient; - -#define ASYNC_WRITE_FLAG_COPY 0x01 //will allocate new buffer to hold the data while sending (else will hold reference to the data given) -#define ASYNC_WRITE_FLAG_MORE 0x02 //will not send PSH flag, meaning that there should be more data to be sent before the application should react. - -typedef std::function AcConnectHandler; -typedef std::function AcAckHandler; -typedef std::function AcErrorHandler; -typedef std::function AcDataHandler; -typedef std::function AcPacketHandler; -typedef std::function AcTimeoutHandler; - -struct tcp_pcb; -struct ip_addr; - -class AsyncClient { - public: - AsyncClient(tcp_pcb* pcb = 0); - ~AsyncClient(); - - AsyncClient & operator=(const AsyncClient &other); - AsyncClient & operator+=(const AsyncClient &other); - - bool operator==(const AsyncClient &other); - - bool operator!=(const AsyncClient &other) { - return !(*this == other); - } - bool connect(const IPAddress& ip, uint16_t port); -#if ESP_IDF_VERSION_MAJOR < 5 - bool connect(const IPv6Address& ip, uint16_t port); -#endif - bool connect(const char *host, uint16_t port); - void close(bool now = false); - void stop(); - int8_t abort(); - bool free(); - - bool canSend();//ack is not pending - size_t space();//space available in the TCP window - size_t add(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY);//add for sending - bool send();//send all data added with the method above - - //write equals add()+send() - size_t write(const char* data); - size_t write(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY); //only when canSend() == true - - uint8_t state(); - bool connecting(); - bool connected(); - bool disconnecting(); - bool disconnected(); - bool freeable();//disconnected or disconnecting - - uint16_t getMss(); - - uint32_t getRxTimeout(); - void setRxTimeout(uint32_t timeout);//no RX data timeout for the connection in seconds - - uint32_t getAckTimeout(); - void setAckTimeout(uint32_t timeout);//no ACK timeout for the last sent packet in milliseconds - - void setNoDelay(bool nodelay); - bool getNoDelay(); - - void setKeepAlive(uint32_t ms, uint8_t cnt); - - uint32_t getRemoteAddress(); - uint16_t getRemotePort(); - uint32_t getLocalAddress(); - uint16_t getLocalPort(); -#if LWIP_IPV6 - ip6_addr_t getRemoteAddress6(); - ip6_addr_t getLocalAddress6(); -#if ESP_IDF_VERSION_MAJOR < 5 - IPv6Address remoteIP6(); - IPv6Address localIP6(); -#else - IPAddress remoteIP6(); - IPAddress localIP6(); -#endif -#endif - - //compatibility - IPAddress remoteIP(); - uint16_t remotePort(); - IPAddress localIP(); - uint16_t localPort(); - - void onConnect(AcConnectHandler cb, void* arg = 0); //on successful connect - void onDisconnect(AcConnectHandler cb, void* arg = 0); //disconnected - void onAck(AcAckHandler cb, void* arg = 0); //ack received - void onError(AcErrorHandler cb, void* arg = 0); //unsuccessful connect or error - void onData(AcDataHandler cb, void* arg = 0); //data received (called if onPacket is not used) - void onPacket(AcPacketHandler cb, void* arg = 0); //data received - void onTimeout(AcTimeoutHandler cb, void* arg = 0); //ack timeout - void onPoll(AcConnectHandler cb, void* arg = 0); //every 125ms when connected - - void ackPacket(struct pbuf * pb);//ack pbuf from onPacket - size_t ack(size_t len); //ack data that you have not acked using the method below - void ackLater(){ _ack_pcb = false; } //will not ack the current packet. Call from onData - - const char * errorToString(int8_t error); - const char * stateToString(); - - //Do not use any of the functions below! - static int8_t _s_poll(void *arg, struct tcp_pcb *tpcb); - static int8_t _s_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *pb, int8_t err); - static int8_t _s_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); - static int8_t _s_lwip_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); - static void _s_error(void *arg, int8_t err); - static int8_t _s_sent(void *arg, struct tcp_pcb *tpcb, uint16_t len); - static int8_t _s_connected(void* arg, struct tcp_pcb *tpcb, int8_t err); - static void _s_dns_found(const char *name, struct ip_addr *ipaddr, void *arg); - - int8_t _recv(tcp_pcb* pcb, pbuf* pb, int8_t err); - tcp_pcb * pcb(){ return _pcb; } - - protected: - bool _connect(ip_addr_t addr, uint16_t port); - - tcp_pcb* _pcb; - int8_t _closed_slot; - - AcConnectHandler _connect_cb; - void* _connect_cb_arg; - AcConnectHandler _discard_cb; - void* _discard_cb_arg; - AcAckHandler _sent_cb; - void* _sent_cb_arg; - AcErrorHandler _error_cb; - void* _error_cb_arg; - AcDataHandler _recv_cb; - void* _recv_cb_arg; - AcPacketHandler _pb_cb; - void* _pb_cb_arg; - AcTimeoutHandler _timeout_cb; - void* _timeout_cb_arg; - AcConnectHandler _poll_cb; - void* _poll_cb_arg; - - bool _ack_pcb; - uint32_t _tx_last_packet; - uint32_t _rx_ack_len; - uint32_t _rx_last_packet; - uint32_t _rx_timeout; - uint32_t _rx_last_ack; - uint32_t _ack_timeout; - uint16_t _connect_port; - - int8_t _close(); - void _free_closed_slot(); - bool _allocate_closed_slot(); - int8_t _connected(tcp_pcb* pcb, int8_t err); - void _error(int8_t err); - int8_t _poll(tcp_pcb* pcb); - int8_t _sent(tcp_pcb* pcb, uint16_t len); - int8_t _fin(tcp_pcb* pcb, int8_t err); - int8_t _lwip_fin(tcp_pcb* pcb, int8_t err); - void _dns_found(struct ip_addr *ipaddr); - - public: - AsyncClient* prev; - AsyncClient* next; -}; - -class AsyncServer { - public: - AsyncServer(IPAddress addr, uint16_t port); -#if ESP_IDF_VERSION_MAJOR < 5 - AsyncServer(IPv6Address addr, uint16_t port); -#endif - AsyncServer(uint16_t port); - ~AsyncServer(); - void onClient(AcConnectHandler cb, void* arg); - void begin(); - void end(); - void setNoDelay(bool nodelay); - bool getNoDelay(); - uint8_t status(); - - //Do not use any of the functions below! - static int8_t _s_accept(void *arg, tcp_pcb* newpcb, int8_t err); - static int8_t _s_accepted(void *arg, AsyncClient* client); - - protected: - uint16_t _port; - bool _bind4 = false; - bool _bind6 = false; - IPAddress _addr; -#if ESP_IDF_VERSION_MAJOR < 5 - IPv6Address _addr6; -#endif - bool _noDelay; - tcp_pcb* _pcb; - AcConnectHandler _connect_cb; - void* _connect_cb_arg; - - int8_t _accept(tcp_pcb* newpcb, int8_t err); - int8_t _accepted(AsyncClient* client); -}; - - -#endif /* ASYNCTCP_H_ */ diff --git a/lib/MqttLogger/src/MqttLogger.cpp b/lib/MqttLogger/src/MqttLogger.cpp index d8d08f7..ea59c0c 100644 --- a/lib/MqttLogger/src/MqttLogger.cpp +++ b/lib/MqttLogger/src/MqttLogger.cpp @@ -7,7 +7,7 @@ MqttLogger::MqttLogger(MqttLoggerMode mode) this->setBufferSize(MQTT_MAX_PACKET_SIZE); } -MqttLogger::MqttLogger(MqttClient& client, const char* topic, MqttLoggerMode mode) +MqttLogger::MqttLogger(esp_mqtt_client_handle_t client, const char* topic, MqttLoggerMode mode) { this->setClient(client); this->setTopic(topic); @@ -19,9 +19,9 @@ MqttLogger::~MqttLogger() { } -void MqttLogger::setClient(MqttClient& client) +void MqttLogger::setClient(esp_mqtt_client_handle_t client) { - this->client = &client; + this->client = client; } void MqttLogger::setTopic(const char* topic) @@ -69,21 +69,22 @@ boolean MqttLogger::setBufferSize(uint16_t size) } // send & reset current buffer -void MqttLogger::sendBuffer() +void MqttLogger::sendBuffer() { if (this->bufferCnt > 0) { bool doSerial = this->mode==MqttLoggerMode::SerialOnly || this->mode==MqttLoggerMode::MqttAndSerial || this->mode==MqttLoggerMode::MqttAndSerialAndWeb || this->mode==MqttLoggerMode::SerialAndWeb; bool doWebSerial = this->mode==MqttLoggerMode::MqttAndSerialAndWeb || this->mode==MqttLoggerMode::SerialAndWeb; - - if (this->mode!=MqttLoggerMode::SerialOnly && this->mode!=MqttLoggerMode::SerialAndWeb && this->client != NULL && this->client->connected()) + + if (this->mode!=MqttLoggerMode::SerialOnly && this->mode!=MqttLoggerMode::SerialAndWeb && this->client != NULL) { - this->client->publish(topic, 0, true, this->buffer, this->bufferCnt); - } else if (this->mode == MqttLoggerMode::MqttAndSerialFallback) + esp_mqtt_client_publish(this->client, topic, (const char*)this->buffer, this->bufferCnt, 0, 1); + } + else if (this->mode == MqttLoggerMode::MqttAndSerialFallback) { doSerial = true; } - if (doSerial) + if (doSerial) { Serial.write(this->buffer, this->bufferCnt); Serial.println(); diff --git a/lib/MqttLogger/src/MqttLogger.h b/lib/MqttLogger/src/MqttLogger.h index d82e62a..cb35fcc 100644 --- a/lib/MqttLogger/src/MqttLogger.h +++ b/lib/MqttLogger/src/MqttLogger.h @@ -11,7 +11,7 @@ #include #include -#include +#include //#include "MycilaWebSerial.h" #define MQTT_MAX_PACKET_SIZE 1024 @@ -32,16 +32,16 @@ private: uint8_t* buffer; uint8_t* bufferEnd; uint16_t bufferCnt = 0, bufferSize = 0; - MqttClient* client; + esp_mqtt_client_handle_t client; MqttLoggerMode mode; void sendBuffer(); public: MqttLogger(MqttLoggerMode mode=MqttLoggerMode::MqttAndSerialFallback); - MqttLogger(MqttClient& client, const char* topic, MqttLoggerMode mode=MqttLoggerMode::MqttAndSerialFallback); + MqttLogger(esp_mqtt_client_handle_t client, const char* topic, MqttLoggerMode mode=MqttLoggerMode::MqttAndSerialFallback); ~MqttLogger(); - void setClient(MqttClient& client); + void setClient(esp_mqtt_client_handle_t client); void setTopic(const char* topic); void setMode(MqttLoggerMode mode); void setRetained(boolean retained); diff --git a/lib/espMqttClient/CMakeLists.txt b/lib/espMqttClient/CMakeLists.txt deleted file mode 100644 index 3702227..0000000 --- a/lib/espMqttClient/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -set(COMPONENT_SRCDIRS - "src" "src/Packets" "src/Transport" -) - -set(COMPONENT_ADD_INCLUDEDIRS - "src" "src/Packets" "src/Transport" -) - -set(COMPONENT_REQUIRES - "arduino-esp32" - "AsyncTCP" -) - -register_component() - -target_compile_definitions(${COMPONENT_TARGET} PUBLIC -DESP32) -target_compile_options(${COMPONENT_TARGET} PRIVATE -fno-rtti) diff --git a/lib/espMqttClient/LICENSE b/lib/espMqttClient/LICENSE deleted file mode 100644 index 1cc5546..0000000 --- a/lib/espMqttClient/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Bert Melis - -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/lib/espMqttClient/README.md b/lib/espMqttClient/README.md deleted file mode 100644 index 7b0bfdd..0000000 --- a/lib/espMqttClient/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# espMqttClient - -MQTT client library for the Espressif devices ESP8266 and ESP32 on the Arduino framework. -Aims to be a non-blocking, fully compliant MQTT 3.1.1 client. - -![platformio](https://github.com/bertmelis/espMqttClient/actions/workflows/build_platformio.yml/badge.svg) -![cpplint](https://github.com/bertmelis/espMqttClient/actions/workflows/cpplint.yml/badge.svg) -![cppcheck](https://github.com/bertmelis/espMqttClient/actions/workflows/cppcheck.yml/badge.svg) -[![PlatformIO Registry](https://badges.registry.platformio.org/packages/bertmelis/library/espMqttClient.svg)](https://registry.platformio.org/libraries/bertmelis/espMqttClient) - -# Features - -- MQTT 3.1.1 compliant library -- Sending and receiving at all QoS levels -- TCP and TCP/TLS using standard WiFiClient and WiFiClientSecure connections -- Virtually unlimited incoming and outgoing payload sizes -- Readable and understandable code -- Fully async clients available via [AsyncTCP](https://github.com/me-no-dev/AsyncTCP) or [ESPAsnycTCP](https://github.com/me-no-dev/ESPAsyncTCP) (no TLS supported). -- Supported platforms: - - Espressif ESP8266 and ESP32 using the Arduino framework - - Espressif ESP32 using the ESP IDF, see [esp idf component](https://docs.espressif.com/projects/arduino-esp32/en/latest/esp-idf_component.html) -- Basic Linux compatibility*. This includes WSL on Windows - - > Linux compatibility is mainly for automatic testing. It relies on a quick and dirty Arduino-style `Client` with a POSIX TCP client underneath and Arduino-style `ClientPosixIPAddress` class. These are lacking many features needed for proper Linux support. - -## Dependencies - -This libraries requires [AsyncTCP](https://github.com/me-no-dev/AsyncTCP) and [ESPAsnycTCP](https://github.com/me-no-dev/ESPAsyncTCP). These libraries are not actively maintained and have some bugs. There are alternatives available on Github but make sure these alternatives fit in your project. - -Because of this, I have removed the explicit dependency. You will have to manually add the libraries so you can choose the version which best suites your code. - -# Documentation - -See [documentation](https://www.emelis.net/espMqttClient/) and the [examples](examples/). - -## Limitations - -### MQTT 3.1.1 Compliancy - -Outgoing messages and session data are not stored in non-volatile memory. Any events like loss of power or sudden resets result in loss of data. Despite this limitation, one could still consider this library as fully complaint based on the non normative remark in point 4.1.1 of the specification. - -### Non-blocking - -This library aims to be fully non-blocking. It is however limited by the underlying `WiFiClient` library which is part of the Arduino framework and has a blocking `connect` method. This is not an issue on ESP32 because the call is offloaded to a separate task. On ESP8266 however, connecting will block until succesful or until the connection timeouts. - -If you need a fully asynchronous MQTT client, you can use `espMqttClientAsync` which uses AsyncTCP/ESPAsyncTCP under the hood. These underlying libraries do not support TLS (anymore). I will not provide support TLS for the async client. - -# Bugs and feature requests - -Please use Github's facilities to get in touch. - -# About this library - -This client wouldn't exist without [Async-mqtt-client](https://github.com/marvinroger/async-mqtt-client). It has been my go-to MQTT client for many years. It was fast, reliable and had features that were non-existing in alternative libraries. However, the underlying async TCP libraries are lacking updates, especially updates related to secure connections. Adapting this library to use up-to-date TCP clients would not be trivial. I eventually decided to write my own MQTT library, from scratch. - -The result is an almost non-blocking library with no external dependencies. The library is almost a drop-in replacement for the async-mqtt-client except a few parameter type changes (eg. `uint8_t*` instead of `char*` for payloads). - -# License - -This library is released under the MIT Licence. A copy is included in the repo. -Parts of this library, most notably the API, are based on [Async MQTT client for ESP8266 and ESP32](https://github.com/marvinroger/async-mqtt-client). diff --git a/lib/espMqttClient/component.mk b/lib/espMqttClient/component.mk deleted file mode 100644 index bb5bb16..0000000 --- a/lib/espMqttClient/component.mk +++ /dev/null @@ -1,3 +0,0 @@ -COMPONENT_ADD_INCLUDEDIRS := src -COMPONENT_SRCDIRS := src -CXXFLAGS += -fno-rtti diff --git a/lib/espMqttClient/docs/_config.yml b/lib/espMqttClient/docs/_config.yml deleted file mode 100644 index 6975b20..0000000 --- a/lib/espMqttClient/docs/_config.yml +++ /dev/null @@ -1,6 +0,0 @@ -theme: jekyll-theme-cayman -title: espMqttClient -description: | - MQTT client library for the Espressif devices ESP8266 and ESP32 on the Arduino framework. - Aims to be a non-blocking fully compliant MQTT 3.1.1 client. -show_downloads: false diff --git a/lib/espMqttClient/docs/index.md b/lib/espMqttClient/docs/index.md deleted file mode 100644 index 4385ee9..0000000 --- a/lib/espMqttClient/docs/index.md +++ /dev/null @@ -1,587 +0,0 @@ -![platformio](https://github.com/bertmelis/espMqttClient/actions/workflows/build_platformio.yml/badge.svg) -![cpplint](https://github.com/bertmelis/espMqttClient/actions/workflows/cpplint.yml/badge.svg) -![cppcheck](https://github.com/bertmelis/espMqttClient/actions/workflows/cppcheck.yml/badge.svg) -[![PlatformIO Registry](https://badges.registry.platformio.org/packages/bertmelis/library/espMqttClient.svg)](https://registry.platformio.org/libraries/bertmelis/espMqttClient) - -# Features - -- MQTT 3.1.1 compliant library -- Sending and receiving at all QoS levels -- TCP and TCP/TLS using standard WiFiClient and WiFiClientSecure connections -- Virtually unlimited incoming and outgoing payload sizes -- Readable and understandable code -- Fully async clients available via [AsyncTCP](https://github.com/me-no-dev/AsyncTCP) or [ESPAsnycTCP](https://github.com/me-no-dev/ESPAsyncTCP) (no TLS supported). -- Supported platforms: - - Espressif ESP8266 and ESP32 using the Arduino framework - - Espressif ESP32 using the ESP IDF, see [esp idf component](https://docs.espressif.com/projects/arduino-esp32/en/latest/esp-idf_component.html) -- Basic Linux compatibility*. This includes WSL on Windows - - > Linux compatibility is mainly for automatic testing. It relies on a quick and dirty Arduino-style `Client` with a POSIX TCP client underneath and Arduino-style `IPAddress` class. These are lacking many features needed for proper Linux support. - -## Dependencies - -This libraries requires [AsyncTCP](https://github.com/me-no-dev/AsyncTCP) and [ESPAsnycTCP](https://github.com/me-no-dev/ESPAsyncTCP). These libraries are not actively maintained and have some bugs. There are alternatives available on Github but make sure these alternatives fit in your project. - -Because of this, I have removed the explicit dependency. You will have to manually add the libraries so you can choose the version which best suites your code. - -# Contents - -1. [Runtime behaviour](#runtime-behaviour) -2. [API Reference](#api-reference) -3. [Compile-time configuration](#compile-time-configuration) -4. [Code samples](#code-samples) - -# Runtime behaviour - -A normal operation cycle of an MQTT client goes like this: - -1. setup the client -2. connect to the broker -3. subscribe/publish/receive -4. disconnect/reconnect when disconnected -5. Cleanly disconnect - -### Setup - -Setting up the client means to tell which host and port to connect to, possible credentials to use and so on. espMqttClient has a set of methods to configure the client. Setup is generally done in the `setup()` function of the Arduino framework. -One important thing to remember is that there are a number of settings that are not stored inside the library: `username`, `password`, `willTopic`, `willPayload`, `clientId` and `host`. Make sure these variables stay available during the lifetime of the `espMqttClient`. - -For TLS secured connections, the relevant methods from `WiFiClientSecure` have been made available to setup the TLS mechanisms. - -### Connecting - -After setting up the client, you are ready to connect. A simple call to `connect()` does the job. If you set an `OnConnectCallback`, you will be notified when the connection has been made. On failure, `OnDisconnectCallback` will be called. Although good code structure can avoid this, you can call `connect()` multiple times. - -### Subscribing, publishing and receiving - -Once connected, you can subscribe, publish and receive. The methods to do this return the packetId of the generated packet or `1` for packets without packetId. In case of an error, the method returns `0`. When the client is not connected, you cannot subscribe, unsubscribe or publish (configurable, see [EMC_ALLOW_NOT_CONNECTED_PUBLISH](#EMC_ALLOW_NOT_CONNECTED_PUBLISH)). - -Receiving packets is done via the `onMessage`-callback. This callback gives you the topic, properties (qos, dup, retain, packetId) and payload. For the payload, you get a pointer to the data, the index, length and total length. On long payloads it is normal that you get multiple callbacks for the same packet. This way, you can receive payloads longer than what could fit in the microcontroller's memory. - - > Beware that MQTT payloads are binary. MQTT payloads are **not** c-strings unless explicitely constructed like that. You therefore can **not** print the payload to your Serial monitor without supporting code. - -### Disconnecting - -You can disconnect from the broker by calling `disconnect()`. If you do not force-disconnect, the client will first send the remaining messages that are in the queue and disconnect afterwards. During this period however, no new incoming PUBLISH messages will be processed. - -# API Reference - -```cpp -espMqttClient() -espMqttClientSecure() -espMqttClientAsync() -``` - -Instantiate a new espMqttClient or espMqttSecure object. -On ESP32, three optional parameters are available: `espMqttClient(bool internalTask = true, uint8_t priority = 1, uint8_t core = 1)`. By default, espMqttclient creates its own task to manage TCP. By setting `internalTask` to false, no task will be created and you will be responsible yourself to call `espMqttClient.loop()`. `priority` changes the priority of the MQTT client task and the core on which it runs (higher priority = more cpu-time). - -For the asynchronous version, use `espMqttClientAsync`. - -### Configuration - -```cpp -espMqttClient& setKeepAlive(uint16_t keepAlive) -``` - -Set the keep alive. Defaults to 15 seconds. - -* **`keepAlive`**: Keep alive in seconds - -```cpp -espMqttClient& setClientId(const char* clientId) -``` - -Set the client ID. Defaults to `esp8266123456` or `esp32123456` where `123456` is the chip ID. -The library only stores a pointer to the client ID. Make sure the variable pointed to stays available throughout the lifetime of espMqttClient. - -- **`clientId`**: Client ID, expects a null-terminated char array (c-string) - -```cpp -espMqttClient& setCleanSession(bool cleanSession) -``` - -Set the CleanSession flag. Defaults to `true`. - -- **`cleanSession`**: clean session wanted or not - -```cpp -espMqttClient& setCredentials(const char* username, const char* password) -``` - -Set the username/password. Defaults to non-auth. -The library only stores a pointer to the username and password. Make sure the variable to pointed stays available throughout the lifetime of espMqttClient. - -- **`username`**: Username, expects a null-terminated char array (c-string) -- **`password`**: Password, expects a null-terminated char array (c-string) - -```cpp -espMqttClient& setWill(const char* topic, uint8_t qos, bool retain, const uint8_t* payload, size_t length) -``` - -Set the Last Will. Defaults to none. -The library only stores a pointer to the topic and payload. Make sure the variable pointed to stays available throughout the lifetime of espMqttClient. - -- **`topic`**: Topic of the LWT, expects a null-terminated char array (c-string) -- **`qos`**: QoS of the LWT -- **`retain`**: Retain flag of the LWT -- **`payload`**: Payload of the LWT. -- **`length`**: Payload length - -```cpp -espMqttClient& setWill(const char* topic, uint8_t qos, bool retain, const char* payload) -``` - -Set the Last Will. Defaults to none. -The library only stores a pointer to the topic and payload. Make sure the variable pointed to stays available throughout the lifetime of espMqttClient. - -- **`topic`**: Topic of the LWT, expects a null-terminated char array (c-string) -- **`qos`**: QoS of the LWT -- **`retain`**: Retain flag of the LWT -- **`payload`**: Payload of the LWT, expects a null-terminated char array (c-string). Its lenght will be calculated using `strlen(payload)` - -```cpp -espMqttClient& setServer(IPAddress ip, uint16_t port) -``` - -Set the server. Mind that when using `espMqttClientSecure` with a certificate, the hostname will be chacked against the certificate. Often IP-addresses are not valid and the connection will fail. - -- **`ip`**: IP of the server -- **`port`**: Port of the server - -```cpp -espMqttClient& setServer(const char* host, uint16_t port) -``` - -Set the server. - -- **`host`**: Host of the server, expects a null-terminated char array (c-string) -- **`port`**: Port of the server - -```cpp -espMqttClient& setTimeout(uint16_t timeout) -``` - -Set the timeout for packets that need acknowledgement. Defaults to 10 seconds. -When no acknowledgement has been received from the broker after sending a packet, the client will retransmit **all** the packets in the queue. - -* **`timeout`**: Timeout in seconds - -#### Options for TLS connections - -All common options from WiFiClientSecure to setup an encrypted connection are made available. These include: - -- `espMqttClientSecure& setInsecure()` -- `espMqttClientSecure& setCACert(const char* rootCA)` (ESP32 only) -- `espMqttClientSecure& setCertificate(const char* clientCa)` (ESP32 only) -- `espMqttClientSecure& setPrivateKey(const char* privateKey)` (ESP32 only) -- `espMqttClientSecure& setPreSharedKey(const char* pskIdent, const char* psKey)` (ESP32 only) -- `espMqttClientSecure& setFingerprint(const uint8_t fingerprint[20])` (ESP8266 only) -- `espMqttClientSecure& setTrustAnchors(const X509List *ta)` (ESP8266 only) -- `espMqttClientSecure& setClientRSACert(const X509List *cert, const PrivateKey *sk)` (ESP8266 only) -- `espMqttClientSecure& setClientECCert(const X509List *cert, const PrivateKey *sk, unsigned allowed_usages, unsigned cert_issuer_key_type)` (ESP8266 only) -- `espMqttClientSecure& setCertStore(CertStoreBase *certStore)` (ESP8266 only) - -For documenation, please visit [ESP8266's documentation](https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/readme.html#bearssl-client-secure-and-server-secure) or [ESP32's documentation](https://github.com/espressif/arduino-esp32/tree/master/libraries/WiFiClientSecure). - -### Events handlers - -```cpp -espMqttClient& onConnect(espMqttClientTypes::OnConnectCallback callback) -``` - -Add a connect event handler. Function signature: `void(bool sessionPresent)` - -- **`callback`**: Function to call - -```cpp -espMqttClient& onDisconnect(espMqttClientTypes::OnDisconnectCallback callback) -``` - -Add a disconnect event handler. Function signature: `void(espMqttClientTypes::DisconnectReason reason)` - -- **`callback`**: Function to call - -```cpp -espMqttClient& onSubscribe(espMqttClientTypes::OnSubscribeCallback callback) -``` - -Add a subscribe acknowledged event handler. Function signature: `void(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* returncodes, size_t len)` - -- **`callback`**: Function to call - -```cpp -espMqttClient& onUnsubscribe(espMqttClientTypes::OnUnsubscribeCallback callback) -``` - -Add an unsubscribe acknowledged event handler. Function signature: `void(uint16_t packetId)` - -- **`callback`**: Function to call - -```cpp -espMqttClient& onMessage(espMqttClientTypes::OnMessageCallback callback) -``` - -Add a publish received event handler. Function signature: `void(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)` - -- **`callback`**: Function to call - -```cpp -espMqttClient& onPublish(espMqttClientTypes::OnPublishCallback callback) -``` - -Add a publish acknowledged event handler. Function signature: `void(uint16_t packetId)` - -- **`callback`**: Function to call - -### Operational functions - -```cpp -bool connected() -``` - -Returns `true` if the client is currently fully connected to the broker. During connecting or disconnecting, it will return `false`. - -```cpp -bool disconnected() -``` - -Returns `true` if the client is currently disconnected from the broker. During disconnecting or connecting, it will return `false`. - -```cpp -bool connect() -``` - -Start the connect procedure. Returns `true` if successful. A positive return value doesn not mean the client is already connected. - -```cpp -bool disconnect(bool force = false) -``` - -Start the disconnect procedure, return `true` if successful. A positive return value doesn not mean the client is already disconnected. -When disconnecting with `force` false, the client first tries to handle all the outgoing messages in the queue and disconnect cleanly afterwards. During this time, no incoming PUBLISH messages are handled. - -- **`force`**: Whether to force the disconnection. Defaults to `false` (clean disconnection). - -```cpp -uint16_t subscribe(const char* topic, uint8_t qos) -``` - -Subscribe to the given topic at the given QoS. Return the packet ID or 0 if failed. - -- **`topic`**: Topic, expects a null-terminated char array (c-string) -- **`qos`**: QoS - -It is also possible to subscribe to multiple topics at once. Just add the topic/qos pairs to the parameters: - -```cpp -uint16_t packetId = yourclient.subscribe(topic1, qos1, topic2, qos2, topic3, qos3); // add as many topics as you like* -``` - -```cpp -uint16_t unsubscribe(const char* topic) -``` - -Unsubscribe from the given topic. Return the packet ID or 0 if failed. - -- **`topic`**: Topic, expects a null-terminated char array (c-string) - -It is also possible to unsubscribe to multiple topics at once. Just add the topics to the parameters: - -```cpp -uint16_t packetId = yourclient.unsubscribe(topic1, topic2, topic3); // add as many topics as you like* -``` - -```cpp -uint16_t publish(const char* topic, uint8_t qos, bool retain, const uint8* payload, size_t length) -``` - -Publish a packet. Return the packet ID (or 1 if QoS 0) or 0 if failed. The topic and payload will be buffered by the library. - -- **`topic`**: Topic, expects a null-terminated char array (c-string) -- **`qos`**: QoS -- **`retain`**: Retain flag -- **`payload`**: Payload -- **`length`**: Payload length - -```cpp -uint16_t publish(const char* topic, uint8_t qos, bool retain, const char* payload) -``` - -Publish a packet. Return the packet ID (or 1 if QoS 0) or 0 if failed. The topic and payload will be buffered by the library. - -- **`topic`**: Topic, expects a null-terminated char array (c-string) -- **`qos`**: QoS -- **`retain`**: Retain flag -- **`payload`**: Payload, expects a null-terminated char array (c-string). Its lenght will be calculated using `strlen(payload)` - -```cpp -uint16_t publish(const char* topic, uint8_t qos, bool retain, espMqttClientTypes::PayloadCallback callback, size_t length) -``` - -Publish a packet with a callback for payload handling. Return the packet ID (or 1 if QoS 0) or 0 if failed. The topic will be buffered by the library. - -- **`topic`**: Topic, expects a null-terminated char array (c-string) -- **`qos`**: QoS -- **`retain`**: Retain flag -- **`callback`**: callback to fetch the payload. - -The callback has the following signature: `size_t callback(uint8_t* data, size_t maxSize, size_t index)`. When the library needs payload data, the callback will be invoked. It is the callback's job to write data indo `data` with a maximum of `maxSize` bytes, according the `index` and return the amount of bytes written. - -```cpp -void clearQueue(bool deleteSessionData = false) -``` - -Clears all queued messages. -Keep in mind that this may also delete any session data and therefore is not MQTT compliant. - -- **`deleteSessionData`**: When true, delete all outgoing messages. Not MQTT compliant! - -```cpp -void loop() -``` - -This is the worker function of the MQTT client. For ESP8266 you must call this function in the Arduino loop. For ESP32 you have to call this function yourself **only if you have disabled the internal task** (see the constructors). - -```cpp -const char* getClientId() const -``` - -Retuns the client ID. - -```cpp -size_t queueSize(); -``` - -Returns the amount of elements, regardless of type, in the queue. - -# Compile time configuration - -A number of constants which influence the behaviour of the client can be set at compile time. You can set these options in the `Config.h` file or pass the values as compiler flags. Because these options are compile-time constants, they are used for all instances of `espMqttClient` you create in your program. - -### EMC_TX_TIMEOUT 10000 - -Timeout in milliseconds before a (qos > 0) message will be retransmitted. - -### EMC_RX_BUFFER_SIZE 1440 - -The client copies incoming data into a buffer before parsing. This sets the buffer size. - -### EMC_TX_BUFFER_SIZE 1440 - -When publishing using the callback, the client fetches data in chunks of EMC_TX_BUFFER_SIZE size. This is not necessarily the same as the actual outging TCP packets. - -### EMC_MAX_TOPIC_LENGTH 128 - -For **incoming** messages, a maximum topic length is set. Topics longer than this will be truncated. - -### EMC_PAYLOAD_BUFFER_SIZE 32 - -Set the incoming payload buffer size for SUBACK messages. When subscribing to multiple topics at once, the acknowledgement contains all the return codes in its payload. The detault of 32 means you can theoretically subscribe to 32 topics at once. - -### EMC_MIN_FREE_MEMORY 4096 - -The client keeps all outgoing packets in a queue which stores its data in heap memory. With this option, you can set the minimum available (contiguous) heap memory that needs to be available for adding a message to the queue. - -### EMC_ESP8266_MULTITHREADING 0 - -Set this to 1 if you use the async version on ESP8266. For the regular client this setting can be kept disabled because the ESP8266 doesn't use multithreading and is only single-core. - -### EMC_ALLOW_NOT_CONNECTED_PUBLISH 1 - -By default, you can publish when the client is not connected. If you don't want this, set this to 0. -Regardless of this setting, after you called `disconnect()`, no messages can be published until fully disconnected. - -### EMC_WAIT_FOR_CONNACK 1 - -espMqttClient waits for the CONNACK (connection acknowledge) packet before starting to send other packets. -The MQTT specification allows to start sending before the broker acknowledges the connection but some brokers -don't allow this (AWS for example doesn't). - -### EMC_CLIENTID_LENGTH 18 + 1 - -The (maximum) length of the client ID. (Keep in mind that this is a c-string. You need to have 1 position available for the null-termination.) - -### EMC_TASK_STACK_SIZE 5120 - -Only used on ESP32. Sets the stack size (in words) of the MQTT client worker task. - -### EMC_MULTIPLE_CALLBACKS - -This macro is by default not enabled so you can add a single callbacks to an event. Assigning a second will overwrite the existing callback. When enabling multiple callbacks, multiple callbacks (with uint32_t id) can be assigned. Removing is done by referencing the id. - -### EMC_USE_WATCHDOG 0 - -(ESP32 only) - -**Experimental** - -You can enable a watchdog on the MQTT task. This is experimental and will probably result in resets because some (framework) function calls block without feeding the dog. - -### EMC_USE_MEMPOOL 0 - -**Experimental** - -When set to `1`, (outgoing) MQTT packets and the outbox data is stored in a memory pool. The memory pool is part of the espMqttClient object and is thus allocated in the same memory type. There are two pools: one to hold the outgoing packets (dynamic size elements) and one for the outbox itself (fixed-size elements). - -#### EMC_NUM_POOL_ELEMENTS 32 - -This config variable is only used when enabling the memory pool. It defines -- the number of elements in the outbox-pool -- the number of blocks that will be allocated in the packet-pool - -#### EMC_SIZE_POOL_ELEMENTS 128 - -This defines the size of one packet-pool element. Together with `EMC_NUM_POOL_ELEMENTS`, you get the total packet-pool size. -The packet-pool can hold any size of element. The configuration only guarantees a minimum of `EMC_NUM_POOL_ELEMENTS` of size `EMC_SIZE_POOL_ELEMENTS` can fit in the pool. - -### Logging - -If needed, you have to enable logging at compile time. This is done differently on ESP32 and ESP8266. - -ESP8266: - -- Enable logging for Arduino [see docs](https://arduino-esp8266.readthedocs.io/en/latest/Troubleshooting/debugging.html) -- Pass the `DEBUG_ESP_MQTT_CLIENT` flag to the compiler - -ESP32 - -- Enable logging for Arduino [see docs](https://docs.espressif.com/projects/arduino-esp32/en/latest/guides/tools_menu.html?#core-debug-level) - -# Code samples - -A number of examples are in the [examples](/examples) directory. These include basic operation on ESP8266 and ESP32. Please examine these to understand the basic operation of the MQTT client. - -Below are examples on specific points for working with this library. - -### Printing payloads - -MQTT 3.1.1 defines no special format for the payload so it is treated as binary. If you want to print a payload to the Arduino serial console, you have to make sure that the payload is null-terminated (c-string). - -```cpp -// option one: print the payload char by char -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - Serial.println("Publish received:"); - Serial.printf(" topic: %s\n payload:", topic); - const char* p = reinterpret_cast(payload); - for (size_t i = 0; i < len; ++i) { - Serial.print(p[i]); - } - Serial.print("\n"); -} -``` - -```cpp -// option two: copy the payload into a c-string -// you cannot just do payload[len] = 0 because you do not own this memory location! -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - Serial.println("Publish received:"); - Serial.printf(" topic: %s\n payload:", topic); - char* strval = new char[len + 1]; - memcpy(strval, payload, len); - strval[len] = "\0"; - Serial.println(strval); - delete[] strval; -} -``` - -### Assembling chunked messages - -The `onMessage`-callback is called as data comes in. So if the data comes in partially, the callback will be called on every receipt of a chunk, with the proper `index`, (chunk)`size` and `total` set. With little code, you can reassemble chunked messages yourself. - -```cpp -const size_t maxPayloadSize = 8192; -uint8_t* payloadbuffer = nullptr; -size_t payloadbufferSize = 0; -size_t payloadbufferIndex = 0; - -void onOversizedMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - // handle oversized messages -} - -void onCompleteMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - // handle assembled messages -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - // payload is bigger then max: return chunked - if (total > maxPayloadSize) { - onOversizedMqttMessage(properties, topic, payload, len, index, total); - return; - } - - // start new packet, increase buffer size if neccesary - if (index == 0) { - if (total > payloadbufferSize) { - delete[] payloadbuffer; - payloadbufferSize = total; - payloadbuffer = new (std::nothrow) uint8_t[payloadbufferSize]; - if (!payloadbuffer) { - // no buffer could be created. you might want to log this somewhere - return; - } - } - payloadbufferIndex = 0; - } - - // add data and dispatch when done - if (payloadBuffer) { - memcpy(&payloadbuffer[payloadbufferIndex], payload, len); - payloadbufferIndex += len; - if (payloadbufferIndex == total) { - // message is complete here - onCompleteMqttMessage(properties, topic, payloadBuffer, total, 0, total); - // optionally: - delete[] payloadBuffer; - payloadBuffer = nullptr; - payloadbufferSize = 0; - } - } -} - -// attach callback to MQTT client -mqttClient.onMessage(onMqttMessage); -``` - -### onMessage callbacks per topic - -espMqttClient allows only one callback for incoming messages. You might want to have specific ones per topic. This example shows one way on how to achieve this. - -Limitations of this code sample: only the first match is served and no wildcard topics allowed. - -```cpp -#include -#include - -// definitions of the std::map where we will store the topic/callback combinations -struct MatchTopic { - bool operator()(const char* a, const char* b) const { - return strcmp(a, b) < 0; - } -}; -std::map topicCallbacks; - -// callbacks per topic -void onTopic1(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - // received a packet on topic 1 -} -void onTopic2(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - // received a packet on topic 2 -} - -// general callback to dispatch to specific handlers -void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - auto it = topicCallbacks.find(topic); - if (it != topicCallbacks.end()) { - // if found, run specific callback - (it->second)(properties, topic, payload, len, index, total); - } else { - // or handle it here - } -} - -// in your Arduino setup() function: -topicCallbacks.emplace("base/topic1", onTopic1); -topicCallbacks.emplace("base/topic2", onTopic2); - -mqttClient.onMessage(onMessage); -``` diff --git a/lib/espMqttClient/docs/mqtt-v3.1.1.pdf b/lib/espMqttClient/docs/mqtt-v3.1.1.pdf deleted file mode 100644 index e4095f1b5e9e4266832bd23cb2c078cb43867d67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1506688 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

    jlk4h|xgEX= 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%0Dh(!sRJkwfq$AnM-IDB`71O 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%0AlB?$PJXP)76fB< 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

    _^xpSAytS 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

    J!X7h|V3`c~a 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

    ^RnJZcMANQ9u4H4f zoEAyG;P3-j1*)nSGI%%Zpi@~SS|(bQ3z4)*7yt@rjoj#LS4Os->aCV?0eAC>t|<`&Tt7mH@U)Sd;g1PzUUZisMEFe${rOA$sT8CxTtu7*{P9;$OQ z<*h0@2V@j+O6L+HjY%bZL&J;1FY(V%TY9iV`Plshifuw>xTeavsgRoFIcQ#9{9|SH z61haunCgIqaK#OWI4$YdLQrM#pt)k&ILFkJSx)4_b7dw6T-1np{8~7N>}vlFM-;{p zlN>Y0wR20wmRZ0@GM~6UyXpjM8TBP6aH`t>= zhk#n_^t^&xO~14%ZR!_}EI#jEXhedAo)3psQo8sLBxr|_ zkGD|Xv^9*LdNhdYyU>7=YE{)ZFLu-KHYVz8WqnX{S)6MH-$%JJX4EIx=bAtTT0TG) z9Eu-nsc~YId^`XA*%@F=hIzIj9lYVuvmdUys3lX?gwV&rTG+C zrNq_+ou4#YKg{h6c%Q-jj-u6i)>`(huLB@ znn6)Dd!@bh!&zxrfLY@Zc3c}ZgHN_sqgKykpj{8VI#{xD_iAwY=g9lFQiw`LV9$+K zI`J5X_|~uE3br{iU3$M5ve>~piFzj=iO9dV?Kyr(qaYVM7{R(gdnXfBkujCZigSme zzKRLir33%KInh2Gm{@Ie$0U9Fk7>zLi7X0#^a zmrYp#vr8%RwM1p|drc_9V^5-RKw^GU>hJ^bkpy=w7|-$UCu9aMu}9C;hq=HDDa6{0I10x!W}mOrbUj;9}K%WEoF-G?t7R(;%! z6ccY(Coxy<Bf#{EZAg>}nR#>&NM{zX4XUkDhv0XwQ}Zeto6Bqp5BRU0liSDM+z> z4|!68(;0}|$)a;0`gZZHvd-2*i(H#CF!|5(PuP?#;Wfl=RcGFdVO%c8TJlViV*Tb& z6!x2YbBd~C_g!4cm3@T>Nx4SD{%J|9tiDTYY)|*|KS@~hH?5cS<8c^V^~jsQ$Z8fD zDBHO72a$FTi_*zNXnXZvNo23aF1mDq7ZG@>CTxCqUg_irLroI>vS}WS(l$_rj=-R8+~e^FEJrE3gIr+quk6RXddOwt2PtJtM57q$5(h$`}= zE|Ze+SGVw6BJllB+0GxtSyho%5VSa~K@O?%ZL;Am{-lW@CEK1T9BcBy*tY^(rniSwsy?W2T?nV7x8Bcw+%DFNR1-vq7<{Rt9uA;@g{Aq`& z7s)&hX`2}v>~piBTJWMkIo}y_IVAK(K=D-;Rc%*rb=wuK`a}0+Yb`46s;~ #e@h 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�lIC-}2-<5XT4-8K~#KRwBCP5Le)>oYgr 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

    e}$=FxRWIsN6_dk9J{xBi8vfThgf3=(p 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%0DwhN*S1;aKX;nrl+b*G1Jp&V#4PF*nvaY1Ocrebq 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 z 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 G`TqstXdS8m diff --git a/lib/espMqttClient/examples/largepayload-esp8266/largepayload-esp8266.ino b/lib/espMqttClient/examples/largepayload-esp8266/largepayload-esp8266.ino deleted file mode 100644 index f64c9e7..0000000 --- a/lib/espMqttClient/examples/largepayload-esp8266/largepayload-esp8266.ino +++ /dev/null @@ -1,106 +0,0 @@ -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 1, 10) -#define MQTT_PORT 1883 - -WiFiEventHandler wifiConnectHandler; -WiFiEventHandler wifiDisconnectHandler; -espMqttClient mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -size_t fetchPayload(uint8_t* dest, size_t len, size_t index) { - Serial.printf("filling buffer at index %zu\n", index); - // fill the buffer with random bytes - // but maybe don't fill the entire buffer - size_t i = 0; - for (; i < len; ++i) { - dest[i] = random(0xFF); - if (dest[i] > 0xFC) { - ++i; // extra increment to compensate 'break' - break; - } - } - return i; -} - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void onWiFiConnect(const WiFiEventStationModeGotIP& event) { - (void) event; - Serial.println("Connected to Wi-Fi."); - connectToMqtt(); -} - -void onWiFiDisconnect(const WiFiEventStationModeDisconnected& event) { - (void) event; - Serial.println("Disconnected from Wi-Fi."); -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - mqttClient.publish("topic/largepayload", 1, false, fetchPayload, 6000); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.setAutoConnect(false); - WiFi.setAutoReconnect(true); - wifiConnectHandler = WiFi.onStationModeGotIP(onWiFiConnect); - wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWiFiDisconnect); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - mqttClient.loop(); - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } -} \ No newline at end of file diff --git a/lib/espMqttClient/examples/notask-esp32/notask-esp32.ino b/lib/espMqttClient/examples/notask-esp32/notask-esp32.ino deleted file mode 100644 index 867d883..0000000 --- a/lib/espMqttClient/examples/notask-esp32/notask-esp32.ino +++ /dev/null @@ -1,148 +0,0 @@ -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 1, 10) -#define MQTT_PORT 1883 - -espMqttClient mqttClient(espMqttClientTypes::UseInternalTask::NO); -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void WiFiEvent(WiFiEvent_t event) { - Serial.printf("[WiFi-event] event: %d\n", event); - switch(event) { - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Serial.println("WiFi connected"); - Serial.println("IP address: "); - Serial.println(WiFi.localIP()); - connectToMqtt(); - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Serial.println("WiFi lost connection"); - break; - default: - break; - } -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe("foo/bar", 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); - mqttClient.publish("foo/bar", 0, true, "test 1"); - Serial.println("Publishing at QoS 0"); - uint16_t packetIdPub1 = mqttClient.publish("foo/bar", 1, true, "test 2"); - Serial.print("Publishing at QoS 1, packetId: "); - Serial.println(packetIdPub1); - uint16_t packetIdPub2 = mqttClient.publish("foo/bar", 2, true, "test 3"); - Serial.print("Publishing at QoS 2, packetId: "); - Serial.println(packetIdPub2); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.persistent(false); - WiFi.setAutoReconnect(true); - WiFi.onEvent(WiFiEvent); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } - - // We used to option not to use the internal task - // so we need to call the loop-method ourselves. - // During connecting it may block. - // Creating a separate task yourself is obviously - // also a possibility. - mqttClient.loop(); -} diff --git a/lib/espMqttClient/examples/ota-esp8266/ota-esp8266.ino b/lib/espMqttClient/examples/ota-esp8266/ota-esp8266.ino deleted file mode 100644 index 90f1326..0000000 --- a/lib/espMqttClient/examples/ota-esp8266/ota-esp8266.ino +++ /dev/null @@ -1,159 +0,0 @@ -#include -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 130, 10) -#define MQTT_PORT 1883 - -#define UPDATE_TOPIC "device/firmware/set" - -WiFiEventHandler wifiConnectHandler; -WiFiEventHandler wifiDisconnectHandler; -espMqttClient mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; -bool disconnectFlag = false; -bool restartFlag = false; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void onWiFiConnect(const WiFiEventStationModeGotIP& event) { - (void) event; - Serial.println("Connected to Wi-Fi."); - connectToMqtt(); -} - -void onWiFiDisconnect(const WiFiEventStationModeDisconnected& event) { - (void) event; - Serial.println("Disconnected from Wi-Fi."); -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe(UPDATE_TOPIC, 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (disconnectFlag) { - restartFlag = true; - return; - } - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void handleUpdate(const uint8_t* payload, size_t length, size_t index, size_t total) { - // The Updater class takes a non-const pointer to write data although it doesn't change the data - uint8_t* data = const_cast(payload); - static size_t written = 0; - Update.runAsync(true); - if (index == 0) { - if (Update.isRunning()) { - Update.end(); - Update.clearError(); - } - Update.begin(total); - written = Update.write(data, length); - Serial.printf("Updating %u/%u\n", written, Update.size()); - } else { - if (!Update.isRunning()) return; - written += Update.write(data, length); - Serial.printf("Updating %u/%u\n", written, Update.size()); - } - if (Update.isFinished()) { - if (Update.end()) { - Serial.println("Update succes"); - disconnectFlag = true; - } else { - Serial.printf("Update error: %u\n", Update.getError()); - Update.printError(Serial); - Update.clearError(); - } - } -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) properties; - if (strcmp(UPDATE_TOPIC, topic) != 0) { - Serial.println("Topic mismatch"); - return; - } - handleUpdate(payload, len, index, total); -} - -void setup() { - Serial.begin(74880); - Serial.println(); - Serial.println(); - - WiFi.setAutoConnect(false); - WiFi.setAutoReconnect(true); - wifiConnectHandler = WiFi.onStationModeGotIP(onWiFiConnect); - wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWiFiDisconnect); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - if (restartFlag) { - Serial.println("Rebooting... See you next time!"); - Serial.flush(); - ESP.reset(); - } - - static uint32_t currentMillis = millis(); - - mqttClient.loop(); - - if (!disconnectFlag && reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } - - if (disconnectFlag) { - // it's safe to call this multiple times - mqttClient.disconnect(); - } -} \ No newline at end of file diff --git a/lib/espMqttClient/examples/simple-esp32-idf/CMakeLists.txt b/lib/espMqttClient/examples/simple-esp32-idf/CMakeLists.txt deleted file mode 100644 index e1f4c03..0000000 --- a/lib/espMqttClient/examples/simple-esp32-idf/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -# 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(SDKCONFIG ${CMAKE_BINARY_DIR}/sdkconfig) - -include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(simple-esp32-idf) \ No newline at end of file diff --git a/lib/espMqttClient/examples/simple-esp32-idf/README.md b/lib/espMqttClient/examples/simple-esp32-idf/README.md deleted file mode 100644 index 8fd90c7..0000000 --- a/lib/espMqttClient/examples/simple-esp32-idf/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This example is for use with [Arduino as a component](https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/esp-idf_component.html) in the ESP-IDF framework. - -Be sure to follow [this section](https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/esp-idf_component.html#adding-local-library) about adding libraries to your project. diff --git a/lib/espMqttClient/examples/simple-esp32-idf/main/CMakeLists.txt b/lib/espMqttClient/examples/simple-esp32-idf/main/CMakeLists.txt deleted file mode 100644 index 475d0f7..0000000 --- a/lib/espMqttClient/examples/simple-esp32-idf/main/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -idf_component_register( - SRCS "main.cpp" - INCLUDE_DIRS "") \ No newline at end of file diff --git a/lib/espMqttClient/examples/simple-esp32-idf/main/main.cpp b/lib/espMqttClient/examples/simple-esp32-idf/main/main.cpp deleted file mode 100644 index 77c8148..0000000 --- a/lib/espMqttClient/examples/simple-esp32-idf/main/main.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 1, 10) -#define MQTT_PORT 1883 - -espMqttClient mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void WiFiEvent(WiFiEvent_t event) { - Serial.printf("[WiFi-event] event: %d\n", event); - switch(event) { - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Serial.println("WiFi connected"); - Serial.println("IP address: "); - Serial.println(WiFi.localIP()); - connectToMqtt(); - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Serial.println("WiFi lost connection"); - break; - default: - break; - } -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe("foo/bar", 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); - mqttClient.publish("foo/bar", 0, true, "test 1"); - Serial.println("Publishing at QoS 0"); - uint16_t packetIdPub1 = mqttClient.publish("foo/bar", 1, true, "test 2"); - Serial.print("Publishing at QoS 1, packetId: "); - Serial.println(packetIdPub1); - uint16_t packetIdPub2 = mqttClient.publish("foo/bar", 2, true, "test 3"); - Serial.print("Publishing at QoS 2, packetId: "); - Serial.println(packetIdPub2); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.persistent(false); - WiFi.setAutoReconnect(true); - WiFi.onEvent(WiFiEvent); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } -} diff --git a/lib/espMqttClient/examples/simple-esp32-idf/sdkconfig.defaults b/lib/espMqttClient/examples/simple-esp32-idf/sdkconfig.defaults deleted file mode 100644 index 4661ad2..0000000 --- a/lib/espMqttClient/examples/simple-esp32-idf/sdkconfig.defaults +++ /dev/null @@ -1,39 +0,0 @@ -# -# Bootloader config -# -CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y -CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y -CONFIG_BOOTLOADER_LOG_LEVEL=0 - -# -# Serial flasher config -# -CONFIG_ESPTOOLPY_FLASHMODE_DIO=y -CONFIG_ESPTOOLPY_FLASHMODE="dio" -CONFIG_ESPTOOLPY_FLASHFREQ_40M=y -CONFIG_ESPTOOLPY_FLASHFREQ="40m" -CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y -CONFIG_ESPTOOLPY_FLASHSIZE="4MB" -# -# Partition Table -# -CONFIG_PARTITION_TABLE_CUSTOM=n - -# -# Arduino Configuration -# -CONFIG_ARDUINO_VARIANT="esp32" -CONFIG_ENABLE_ARDUINO_DEPENDS=y -CONFIG_AUTOSTART_ARDUINO=y - -# -# FreeRTOS -# -# 1000 require for Arduino -CONFIG_FREERTOS_HZ=1000 - -#ASYNC_TCP -CONFIG_ASYNC_TCP_RUN_NO_AFFINITY=y - -#MBEDTLS -CONFIG_MBEDTLS_PSK_MODES=y \ No newline at end of file diff --git a/lib/espMqttClient/examples/simple-esp32/simple-esp32.ino b/lib/espMqttClient/examples/simple-esp32/simple-esp32.ino deleted file mode 100644 index 1a5de37..0000000 --- a/lib/espMqttClient/examples/simple-esp32/simple-esp32.ino +++ /dev/null @@ -1,141 +0,0 @@ -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 1, 10) -#define MQTT_PORT 1883 - -espMqttClient mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void WiFiEvent(WiFiEvent_t event) { - Serial.printf("[WiFi-event] event: %d\n", event); - switch(event) { - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Serial.println("WiFi connected"); - Serial.println("IP address: "); - Serial.println(WiFi.localIP()); - connectToMqtt(); - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Serial.println("WiFi lost connection"); - break; - default: - break; - } -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe("foo/bar", 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); - mqttClient.publish("foo/bar", 0, true, "test 1"); - Serial.println("Publishing at QoS 0"); - uint16_t packetIdPub1 = mqttClient.publish("foo/bar", 1, true, "test 2"); - Serial.print("Publishing at QoS 1, packetId: "); - Serial.println(packetIdPub1); - uint16_t packetIdPub2 = mqttClient.publish("foo/bar", 2, true, "test 3"); - Serial.print("Publishing at QoS 2, packetId: "); - Serial.println(packetIdPub2); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.persistent(false); - WiFi.setAutoReconnect(true); - WiFi.onEvent(WiFiEvent); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } -} diff --git a/lib/espMqttClient/examples/simple-esp8266/simple-esp8266.ino b/lib/espMqttClient/examples/simple-esp8266/simple-esp8266.ino deleted file mode 100644 index 2d54e12..0000000 --- a/lib/espMqttClient/examples/simple-esp8266/simple-esp8266.ino +++ /dev/null @@ -1,139 +0,0 @@ -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 1, 10) -#define MQTT_PORT 1883 - -WiFiEventHandler wifiConnectHandler; -WiFiEventHandler wifiDisconnectHandler; -espMqttClient mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void onWiFiConnect(const WiFiEventStationModeGotIP& event) { - (void) event; - Serial.println("Connected to Wi-Fi."); - connectToMqtt(); -} - -void onWiFiDisconnect(const WiFiEventStationModeDisconnected& event) { - (void) event; - Serial.println("Disconnected from Wi-Fi."); -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe("test/lol", 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); - mqttClient.publish("test/lol", 0, true, "test 1"); - Serial.println("Publishing at QoS 0"); - uint16_t packetIdPub1 = mqttClient.publish("test/lol", 1, true, "test 2"); - Serial.print("Publishing at QoS 1, packetId: "); - Serial.println(packetIdPub1); - uint16_t packetIdPub2 = mqttClient.publish("test/lol", 2, true, "test 3"); - Serial.print("Publishing at QoS 2, packetId: "); - Serial.println(packetIdPub2); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.setAutoConnect(false); - WiFi.setAutoReconnect(true); - wifiConnectHandler = WiFi.onStationModeGotIP(onWiFiConnect); - wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWiFiDisconnect); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - mqttClient.loop(); - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } -} \ No newline at end of file diff --git a/lib/espMqttClient/examples/simple-linux/main.cpp b/lib/espMqttClient/examples/simple-linux/main.cpp deleted file mode 100644 index 54daa86..0000000 --- a/lib/espMqttClient/examples/simple-linux/main.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include -#include -#include - -#define MQTT_HOST IPAddress(192,168,1,10) -#define MQTT_PORT 1883 - -espMqttClient mqttClient; -std::atomic_bool exitProgram(false); - -void connectToMqtt() { - std::cout << "Connecting to MQTT..." << std::endl; - mqttClient.connect(); -} - -void onMqttConnect(bool sessionPresent) { - std::cout << "Connected to MQTT." << std::endl; - std::cout << "Session present: " << sessionPresent << std::endl; - uint16_t packetIdSub = mqttClient.subscribe("test/lol", 2); - std::cout << "Subscribing at QoS 2, packetId: " << packetIdSub << std::endl; - mqttClient.publish("test/lol", 0, true, "test 1"); - std::cout << "Publishing at QoS 0" << std::endl; - uint16_t packetIdPub1 = mqttClient.publish("test/lol", 1, true, "test 2"); - std::cout << "Publishing at QoS 1, packetId: " << packetIdPub1 << std::endl; - uint16_t packetIdPub2 = mqttClient.publish("test/lol", 2, true, "test 3"); - std::cout << "Publishing at QoS 2, packetId: " << packetIdPub2 << std::endl; -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - std::cout << "Disconnected from MQTT: %u.\n" << unsigned(static_cast(reason)) << std::endl; - exitProgram = true; -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - std::cout << "Subscribe acknowledged." << std::endl; - std::cout << " packetId: " << packetId << std::endl; - for (size_t i = 0; i < len; ++i) { - std::cout << " qos: " << unsigned(static_cast(codes[i])) << std::endl; - } -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - std::cout << "Publish received." << std::endl; - std::cout << " topic: " << topic << std::endl; - std::cout << " qos: " << unsigned(properties.qos) << std::endl; - std::cout << " dup: " << properties.dup << std::endl; - std::cout << " retain: " << properties.retain << std::endl; - std::cout << " len: " << len << std::endl; - std::cout << " index: " << index << std::endl; - std::cout << " total: " << total << std::endl; -} - -void onMqttPublish(uint16_t packetId) { - std::cout << "Publish acknowledged." << std::endl; - std::cout << " packetId: " << packetId << std::endl; -} - -void ClientLoop(void* arg) { - (void) arg; - for(;;) { - mqttClient.loop(); // includes a yield - if (exitProgram) break; - } -} - -int main() { - std::cout << "Setting up sample MQTT client" << std::endl; - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - std::cout << "Starting sample MQTT client" << std::endl; - std::thread t = std::thread(ClientLoop, nullptr); - - connectToMqtt(); - - while(1) { - if (exitProgram) break; - std::this_thread::yield(); - } - - t.join(); - return EXIT_SUCCESS; -} diff --git a/lib/espMqttClient/examples/simple-linux/platformio.ini b/lib/espMqttClient/examples/simple-linux/platformio.ini deleted file mode 100644 index 565336f..0000000 --- a/lib/espMqttClient/examples/simple-linux/platformio.ini +++ /dev/null @@ -1,29 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - -;[platformio] -;default_envs = esp8266 - -[common] -build_flags = - -D DEBUG_ESP_MQTT_CLIENT=1 - -std=c++11 - -pthread - -Wall - -Wextra - -Werror - -[env:native] -platform = native -build_flags = - ${common.build_flags} - -D EMC_RX_BUFFER_SIZE=1500 -build_type = debug -lib_compat_mode = off diff --git a/lib/espMqttClient/examples/simpleAsync-esp32/simpleAsync-esp32.ino b/lib/espMqttClient/examples/simpleAsync-esp32/simpleAsync-esp32.ino deleted file mode 100644 index 109bcf0..0000000 --- a/lib/espMqttClient/examples/simpleAsync-esp32/simpleAsync-esp32.ino +++ /dev/null @@ -1,141 +0,0 @@ -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 1, 10) -#define MQTT_PORT 1883 - -espMqttClientAsync mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void WiFiEvent(WiFiEvent_t event) { - Serial.printf("[WiFi-event] event: %d\n", event); - switch(event) { - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Serial.println("WiFi connected"); - Serial.println("IP address: "); - Serial.println(WiFi.localIP()); - connectToMqtt(); - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Serial.println("WiFi lost connection"); - break; - default: - break; - } -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe("foo/bar", 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); - mqttClient.publish("foo/bar", 0, true, "test 1"); - Serial.println("Publishing at QoS 0"); - uint16_t packetIdPub1 = mqttClient.publish("foo/bar", 1, true, "test 2"); - Serial.print("Publishing at QoS 1, packetId: "); - Serial.println(packetIdPub1); - uint16_t packetIdPub2 = mqttClient.publish("foo/bar", 2, true, "test 3"); - Serial.print("Publishing at QoS 2, packetId: "); - Serial.println(packetIdPub2); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.persistent(false); - WiFi.setAutoReconnect(true); - WiFi.onEvent(WiFiEvent); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } -} diff --git a/lib/espMqttClient/examples/simpleAsync-esp8266/simpleAsync-esp8266.ino b/lib/espMqttClient/examples/simpleAsync-esp8266/simpleAsync-esp8266.ino deleted file mode 100644 index 804caa1..0000000 --- a/lib/espMqttClient/examples/simpleAsync-esp8266/simpleAsync-esp8266.ino +++ /dev/null @@ -1,138 +0,0 @@ -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST IPAddress(192, 168, 1, 10) -#define MQTT_PORT 1883 - -WiFiEventHandler wifiConnectHandler; -WiFiEventHandler wifiDisconnectHandler; -espMqttClientAsync mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void onWiFiConnect(const WiFiEventStationModeGotIP& event) { - (void) event; - Serial.println("Connected to Wi-Fi."); - connectToMqtt(); -} - -void onWiFiDisconnect(const WiFiEventStationModeDisconnected& event) { - (void) event; - Serial.println("Disconnected from Wi-Fi."); -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe("test/lol", 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); - mqttClient.publish("test/lol", 0, true, "test 1"); - Serial.println("Publishing at QoS 0"); - uint16_t packetIdPub1 = mqttClient.publish("test/lol", 1, true, "test 2"); - Serial.print("Publishing at QoS 1, packetId: "); - Serial.println(packetIdPub1); - uint16_t packetIdPub2 = mqttClient.publish("test/lol", 2, true, "test 3"); - Serial.print("Publishing at QoS 2, packetId: "); - Serial.println(packetIdPub2); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.setAutoConnect(false); - WiFi.setAutoReconnect(true); - wifiConnectHandler = WiFi.onStationModeGotIP(onWiFiConnect); - wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWiFiDisconnect); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } -} \ No newline at end of file diff --git a/lib/espMqttClient/examples/tls-esp32/tls-esp32.ino b/lib/espMqttClient/examples/tls-esp32/tls-esp32.ino deleted file mode 100644 index ce75a5a..0000000 --- a/lib/espMqttClient/examples/tls-esp32/tls-esp32.ino +++ /dev/null @@ -1,170 +0,0 @@ -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST "mqtt.yourhost.com" -#define MQTT_PORT 8883 -#define MQTT_USER "username" -#define MQTT_PASS "password" - -const char rootCA[] = \ - "-----BEGIN CERTIFICATE-----\n" \ - " add your certificate here \n" \ - "-----END CERTIFICATE-----\n"; - -espMqttClientSecure mqttClient(espMqttClientTypes::UseInternalTask::NO); -static TaskHandle_t taskHandle; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void WiFiEvent(WiFiEvent_t event) { - Serial.printf("[WiFi-event] event: %d\n", event); - switch(event) { - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Serial.println("WiFi connected"); - Serial.println("IP address: "); - Serial.println(WiFi.localIP()); - connectToMqtt(); - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Serial.println("WiFi lost connection"); - break; - default: - break; - } -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - - uint16_t packetIdSub0 = mqttClient.subscribe("foo/bar/0", 0); - Serial.print("Subscribing at QoS 0, packetId: "); - Serial.println(packetIdSub0); - - uint16_t packetIdPub0 = mqttClient.publish("foo/bar/0", 0, false, "test"); - Serial.println("Publishing at QoS 0, packetId: "); - Serial.println(packetIdPub0); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void networkingTask() { - for (;;) { - mqttClient.loop(); - } -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.persistent(false); - WiFi.setAutoReconnect(true); - WiFi.onEvent(WiFiEvent); - - //mqttClient.setInsecure(); - mqttClient.setCACert(rootCA); - mqttClient.setCredentials(MQTT_USER, MQTT_PASS); - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - mqttClient.setCleanSession(true); - - xTaskCreatePinnedToCore((TaskFunction_t)networkingTask, "mqttclienttask", 5120, nullptr, 1, &taskHandle, 0); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } - - static uint32_t lastMillis = 0; - if (currentMillis - lastMillis > 5000) { - lastMillis = currentMillis; - Serial.printf("heap: %u\n", ESP.getFreeHeap()); - } - - static uint32_t millisDisconnect = 0; - if (currentMillis - millisDisconnect > 60000) { - millisDisconnect = currentMillis; - mqttClient.disconnect(); - } -} diff --git a/lib/espMqttClient/examples/tls-esp8266/tls-esp8266.ino b/lib/espMqttClient/examples/tls-esp8266/tls-esp8266.ino deleted file mode 100644 index f2cb209..0000000 --- a/lib/espMqttClient/examples/tls-esp8266/tls-esp8266.ino +++ /dev/null @@ -1,144 +0,0 @@ -#include -#include - -#include - -#define WIFI_SSID "yourSSID" -#define WIFI_PASSWORD "yourpass" - -#define MQTT_HOST "test.mosquitto.org" -#define MQTT_PORT 1883 - -// test.mosquitto.org -const uint8_t fingerprint[] = {0xee, 0xbc, 0x4b, 0xf8, 0x57, 0xe3, 0xd3, 0xe4, 0x07, 0x54, 0x23, 0x1e, 0xf0, 0xc8, 0xa1, 0x56, 0xe0, 0xd3, 0x1a, 0x1c}; - -WiFiEventHandler wifiConnectHandler; -WiFiEventHandler wifiDisconnectHandler; -espMqttClientSecure mqttClient; -bool reconnectMqtt = false; -uint32_t lastReconnect = 0; - -void connectToWiFi() { - Serial.println("Connecting to Wi-Fi..."); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); -} - -void connectToMqtt() { - Serial.println("Connecting to MQTT..."); - if (!mqttClient.connect()) { - reconnectMqtt = true; - lastReconnect = millis(); - Serial.println("Connecting failed."); - } else { - reconnectMqtt = false; - } -} - -void onWiFiConnect(const WiFiEventStationModeGotIP& event) { - (void) event; - Serial.println("Connected to Wi-Fi."); - connectToMqtt(); -} - -void onWiFiDisconnect(const WiFiEventStationModeDisconnected& event) { - (void) event; - Serial.println("Disconnected from Wi-Fi."); -} - -void onMqttConnect(bool sessionPresent) { - Serial.println("Connected to MQTT."); - Serial.print("Session present: "); - Serial.println(sessionPresent); - uint16_t packetIdSub = mqttClient.subscribe("test/lol", 2); - Serial.print("Subscribing at QoS 2, packetId: "); - Serial.println(packetIdSub); - mqttClient.publish("test/lol", 0, true, "test 1"); - Serial.println("Publishing at QoS 0"); - uint16_t packetIdPub1 = mqttClient.publish("test/lol", 1, true, "test 2"); - Serial.print("Publishing at QoS 1, packetId: "); - Serial.println(packetIdPub1); - uint16_t packetIdPub2 = mqttClient.publish("test/lol", 2, true, "test 3"); - Serial.print("Publishing at QoS 2, packetId: "); - Serial.println(packetIdPub2); -} - -void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) { - Serial.printf("Disconnected from MQTT: %u.\n", static_cast(reason)); - - if (WiFi.isConnected()) { - reconnectMqtt = true; - lastReconnect = millis(); - } -} - -void onMqttSubscribe(uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* codes, size_t len) { - Serial.println("Subscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); - for (size_t i = 0; i < len; ++i) { - Serial.print(" qos: "); - Serial.println(static_cast(codes[i])); - } -} - -void onMqttUnsubscribe(uint16_t packetId) { - Serial.println("Unsubscribe acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - (void) payload; - Serial.println("Publish received."); - Serial.print(" topic: "); - Serial.println(topic); - Serial.print(" qos: "); - Serial.println(properties.qos); - Serial.print(" dup: "); - Serial.println(properties.dup); - Serial.print(" retain: "); - Serial.println(properties.retain); - Serial.print(" len: "); - Serial.println(len); - Serial.print(" index: "); - Serial.println(index); - Serial.print(" total: "); - Serial.println(total); -} - -void onMqttPublish(uint16_t packetId) { - Serial.println("Publish acknowledged."); - Serial.print(" packetId: "); - Serial.println(packetId); -} - -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println(); - - WiFi.setAutoConnect(false); - WiFi.setAutoReconnect(true); - wifiConnectHandler = WiFi.onStationModeGotIP(onWiFiConnect); - wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWiFiDisconnect); - - mqttClient.onConnect(onMqttConnect); - mqttClient.onDisconnect(onMqttDisconnect); - mqttClient.onSubscribe(onMqttSubscribe); - mqttClient.onUnsubscribe(onMqttUnsubscribe); - mqttClient.onMessage(onMqttMessage); - mqttClient.onPublish(onMqttPublish); - mqttClient.setServer(MQTT_HOST, MQTT_PORT); - mqttClient.setFingerprint(fingerprint); - - connectToWiFi(); -} - -void loop() { - static uint32_t currentMillis = millis(); - - mqttClient.loop(); - if (reconnectMqtt && currentMillis - lastReconnect > 5000) { - connectToMqtt(); - } -} \ No newline at end of file diff --git a/lib/espMqttClient/keywords.txt b/lib/espMqttClient/keywords.txt deleted file mode 100644 index 3807482..0000000 --- a/lib/espMqttClient/keywords.txt +++ /dev/null @@ -1,61 +0,0 @@ -# Datatypes (KEYWORD1) -espMqttClient KEYWORD1 -espMqttClientSecure KEYWORD1 - -OnConnectCallback KEYWORD1 -OnDisconnectCallback KEYWORD1 -OnSubscribeCallback KEYWORD1 -OnUnsubscribeCallback KEYWORD1 -OnMessageCallback KEYWORD1 -OnPublishCallback KEYWORD1 - -# Methods and Functions (KEYWORD2) -setKeepAlive KEYWORD2 -setClientId KEYWORD2 -setCleanSession KEYWORD2 -setCredentials KEYWORD2 -setWill KEYWORD2 -setServer KEYWORD2 - -setInsecure KEYWORD2 -setCACert KEYWORD2 -setCertificate KEYWORD2 -setPrivateKey KEYWORD2 -setPreSharedKey KEYWORD2 -setFingerprint KEYWORD2 -setTrustAnchors KEYWORD2 -setClientRSACert KEYWORD2 -setClientECCert KEYWORD2 -setCertStore KEYWORD2 - -onConnect KEYWORD2 -onDisconnect KEYWORD2 -onSubscribe KEYWORD2 -onUnsubscribe KEYWORD2 -onMessage KEYWORD2 -onPublish KEYWORD2 - -connected KEYWORD2 -connect KEYWORD2 -disconnect KEYWORD2 -subscribe KEYWORD2 -unsubscribe KEYWORD2 -publish KEYWORD2 -clearQueue KEYWORD2 -loop KEYWORD2 -getClientId KEYWORD2 -queueSize KEYWORD2 - -# Structures (KEYWORD3) -espMqttClientTypes KEYWORD3 -MessageProperties KEYWORD3 -DisconnectReason KEYWORD3 - -# Constants (LITERAL1) -TCP_DISCONNECTED LITERAL1 -MQTT_UNACCEPTABLE_PROTOCOL_VERSION LITERAL1 -MQTT_IDENTIFIER_REJECTED LITERAL1 -MQTT_SERVER_UNAVAILABLE LITERAL1 -MQTT_MALFORMED_CREDENTIALS LITERAL1 -MQTT_NOT_AUTHORIZED LITERAL1 -TLS_BAD_FINGERPRINT LITERAL1 diff --git a/lib/espMqttClient/library.json b/lib/espMqttClient/library.json deleted file mode 100644 index 21c8429..0000000 --- a/lib/espMqttClient/library.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "espMqttClient", - "keywords": "iot, home, automation, mqtt, client, esp8266, esp32", - "description": "an MQTT client for the Arduino framework for ESP8266 / ESP32", - "authors": - { - "name": "Bert Melis", - "url": "https://github.com/bertmelis" - }, - "license": "MIT", - "homepage": "https://github.com/bertmelis/espMqttClient", - "repository": - { - "type": "git", - "url": "https://github.com/bertmelis/espMqttClient.git" - }, - "version": "1.7.0", - "frameworks": "arduino", - "platforms": ["espressif8266", "espressif32"], - "headers": ["espMqttClient.h", "espMqttClientAsync.h"], - "build": - { - "libLDFMode": "deep+" - } -} diff --git a/lib/espMqttClient/library.properties b/lib/espMqttClient/library.properties deleted file mode 100644 index 3b906a9..0000000 --- a/lib/espMqttClient/library.properties +++ /dev/null @@ -1,9 +0,0 @@ -name=espMqttClient -version=1.7.0 -author=Bert Melis -maintainer=Bert Melis -sentence=an MQTT client for the Arduino framework for ESP8266 / ESP32 -paragraph= -category=Communication -url=https://github.com/bertmelis/espMqttClient -architectures=esp8266,esp32 \ No newline at end of file diff --git a/lib/espMqttClient/platformio.ini b/lib/espMqttClient/platformio.ini deleted file mode 100644 index 219ed3b..0000000 --- a/lib/espMqttClient/platformio.ini +++ /dev/null @@ -1,43 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - -;[platformio] -;default_envs = esp8266 - -[common] -build_flags = - -D DEBUG_ESP_MQTT_CLIENT=1 - -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE - -Wall - -Wextra - -std=c++11 - -pthread - -ggdb3 - -[env:native] -platform = native -test_build_src = yes -build_flags = - ${common.build_flags} - -lgcov - --coverage - -D EMC_RX_BUFFER_SIZE=100 - -D EMC_TX_BUFFER_SIZE=10 - -D EMC_MULTIPLE_CALLBACKS=1 - -D EMC_USE_MEMPOOL=1 -;extra_scripts = test-coverage.py -build_type = debug -test_testing_command = - valgrind - --leak-check=full - --show-leak-kinds=all - --track-origins=yes - --error-exitcode=1 - ${platformio.build_dir}/${this.__env__}/program diff --git a/lib/espMqttClient/scripts/get-fingerprint.py b/lib/espMqttClient/scripts/get-fingerprint.py deleted file mode 100644 index 22c078b..0000000 --- a/lib/espMqttClient/scripts/get-fingerprint.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python - -# https://github.com/marvinroger/async-mqtt-client/blob/develop/scripts/get-fingerprint/get-fingerprint.py - -import argparse -import ssl -import hashlib - -parser = argparse.ArgumentParser(description='Compute SSL/TLS fingerprints.') -parser.add_argument('--host', required=True) -parser.add_argument('--port', default=8883) - -args = parser.parse_args() -print(args.host) - -cert_pem = ssl.get_server_certificate((args.host, args.port)) -cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) - -md5 = hashlib.md5(cert_der).hexdigest() -sha1 = hashlib.sha1(cert_der).hexdigest() -sha256 = hashlib.sha256(cert_der).hexdigest() -print("MD5: " + md5) -print("SHA1: " + sha1) -print("SHA256: " + sha256) - -print("\nSHA1 as array initializer:") -print("const uint8_t fingerprint[] = {0x" + ", 0x".join([sha1[i:i+2] for i in range(0, len(sha1), 2)]) + "};") - -print("\nSHA1 as function call:") -print("mqttClient.addServerFingerprint((const uint8_t[]){0x" + ", 0x".join([sha1[i:i+2] for i in range(0, len(sha1), 2)]) + "});") \ No newline at end of file diff --git a/lib/espMqttClient/src/Config.h b/lib/espMqttClient/src/Config.h deleted file mode 100644 index 935f7e1..0000000 --- a/lib/espMqttClient/src/Config.h +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#ifndef EMC_TX_TIMEOUT -#define EMC_TX_TIMEOUT 10000 -#endif - -#ifndef EMC_RX_BUFFER_SIZE -#define EMC_RX_BUFFER_SIZE 1440 -#endif - -#ifndef EMC_TX_BUFFER_SIZE -#define EMC_TX_BUFFER_SIZE 1440 -#endif - -#ifndef EMC_MAX_TOPIC_LENGTH -#define EMC_MAX_TOPIC_LENGTH 128 -#endif - -#ifndef EMC_PAYLOAD_BUFFER_SIZE -#define EMC_PAYLOAD_BUFFER_SIZE 32 -#endif - -#ifndef EMC_MIN_FREE_MEMORY -#define EMC_MIN_FREE_MEMORY 16384 -#endif - -#ifndef EMC_ESP8266_MULTITHREADING -#define EMC_ESP8266_MULTITHREADING 0 -#endif - -#ifndef EMC_ALLOW_NOT_CONNECTED_PUBLISH -#define EMC_ALLOW_NOT_CONNECTED_PUBLISH 1 -#endif - -#ifndef EMC_WAIT_FOR_CONNACK -#define EMC_WAIT_FOR_CONNACK 1 -#endif - -#ifndef EMC_CLIENTID_LENGTH -// esp8266abc123 and esp32abcdef123456 -#define EMC_CLIENTID_LENGTH 23 + 1 -#endif - -#ifndef EMC_TASK_STACK_SIZE -#define EMC_TASK_STACK_SIZE 5120 -#endif - -#ifndef EMC_MULTIPLE_CALLBACKS -#define EMC_MULTIPLE_CALLBACKS 0 -#endif - -#ifndef EMC_USE_WATCHDOG -#define EMC_USE_WATCHDOG 0 -#endif - -#ifndef EMC_USE_MEMPOOL -#define EMC_USE_MEMPOOL 0 -#endif - -#if EMC_USE_MEMPOOL - #ifndef EMC_NUM_POOL_ELEMENTS - #define EMC_NUM_POOL_ELEMENTS 32 - #endif - #ifndef EMC_SIZE_POOL_ELEMENTS - #define EMC_SIZE_POOL_ELEMENTS 128 - #endif -#endif diff --git a/lib/espMqttClient/src/Helpers.h b/lib/espMqttClient/src/Helpers.h deleted file mode 100644 index 05ab136..0000000 --- a/lib/espMqttClient/src/Helpers.h +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP32) - #include // millis(), ESP.getFreeHeap(); - #include "freertos/FreeRTOS.h" - #include "freertos/task.h" - #include "esp_task_wdt.h" - #define EMC_SEMAPHORE_TAKE() xSemaphoreTake(_xSemaphore, portMAX_DELAY) - #define EMC_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) - #define EMC_GET_FREE_MEMORY() std::max(ESP.getMaxAllocHeap(), ESP.getMaxAllocPsram()) - #define EMC_YIELD() vTaskDelay(1) - #define EMC_GENERATE_CLIENTID(x) snprintf(x, EMC_CLIENTID_LENGTH, "esp32%06llx", ESP.getEfuseMac()); -#elif defined(ARDUINO_ARCH_ESP8266) - #include // millis(), ESP.getFreeHeap(); - #if EMC_ESP8266_MULTITHREADING - // This lib doesn't run use multithreading on ESP8266 - // _xSemaphore defined as std::atomic - #define EMC_SEMAPHORE_TAKE() while (_xSemaphore) { /*ESP.wdtFeed();*/ } _xSemaphore = true - #define EMC_SEMAPHORE_GIVE() _xSemaphore = false - #else - #define EMC_SEMAPHORE_TAKE() - #define EMC_SEMAPHORE_GIVE() - #endif - #define EMC_GET_FREE_MEMORY() ESP.getMaxFreeBlockSize() - // no need to yield for ESP8266, the Arduino framework does this internally - // yielding in async is forbidden (will crash) - #define EMC_YIELD() - #define EMC_GENERATE_CLIENTID(x) snprintf(x, EMC_CLIENTID_LENGTH, "esp8266%06x", ESP.getChipId()); -#elif defined(__linux__) - #include // NOLINT [build/c++11] - #include // NOLINT [build/c++11] for yield() - #define millis() std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count() - #define EMC_GET_FREE_MEMORY() 1000000000 - #define EMC_YIELD() std::this_thread::yield() - #define EMC_GENERATE_CLIENTID(x) snprintf(x, EMC_CLIENTID_LENGTH, "Client%04d%04d%04d", rand()%10000, rand()%10000, rand()%10000) - #include // NOLINT [build/c++11] - #define EMC_SEMAPHORE_TAKE() mtx.lock(); - #define EMC_SEMAPHORE_GIVE() mtx.unlock(); -#else - #error Target platform not supported -#endif diff --git a/lib/espMqttClient/src/Logging.h b/lib/espMqttClient/src/Logging.h deleted file mode 100644 index 3ba096a..0000000 --- a/lib/espMqttClient/src/Logging.h +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP32) - #include - #include "freertos/FreeRTOS.h" - #include "freertos/task.h" - #if defined(DEBUG_ESP_MQTT_CLIENT) - // Logging is en/disabled by Arduino framework macros - #define emc_log_i(...) log_i(__VA_ARGS__) - #define emc_log_e(...) log_e(__VA_ARGS__) - #define emc_log_w(...) log_w(__VA_ARGS__) - #else - // Logging is disabled - #define emc_log_i(...) - #define emc_log_e(...) - #define emc_log_w(...) - #endif -#elif defined(ARDUINO_ARCH_ESP8266) - #if defined(DEBUG_ESP_PORT) && defined(DEBUG_ESP_MQTT_CLIENT) - #include - #define emc_log_i(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") - #define emc_log_e(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") - #define emc_log_w(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") - #else - #define emc_log_i(...) - #define emc_log_e(...) - #define emc_log_w(...) - #endif -#else - // when building for PC, always show debug statements as part of testing suite - #include - #define emc_log_i(...) std::cout << "[I] " << __FILE__ ":" << __LINE__ << ": "; printf(__VA_ARGS__); std::cout << std::endl - #define emc_log_e(...) std::cout << "[E] " << __FILE__ ":" << __LINE__ << ": "; printf(__VA_ARGS__); std::cout << std::endl - #define emc_log_w(...) std::cout << "[W] " << __FILE__ ":" << __LINE__ << ": "; printf(__VA_ARGS__); std::cout << std::endl -#endif diff --git a/lib/espMqttClient/src/MemoryPool/LICENSE b/lib/espMqttClient/src/MemoryPool/LICENSE deleted file mode 100644 index 526a0c7..0000000 --- a/lib/espMqttClient/src/MemoryPool/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Bert Melis - -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/lib/espMqttClient/src/MemoryPool/README.md b/lib/espMqttClient/src/MemoryPool/README.md deleted file mode 100644 index 81b6fd4..0000000 --- a/lib/espMqttClient/src/MemoryPool/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# Memory Pool - -EARLY VERSION. USE AT OWN RISK. - -### Description - -This is a simple memory pool that doesn't solve the fragmentation problem but contains it. Inside the pool you will still suffer memory fragmentation. The upside is that you're not restricted on memory size. As long as it fits in the pool, you can request any size! - -For applications where the (maximum) size to allocate is known, a simple fixed block size memory pool is available. There is no memory fragmentation happening in this case. The downside is wastage of memory if you need less then the specified blocksize. - -#### Features - -- pool memory is statically allocated -- pool size adjusts on architecture -- no size calculation required: input number of blocks and size of block -- header-only library -- Variable size pool: no restriction on allocated size -- Variable size pool: malloc and free are O(n); The number of allocated blocks affects lookup. -- Fixed size pool: malloc and free are O(1). - -[![Test with Platformio](https://github.com/bertmelis/MemoryPool/actions/workflows/test-platformio.yml/badge.svg)](https://github.com/bertmelis/MemoryPool/actions/workflows/test-platformio.yml) -[![cpplint](https://github.com/bertmelis/MemoryPool/actions/workflows/cpplint.yml/badge.svg)](https://github.com/bertmelis/MemoryPool/actions/workflows/cpplint.yml) - - -### Usage - -#### Variable size pool - -```cpp -#include - -Struct MyStruct { - unsigned int id; - std::size_t size; - unsigned char data[256]; -}; - -// pool will be able to hold 10 blocks the size of MyStruct -MemoryPool::Variable<10, sizeof(MyStruct)> pool; - -// you can allocate the specified blocksize -// allocation is done in number of 'unsigned char' -MyStruct* s = reinterpret_cast(pool.malloc(sizeof(MyStruct))); - -// you can allocate less than the specified blocksize -int* i = reinterpret_cast(pool.malloc(sizeof(int))); - -// you can allocate more than the specified blocksize -unsigned char* m = reinterpret_cast(pool.malloc(400)); - -pool.free(s); -pool.free(i); -pool.free(m); -``` - -#### Fixed size pool - -```cpp -#include - -Struct MyStruct { - unsigned int id; - std::size_t size; - unsigned char data[256]; -}; - -// pool will be able to hold 10 blocks the size of MyStruct -MemoryPool::Fixed<10, sizeof(MyStruct)> pool; - -// there is no size argument in the malloc function! -MyStruct* s = reinterpret_cast(pool.malloc()); - -// you can allocate less than the specified blocksize -int* i = reinterpret_cast(pool.malloc()); - -pool.free(s); -pool.free(i); -``` - -#### How it works - -##### Variable size pool - -Free blocks are organized as a linked list with their header (contains pointer to next and size). An allocated block also has this header with it's pointer set to `nullptr`. Therefore, each allocation wastes memory the size of the header (`sizeof(void*) + sizeof(std::size_t)`). On creation, the pool calculations the needed space to store the number of blocks wich each their header. - -However, memory allocation isn't restricted the the specified blocksize. So in reality, you can allocate more if you allocate larger chunks because less memory blocks means less headers. After all, memory needs to be contiguous. - -If you inspect the pool you'll see that a free pool only has one big block. - -Allocation is linear: the pool is iterated until a suitable spot is found. -Freeing is also linear as the pool is traversed to insert the chunk in the linked list of free blocks - -When freeing, free blocks which are adjacent are combined into one. - -##### Fixed size pool - -The fixed size pool is implemented as an array. Free blocks are saved as a linked list in this array. - -### Bugs and feature requests - -Please use Github's facilities to get in touch. - -### License - -This library is released under the MIT Licence. A copy is included in the repo. diff --git a/lib/espMqttClient/src/MemoryPool/keywords.txt b/lib/espMqttClient/src/MemoryPool/keywords.txt deleted file mode 100644 index ef87ce2..0000000 --- a/lib/espMqttClient/src/MemoryPool/keywords.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Datatypes (KEYWORD1) -Fixed KEYWORD1 -Variable KEYWORD1 - -# Methods and Functions (KEYWORD2) -malloc KEYWORD2 -free KEYWORD2 -freeMemory KEYWORD2 -maxBlockSize KEYWORD2 -print KEYWORD2 - -# Structures (KEYWORD3) -# structure KEYWORD3 - -# Constants (LITERAL1) -MemoryPool LITERAL1 diff --git a/lib/espMqttClient/src/MemoryPool/library.json b/lib/espMqttClient/src/MemoryPool/library.json deleted file mode 100644 index f9e6116..0000000 --- a/lib/espMqttClient/src/MemoryPool/library.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "MemoryPool", - "keywords": "memory", - "description": "A simple memory pool for fixed and variable sizes", - "authors": - { - "name": "Bert Melis", - "url": "https://github.com/bertmelis" - }, - "license": "MIT", - "homepage": "https://github.com/bertmelis/MemoryPool", - "repository": - { - "type": "git", - "url": "https://github.com/bertmelis/MemoryPool.git" - }, - "version": "0.1.0", - "frameworks": "*", - "platforms": "*", - "headers": ["MemoryPool.h"] - } \ No newline at end of file diff --git a/lib/espMqttClient/src/MemoryPool/library.properties b/lib/espMqttClient/src/MemoryPool/library.properties deleted file mode 100644 index a46b50f..0000000 --- a/lib/espMqttClient/src/MemoryPool/library.properties +++ /dev/null @@ -1,10 +0,0 @@ -name=MemoryPool -version=0.1.0 -author=Bert Melis -maintainer=Bert Melis -sentence=A simple memory pool for fixed and variable sizes -paragraph= -category=Other -url=https://github.com/bertmelis/MemoryPool -architectures=* -includes=MemoryPool.h \ No newline at end of file diff --git a/lib/espMqttClient/src/MemoryPool/src/Fixed.h b/lib/espMqttClient/src/MemoryPool/src/Fixed.h deleted file mode 100644 index b68dbd1..0000000 --- a/lib/espMqttClient/src/MemoryPool/src/Fixed.h +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright (c) 2024 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include // std::size_t -#include // assert -#if _GLIBCXX_HAS_GTHREADS -#include // NOLINT [build/c++11] std::mutex, std::lock_guard -#else -#warning "The memory pool is not thread safe" -#endif - -#ifdef MEMPOL_DEBUG -#include -#endif - -namespace MemoryPool { - -template -class Fixed { - public: - Fixed() // cppcheck-suppress uninitMemberVar - : _buffer{0} - , _head(_buffer) { - unsigned char* b = _head; - std::size_t adjustedBlocksize = sizeof(std::size_t) > blocksize ? sizeof(std::size_t) : blocksize; - for (std::size_t i = 0; i < nrBlocks - 1; ++i) { - *reinterpret_cast(b) = b + adjustedBlocksize; - b += adjustedBlocksize; - } - *reinterpret_cast(b) = nullptr; - } - - // no copy nor move - Fixed (const Fixed&) = delete; - Fixed& operator= (const Fixed&) = delete; - - void* malloc() { - #if _GLIBCXX_HAS_GTHREADS - const std::lock_guard lockGuard(_mutex); - #endif - if (_head) { - void* retVal = _head; - _head = *reinterpret_cast(_head); - return retVal; - } - return nullptr; - } - - void free(void* ptr) { - if (!ptr) return; - #if _GLIBCXX_HAS_GTHREADS - const std::lock_guard lockGuard(_mutex); - #endif - *reinterpret_cast(ptr) = _head; - _head = reinterpret_cast(ptr); - } - - std::size_t freeMemory() { - #if _GLIBCXX_HAS_GTHREADS - const std::lock_guard lockGuard(_mutex); - #endif - unsigned char* i = _head; - std::size_t retVal = 0; - while (i) { - retVal += blocksize; - i = reinterpret_cast(i)[0]; - } - return retVal; - } - - #ifdef MEMPOL_DEBUG - void print() { - std::size_t adjustedBlocksize = sizeof(std::size_t) > blocksize ? sizeof(std::size_t) : blocksize; - std::cout << "+--------------------" << std::endl; - std::cout << "|start:" << reinterpret_cast(_buffer) << std::endl; - std::cout << "|blocks:" << nrBlocks << std::endl; - std::cout << "|blocksize:" << adjustedBlocksize << std::endl; - std::cout << "|head: " << reinterpret_cast(_head) << std::endl; - unsigned char* currentBlock = _buffer; - - for (std::size_t i = 0; i < nrBlocks; ++i) { - std::cout << "|" << i + 1 << ": " << reinterpret_cast(currentBlock) << std::endl; - if (_isFree(currentBlock)) { - std::cout << "| free" << std::endl; - std::cout << "| next: " << reinterpret_cast(*reinterpret_cast(currentBlock)) << std::endl; - } else { - std::cout << "| allocated" << std::endl; - } - currentBlock += adjustedBlocksize; - } - std::cout << "+--------------------" << std::endl; - } - - bool _isFree(const unsigned char* ptr) { - unsigned char* b = _head; - while (b) { - if (b == ptr) return true; - b = *reinterpret_cast(b); - } - return false; - } - #endif - - private: - unsigned char _buffer[nrBlocks * (sizeof(std::size_t) > blocksize ? sizeof(std::size_t) : blocksize)]; - unsigned char* _head; - #if _GLIBCXX_HAS_GTHREADS - std::mutex _mutex; - #endif -}; - -} // end namespace MemoryPool diff --git a/lib/espMqttClient/src/MemoryPool/src/MemoryPool.h b/lib/espMqttClient/src/MemoryPool/src/MemoryPool.h deleted file mode 100644 index 5b198ea..0000000 --- a/lib/espMqttClient/src/MemoryPool/src/MemoryPool.h +++ /dev/null @@ -1,12 +0,0 @@ -/* -Copyright (c) 2024 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include "Variable.h" -#include "Fixed.h" diff --git a/lib/espMqttClient/src/MemoryPool/src/Variable.h b/lib/espMqttClient/src/MemoryPool/src/Variable.h deleted file mode 100644 index 563bf49..0000000 --- a/lib/espMqttClient/src/MemoryPool/src/Variable.h +++ /dev/null @@ -1,242 +0,0 @@ -/* -Copyright (c) 2024 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include // std::size_t -#include // assert -#if _GLIBCXX_HAS_GTHREADS -#include // NOLINT [build/c++11] std::mutex, std::lock_guard -#else -#warning "The memory pool is not thread safe" -#endif - -#ifdef MEMPOL_DEBUG -#include -#endif - -namespace MemoryPool { - -template -class Variable { - public: - Variable() - : _buffer{0} - , _head(nullptr) - #ifdef MEMPOL_DEBUG - , _bufferSize(0) - #endif - { - std::size_t _normBlocksize = blocksize / sizeof(BlockHeader) + ((blocksize % sizeof(BlockHeader)) ? 1 : 0); - size_t nrBlocksToAlloc = nrBlocks * (_normBlocksize + 1); - BlockHeader* h = reinterpret_cast(_buffer); - h->next = nullptr; - h->size = nrBlocksToAlloc; - _head = h; - - #ifdef MEMPOL_DEBUG - _bufferSize = nrBlocksToAlloc; - #endif - } - - // no copy nor move - Variable (const Variable&) = delete; - Variable& operator= (const Variable&) = delete; - - void* malloc(size_t size) { - #if _GLIBCXX_HAS_GTHREADS - const std::lock_guard lockGuard(_mutex); - #endif - if (size == 0) return nullptr; - - size = (size / sizeof(BlockHeader) + (size % sizeof(BlockHeader) != 0)) + 1; // count by BlockHeader size, add 1 for header - - #ifdef MEMPOL_DEBUG - std::cout << "malloc (raw) " << size << std::endl; - std::cout << "malloc (adj) " << size << " - "; - #endif - - BlockHeader* currentBlock = _head; - BlockHeader* previousBlock = nullptr; - void* retVal = nullptr; - - // iterate through linked free blocks - while (currentBlock) { - // consume whole block is size equals required size - if (currentBlock->size == size) { - if (previousBlock) previousBlock->next = currentBlock->next; - break; - - // split block if size is larger and add second part to list of free blocks - } else if (currentBlock->size > size) { - BlockHeader* newBlock = currentBlock + size; - if (previousBlock) previousBlock->next = newBlock; - newBlock->next = currentBlock->next; - newBlock->size = currentBlock->size - size; - currentBlock->next = newBlock; - break; - } - previousBlock = currentBlock; - currentBlock = currentBlock->next; - } - - if (currentBlock) { - if (currentBlock == _head) { - _head = currentBlock->next; - } - currentBlock->size = size; - currentBlock->next = nullptr; // used when freeing memory - retVal = currentBlock + 1; - #ifdef MEMPOL_DEBUG - std::cout << "ok" << std::endl; - #endif - } else { - #ifdef MEMPOL_DEBUG - std::cout << "nok" << std::endl; - #endif - (void)0; - } - - return retVal; - } - - void free(void* ptr) { - if (!ptr) return; - // check if ptr points to region in _buffer - - #ifdef MEMPOL_DEBUG - std::cout << "free " << static_cast(reinterpret_cast(ptr) - 1) << std::endl; - #endif - - #if _GLIBCXX_HAS_GTHREADS - const std::lock_guard lockGuard(_mutex); - #endif - - BlockHeader* toFree = reinterpret_cast(ptr) - 1; - BlockHeader* previous = reinterpret_cast(_buffer); - BlockHeader* next = _head; - - // toFree is the only free block - if (!next) { - _head = toFree; - return; - } - - while (previous) { - if (!next || toFree < next) { - // 1. add block to linked list of free blocks - if (toFree < _head) { - toFree->next = _head; - _head = toFree; - } else { - previous->next = toFree; - toFree->next = next; - } - - // 2. merge with previous if adjacent - if (toFree > _head && toFree == previous + previous->size) { - previous->size += toFree->size; - previous->next = toFree->next; - toFree = previous; // used in next check - } - - // 3. merge with next if adjacent - if (toFree + toFree->size == next) { - toFree->size += next->size; - toFree->next = next->next; - } - - // 4. done - return; - } - previous = next; - next = next->next; - } - } - - std::size_t freeMemory() { - #if _GLIBCXX_HAS_GTHREADS - const std::lock_guard lockGuard(_mutex); - #endif - size_t retVal = 0; - BlockHeader* currentBlock = reinterpret_cast(_head); - - while (currentBlock) { - retVal += currentBlock->size - 1; - currentBlock = currentBlock->next; - } - - return retVal * sizeof(BlockHeader); - } - - std::size_t maxBlockSize() { - #if _GLIBCXX_HAS_GTHREADS - const std::lock_guard lockGuard(_mutex); - #endif - size_t retVal = 0; - BlockHeader* currentBlock = reinterpret_cast(_head); - - while (currentBlock) { - retVal = (currentBlock->size - 1 > retVal) ? currentBlock->size - 1 : retVal; - currentBlock = currentBlock->next; - } - - return retVal * sizeof(BlockHeader); - } - - #ifdef MEMPOL_DEBUG - void print() { - std::cout << "+--------------------" << std::endl; - std::cout << "|start:" << static_cast(_buffer) << std::endl; - std::cout << "|size:" << _bufferSize << std::endl; - std::cout << "|headersize:" << sizeof(BlockHeader) << std::endl; - std::cout << "|head: " << static_cast(_head) << std::endl; - BlockHeader* nextFreeBlock = _head; - BlockHeader* currentBlock = reinterpret_cast(_buffer); - size_t blockNumber = 1; - while (currentBlock < reinterpret_cast(_buffer) + _bufferSize) { - std::cout << "|" << blockNumber << ": " << static_cast(currentBlock) << std::endl; - std::cout << "| " << static_cast(currentBlock->next) << std::endl; - std::cout << "| " << currentBlock->size << std::endl; - if (currentBlock == nextFreeBlock) { - std::cout << "| free" << std::endl; - nextFreeBlock = nextFreeBlock->next; - } else { - std::cout << "| allocated" << std::endl; - } - ++blockNumber; - currentBlock += currentBlock->size; - } - std::cout << "+--------------------" << std::endl; - } - #endif - - private: - struct BlockHeader { - BlockHeader* next; - std::size_t size; - }; - /* - pool size is aligned to sizeof(BlockHeader). - requested blocksize is therefore multiple of blockheader (rounded up) - total size = nr requested blocks * multiplier * blockheadersize - - see constructor for calculation - */ - unsigned char _buffer[(nrBlocks * ((blocksize / sizeof(BlockHeader) + ((blocksize % sizeof(BlockHeader)) ? 1 : 0)) + 1)) * sizeof(BlockHeader)]; - BlockHeader* _head; - #if _GLIBCXX_HAS_GTHREADS - std::mutex _mutex; - #endif - - #ifdef MEMPOL_DEBUG - std::size_t _bufferSize; - #endif -}; - -} // end namespace MemoryPool diff --git a/lib/espMqttClient/src/MqttClient.cpp b/lib/espMqttClient/src/MqttClient.cpp deleted file mode 100644 index dc21f74..0000000 --- a/lib/espMqttClient/src/MqttClient.cpp +++ /dev/null @@ -1,746 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "MqttClient.h" - -using espMqttClientInternals::Packet; -using espMqttClientInternals::PacketType; -using espMqttClientTypes::DisconnectReason; -using espMqttClientTypes::Error; - -MqttClient::MqttClient(espMqttClientTypes::UseInternalTask useInternalTask, uint8_t priority, uint8_t core) -: _useInternalTask(useInternalTask) -, _transport(nullptr) -, _onConnectCallback(nullptr) -, _onDisconnectCallback(nullptr) -, _onSubscribeCallback(nullptr) -, _onUnsubscribeCallback(nullptr) -, _onMessageCallback(nullptr) -, _onPublishCallback(nullptr) -, _onErrorCallback(nullptr) -, _clientId(nullptr) -, _ip() -, _host(nullptr) -, _port(1883) -, _useIp(false) -, _keepAlive(15000) -, _cleanSession(true) -, _username(nullptr) -, _password(nullptr) -, _willTopic(nullptr) -, _willPayload(nullptr) -, _willPayloadLength(0) -, _willQos(0) -, _willRetain(false) -, _timeout(EMC_TX_TIMEOUT) -, _state(State::disconnected) -, _generatedClientId{0} -, _packetId(0) -#if defined(ARDUINO_ARCH_ESP32) -, _xSemaphore(nullptr) -, _taskHandle(nullptr) -#endif -, _rxBuffer{0} -, _outbox() -, _bytesSent(0) -, _parser() -, _lastClientActivity(0) -, _lastServerActivity(0) -, _pingSent(false) -, _disconnectReason(DisconnectReason::TCP_DISCONNECTED) -#if defined(ARDUINO_ARCH_ESP32) && ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO -, _highWaterMark(4294967295) -#endif - { - EMC_GENERATE_CLIENTID(_generatedClientId); -#if defined(ARDUINO_ARCH_ESP32) - _xSemaphore = xSemaphoreCreateMutex(); - EMC_SEMAPHORE_GIVE(); // release before first use - if (_useInternalTask == espMqttClientTypes::UseInternalTask::YES) { - xTaskCreatePinnedToCore((TaskFunction_t)_loop, "mqttclient", EMC_TASK_STACK_SIZE, this, priority, &_taskHandle, core); - } -#else - (void) useInternalTask; - (void) priority; - (void) core; -#endif - _clientId = _generatedClientId; -} - -MqttClient::~MqttClient() { - disconnect(true); - _clearQueue(2); -#if defined(ARDUINO_ARCH_ESP32) - vSemaphoreDelete(_xSemaphore); - if (_useInternalTask == espMqttClientTypes::UseInternalTask::YES) { - #if EMC_USE_WATCHDOG - esp_task_wdt_delete(_taskHandle); // not sure if this is really needed - #endif - vTaskDelete(_taskHandle); - } -#endif -} - -bool MqttClient::connected() const { - if (_state == State::connected) return true; - return false; -} - -bool MqttClient::disconnected() const { - if (_state == State::disconnected) return true; - return false; -} - -bool MqttClient::connect() { - bool result = false; - if (_state == State::disconnected) { - EMC_SEMAPHORE_TAKE(); - if (_addPacketFront(_cleanSession, - _username, - _password, - _willTopic, - _willRetain, - _willQos, - _willPayload, - _willPayloadLength, - (uint16_t)(_keepAlive / 1000), // 32b to 16b doesn't overflow because it comes from 16b orignally - _clientId)) { - result = true; - _setState(State::connectingTcp1); - #if defined(ARDUINO_ARCH_ESP32) - if (_useInternalTask == espMqttClientTypes::UseInternalTask::YES) { - vTaskResume(_taskHandle); - } - #endif - } else { - emc_log_e("Could not create CONNECT packet"); - EMC_SEMAPHORE_GIVE(); - _onError(0, Error::OUT_OF_MEMORY); - EMC_SEMAPHORE_TAKE(); - } - EMC_SEMAPHORE_GIVE(); - } - return result; -} - -bool MqttClient::disconnect(bool force) { - if (force && _state != State::disconnected && _state != State::disconnectingTcp1 && _state != State::disconnectingTcp2) { - _setState(State::disconnectingTcp1); - return true; - } - if (!force && _state == State::connected) { - _setState(State::disconnectingMqtt1); - return true; - } - return false; -} - -uint16_t MqttClient::publish(const char* topic, uint8_t qos, bool retain, const uint8_t* payload, size_t length) { - #if !EMC_ALLOW_NOT_CONNECTED_PUBLISH - if (_state != State::connected) { - #else - if (_state > State::connected) { - #endif - return 0; - } - EMC_SEMAPHORE_TAKE(); - uint16_t packetId = (qos > 0) ? _getNextPacketId() : 1; - if (!_addPacket(packetId, topic, payload, length, qos, retain)) { - emc_log_e("Could not create PUBLISH packet"); - EMC_SEMAPHORE_GIVE(); - _onError(packetId, Error::OUT_OF_MEMORY); - EMC_SEMAPHORE_TAKE(); - packetId = 0; - } - EMC_SEMAPHORE_GIVE(); - return packetId; -} - -uint16_t MqttClient::publish(const char* topic, uint8_t qos, bool retain, const char* payload) { - size_t len = strlen(payload); - return publish(topic, qos, retain, reinterpret_cast(payload), len); -} - -uint16_t MqttClient::publish(const char* topic, uint8_t qos, bool retain, espMqttClientTypes::PayloadCallback callback, size_t length) { - #if !EMC_ALLOW_NOT_CONNECTED_PUBLISH - if (_state != State::connected) { - #else - if (_state > State::connected) { - #endif - return 0; - } - EMC_SEMAPHORE_TAKE(); - uint16_t packetId = (qos > 0) ? _getNextPacketId() : 1; - if (!_addPacket(packetId, topic, callback, length, qos, retain)) { - emc_log_e("Could not create PUBLISH packet"); - EMC_SEMAPHORE_GIVE(); - _onError(packetId, Error::OUT_OF_MEMORY); - EMC_SEMAPHORE_TAKE(); - packetId = 0; - } - EMC_SEMAPHORE_GIVE(); - return packetId; -} - -void MqttClient::clearQueue(bool deleteSessionData) { - EMC_SEMAPHORE_TAKE(); - _clearQueue(deleteSessionData ? 2 : 0); - EMC_SEMAPHORE_GIVE(); -} - -const char* MqttClient::getClientId() const { - return _clientId; -} - -size_t MqttClient::queueSize() { - size_t ret = 0; - EMC_SEMAPHORE_TAKE(); - ret = _outbox.size(); - EMC_SEMAPHORE_GIVE(); - return ret; -} - -void MqttClient::loop() { - switch (_state) { - case State::disconnected: - #if defined(ARDUINO_ARCH_ESP32) - if (_useInternalTask == espMqttClientTypes::UseInternalTask::YES) { - vTaskSuspend(_taskHandle); - } - #endif - break; - case State::connectingTcp1: - if (_useIp ? _transport->connect(_ip, _port) : _transport->connect(_host, _port)) { - _setState(State::connectingTcp2); - } else { - _setState(State::disconnectingTcp1); - _disconnectReason = DisconnectReason::TCP_DISCONNECTED; - break; - } - // Falling through to speed up connecting on blocking transport 'connect' implementations - [[fallthrough]]; - case State::connectingTcp2: - if (_transport->connected()) { - _parser.reset(); - _lastClientActivity = _lastServerActivity = millis(); - _setState(State::connectingMqtt); - } else if (_transport->disconnected()) { // sync: implemented as "not connected"; async: depending on state of pcb in underlying lib - _setState(State::disconnectingTcp1); - _disconnectReason = DisconnectReason::TCP_DISCONNECTED; - } - break; - case State::connectingMqtt: - #if EMC_WAIT_FOR_CONNACK - if (_transport->connected()) { - EMC_SEMAPHORE_TAKE(); - _sendPacket(); - _checkIncoming(); - _checkPing(); - EMC_SEMAPHORE_GIVE(); - } else { - _setState(State::disconnectingTcp1); - _disconnectReason = DisconnectReason::TCP_DISCONNECTED; - } - break; - #else - // receipt of CONNACK packet will set state to CONNECTED - // client however is allowed to send packets before CONNACK is received - // so we fall through to 'connected' - [[fallthrough]]; - #endif - case State::connected: - [[fallthrough]]; - case State::disconnectingMqtt2: - if (_transport->connected()) { - // CONNECT packet is first in the queue - EMC_SEMAPHORE_TAKE(); - _checkOutbox(); - _checkIncoming(); - _checkPing(); - _checkTimeout(); - EMC_SEMAPHORE_GIVE(); - } else { - _setState(State::disconnectingTcp1); - _disconnectReason = DisconnectReason::TCP_DISCONNECTED; - } - break; - case State::disconnectingMqtt1: - EMC_SEMAPHORE_TAKE(); - if (_outbox.empty()) { - if (!_addPacket(PacketType.DISCONNECT)) { - EMC_SEMAPHORE_GIVE(); - emc_log_e("Could not create DISCONNECT packet"); - _onError(0, Error::OUT_OF_MEMORY); - EMC_SEMAPHORE_TAKE(); - } else { - _setState(State::disconnectingMqtt2); - } - } - _checkOutbox(); - _checkIncoming(); - _checkPing(); - _checkTimeout(); - EMC_SEMAPHORE_GIVE(); - break; - case State::disconnectingTcp1: - _transport->stop(); - _setState(State::disconnectingTcp2); - break; // keep break to accomodate async clients - case State::disconnectingTcp2: - if (_transport->disconnected()) { - EMC_SEMAPHORE_TAKE(); - _clearQueue(0); - EMC_SEMAPHORE_GIVE(); - _bytesSent = 0; - _setState(State::disconnected); - if (_onDisconnectCallback) { - _onDisconnectCallback(_disconnectReason); - } - } - break; - // all cases covered, no default case - } - EMC_YIELD(); - #if defined(ARDUINO_ARCH_ESP32) && ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO - size_t waterMark = uxTaskGetStackHighWaterMark(NULL); - if (waterMark < _highWaterMark) { - _highWaterMark = waterMark; - emc_log_i("Stack usage: %zu/%i", EMC_TASK_STACK_SIZE - _highWaterMark, EMC_TASK_STACK_SIZE); - } - #endif -} - -#if defined(ARDUINO_ARCH_ESP32) -void MqttClient::_loop(MqttClient* c) { - #if EMC_USE_WATCHDOG - if (esp_task_wdt_add(NULL) != ESP_OK) { - emc_log_e("Failed to add async task to WDT"); - } - #endif - for (;;) { - c->loop(); - #if EMC_USE_WATCHDOG - esp_task_wdt_reset(); - #endif - } -} -#endif - -inline void MqttClient::_setState(State newState) { - emc_log_i("state %i --> %i", static_cast::type>(_state.load()), static_cast::type>(newState)); - _state = newState; -} - -uint16_t MqttClient::_getNextPacketId() { - ++_packetId; - if (_packetId == 0) ++_packetId; - return _packetId; -} - -void MqttClient::_checkOutbox() { - while (_sendPacket() > 0) { - if (!_advanceOutbox()) { - break; - } - } -} - -int MqttClient::_sendPacket() { - OutgoingPacket* packet = _outbox.getCurrent(); - - size_t written = 0; - if (packet) { - size_t wantToWrite = packet->packet.available(_bytesSent); - if (wantToWrite == 0) { - return 0; - } - written = _transport->write(packet->packet.data(_bytesSent), wantToWrite); - packet->timeSent = millis(); - _lastClientActivity = millis(); - _bytesSent += written; - emc_log_i("tx %zu/%zu (%02x)", _bytesSent, packet->packet.size(), packet->packet.packetType()); - } - return written; -} - -bool MqttClient::_advanceOutbox() { - OutgoingPacket* packet = _outbox.getCurrent(); - if (packet && _bytesSent == packet->packet.size()) { - if ((packet->packet.packetType()) == PacketType.DISCONNECT) { - _setState(State::disconnectingTcp1); - _disconnectReason = DisconnectReason::USER_OK; - } - if (packet->packet.removable()) { - _outbox.removeCurrent(); - } else { - // we already set 'dup' here, in case we have to retry - if ((packet->packet.packetType()) == PacketType.PUBLISH) packet->packet.setDup(); - _outbox.next(); - } - packet = _outbox.getCurrent(); - _bytesSent = 0; - } - return packet; -} - -void MqttClient::_checkIncoming() { - int32_t remainingBufferLength = _transport->read(_rxBuffer, EMC_RX_BUFFER_SIZE); - if (remainingBufferLength > 0) { - _lastServerActivity = millis(); - emc_log_i("rx len %i", remainingBufferLength); - size_t bytesParsed = 0; - size_t index = 0; - while (remainingBufferLength > 0) { - espMqttClientInternals::ParserResult result = _parser.parse(&_rxBuffer[index], remainingBufferLength, &bytesParsed); - if (result == espMqttClientInternals::ParserResult::packet) { - espMqttClientInternals::MQTTPacketType packetType = _parser.getPacket().fixedHeader.packetType & 0xF0; - if (_state == State::connectingMqtt && packetType != PacketType.CONNACK) { - emc_log_w("Disconnecting, expected CONNACK - protocol error"); - _setState(State::disconnectingTcp1); - return; - } - switch (packetType) { - case PacketType.CONNACK: - _onConnack(); - if (_state != State::connected) { - return; - } - break; - case PacketType.PUBLISH: - if (_state >= State::disconnectingMqtt1) break; // stop processing incoming once user has called disconnect - _onPublish(); - break; - case PacketType.PUBACK: - _onPuback(); - break; - case PacketType.PUBREC: - _onPubrec(); - break; - case PacketType.PUBREL: - _onPubrel(); - break; - case PacketType.PUBCOMP: - _onPubcomp(); - break; - case PacketType.SUBACK: - _onSuback(); - break; - case PacketType.UNSUBACK: - _onUnsuback(); - break; - case PacketType.PINGRESP: - _pingSent = false; - break; - } - } else if (result == espMqttClientInternals::ParserResult::protocolError) { - emc_log_w("Disconnecting, protocol error"); - _setState(State::disconnectingTcp1); - _disconnectReason = DisconnectReason::TCP_DISCONNECTED; - return; - } - remainingBufferLength -= bytesParsed; - index += bytesParsed; - emc_log_i("Parsed %zu - remaining %i", bytesParsed, remainingBufferLength); - bytesParsed = 0; - } - } -} - -void MqttClient::_checkPing() { - if (_keepAlive == 0) return; // keepalive is disabled - - uint32_t currentMillis = millis(); - - // disconnect when server was inactive for twice the keepalive time - if (currentMillis - _lastServerActivity > 2 * _keepAlive) { - emc_log_w("Disconnecting, server exceeded keepalive"); - _setState(State::disconnectingTcp1); - _disconnectReason = DisconnectReason::TCP_DISCONNECTED; - return; - } - - // send ping when client was inactive during the keepalive time - // or when server hasn't responded within keepalive time (typically due to QOS 0) - if (!_pingSent && - ((currentMillis - _lastClientActivity > _keepAlive) || - (currentMillis - _lastServerActivity > _keepAlive))) { - if (!_addPacket(PacketType.PINGREQ)) { - emc_log_e("Could not create PING packet"); - return; - } - _pingSent = true; - } -} - -void MqttClient::_checkTimeout() { - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - // check that we're not busy sending - // don't check when first item hasn't been sent yet - if (it && _bytesSent == 0 && it.get() != _outbox.getCurrent()) { - if (millis() - it.get()->timeSent > _timeout) { - emc_log_w("Packet ack timeout, retrying"); - _outbox.resetCurrent(); - } - } -} - -void MqttClient::_onConnack() { - if (_parser.getPacket().variableHeader.fixed.connackVarHeader.returnCode == 0x00) { - _pingSent = false; // reset after keepalive timeout disconnect - _setState(State::connected); - _advanceOutbox(); - if (_parser.getPacket().variableHeader.fixed.connackVarHeader.sessionPresent == 0) { - _clearQueue(1); - } - if (_onConnectCallback) { - EMC_SEMAPHORE_GIVE(); - _onConnectCallback(_parser.getPacket().variableHeader.fixed.connackVarHeader.sessionPresent); - EMC_SEMAPHORE_TAKE(); - } - } else { - _setState(State::disconnectingTcp1); - // cast is safe because the parser already checked for a valid return code - _disconnectReason = static_cast(_parser.getPacket().variableHeader.fixed.connackVarHeader.returnCode); - } -} - -void MqttClient::_onPublish() { - const espMqttClientInternals::IncomingPacket& p = _parser.getPacket(); - uint8_t qos = p.qos(); - bool retain = p.retain(); - bool dup = p.dup(); - uint16_t packetId = p.variableHeader.fixed.packetId; - bool callback = true; - if (qos == 1) { - if (p.payload.index + p.payload.length == p.payload.total) { - if (!_addPacket(PacketType.PUBACK, packetId)) { - emc_log_e("Could not create PUBACK packet"); - } - } - } else if (qos == 2) { - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - while (it) { - if ((it.get()->packet.packetType()) == PacketType.PUBREC && it.get()->packet.packetId() == packetId) { - callback = false; - emc_log_e("QoS2 packet previously delivered"); - break; - } - ++it; - } - if (p.payload.index + p.payload.length == p.payload.total) { - if (!_addPacket(PacketType.PUBREC, packetId)) { - emc_log_e("Could not create PUBREC packet"); - } - } - } - if (callback && _onMessageCallback) { - EMC_SEMAPHORE_GIVE(); - _onMessageCallback({qos, dup, retain, packetId}, - p.variableHeader.topic, - p.payload.data, - p.payload.length, - p.payload.index, - p.payload.total); - EMC_SEMAPHORE_TAKE(); - } -} - -void MqttClient::_onPuback() { - bool callback = false; - uint16_t idToMatch = _parser.getPacket().variableHeader.fixed.packetId; - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - while (it) { - // PUBACKs come in the order PUBs are sent. So we only check the first PUB packet in outbox - // if it doesn't match the ID, return - if ((it.get()->packet.packetType()) == PacketType.PUBLISH) { - if (it.get()->packet.packetId() == idToMatch) { - callback = true; - _outbox.remove(it); - break; - } - emc_log_w("Received out of order PUBACK"); - break; - } - ++it; - } - if (callback) { - if (_onPublishCallback) { - EMC_SEMAPHORE_GIVE(); - _onPublishCallback(idToMatch); - EMC_SEMAPHORE_TAKE(); - } - } else { - emc_log_w("No matching PUBLISH packet found"); - } -} - -void MqttClient::_onPubrec() { - bool success = false; - uint16_t idToMatch = _parser.getPacket().variableHeader.fixed.packetId; - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - while (it) { - // PUBRECs come in the order PUBs are sent. So we only check the first PUB packet in outbox - // if it doesn't match the ID, return - if ((it.get()->packet.packetType()) == PacketType.PUBLISH) { - if (it.get()->packet.packetId() == idToMatch) { - if (!_addPacket(PacketType.PUBREL, idToMatch)) { - emc_log_e("Could not create PUBREL packet"); - } - _outbox.remove(it); - success = true; - break; - } - emc_log_w("Received out of order PUBREC"); - break; - } - ++it; - } - if (!success) { - emc_log_w("No matching PUBLISH packet found"); - } -} - -void MqttClient::_onPubrel() { - bool success = false; - uint16_t idToMatch = _parser.getPacket().variableHeader.fixed.packetId; - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - while (it) { - // PUBRELs come in the order PUBRECs are sent. So we only check the first PUBREC packet in outbox - // if it doesn't match the ID, return - if ((it.get()->packet.packetType()) == PacketType.PUBREC) { - if (it.get()->packet.packetId() == idToMatch) { - if (!_addPacket(PacketType.PUBCOMP, idToMatch)) { - emc_log_e("Could not create PUBCOMP packet"); - } - _outbox.remove(it); - success = true; - break; - } - emc_log_w("Received out of order PUBREL"); - break; - } - ++it; - } - if (!success) { - emc_log_w("No matching PUBREC packet found"); - } -} - -void MqttClient::_onPubcomp() { - bool callback = false; - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - uint16_t idToMatch = _parser.getPacket().variableHeader.fixed.packetId; - while (it) { - // PUBCOMPs come in the order PUBRELs are sent. So we only check the first PUBREL packet in outbox - // if it doesn't match the ID, return - if ((it.get()->packet.packetType()) == PacketType.PUBREL) { - if (it.get()->packet.packetId() == idToMatch) { - callback = true; - _outbox.remove(it); - break; - } - emc_log_w("Received out of order PUBCOMP"); - break; - } - ++it; - } - if (callback) { - if (_onPublishCallback) { - EMC_SEMAPHORE_GIVE(); - _onPublishCallback(idToMatch); - EMC_SEMAPHORE_TAKE(); - } - } else { - emc_log_w("No matching PUBREL packet found"); - } -} - -void MqttClient::_onSuback() { - bool callback = false; - uint16_t idToMatch = _parser.getPacket().variableHeader.fixed.packetId; - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - while (it) { - if (((it.get()->packet.packetType()) == PacketType.SUBSCRIBE) && it.get()->packet.packetId() == idToMatch) { - callback = true; - _outbox.remove(it); - break; - } - ++it; - } - if (callback) { - if (_onSubscribeCallback) { - EMC_SEMAPHORE_GIVE(); - _onSubscribeCallback(idToMatch, reinterpret_cast(_parser.getPacket().payload.data), _parser.getPacket().payload.total); - EMC_SEMAPHORE_TAKE(); - } - } else { - emc_log_w("received SUBACK without SUB"); - } -} - -void MqttClient::_onUnsuback() { - bool callback = false; - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - uint16_t idToMatch = _parser.getPacket().variableHeader.fixed.packetId; - while (it) { - if (it.get()->packet.packetId() == idToMatch) { - callback = true; - _outbox.remove(it); - break; - } - ++it; - } - if (callback) { - if (_onUnsubscribeCallback) { - EMC_SEMAPHORE_GIVE(); - _onUnsubscribeCallback(idToMatch); - EMC_SEMAPHORE_TAKE(); - } - } else { - emc_log_w("received UNSUBACK without UNSUB"); - } -} - -void MqttClient::_clearQueue(int clearData) { - emc_log_i("clearing queue (clear session: %d)", clearData); - espMqttClientInternals::Outbox::Iterator it = _outbox.front(); - if (clearData == 0) { - // keep PUB (qos > 0, aka packetID != 0), PUBREC and PUBREL - // Spec only mentions PUB and PUBREL but this lib implements method B from point 4.3.3 (Fig. 4.3) - // and stores the packet id in the PUBREC packet. So we also must keep PUBREC. - while (it) { - espMqttClientInternals::MQTTPacketType type = it.get()->packet.packetType(); - if (type == PacketType.PUBREC || - type == PacketType.PUBREL || - (type == PacketType.PUBLISH && it.get()->packet.packetId() != 0)) { - ++it; - } else { - _outbox.remove(it); - } - } - } else if (clearData == 1) { - // keep PUB - while (it) { - if (it.get()->packet.packetType() == PacketType.PUBLISH) { - ++it; - } else { - _outbox.remove(it); - } - } - } else { // clearData == 2 - while (it) { - _outbox.remove(it); - } - } -} - -void MqttClient::_onError(uint16_t packetId, espMqttClientTypes::Error error) { - if (_onErrorCallback) { - _onErrorCallback(packetId, error); - } -} diff --git a/lib/espMqttClient/src/MqttClient.h b/lib/espMqttClient/src/MqttClient.h deleted file mode 100644 index eaf9d2d..0000000 --- a/lib/espMqttClient/src/MqttClient.h +++ /dev/null @@ -1,201 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -API is based on the original work of Marvin Roger: -https://github.com/marvinroger/async-mqtt-client - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include -#include - -#include "Helpers.h" -#include "Config.h" -#include "TypeDefs.h" -#include "Logging.h" -#include "Outbox.h" -#include "Packets/Packet.h" -#include "Packets/Parser.h" -#include "Transport/Transport.h" - -class MqttClient { - public: - virtual ~MqttClient(); - bool connected() const; - bool disconnected() const; - bool connect(); - bool disconnect(bool force = false); - template - uint16_t subscribe(const char* topic, uint8_t qos, Args&&... args) { - uint16_t packetId = 0; - if (_state != State::connected) { - return packetId; - } else { - EMC_SEMAPHORE_TAKE(); - packetId = _getNextPacketId(); - if (!_addPacket(packetId, topic, qos, std::forward(args) ...)) { - emc_log_e("Could not create SUBSCRIBE packet"); - packetId = 0; - } - EMC_SEMAPHORE_GIVE(); - } - return packetId; - } - template - uint16_t unsubscribe(const char* topic, Args&&... args) { - uint16_t packetId = 0; - if (_state != State::connected) { - return packetId; - } else { - EMC_SEMAPHORE_TAKE(); - packetId = _getNextPacketId(); - if (!_addPacket(packetId, topic, std::forward(args) ...)) { - emc_log_e("Could not create UNSUBSCRIBE packet"); - packetId = 0; - } - EMC_SEMAPHORE_GIVE(); - } - return packetId; - } - uint16_t publish(const char* topic, uint8_t qos, bool retain, const uint8_t* payload, size_t length); - uint16_t publish(const char* topic, uint8_t qos, bool retain, const char* payload); - uint16_t publish(const char* topic, uint8_t qos, bool retain, espMqttClientTypes::PayloadCallback callback, size_t length); - void clearQueue(bool deleteSessionData = false); // Not MQTT compliant and may cause unpredictable results when `deleteSessionData` = true! - const char* getClientId() const; - size_t queueSize(); // No const because of mutex - void loop(); - - protected: - explicit MqttClient(espMqttClientTypes::UseInternalTask useInternalTask, uint8_t priority = 1, uint8_t core = 1); - espMqttClientTypes::UseInternalTask _useInternalTask; - espMqttClientInternals::Transport* _transport; - - espMqttClientTypes::OnConnectCallback _onConnectCallback; - espMqttClientTypes::OnDisconnectCallback _onDisconnectCallback; - espMqttClientTypes::OnSubscribeCallback _onSubscribeCallback; - espMqttClientTypes::OnUnsubscribeCallback _onUnsubscribeCallback; - espMqttClientTypes::OnMessageCallback _onMessageCallback; - espMqttClientTypes::OnPublishCallback _onPublishCallback; - espMqttClientTypes::OnErrorCallback _onErrorCallback; - typedef void(*mqttClientHook)(void*); - const char* _clientId; - IPAddress _ip; - const char* _host; - uint16_t _port; - bool _useIp; - uint32_t _keepAlive; - bool _cleanSession; - const char* _username; - const char* _password; - const char* _willTopic; - const uint8_t* _willPayload; - uint16_t _willPayloadLength; - uint8_t _willQos; - bool _willRetain; - uint32_t _timeout; - - // state is protected to allow state changes by the transport system, defined in child classes - // eg. to allow AsyncTCP - enum class State { - disconnected = 0, - connectingTcp1 = 1, - connectingTcp2 = 2, - connectingMqtt = 3, - connected = 4, - disconnectingMqtt1 = 5, - disconnectingMqtt2 = 6, - disconnectingTcp1 = 7, - disconnectingTcp2 = 8 - }; - std::atomic _state; - inline void _setState(State newState); - - private: - char _generatedClientId[EMC_CLIENTID_LENGTH]; - uint16_t _packetId; - -#if defined(ARDUINO_ARCH_ESP32) - SemaphoreHandle_t _xSemaphore; - TaskHandle_t _taskHandle; - static void _loop(MqttClient* c); -#elif defined(ARDUINO_ARCH_ESP8266) && EMC_ESP8266_MULTITHREADING - std::atomic _xSemaphore = false; -#elif defined(__linux__) - std::mutex mtx; -#endif - - uint8_t _rxBuffer[EMC_RX_BUFFER_SIZE]; - struct OutgoingPacket { - uint32_t timeSent; - espMqttClientInternals::Packet packet; - template - OutgoingPacket(uint32_t t, espMqttClientTypes::Error& error, Args&&... args) : // NOLINT(runtime/references) - timeSent(t), - packet(error, std::forward(args) ...) {} - }; - espMqttClientInternals::Outbox _outbox; - size_t _bytesSent; - espMqttClientInternals::Parser _parser; - uint32_t _lastClientActivity; - uint32_t _lastServerActivity; - bool _pingSent; - espMqttClientTypes::DisconnectReason _disconnectReason; - - uint16_t _getNextPacketId(); - - template - bool _addPacket(Args&&... args) { - espMqttClientTypes::Error error(espMqttClientTypes::Error::SUCCESS); - espMqttClientInternals::Outbox::Iterator it = _outbox.emplace(0, error, std::forward(args) ...); - if (it && error == espMqttClientTypes::Error::SUCCESS) { - return true; - } else { - if (it) _outbox.remove(it); - return false; - } - } - - template - bool _addPacketFront(Args&&... args) { - espMqttClientTypes::Error error(espMqttClientTypes::Error::SUCCESS); - espMqttClientInternals::Outbox::Iterator it = _outbox.emplaceFront(0, error, std::forward(args) ...); - if (it && error == espMqttClientTypes::Error::SUCCESS) { - return true; - } else { - if (it) _outbox.remove(it); - return false; - } - } - - void _checkOutbox(); - int _sendPacket(); - bool _advanceOutbox(); - void _checkIncoming(); - void _checkPing(); - void _checkTimeout(); - - void _onConnack(); - void _onPublish(); - void _onPuback(); - void _onPubrec(); - void _onPubrel(); - void _onPubcomp(); - void _onSuback(); - void _onUnsuback(); - - void _clearQueue(int clearData); // 0: keep session, - // 1: keep only PUBLISH qos > 0 - // 2: delete all - void _onError(uint16_t packetId, espMqttClientTypes::Error error); - - #if defined(ARDUINO_ARCH_ESP32) - #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO - size_t _highWaterMark; - #endif - #endif -}; diff --git a/lib/espMqttClient/src/MqttClientSetup.h b/lib/espMqttClient/src/MqttClientSetup.h deleted file mode 100644 index 4ef7307..0000000 --- a/lib/espMqttClient/src/MqttClientSetup.h +++ /dev/null @@ -1,245 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -API is based on the original work of Marvin Roger: -https://github.com/marvinroger/async-mqtt-client - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include "MqttClient.h" - -#if EMC_MULTIPLE_CALLBACKS -#include -#include -#endif - -template -class MqttClientSetup : public MqttClient { - public: - T& setKeepAlive(uint16_t keepAlive) { - _keepAlive = keepAlive * 1000; // s to ms conversion, will also do 16 to 32 bit conversion - return static_cast(*this); - } - - T& setClientId(const char* clientId) { - _clientId = clientId; - return static_cast(*this); - } - - T& setCleanSession(bool cleanSession) { - _cleanSession = cleanSession; - return static_cast(*this); - } - - T& setCredentials(const char* username, const char* password) { - _username = username; - _password = password; - return static_cast(*this); - } - - T& setWill(const char* topic, uint8_t qos, bool retain, const uint8_t* payload, size_t length) { - _willTopic = topic; - _willQos = qos; - _willRetain = retain; - _willPayload = payload; - if (!_willPayload) { - _willPayloadLength = 0; - } else { - _willPayloadLength = length; - } - return static_cast(*this); - } - - T& setWill(const char* topic, uint8_t qos, bool retain, const char* payload) { - return setWill(topic, qos, retain, reinterpret_cast(payload), strlen(payload)); - } - - T& setServer(IPAddress ip, uint16_t port) { - _ip = ip; - _port = port; - _useIp = true; - return static_cast(*this); - } - - T& setServer(const char* host, uint16_t port) { - _host = host; - _port = port; - _useIp = false; - return static_cast(*this); - } - - T& setTimeout(uint16_t timeout) { - _timeout = timeout * 1000; // s to ms conversion, will also do 16 to 32 bit conversion - return static_cast(*this); - } - - T& onConnect(espMqttClientTypes::OnConnectCallback callback, uint32_t id = 0) { - #if EMC_MULTIPLE_CALLBACKS - _onConnectCallbacks.emplace_back(callback, id); - #else - (void) id; - _onConnectCallback = callback; - #endif - return static_cast(*this); - } - - T& onDisconnect(espMqttClientTypes::OnDisconnectCallback callback, uint32_t id = 0) { - #if EMC_MULTIPLE_CALLBACKS - _onDisconnectCallbacks.emplace_back(callback, id); - #else - (void) id; - _onDisconnectCallback = callback; - #endif - return static_cast(*this); - } - - T& onSubscribe(espMqttClientTypes::OnSubscribeCallback callback, uint32_t id = 0) { - #if EMC_MULTIPLE_CALLBACKS - _onSubscribeCallbacks.emplace_back(callback, id); - #else - (void) id; - _onSubscribeCallback = callback; - #endif - return static_cast(*this); - } - - T& onUnsubscribe(espMqttClientTypes::OnUnsubscribeCallback callback, uint32_t id = 0) { - #if EMC_MULTIPLE_CALLBACKS - _onUnsubscribeCallbacks.emplace_back(callback, id); - #else - (void) id; - _onUnsubscribeCallback = callback; - #endif - return static_cast(*this); - } - - T& onMessage(espMqttClientTypes::OnMessageCallback callback, uint32_t id = 0) { - #if EMC_MULTIPLE_CALLBACKS - _onMessageCallbacks.emplace_back(callback, id); - #else - (void) id; - _onMessageCallback = callback; - #endif - return static_cast(*this); - } - - T& onPublish(espMqttClientTypes::OnPublishCallback callback, uint32_t id = 0) { - #if EMC_MULTIPLE_CALLBACKS - _onPublishCallbacks.emplace_back(callback, id); - #else - (void) id; - _onPublishCallback = callback; - #endif - return static_cast(*this); - } - - #if EMC_MULTIPLE_CALLBACKS - T& removeOnConnect(uint32_t id) { - for (auto it = _onConnectCallbacks.begin(); it != _onConnectCallbacks.end(); ++it) { - if (it->second == id) { - _onConnectCallbacks.erase(it); - break; - } - } - return static_cast(*this); - } - - T& removeOnDisconnect(uint32_t id) { - for (auto it = _onDisconnectCallbacks.begin(); it != _onDisconnectCallbacks.end(); ++it) { - if (it->second == id) { - _onDisconnectCallbacks.erase(it); - break; - } - } - return static_cast(*this); - } - - T& removeOnSubscribe(uint32_t id) { - for (auto it = _onSubscribeCallbacks.begin(); it != _onSubscribeCallbacks.end(); ++it) { - if (it->second == id) { - _onSubscribeCallbacks.erase(it); - break; - } - } - return static_cast(*this); - } - - T& removeOnUnsubscribe(uint32_t id) { - for (auto it = _onUnsubscribeCallbacks.begin(); it != _onUnsubscribeCallbacks.end(); ++it) { - if (it->second == id) { - _onUnsubscribeCallbacks.erase(it); - break; - } - } - return static_cast(*this); - } - - T& removeOnMessage(uint32_t id) { - for (auto it = _onMessageCallbacks.begin(); it != _onMessageCallbacks.end(); ++it) { - if (it->second == id) { - _onMessageCallbacks.erase(it); - break; - } - } - return static_cast(*this); - } - - T& removeOnPublish(uint32_t id) { - for (auto it = _onPublishCallbacks.begin(); it != _onPublishCallbacks.end(); ++it) { - if (it->second == id) { - _onPublishCallbacks.erase(it); - break; - } - } - return static_cast(*this); - } - #endif - - /* - T& onError(espMqttClientTypes::OnErrorCallback callback) { - _onErrorCallback = callback; - return static_cast(*this); - } - */ - - protected: - explicit MqttClientSetup(espMqttClientTypes::UseInternalTask useInternalTask, uint8_t priority = 1, uint8_t core = 1) - : MqttClient(useInternalTask, priority, core) { - #if EMC_MULTIPLE_CALLBACKS - _onConnectCallback = [this](bool sessionPresent) { - for (auto callback : _onConnectCallbacks) if (callback.first) callback.first(sessionPresent); - }; - _onDisconnectCallback = [this](espMqttClientTypes::DisconnectReason reason) { - for (auto callback : _onDisconnectCallbacks) if (callback.first) callback.first(reason); - }; - _onSubscribeCallback = [this](uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* returncodes, size_t len) { - for (auto callback : _onSubscribeCallbacks) if (callback.first) callback.first(packetId, returncodes, len); - }; - _onUnsubscribeCallback = [this](int16_t packetId) { - for (auto callback : _onUnsubscribeCallbacks) if (callback.first) callback.first(packetId); - }; - _onMessageCallback = [this](const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - for (auto callback : _onMessageCallbacks) if (callback.first) callback.first(properties, topic, payload, len, index, total); - }; - _onPublishCallback = [this](uint16_t packetId) { - for (auto callback : _onPublishCallbacks) if (callback.first) callback.first(packetId); - }; - #else - // empty - #endif - } - - #if EMC_MULTIPLE_CALLBACKS - std::list> _onConnectCallbacks; - std::list> _onDisconnectCallbacks; - std::list> _onSubscribeCallbacks; - std::list> _onUnsubscribeCallbacks; - std::list> _onMessageCallbacks; - std::list> _onPublishCallbacks; - #endif -}; diff --git a/lib/espMqttClient/src/Outbox.h b/lib/espMqttClient/src/Outbox.h deleted file mode 100644 index 4f9971c..0000000 --- a/lib/espMqttClient/src/Outbox.h +++ /dev/null @@ -1,255 +0,0 @@ - -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if EMC_USE_MEMPOOL - #include "MemoryPool/src/MemoryPool.h" - #include "Config.h" -#else - #include // new (std::nothrow) -#endif -#include // std::forward - -namespace espMqttClientInternals { - -/** - * @brief Singly linked queue with builtin non-invalidating forward iterator - * - * Queue items can only be emplaced, at front and back of the queue. - * Remove items using an iterator or the builtin iterator. - */ - -template -class Outbox { - public: - Outbox() - : _first(nullptr) - , _last(nullptr) - , _current(nullptr) - , _prev(nullptr) - #if EMC_USE_MEMPOOL - , _memPool() - #endif - {} - ~Outbox() { - while (_first) { - Node* n = _first->next; - #if EMC_USE_MEMPOOL - _first->~Node(); - _memPool.free(_first); - #else - delete _first; - #endif - _first = n; - } - } - - struct Node { - public: - template - explicit Node(Args&&... args) - : data(std::forward(args) ...) - , next(nullptr) { - // empty - } - - T data; - Node* next; - }; - - class Iterator { - friend class Outbox; - public: - void operator++() { - if (_node) { - _prev = _node; - _node = _node->next; - } - } - - explicit operator bool() const { - if (_node) return true; - return false; - } - - T* get() const { - if (_node) return &(_node->data); - return nullptr; - } - - private: - Node* _node = nullptr; - Node* _prev = nullptr; - }; - - // add node to back, advance current to new if applicable - template - Iterator emplace(Args&&... args) { - Iterator it; - #if EMC_USE_MEMPOOL - void* buf = _memPool.malloc(); - Node* node = nullptr; - if (buf) { - node = new(buf) Node(std::forward(args) ...); - } - #else - Node* node = new(std::nothrow) Node(std::forward(args) ...); - #endif - if (node != nullptr) { - if (!_first) { - // queue is empty - _first = _current = node; - } else { - // queue has at least one item - _last->next = node; - it._prev = _last; - } - _last = node; - it._node = node; - // point current to newly created if applicable - if (!_current) { - _current = _last; - } - } - return it; - } - - // add item to front, current points to newly created front. - template - Iterator emplaceFront(Args&&... args) { - Iterator it; - #if EMC_USE_MEMPOOL - void* buf = _memPool.malloc(); - Node* node = nullptr; - if (buf) { - node = new(buf) Node(std::forward(args) ...); - } - #else - Node* node = new(std::nothrow) Node(std::forward(args) ...); - #endif - if (node != nullptr) { - if (!_first) { - // queue is empty - _last = node; - } else { - // queue has at least one item - node->next = _first; - } - _current = _first = node; - _prev = nullptr; - it._node = node; - } - return it; - } - - // remove node at iterator, iterator points to next - void remove(Iterator& it) { // NOLINT(runtime/references) - if (!it) return; - Node* node = it._node; - Node* prev = it._prev; - ++it; - _remove(prev, node); - } - - // remove current node, current points to next - void removeCurrent() { - _remove(_prev, _current); - } - - // Get current item or return nullptr - T* getCurrent() const { - if (_current) return &(_current->data); - return nullptr; - } - - void resetCurrent() { - _current = _first; - } - - Iterator front() const { - Iterator it; - it._node = _first; - return it; - } - - // Advance current item - void next() { - if (_current) { - _prev = _current; - _current = _current->next; - } - } - - // Outbox is empty - bool empty() { - if (!_first) return true; - return false; - } - - size_t size() const { - Node* n = _first; - size_t count = 0; - while (n) { - n = n->next; - ++count; - } - return count; - } - - private: - Node* _first; - Node* _last; - Node* _current; - Node* _prev; // element just before _current - #if EMC_USE_MEMPOOL - MemoryPool::Fixed _memPool; - #endif - - void _remove(Node* prev, Node* node) { - if (!node) return; - - // set current to next, node->next may be nullptr - if (_current == node) { - _current = node->next; - } - - if (_prev == node) { - _prev = prev; - } - - // only one element in outbox - if (_first == _last) { - _first = _last = nullptr; - - // delete first el in longer outbox - } else if (_first == node) { - _first = node->next; - - // delete last in longer outbox - } else if (_last == node) { - _last = prev; - _last->next = nullptr; - - // delete somewhere in the middle - } else { - prev->next = node->next; - } - - // finally, delete the node - #if EMC_USE_MEMPOOL - node->~Node(); - _memPool.free(node); - #else - delete node; - #endif - } -}; - -} // end namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/Constants.h b/lib/espMqttClient/src/Packets/Constants.h deleted file mode 100644 index ee92e31..0000000 --- a/lib/espMqttClient/src/Packets/Constants.h +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -Parts are based on the original work of Marvin Roger: -https://github.com/marvinroger/async-mqtt-client - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include - -namespace espMqttClientInternals { - -constexpr const char PROTOCOL[] = "MQTT"; -constexpr const uint8_t PROTOCOL_LEVEL = 0b00000100; - -typedef uint8_t MQTTPacketType; - -constexpr struct { - const uint8_t RESERVED1 = 0; - const uint8_t CONNECT = 1 << 4; - const uint8_t CONNACK = 2 << 4; - const uint8_t PUBLISH = 3 << 4; - const uint8_t PUBACK = 4 << 4; - const uint8_t PUBREC = 5 << 4; - const uint8_t PUBREL = 6 << 4; - const uint8_t PUBCOMP = 7 << 4; - const uint8_t SUBSCRIBE = 8 << 4; - const uint8_t SUBACK = 9 << 4; - const uint8_t UNSUBSCRIBE = 10 << 4; - const uint8_t UNSUBACK = 11 << 4; - const uint8_t PINGREQ = 12 << 4; - const uint8_t PINGRESP = 13 << 4; - const uint8_t DISCONNECT = 14 << 4; - const uint8_t RESERVED2 = 1 << 4; -} PacketType; - -constexpr struct { - const uint8_t CONNECT_RESERVED = 0x00; - const uint8_t CONNACK_RESERVED = 0x00; - const uint8_t PUBLISH_DUP = 0x08; - const uint8_t PUBLISH_QOS0 = 0x00; - const uint8_t PUBLISH_QOS1 = 0x02; - const uint8_t PUBLISH_QOS2 = 0x04; - const uint8_t PUBLISH_QOSRESERVED = 0x06; - const uint8_t PUBLISH_RETAIN = 0x01; - const uint8_t PUBACK_RESERVED = 0x00; - const uint8_t PUBREC_RESERVED = 0x00; - const uint8_t PUBREL_RESERVED = 0x02; - const uint8_t PUBCOMP_RESERVED = 0x00; - const uint8_t SUBSCRIBE_RESERVED = 0x02; - const uint8_t SUBACK_RESERVED = 0x00; - const uint8_t UNSUBSCRIBE_RESERVED = 0x02; - const uint8_t UNSUBACK_RESERVED = 0x00; - const uint8_t PINGREQ_RESERVED = 0x00; - const uint8_t PINGRESP_RESERVED = 0x00; - const uint8_t DISCONNECT_RESERVED = 0x00; - const uint8_t RESERVED2_RESERVED = 0x00; -} HeaderFlag; - -constexpr struct { - const uint8_t USERNAME = 0x80; - const uint8_t PASSWORD = 0x40; - const uint8_t WILL_RETAIN = 0x20; - const uint8_t WILL_QOS0 = 0x00; - const uint8_t WILL_QOS1 = 0x08; - const uint8_t WILL_QOS2 = 0x10; - const uint8_t WILL = 0x04; - const uint8_t CLEAN_SESSION = 0x02; - const uint8_t RESERVED = 0x00; -} ConnectFlag; - -} // end namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/Packet.cpp b/lib/espMqttClient/src/Packets/Packet.cpp deleted file mode 100644 index 14d241b..0000000 --- a/lib/espMqttClient/src/Packets/Packet.cpp +++ /dev/null @@ -1,454 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "Packet.h" - -namespace espMqttClientInternals { - -#if EMC_USE_MEMPOOL -MemoryPool::Variable Packet::_memPool; -#endif - -Packet::~Packet() { - #if EMC_USE_MEMPOOL - _memPool.free(_data); - #else - free(_data); - #endif -} - -size_t Packet::available(size_t index) { - if (index >= _size) return 0; - if (!_getPayload) return _size - index; - return _chunkedAvailable(index); -} - -const uint8_t* Packet::data(size_t index) const { - if (!_getPayload) { - if (!_data) return nullptr; - if (index >= _size) return nullptr; - return &_data[index]; - } - return _chunkedData(index); -} - -size_t Packet::size() const { - return _size; -} - -void Packet::setDup() { - if (!_data) return; - if (packetType() != PacketType.PUBLISH) return; - if (_packetId == 0) return; - _data[0] |= 0x08; -} - -uint16_t Packet::packetId() const { - return _packetId; -} - -MQTTPacketType Packet::packetType() const { - if (_data) return static_cast(_data[0] & 0xF0); - return static_cast(0); -} - -bool Packet::removable() const { - if (_packetId == 0) return true; - if ((packetType() == PacketType.PUBACK) || (packetType() == PacketType.PUBCOMP)) return true; - return false; -} - -Packet::Packet(espMqttClientTypes::Error& error, - bool cleanSession, - const char* username, - const char* password, - const char* willTopic, - bool willRetain, - uint8_t willQos, - const uint8_t* willPayload, - uint16_t willPayloadLength, - uint16_t keepAlive, - const char* clientId) -: _packetId(0) -, _data(nullptr) -, _size(0) -, _payloadIndex(0) -, _payloadStartIndex(0) -, _payloadEndIndex(0) -, _getPayload(nullptr) { - if (willPayload && willPayloadLength == 0) { - size_t length = strlen(reinterpret_cast(willPayload)); - if (length > UINT16_MAX) { - emc_log_w("Payload length truncated (l:%zu)", length); - willPayloadLength = UINT16_MAX; - } else { - willPayloadLength = length; - } - } - if (!clientId || strlen(clientId) == 0) { - emc_log_w("clientId not set error"); - error = espMqttClientTypes::Error::MALFORMED_PARAMETER; - return; - } - - // Calculate size - size_t remainingLength = - 6 + // protocol - 1 + // protocol level - 1 + // connect flags - 2 + // keepalive - 2 + strlen(clientId) + - (willTopic ? 2 + strlen(willTopic) + 2 + willPayloadLength : 0) + - (username ? 2 + strlen(username) : 0) + - (password ? 2 + strlen(password) : 0); - - // allocate memory - if (!_allocate(remainingLength, false)) { - error = espMqttClientTypes::Error::OUT_OF_MEMORY; - return; - } - - // serialize - size_t pos = 0; - - // FIXED HEADER - _data[pos++] = PacketType.CONNECT | HeaderFlag.CONNECT_RESERVED; - pos += encodeRemainingLength(remainingLength, &_data[pos]); - pos += encodeString(PROTOCOL, &_data[pos]); - _data[pos++] = PROTOCOL_LEVEL; - uint8_t connectFlags = 0; - if (cleanSession) connectFlags |= espMqttClientInternals::ConnectFlag.CLEAN_SESSION; - if (username != nullptr) connectFlags |= espMqttClientInternals::ConnectFlag.USERNAME; - if (password != nullptr) connectFlags |= espMqttClientInternals::ConnectFlag.PASSWORD; - if (willTopic != nullptr) { - connectFlags |= espMqttClientInternals::ConnectFlag.WILL; - if (willRetain) connectFlags |= espMqttClientInternals::ConnectFlag.WILL_RETAIN; - switch (willQos) { - case 0: - connectFlags |= espMqttClientInternals::ConnectFlag.WILL_QOS0; - break; - case 1: - connectFlags |= espMqttClientInternals::ConnectFlag.WILL_QOS1; - break; - case 2: - connectFlags |= espMqttClientInternals::ConnectFlag.WILL_QOS2; - break; - } - } - _data[pos++] = connectFlags; - _data[pos++] = keepAlive >> 8; - _data[pos++] = keepAlive & 0xFF; - - // PAYLOAD - // client ID - pos += encodeString(clientId, &_data[pos]); - // will - if (willTopic != nullptr && willPayload != nullptr) { - pos += encodeString(willTopic, &_data[pos]); - _data[pos++] = willPayloadLength >> 8; - _data[pos++] = willPayloadLength & 0xFF; - memcpy(&_data[pos], willPayload, willPayloadLength); - pos += willPayloadLength; - } - // credentials - if (username != nullptr) pos += encodeString(username, &_data[pos]); - if (password != nullptr) encodeString(password, &_data[pos]); - - error = espMqttClientTypes::Error::SUCCESS; -} - -Packet::Packet(espMqttClientTypes::Error& error, - uint16_t packetId, - const char* topic, - const uint8_t* payload, - size_t payloadLength, - uint8_t qos, - bool retain) -: _packetId(packetId) -, _data(nullptr) -, _size(0) -, _payloadIndex(0) -, _payloadStartIndex(0) -, _payloadEndIndex(0) -, _getPayload(nullptr) { - size_t remainingLength = - 2 + strlen(topic) + // topic length + topic - 2 + // packet ID - payloadLength; - - if (qos == 0) { - remainingLength -= 2; - _packetId = 0; - } - - if (!_allocate(remainingLength, true)) { - error = espMqttClientTypes::Error::OUT_OF_MEMORY; - return; - } - - size_t pos = _fillPublishHeader(packetId, topic, remainingLength, qos, retain); - - // PAYLOAD - memcpy(&_data[pos], payload, payloadLength); - - error = espMqttClientTypes::Error::SUCCESS; -} - -Packet::Packet(espMqttClientTypes::Error& error, - uint16_t packetId, - const char* topic, - espMqttClientTypes::PayloadCallback payloadCallback, - size_t payloadLength, - uint8_t qos, - bool retain) -: _packetId(packetId) -, _data(nullptr) -, _size(0) -, _payloadIndex(0) -, _payloadStartIndex(0) -, _payloadEndIndex(0) -, _getPayload(payloadCallback) { - size_t remainingLength = - 2 + strlen(topic) + // topic length + topic - 2 + // packet ID - payloadLength; - - if (qos == 0) { - remainingLength -= 2; - _packetId = 0; - } - - if (!_allocate(remainingLength - payloadLength + std::min(payloadLength, static_cast(EMC_RX_BUFFER_SIZE)), true)) { - error = espMqttClientTypes::Error::OUT_OF_MEMORY; - return; - } - - size_t pos = _fillPublishHeader(packetId, topic, remainingLength, qos, retain); - - // payload will be added by 'Packet::available' - _size = pos + payloadLength; - _payloadIndex = pos; - _payloadStartIndex = _payloadIndex; - _payloadEndIndex = _payloadIndex; - - error = espMqttClientTypes::Error::SUCCESS; -} - -Packet::Packet(espMqttClientTypes::Error& error, uint16_t packetId, const char* topic, uint8_t qos) -: _packetId(packetId) -, _data(nullptr) -, _size(0) -, _payloadIndex(0) -, _payloadStartIndex(0) -, _payloadEndIndex(0) -, _getPayload(nullptr) { - SubscribeItem list[1] = {topic, qos}; - _createSubscribe(error, list, 1); -} - -Packet::Packet(espMqttClientTypes::Error& error, MQTTPacketType type, uint16_t packetId) -: _packetId(packetId) -, _data(nullptr) -, _size(0) -, _payloadIndex(0) -, _payloadStartIndex(0) -, _payloadEndIndex(0) -, _getPayload(nullptr) { - if (!_allocate(2, true)) { - error = espMqttClientTypes::Error::OUT_OF_MEMORY; - return; - } - - size_t pos = 0; - _data[pos] = type; - if (type == PacketType.PUBREL) { - _data[pos++] |= HeaderFlag.PUBREL_RESERVED; - } else { - pos++; - } - pos += encodeRemainingLength(2, &_data[pos]); - _data[pos++] = packetId >> 8; - _data[pos] = packetId & 0xFF; - - error = espMqttClientTypes::Error::SUCCESS; -} - -Packet::Packet(espMqttClientTypes::Error& error, uint16_t packetId, const char* topic) -: _packetId(packetId) -, _data(nullptr) -, _size(0) -, _payloadIndex(0) -, _payloadStartIndex(0) -, _payloadEndIndex(0) -, _getPayload(nullptr) { - const char* list[1] = {topic}; - _createUnsubscribe(error, list, 1); -} - -Packet::Packet(espMqttClientTypes::Error& error, MQTTPacketType type) -: _packetId(0) -, _data(nullptr) -, _size(0) -, _payloadIndex(0) -, _payloadStartIndex(0) -, _payloadEndIndex(0) -, _getPayload(nullptr) { - if (!_allocate(0, true)) { - error = espMqttClientTypes::Error::OUT_OF_MEMORY; - return; - } - _data[0] |= type; - - error = espMqttClientTypes::Error::SUCCESS; -} - - -bool Packet::_allocate(size_t remainingLength, bool check) { - #if EMC_USE_MEMPOOL - (void) check; - #else - if (check && EMC_GET_FREE_MEMORY() < EMC_MIN_FREE_MEMORY) { - emc_log_w("Packet buffer not allocated: low memory"); - return false; - } - #endif - _size = 1 + remainingLengthLength(remainingLength) + remainingLength; - #if EMC_USE_MEMPOOL - _data = reinterpret_cast(_memPool.malloc(_size)); - #else - _data = reinterpret_cast(malloc(_size)); - #endif - if (!_data) { - _size = 0; - emc_log_w("Alloc failed (l:%zu)", _size); - return false; - } - emc_log_i("Alloc (l:%zu)", _size); - memset(_data, 0, _size); - return true; -} - -size_t Packet::_fillPublishHeader(uint16_t packetId, - const char* topic, - size_t remainingLength, - uint8_t qos, - bool retain) { - size_t index = 0; - - // FIXED HEADER - _data[index] = PacketType.PUBLISH; - if (retain) _data[index] |= HeaderFlag.PUBLISH_RETAIN; - if (qos == 0) { - _data[index++] |= HeaderFlag.PUBLISH_QOS0; - } else if (qos == 1) { - _data[index++] |= HeaderFlag.PUBLISH_QOS1; - } else if (qos == 2) { - _data[index++] |= HeaderFlag.PUBLISH_QOS2; - } - index += encodeRemainingLength(remainingLength, &_data[index]); - - // VARIABLE HEADER - index += encodeString(topic, &_data[index]); - if (qos > 0) { - _data[index++] = packetId >> 8; - _data[index++] = packetId & 0xFF; - } - - return index; -} - -void Packet::_createSubscribe(espMqttClientTypes::Error& error, - SubscribeItem* list, - size_t numberTopics) { - // Calculate size - size_t payload = 0; - for (size_t i = 0; i < numberTopics; ++i) { - payload += 2 + strlen(list[i].topic) + 1; // length bytes, string, qos - } - size_t remainingLength = 2 + payload; // packetId + payload - - // allocate memory - if (!_allocate(remainingLength, true)) { - error = espMqttClientTypes::Error::OUT_OF_MEMORY; - return; - } - - // serialize - size_t pos = 0; - _data[pos++] = PacketType.SUBSCRIBE | HeaderFlag.SUBSCRIBE_RESERVED; - pos += encodeRemainingLength(remainingLength, &_data[pos]); - _data[pos++] = _packetId >> 8; - _data[pos++] = _packetId & 0xFF; - for (size_t i = 0; i < numberTopics; ++i) { - pos += encodeString(list[i].topic, &_data[pos]); - _data[pos++] = list[i].qos; - } - - error = espMqttClientTypes::Error::SUCCESS; -} - -void Packet::_createUnsubscribe(espMqttClientTypes::Error& error, - const char** list, - size_t numberTopics) { - // Calculate size - size_t payload = 0; - for (size_t i = 0; i < numberTopics; ++i) { - payload += 2 + strlen(list[i]); // length bytes, string - } - size_t remainingLength = 2 + payload; // packetId + payload - - // allocate memory - if (!_allocate(remainingLength, true)) { - error = espMqttClientTypes::Error::OUT_OF_MEMORY; - return; - } - - // serialize - size_t pos = 0; - _data[pos++] = PacketType.UNSUBSCRIBE | HeaderFlag.UNSUBSCRIBE_RESERVED; - pos += encodeRemainingLength(remainingLength, &_data[pos]); - _data[pos++] = _packetId >> 8; - _data[pos++] = _packetId & 0xFF; - for (size_t i = 0; i < numberTopics; ++i) { - pos += encodeString(list[i], &_data[pos]); - } - - error = espMqttClientTypes::Error::SUCCESS; -} - -size_t Packet::_chunkedAvailable(size_t index) { - // index vs size check done in 'available(index)' - - // index points to header or first payload byte - if (index < _payloadIndex) { - if (_size > _payloadIndex && _payloadEndIndex != 0) { - size_t copied = _getPayload(&_data[_payloadIndex], std::min(static_cast(EMC_TX_BUFFER_SIZE), _size - _payloadStartIndex), index); - _payloadStartIndex = _payloadIndex; - _payloadEndIndex = _payloadStartIndex + copied - 1; - } - - // index points to payload unavailable - } else if (index > _payloadEndIndex || _payloadStartIndex > index) { - _payloadStartIndex = index; - size_t copied = _getPayload(&_data[_payloadIndex], std::min(static_cast(EMC_TX_BUFFER_SIZE), _size - _payloadStartIndex), index); - _payloadEndIndex = _payloadStartIndex + copied - 1; - } - - // now index points to header or payload available - return _payloadEndIndex - index + 1; -} - -const uint8_t* Packet::_chunkedData(size_t index) const { - // CAUTION!! available(index) has to be called first to check available data and possibly fill payloadbuffer - if (index < _payloadIndex) { - return &_data[index]; - } - return &_data[index - _payloadStartIndex + _payloadIndex]; -} - -} // end namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/Packet.h b/lib/espMqttClient/src/Packets/Packet.h deleted file mode 100644 index 5d0b67b..0000000 --- a/lib/espMqttClient/src/Packets/Packet.h +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include -#include - -#include "Constants.h" -#include "../Config.h" -#include "../TypeDefs.h" -#include "../Helpers.h" -#include "../Logging.h" -#include "RemainingLength.h" -#include "StringUtil.h" - -#if EMC_USE_MEMPOOL - #include "MemoryPool/src/MemoryPool.h" -#endif - -namespace espMqttClientInternals { - -class Packet { - public: - ~Packet(); - size_t available(size_t index); - const uint8_t* data(size_t index) const; - - size_t size() const; - void setDup(); - uint16_t packetId() const; - MQTTPacketType packetType() const; - bool removable() const; - - protected: - uint16_t _packetId; // save as separate variable: will be accessed frequently - uint8_t* _data; - size_t _size; - - // variables for chunked payload handling - size_t _payloadIndex; - size_t _payloadStartIndex; - size_t _payloadEndIndex; - espMqttClientTypes::PayloadCallback _getPayload; - - struct SubscribeItem { - const char* topic; - uint8_t qos; - }; - - public: - // CONNECT - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - bool cleanSession, - const char* username, - const char* password, - const char* willTopic, - bool willRetain, - uint8_t willQos, - const uint8_t* willPayload, - uint16_t willPayloadLength, - uint16_t keepAlive, - const char* clientId); - // PUBLISH - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - uint16_t packetId, - const char* topic, - const uint8_t* payload, - size_t payloadLength, - uint8_t qos, - bool retain); - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - uint16_t packetId, - const char* topic, - espMqttClientTypes::PayloadCallback payloadCallback, - size_t payloadLength, - uint8_t qos, - bool retain); - // SUBSCRIBE - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - uint16_t packetId, - const char* topic, - uint8_t qos); - template - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - uint16_t packetId, - const char* topic1, - uint8_t qos1, - const char* topic2, - uint8_t qos2, - Args&& ... args) - : _packetId(packetId) - , _data(nullptr) - , _size(0) - , _payloadIndex(0) - , _payloadStartIndex(0) - , _payloadEndIndex(0) - , _getPayload(nullptr) { - static_assert(sizeof...(Args) % 2 == 0, "Subscribe should be in topic/qos pairs"); - size_t numberTopics = 2 + (sizeof...(Args) / 2); - SubscribeItem list[numberTopics] = {topic1, qos1, topic2, qos2, args...}; - _createSubscribe(error, list, numberTopics); - } - // UNSUBSCRIBE - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - uint16_t packetId, - const char* topic); - template - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - uint16_t packetId, - const char* topic1, - const char* topic2, - Args&& ... args) - : _packetId(packetId) - , _data(nullptr) - , _size(0) - , _payloadIndex(0) - , _payloadStartIndex(0) - , _payloadEndIndex(0) - , _getPayload(nullptr) { - size_t numberTopics = 2 + sizeof...(Args); - const char* list[numberTopics] = {topic1, topic2, args...}; - _createUnsubscribe(error, list, numberTopics); - } - // PUBACK, PUBREC, PUBREL, PUBCOMP - Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - MQTTPacketType type, - uint16_t packetId); - // PING, DISCONN - explicit Packet(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - MQTTPacketType type); - - private: - // pass remainingLength = total size - header - remainingLengthLength! - bool _allocate(size_t remainingLength, bool check); - - // fills header and returns index of next available byte in buffer - size_t _fillPublishHeader(uint16_t packetId, - const char* topic, - size_t remainingLength, - uint8_t qos, - bool retain); - void _createSubscribe(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - SubscribeItem* list, - size_t numberTopics); - void _createUnsubscribe(espMqttClientTypes::Error& error, // NOLINT(runtime/references) - const char** list, - size_t numberTopics); - - size_t _chunkedAvailable(size_t index); - const uint8_t* _chunkedData(size_t index) const; - - #if EMC_USE_MEMPOOL - static MemoryPool::Variable _memPool; - #endif -}; - -} // end namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/Parser.cpp b/lib/espMqttClient/src/Packets/Parser.cpp deleted file mode 100644 index 07998a3..0000000 --- a/lib/espMqttClient/src/Packets/Parser.cpp +++ /dev/null @@ -1,316 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "Parser.h" - -namespace espMqttClientInternals { - -uint8_t IncomingPacket::qos() const { - if ((fixedHeader.packetType & 0xF0) != PacketType.PUBLISH) return 0; - return (fixedHeader.packetType & 0x06) >> 1; // mask 0x00000110 -} - -bool IncomingPacket::retain() const { - if ((fixedHeader.packetType & 0xF0) != PacketType.PUBLISH) return 0; - return fixedHeader.packetType & 0x01; // mask 0x00000001 -} - -bool IncomingPacket::dup() const { - if ((fixedHeader.packetType & 0xF0) != PacketType.PUBLISH) return 0; - return fixedHeader.packetType & 0x08; // mask 0x00001000 -} - -void IncomingPacket::reset() { - fixedHeader.packetType = 0; - variableHeader.topicLength = 0; - variableHeader.fixed.packetId = 0; - payload.index = 0; - payload.length = 0; -} - -Parser::Parser() -: _data(nullptr) -, _len(0) -, _bytesRead(0) -, _bytePos(0) -, _parse(_fixedHeader) -, _packet() -, _payloadBuffer{0} { - // empty -} - -ParserResult Parser::parse(const uint8_t* data, size_t len, size_t* bytesRead) { - _data = data; - _len = len; - _bytesRead = 0; - ParserResult result = ParserResult::awaitData; - while (result == ParserResult::awaitData && _bytesRead < _len) { - result = _parse(this); - ++_bytesRead; - } - (*bytesRead) += _bytesRead; - return result; -} - -const IncomingPacket& Parser::getPacket() const { - return _packet; -} - -void Parser::reset() { - _parse = _fixedHeader; - _bytesRead = 0; - _bytePos = 0; - _packet.reset(); -} - -ParserResult Parser::_fixedHeader(Parser* p) { - p->_packet.reset(); - p->_packet.fixedHeader.packetType = p->_data[p->_bytesRead]; - - // keep PUBLISH out of the switch and handle in separate if/else - if ((p->_packet.fixedHeader.packetType & 0xF0) == PacketType.PUBLISH) { - uint8_t headerFlags = p->_packet.fixedHeader.packetType & 0x0F; - /* flags can be: 0b0000 --> no dup, qos 0, no retain - 0x0001 --> no dup, qos 0, retain - 0x0010 --> no dup, qos 1, no retain - 0x0011 --> no dup, qos 1, retain - 0x0100 --> no dup, qos 2, no retain - 0x0101 --> no dup, qos 2, retain - 0x1010 --> dup, qos 1, no retain - 0x1011 --> dup, qos 1, retain - 0x1100 --> dup, qos 2, no retain - 0x1101 --> dup, qos 2, retain - */ - if (headerFlags <= 0x05 || headerFlags >= 0x0A) { - p->_parse = _remainingLengthVariable; - p->_bytePos = 0; - } else { - emc_log_w("Invalid packet header: 0x%02x", p->_packet.fixedHeader.packetType); - return ParserResult::protocolError; - } - } else { - switch (p->_packet.fixedHeader.packetType) { - case PacketType.CONNACK | HeaderFlag.CONNACK_RESERVED: - case PacketType.PUBACK | HeaderFlag.PUBACK_RESERVED: - case PacketType.PUBREC | HeaderFlag.PUBREC_RESERVED: - case PacketType.PUBREL | HeaderFlag.PUBREL_RESERVED: - case PacketType.PUBCOMP | HeaderFlag.PUBCOMP_RESERVED: - case PacketType.UNSUBACK | HeaderFlag.UNSUBACK_RESERVED: - p->_parse = _remainingLengthFixed; - break; - case PacketType.SUBACK | HeaderFlag.SUBACK_RESERVED: - p->_parse = _remainingLengthVariable; - p->_bytePos = 0; - break; - case PacketType.PINGRESP | HeaderFlag.PINGRESP_RESERVED: - p->_parse = _remainingLengthNone; - break; - default: - emc_log_w("Invalid packet header: 0x%02x", p->_packet.fixedHeader.packetType); - return ParserResult::protocolError; - } - } - emc_log_i("Packet type: 0x%02x", p->_packet.fixedHeader.packetType); - return ParserResult::awaitData; -} - -ParserResult Parser::_remainingLengthFixed(Parser* p) { - p->_packet.fixedHeader.remainingLength.remainingLength = p->_data[p->_bytesRead]; - - if (p->_packet.fixedHeader.remainingLength.remainingLength == 2) { // variable header is 2 bytes long - if ((p->_packet.fixedHeader.packetType & 0xF0) != PacketType.CONNACK) { - p->_parse = _varHeaderPacketId1; - } else { - p->_parse = _varHeaderConnack1; - } - emc_log_i("Remaining length: %zu", p->_packet.fixedHeader.remainingLength.remainingLength); - return ParserResult::awaitData; - } - p->_parse = _fixedHeader; - emc_log_w("Invalid remaining length (fixed): %zu", p->_packet.fixedHeader.remainingLength.remainingLength); - return ParserResult::protocolError; -} - -ParserResult Parser::_remainingLengthVariable(Parser* p) { - p->_packet.fixedHeader.remainingLength.remainingLengthRaw[p->_bytePos] = p->_data[p->_bytesRead]; - if (p->_packet.fixedHeader.remainingLength.remainingLengthRaw[p->_bytePos] & 0x80) { - p->_bytePos++; - if (p->_bytePos == 4) { - emc_log_w("Invalid remaining length (variable)"); - return ParserResult::protocolError; - } else { - return ParserResult::awaitData; - } - } - - // no need to check for negative decoded length, check is already done - p->_packet.fixedHeader.remainingLength.remainingLength = decodeRemainingLength(p->_packet.fixedHeader.remainingLength.remainingLengthRaw); - - if ((p->_packet.fixedHeader.packetType & 0xF0) == PacketType.PUBLISH) { - p->_parse = _varHeaderTopicLength1; - emc_log_i("Remaining length: %zu", p->_packet.fixedHeader.remainingLength.remainingLength); - return ParserResult::awaitData; - } else { - int32_t payloadSize = p->_packet.fixedHeader.remainingLength.remainingLength - 2; // total - packet ID - if (0 < payloadSize && payloadSize < EMC_PAYLOAD_BUFFER_SIZE) { - p->_bytePos = 0; - p->_packet.payload.data = p->_payloadBuffer; - p->_packet.payload.index = 0; - p->_packet.payload.length = payloadSize; - p->_packet.payload.total = payloadSize; - p->_parse = _varHeaderPacketId1; - emc_log_i("Remaining length: %zu", p->_packet.fixedHeader.remainingLength.remainingLength); - return ParserResult::awaitData; - } else { - emc_log_w("Invalid payload length"); - } - } - p->_parse = _fixedHeader; - return ParserResult::protocolError; -} - -ParserResult Parser::_remainingLengthNone(Parser* p) { - p->_packet.fixedHeader.remainingLength.remainingLength = p->_data[p->_bytesRead]; - p->_parse = _fixedHeader; - if (p->_packet.fixedHeader.remainingLength.remainingLength == 0) { - emc_log_i("Remaining length: %zu", p->_packet.fixedHeader.remainingLength.remainingLength); - return ParserResult::packet; - } - emc_log_w("Invalid remaining length (none)"); - return ParserResult::protocolError; -} - -ParserResult Parser::_varHeaderConnack1(Parser* p) { - uint8_t data = p->_data[p->_bytesRead]; - if (data < 2) { // session present flag: equal to 0 or 1 - p->_packet.variableHeader.fixed.connackVarHeader.sessionPresent = data; - p->_parse = _varHeaderConnack2; - return ParserResult::awaitData; - } - p->_parse = _fixedHeader; - emc_log_w("Invalid session flags"); - return ParserResult::protocolError; -} - -ParserResult Parser::_varHeaderConnack2(Parser* p) { - uint8_t data = p->_data[p->_bytesRead]; - p->_parse = _fixedHeader; - if (data <= 5) { // connect return code max is 5 - p->_packet.variableHeader.fixed.connackVarHeader.returnCode = data; - emc_log_i("Packet complete"); - return ParserResult::packet; - } - emc_log_w("Invalid connack return code"); - return ParserResult::protocolError; -} - -ParserResult Parser::_varHeaderPacketId1(Parser* p) { - p->_packet.variableHeader.fixed.packetId |= p->_data[p->_bytesRead] << 8; - p->_parse = _varHeaderPacketId2; - return ParserResult::awaitData; -} - -ParserResult Parser::_varHeaderPacketId2(Parser* p) { - p->_packet.variableHeader.fixed.packetId |= p->_data[p->_bytesRead]; - p->_parse = _fixedHeader; - if (p->_packet.variableHeader.fixed.packetId != 0) { - emc_log_i("Packet variable header complete"); - if ((p->_packet.fixedHeader.packetType & 0xF0) == PacketType.SUBACK) { - p->_parse = _payloadSuback; - return ParserResult::awaitData; - } else if ((p->_packet.fixedHeader.packetType & 0xF0) == PacketType.PUBLISH) { - p->_packet.payload.total -= 2; // substract packet id length from payload - if (p->_packet.payload.total == 0) { - p->_parse = _fixedHeader; - return ParserResult::packet; - } else { - p->_parse = _payloadPublish; - } - return ParserResult::awaitData; - } else { - return ParserResult::packet; - } - } else { - emc_log_w("Invalid packet id"); - return ParserResult::protocolError; - } -} - -ParserResult Parser::_varHeaderTopicLength1(Parser* p) { - p->_packet.variableHeader.topicLength = p->_data[p->_bytesRead] << 8; - p->_parse = _varHeaderTopicLength2; - return ParserResult::awaitData; -} - -ParserResult Parser::_varHeaderTopicLength2(Parser* p) { - p->_packet.variableHeader.topicLength |= p->_data[p->_bytesRead]; - size_t maxTopicLength = - p->_packet.fixedHeader.remainingLength.remainingLength - - 2 // topic length bytes - - ((p->_packet.fixedHeader.packetType & (HeaderFlag.PUBLISH_QOS1 | HeaderFlag.PUBLISH_QOS2)) ? 2 : 0); - if (p->_packet.variableHeader.topicLength <= maxTopicLength) { - p->_parse = _varHeaderTopic; - p->_bytePos = 0; - p->_packet.payload.total = p->_packet.fixedHeader.remainingLength.remainingLength - 2 - p->_packet.variableHeader.topicLength; - return ParserResult::awaitData; - } - emc_log_w("Invalid topic length: %u > %zu", p->_packet.variableHeader.topicLength, maxTopicLength); - p->_parse = _fixedHeader; - return ParserResult::protocolError; -} - -ParserResult Parser::_varHeaderTopic(Parser* p) { - // no checking for character [MQTT-3.3.2-1] [MQTT-3.3.2-2] - p->_packet.variableHeader.topic[p->_bytePos] = static_cast(p->_data[p->_bytesRead]); - p->_bytePos++; - if (p->_bytePos == p->_packet.variableHeader.topicLength || p->_bytePos == EMC_MAX_TOPIC_LENGTH) { - p->_packet.variableHeader.topic[p->_bytePos] = 0x00; // add c-string delimiter - emc_log_i("Packet variable header topic complete"); - if (p->_packet.fixedHeader.packetType & (HeaderFlag.PUBLISH_QOS1 | HeaderFlag.PUBLISH_QOS2)) { - p->_parse = _varHeaderPacketId1; - } else if (p->_packet.payload.total == 0) { - p->_parse = _fixedHeader; - return ParserResult::packet; - } else { - p->_parse = _payloadPublish; - } - } - return ParserResult::awaitData; -} - -ParserResult Parser::_payloadSuback(Parser* p) { - uint8_t data = p->_data[p->_bytesRead]; - if (data < 0x03 || data == 0x80) { - p->_payloadBuffer[p->_bytePos] = data; - p->_bytePos++; - } else { - p->_parse = _fixedHeader; - emc_log_w("Invalid suback return code"); - return ParserResult::protocolError; - } - if (p->_bytePos == p->_packet.payload.total) { - p->_parse = _fixedHeader; - emc_log_i("Packet complete"); - return ParserResult::packet; - } - return ParserResult::awaitData; -} - -ParserResult Parser::_payloadPublish(Parser* p) { - p->_packet.payload.index += p->_packet.payload.length; - p->_packet.payload.data = &p->_data[p->_bytesRead]; - emc_log_i("payload: index %zu, total %zu, avail %zu/%zu", p->_packet.payload.index, p->_packet.payload.total, p->_len - p->_bytesRead, p->_len); - p->_packet.payload.length = std::min(p->_len - p->_bytesRead, p->_packet.payload.total - p->_packet.payload.index); - p->_bytesRead += p->_packet.payload.length - 1; // compensate for increment in _parse-loop - if (p->_packet.payload.index + p->_packet.payload.length == p->_packet.payload.total) { - p->_parse = _fixedHeader; - } - return ParserResult::packet; -} - -} // end namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/Parser.h b/lib/espMqttClient/src/Packets/Parser.h deleted file mode 100644 index 2f6334e..0000000 --- a/lib/espMqttClient/src/Packets/Parser.h +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include -#include -#include - -#include "../Config.h" -#include "Constants.h" -#include "../Logging.h" -#include "RemainingLength.h" - -namespace espMqttClientInternals { - -struct IncomingPacket { - struct __attribute__((__packed__)) { - MQTTPacketType packetType; - union { - size_t remainingLength; - uint8_t remainingLengthRaw[4]; - } remainingLength; - } fixedHeader; - struct __attribute__((__packed__)) { - uint16_t topicLength; - char topic[EMC_MAX_TOPIC_LENGTH + 1]; // + 1 for c-string delimiter - union { - struct { - uint8_t sessionPresent; - uint8_t returnCode; - } connackVarHeader; - uint16_t packetId; - } fixed; - } variableHeader; - struct { - const uint8_t* data; - size_t length; - size_t index; - size_t total; - } payload; - - uint8_t qos() const; - bool retain() const; - bool dup() const; - void reset(); -}; - -enum class ParserResult : uint8_t { - awaitData, - packet, - protocolError -}; - -class Parser; -typedef ParserResult(*ParserFunc)(Parser*); - -class Parser { - public: - Parser(); - ParserResult parse(const uint8_t* data, size_t len, size_t* bytesRead); - const IncomingPacket& getPacket() const; - void reset(); - - private: - // keep data variables in class to avoid copying on every iteration of the parser - const uint8_t* _data; - size_t _len; - size_t _bytesRead; - size_t _bytePos; - ParserFunc _parse; - IncomingPacket _packet; - uint8_t _payloadBuffer[EMC_PAYLOAD_BUFFER_SIZE]; - - static ParserResult _fixedHeader(Parser* p); - static ParserResult _remainingLengthFixed(Parser* p); - static ParserResult _remainingLengthNone(Parser* p); - static ParserResult _remainingLengthVariable(Parser* p); - - - static ParserResult _varHeaderConnack1(Parser* p); - static ParserResult _varHeaderConnack2(Parser* p); - - static ParserResult _varHeaderPacketId1(Parser* p); - static ParserResult _varHeaderPacketId2(Parser* p); - - static ParserResult _varHeaderTopicLength1(Parser* p); - static ParserResult _varHeaderTopicLength2(Parser* p); - static ParserResult _varHeaderTopic(Parser* p); - - static ParserResult _payloadSuback(Parser* p); - static ParserResult _payloadPublish(Parser* p); -}; - -} // end namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/RemainingLength.cpp b/lib/espMqttClient/src/Packets/RemainingLength.cpp deleted file mode 100644 index d8644a3..0000000 --- a/lib/espMqttClient/src/Packets/RemainingLength.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "RemainingLength.h" - -namespace espMqttClientInternals { - -int32_t decodeRemainingLength(const uint8_t* stream) { - uint32_t multiplier = 1; - int32_t remainingLength = 0; - uint8_t currentByte = 0; - uint8_t encodedByte; - - do { - encodedByte = stream[currentByte++]; - remainingLength += (encodedByte & 127) * multiplier; - if (multiplier > 128 * 128 * 128) { - emc_log_e("Malformed Remaining Length"); - return -1; - } - multiplier *= 128; - } while ((encodedByte & 128) != 0); - - return remainingLength; -} - -uint8_t remainingLengthLength(uint32_t remainingLength) { - if (remainingLength < 128) return 1; - if (remainingLength < 16384) return 2; - if (remainingLength < 2097152) return 3; - if (remainingLength > 268435455) return 0; - return 4; -} - -uint8_t encodeRemainingLength(uint32_t remainingLength, uint8_t* destination) { - uint8_t currentByte = 0; - uint8_t bytesNeeded = 0; - - do { - uint8_t encodedByte = remainingLength % 128; - remainingLength /= 128; - if (remainingLength > 0) { - encodedByte = encodedByte | 128; - } - destination[currentByte++] = encodedByte; - bytesNeeded++; - } while (remainingLength > 0); - - return bytesNeeded; -} - -} // namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/RemainingLength.h b/lib/espMqttClient/src/Packets/RemainingLength.h deleted file mode 100644 index 0b84e23..0000000 --- a/lib/espMqttClient/src/Packets/RemainingLength.h +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include - -#include "../Logging.h" - -namespace espMqttClientInternals { - -// Calculations are based on non normative comment in section 2.2.3 Remaining Length of the MQTT specification - -// returns decoded length based on input stream -// stream is expected to contain full encoded remaining length -// return -1 on error. -int32_t decodeRemainingLength(const uint8_t* stream); - - -// returns the number of bytes needed to encode the remaining length -uint8_t remainingLengthLength(uint32_t remainingLength); - -// encodes the given remaining length to destination and returns number of bytes used -// destination is expected to be large enough to hold the number of bytes needed -uint8_t encodeRemainingLength(uint32_t remainingLength, uint8_t* destination); - -} // namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/StringUtil.cpp b/lib/espMqttClient/src/Packets/StringUtil.cpp deleted file mode 100644 index 7cd3dd8..0000000 --- a/lib/espMqttClient/src/Packets/StringUtil.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "StringUtil.h" - -namespace espMqttClientInternals { - -size_t encodeString(const char* source, uint8_t* dest) { - size_t length = strlen(source); - if (length > 65535) { - emc_log_e("String length error"); - return 0; - } - - dest[0] = static_cast(length) >> 8; - dest[1] = static_cast(length) & 0xFF; - memcpy(&dest[2], source, length); - return 2 + length; -} - -} // namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Packets/StringUtil.h b/lib/espMqttClient/src/Packets/StringUtil.h deleted file mode 100644 index 7f1e1e8..0000000 --- a/lib/espMqttClient/src/Packets/StringUtil.h +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include -#include // memcpy - -#include "../Logging.h" - -namespace espMqttClientInternals { - -// encodes the given source string into destination and returns number of bytes used -// destination is expected to be large enough to hold the number of bytes needed -size_t encodeString(const char* source, uint8_t* dest); - -} // namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/Transport/ClientAsync.cpp b/lib/espMqttClient/src/Transport/ClientAsync.cpp deleted file mode 100644 index 4f8d69e..0000000 --- a/lib/espMqttClient/src/Transport/ClientAsync.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#include "ClientAsync.h" - -namespace espMqttClientInternals { - -ClientAsync::ClientAsync() -: client() -, availableData(0) -, bufData(nullptr) { - // empty -} - -bool ClientAsync::connect(IPAddress ip, uint16_t port) { - return client.connect(ip, port); -} - -bool ClientAsync::connect(const char* host, uint16_t port) { - return client.connect(host, port); -} - -size_t ClientAsync::write(const uint8_t* buf, size_t size) { - return client.write(reinterpret_cast(buf), size); -} - -int ClientAsync::read(uint8_t* buf, size_t size) { - size_t willRead = std::min(size, availableData); - memcpy(buf, bufData, std::min(size, availableData)); - if (availableData > size) { - emc_log_w("Buffer is smaller than available data: %zu - %zu", size, availableData); - } - availableData = 0; - return willRead; -} - -void ClientAsync::stop() { - client.close(false); -} - -bool ClientAsync::connected() { - return client.connected(); -} - -bool ClientAsync::disconnected() { - return client.disconnected(); -} - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientAsync.h b/lib/espMqttClient/src/Transport/ClientAsync.h deleted file mode 100644 index c3ddd03..0000000 --- a/lib/espMqttClient/src/Transport/ClientAsync.h +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#pragma once - -#if defined(ARDUINO_ARCH_ESP32) - #include "freertos/FreeRTOS.h" - #include -#elif defined(ARDUINO_ARCH_ESP8266) - #include -#endif - -#include "Transport.h" -#include "../Config.h" -#include "../Logging.h" - -namespace espMqttClientInternals { - -class ClientAsync : public Transport { - public: - ClientAsync(); - bool connect(IPAddress ip, uint16_t port) override; - bool connect(const char* host, uint16_t port) override; - size_t write(const uint8_t* buf, size_t size) override; - int read(uint8_t* buf, size_t size) override; - void stop() override; - bool connected() override; - bool disconnected() override; - - AsyncClient client; - size_t availableData; - uint8_t* bufData; -}; - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientPosix.cpp b/lib/espMqttClient/src/Transport/ClientPosix.cpp deleted file mode 100644 index 4b086d1..0000000 --- a/lib/espMqttClient/src/Transport/ClientPosix.cpp +++ /dev/null @@ -1,130 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "ClientPosix.h" - -#if defined(__linux__) - -namespace espMqttClientInternals { - -ClientPosix::ClientPosix() -: _sockfd(-1) -, _host() { - // empty -} - -ClientPosix::~ClientPosix() { - ClientPosix::stop(); -} - -bool ClientPosix::connect(IPAddress ip, uint16_t port) { - if (connected()) stop(); - - _sockfd = ::socket(AF_INET, SOCK_STREAM, 0); - if (_sockfd < 0) { - emc_log_e("Error %d: \"%s\" opening socket", errno, strerror(errno)); - } - - int flag = 1; - if (setsockopt(_sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int)) < 0) { - emc_log_e("Error %d: \"%s\" disabling nagle", errno, strerror(errno)); - } - - memset(&_host, 0, sizeof(_host)); - _host.sin_family = AF_INET; - _host.sin_addr.s_addr = htonl(uint32_t(ip)); - _host.sin_port = ::htons(port); - - int ret = ::connect(_sockfd, reinterpret_cast(&_host), sizeof(_host)); - - if (ret < 0) { - emc_log_e("Error connecting: %d - (%d) %s", ret, errno, strerror(errno)); - return false; - } - - emc_log_i("Socket connected"); - return true; -} - -bool ClientPosix::connect(const char* hostname, uint16_t port) { - IPAddress ipAddress = _hostToIP(hostname); - if (ipAddress == IPAddress(0)) { - emc_log_e("No such host '%s'", hostname); - return false; - } - return connect(ipAddress, port); -} - -size_t ClientPosix::write(const uint8_t* buf, size_t size) { - return ::send(_sockfd, buf, size, 0); -} - -int ClientPosix::read(uint8_t* buf, size_t size) { - int ret = ::recv(_sockfd, buf, size, MSG_DONTWAIT); - /* - if (ret < 0) { - emc_log_e("Error reading: %s", strerror(errno)); - } - */ - return ret; -} - -void ClientPosix::stop() { - if (_sockfd >= 0) { - ::close(_sockfd); - _sockfd = -1; - } -} - -bool ClientPosix::connected() { - return _sockfd >= 0; -} - -bool ClientPosix::disconnected() { - return _sockfd < 0; -} - -IPAddress ClientPosix::_hostToIP(const char* hostname) { - IPAddress returnIP(0); - struct addrinfo hints, *servinfo, *p; - struct sockaddr_in *h; - int rv; - -// Set up request addrinfo struct - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - emc_log_i("Looking for '%s'", hostname); - -// ask for host data - if ((rv = getaddrinfo(hostname, NULL, &hints, &servinfo)) != 0) { - emc_log_e("getaddrinfo: %s", gai_strerror(rv)); - return returnIP; - } - - // loop through all the results and connect to the first we can - for (p = servinfo; p != NULL; p = p->ai_next) { - h = (struct sockaddr_in *)p->ai_addr; - returnIP = ::htonl(h->sin_addr.s_addr); - if (returnIP != IPAddress(0)) break; - } - // Release allocated memory - freeaddrinfo(servinfo); - - if (returnIP != IPAddress(0)) { - emc_log_i("Host '%s' = %u", hostname, (uint32_t)returnIP); - } else { - emc_log_e("No IP for '%s' found", hostname); - } - return returnIP; -} - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientPosix.h b/lib/espMqttClient/src/Transport/ClientPosix.h deleted file mode 100644 index adffaa1..0000000 --- a/lib/espMqttClient/src/Transport/ClientPosix.h +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(__linux__) - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Transport.h" // includes IPAddress -#include "../Logging.h" - -#ifndef EMC_POSIX_PEEK_SIZE -#define EMC_POSIX_PEEK_SIZE 1500 -#endif - -namespace espMqttClientInternals { - -class ClientPosix : public Transport { - public: - ClientPosix(); - ~ClientPosix(); - bool connect(IPAddress ip, uint16_t port) override; - bool connect(const char* hostname, uint16_t port) override; - size_t write(const uint8_t* buf, size_t size) override; - int read(uint8_t* buf, size_t size) override; - void stop() override; - bool connected() override; - bool disconnected() override; - - protected: - int _sockfd; - sockaddr_in _host; - - IPAddress _hostToIP(const char* hostname); -}; - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientPosixIPAddress.cpp b/lib/espMqttClient/src/Transport/ClientPosixIPAddress.cpp deleted file mode 100644 index c6a78fc..0000000 --- a/lib/espMqttClient/src/Transport/ClientPosixIPAddress.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#if defined(__linux__) - -#include "ClientPosixIPAddress.h" - -IPAddress::IPAddress() -: _address(0) { - // empty -} - -IPAddress::IPAddress(uint8_t p0, uint8_t p1, uint8_t p2, uint8_t p3) -: _address(0) { - _address = (uint32_t)p0 << 24 | (uint32_t)p1 << 16 | (uint32_t)p2 << 8 | p3; -} - -IPAddress::IPAddress(uint32_t address) -: _address(address) { - // empty -} - -IPAddress::operator uint32_t() { - return _address; -} - -bool IPAddress::operator==(IPAddress other) { - return _address == other._address; -} - -bool IPAddress::operator!=(IPAddress other) { - return _address != other._address; -} - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientPosixIPAddress.h b/lib/espMqttClient/src/Transport/ClientPosixIPAddress.h deleted file mode 100644 index 9941ec5..0000000 --- a/lib/espMqttClient/src/Transport/ClientPosixIPAddress.h +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(ARDUINO) - #include -#else - -#include - -class IPAddress { - public: - IPAddress(); - IPAddress(uint8_t p0, uint8_t p1, uint8_t p2, uint8_t p3); - IPAddress(uint32_t address); // NOLINT(runtime/explicit) - operator uint32_t(); - bool operator==(IPAddress other); - bool operator!=(IPAddress other); - - protected: - uint32_t _address; -}; - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientSecureSync.cpp b/lib/espMqttClient/src/Transport/ClientSecureSync.cpp deleted file mode 100644 index 36288c6..0000000 --- a/lib/espMqttClient/src/Transport/ClientSecureSync.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#include "ClientSecureSync.h" -#include // socket options - -namespace espMqttClientInternals { - -ClientSecureSync::ClientSecureSync() -: client() { - // empty -} - -bool ClientSecureSync::connect(IPAddress ip, uint16_t port) { - bool ret = client.connect(ip, port); // implicit conversion of return code int --> bool - if (ret) { - #if defined(ARDUINO_ARCH_ESP8266) - client.setNoDelay(true); - #elif defined(ARDUINO_ARCH_ESP32) - // Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure - int val = true; - client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); - #endif - } - return ret; -} - -bool ClientSecureSync::connect(const char* host, uint16_t port) { - bool ret = client.connect(host, port); // implicit conversion of return code int --> bool - if (ret) { - #if defined(ARDUINO_ARCH_ESP8266) - client.setNoDelay(true); - #elif defined(ARDUINO_ARCH_ESP32) - // Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure - int val = true; - client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); - #endif - } - return ret; -} - -size_t ClientSecureSync::write(const uint8_t* buf, size_t size) { - return client.write(buf, size); -} - -int ClientSecureSync::read(uint8_t* buf, size_t size) { - return client.read(buf, size); -} - -void ClientSecureSync::stop() { - client.stop(); -} - -bool ClientSecureSync::connected() { - return client.connected(); -} - -bool ClientSecureSync::disconnected() { - return !client.connected(); -} - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientSecureSync.h b/lib/espMqttClient/src/Transport/ClientSecureSync.h deleted file mode 100644 index b81681e..0000000 --- a/lib/espMqttClient/src/Transport/ClientSecureSync.h +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#include // includes IPAddress - -#include "Transport.h" - -namespace espMqttClientInternals { - -class ClientSecureSync : public Transport { - public: - ClientSecureSync(); - bool connect(IPAddress ip, uint16_t port) override; - bool connect(const char* host, uint16_t port) override; - size_t write(const uint8_t* buf, size_t size) override; - int read(uint8_t* buf, size_t size) override; - void stop() override; - bool connected() override; - bool disconnected() override; - WiFiClientSecure client; -}; - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientSync.cpp b/lib/espMqttClient/src/Transport/ClientSync.cpp deleted file mode 100644 index b2c4045..0000000 --- a/lib/espMqttClient/src/Transport/ClientSync.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#include "ClientSync.h" -#include // socket options - -namespace espMqttClientInternals { - -ClientSync::ClientSync() -: client() { - // empty -} - -bool ClientSync::connect(IPAddress ip, uint16_t port) { - bool ret = client.connect(ip, port); // implicit conversion of return code int --> bool - if (ret) { - #if defined(ARDUINO_ARCH_ESP8266) - client.setNoDelay(true); - #elif defined(ARDUINO_ARCH_ESP32) - // Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure (for consistency also here) - int val = true; - client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); - #endif - } - return ret; -} - -bool ClientSync::connect(const char* host, uint16_t port) { - bool ret = client.connect(host, port); // implicit conversion of return code int --> bool - if (ret) { - #if defined(ARDUINO_ARCH_ESP8266) - client.setNoDelay(true); - #elif defined(ARDUINO_ARCH_ESP32) - // Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure (for consistency also here) - int val = true; - client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); - #endif - } - return ret; -} - -size_t ClientSync::write(const uint8_t* buf, size_t size) { - return client.write(buf, size); -} - -int ClientSync::read(uint8_t* buf, size_t size) { - return client.read(buf, size); -} - -void ClientSync::stop() { - client.stop(); -} - -bool ClientSync::connected() { - return client.connected(); -} - -bool ClientSync::disconnected() { - return !client.connected(); -} - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/ClientSync.h b/lib/espMqttClient/src/Transport/ClientSync.h deleted file mode 100644 index ccfbdba..0000000 --- a/lib/espMqttClient/src/Transport/ClientSync.h +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#include // includes IPAddress - -#include "Transport.h" - -namespace espMqttClientInternals { - -class ClientSync : public Transport { - public: - ClientSync(); - bool connect(IPAddress ip, uint16_t port) override; - bool connect(const char* host, uint16_t port) override; - size_t write(const uint8_t* buf, size_t size) override; - int read(uint8_t* buf, size_t size) override; - void stop() override; - bool connected() override; - bool disconnected() override; - WiFiClient client; -}; - -} // namespace espMqttClientInternals - -#endif diff --git a/lib/espMqttClient/src/Transport/Transport.h b/lib/espMqttClient/src/Transport/Transport.h deleted file mode 100644 index d368d01..0000000 --- a/lib/espMqttClient/src/Transport/Transport.h +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include // size_t - -#include "ClientPosixIPAddress.h" - -namespace espMqttClientInternals { - -class Transport { - public: - virtual bool connect(IPAddress ip, uint16_t port) = 0; - virtual bool connect(const char* host, uint16_t port) = 0; - virtual size_t write(const uint8_t* buf, size_t size) = 0; - virtual int read(uint8_t* buf, size_t size) = 0; - virtual void stop() = 0; - virtual bool connected() = 0; - virtual bool disconnected() = 0; -}; - -} // namespace espMqttClientInternals diff --git a/lib/espMqttClient/src/TypeDefs.cpp b/lib/espMqttClient/src/TypeDefs.cpp deleted file mode 100644 index 4f92c1f..0000000 --- a/lib/espMqttClient/src/TypeDefs.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -Parts are based on the original work of Marvin Roger: -https://github.com/marvinroger/async-mqtt-client - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "TypeDefs.h" - -namespace espMqttClientTypes { - -const char* disconnectReasonToString(DisconnectReason reason) { - switch (reason) { - case DisconnectReason::USER_OK: return "No error"; - case DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: return "Unacceptable protocol version"; - case DisconnectReason::MQTT_IDENTIFIER_REJECTED: return "Identified rejected"; - case DisconnectReason::MQTT_SERVER_UNAVAILABLE: return "Server unavailable"; - case DisconnectReason::MQTT_MALFORMED_CREDENTIALS: return "Malformed credentials"; - case DisconnectReason::MQTT_NOT_AUTHORIZED: return "Not authorized"; - case DisconnectReason::TLS_BAD_FINGERPRINT: return "Bad fingerprint"; - case DisconnectReason::TCP_DISCONNECTED: return "TCP disconnected"; - default: return ""; - } -} - -const char* subscribeReturncodeToString(SubscribeReturncode returnCode) { - switch (returnCode) { - case SubscribeReturncode::QOS0: return "QoS 0"; - case SubscribeReturncode::QOS1: return "QoS 1"; - case SubscribeReturncode::QOS2: return "QoS 2"; - case SubscribeReturncode::FAIL: return "Failed"; - default: return ""; - } -} - -const char* errorToString(Error error) { - switch (error) { - case Error::SUCCESS: return "Success"; - case Error::OUT_OF_MEMORY: return "Out of memory"; - case Error::MAX_RETRIES: return "Maximum retries exceeded"; - case Error::MALFORMED_PARAMETER: return "Malformed parameters"; - case Error::MISC_ERROR: return "Misc error"; - default: return ""; - } -} - -} // end namespace espMqttClientTypes diff --git a/lib/espMqttClient/src/TypeDefs.h b/lib/espMqttClient/src/TypeDefs.h deleted file mode 100644 index 0f15360..0000000 --- a/lib/espMqttClient/src/TypeDefs.h +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -Parts are based on the original work of Marvin Roger: -https://github.com/marvinroger/async-mqtt-client - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#include -#include -#include - -namespace espMqttClientTypes { - -enum class DisconnectReason : uint8_t { - USER_OK = 0, - MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, - MQTT_IDENTIFIER_REJECTED = 2, - MQTT_SERVER_UNAVAILABLE = 3, - MQTT_MALFORMED_CREDENTIALS = 4, - MQTT_NOT_AUTHORIZED = 5, - TLS_BAD_FINGERPRINT = 6, - TCP_DISCONNECTED = 7 -}; - -const char* disconnectReasonToString(DisconnectReason reason); - -enum class SubscribeReturncode : uint8_t { - QOS0 = 0x00, - QOS1 = 0x01, - QOS2 = 0x02, - FAIL = 0X80 -}; - -const char* subscribeReturncodeToString(SubscribeReturncode returnCode); - -enum class Error : uint8_t { - SUCCESS = 0, - OUT_OF_MEMORY = 1, - MAX_RETRIES = 2, - MALFORMED_PARAMETER = 3, - MISC_ERROR = 4 -}; - -const char* errorToString(Error error); - -struct MessageProperties { - uint8_t qos; - bool dup; - bool retain; - uint16_t packetId; -}; - -typedef std::function OnConnectCallback; -typedef std::function OnDisconnectCallback; -typedef std::function OnSubscribeCallback; -typedef std::function OnUnsubscribeCallback; -typedef std::function OnMessageCallback; -typedef std::function OnPublishCallback; -typedef std::function PayloadCallback; -typedef std::function OnErrorCallback; - -enum class UseInternalTask { - NO = 0, - YES = 1, -}; - -} // end namespace espMqttClientTypes diff --git a/lib/espMqttClient/src/espMqttClient.cpp b/lib/espMqttClient/src/espMqttClient.cpp deleted file mode 100644 index 833ece1..0000000 --- a/lib/espMqttClient/src/espMqttClient.cpp +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#include "espMqttClient.h" - -#if defined(ARDUINO_ARCH_ESP8266) -espMqttClient::espMqttClient() -: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO) -, _client() { - _transport = &_client; -} - -espMqttClientSecure::espMqttClientSecure() -: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO) -, _client() { - _transport = &_client; -} - -espMqttClientSecure& espMqttClientSecure::setInsecure() { - _client.client.setInsecure(); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setFingerprint(const uint8_t fingerprint[20]) { - _client.client.setFingerprint(fingerprint); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setTrustAnchors(const X509List *ta) { - _client.client.setTrustAnchors(ta); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setClientRSACert(const X509List *cert, const PrivateKey *sk) { - _client.client.setClientRSACert(cert, sk); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setClientECCert(const X509List *cert, const PrivateKey *sk, unsigned allowed_usages, unsigned cert_issuer_key_type) { - _client.client.setClientECCert(cert, sk, allowed_usages, cert_issuer_key_type); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setCertStore(CertStoreBase *certStore) { - _client.client.setCertStore(certStore); - return *this; -} -#endif - -#if defined(ARDUINO_ARCH_ESP32) -espMqttClient::espMqttClient(espMqttClientTypes::UseInternalTask useInternalTask) -: MqttClientSetup(useInternalTask) -, _client() { - _transport = &_client; -} - -espMqttClient::espMqttClient(uint8_t priority, uint8_t core) -: MqttClientSetup(espMqttClientTypes::UseInternalTask::YES, priority, core) -, _client() { - _transport = &_client; -} - -espMqttClientSecure::espMqttClientSecure(espMqttClientTypes::UseInternalTask useInternalTask) -: MqttClientSetup(useInternalTask) -, _client() { - _transport = &_client; -} - -espMqttClientSecure::espMqttClientSecure(uint8_t priority, uint8_t core) -: MqttClientSetup(espMqttClientTypes::UseInternalTask::YES, priority, core) -, _client() { - _transport = &_client; -} - -espMqttClientSecure& espMqttClientSecure::setInsecure() { - _client.client.setInsecure(); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setCACert(const char* rootCA) { - _client.client.setCACert(rootCA); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setCertificate(const char* clientCa) { - _client.client.setCertificate(clientCa); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setPrivateKey(const char* privateKey) { - _client.client.setPrivateKey(privateKey); - return *this; -} - -espMqttClientSecure& espMqttClientSecure::setPreSharedKey(const char* pskIdent, const char* psKey) { - _client.client.setPreSharedKey(pskIdent, psKey); - return *this; -} - -#endif - -#if defined(__linux__) -espMqttClient::espMqttClient() -: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO) -, _client() { - _transport = &_client; -} -#endif diff --git a/lib/espMqttClient/src/espMqttClient.h b/lib/espMqttClient/src/espMqttClient.h deleted file mode 100644 index 4e44801..0000000 --- a/lib/espMqttClient/src/espMqttClient.h +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -API is based on the original work of Marvin Roger: -https://github.com/marvinroger/async-mqtt-client - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) -#include "Transport/ClientSync.h" -#include "Transport/ClientSecureSync.h" -#elif defined(__linux__) -#include "Transport/ClientPosix.h" -#endif - -#include "MqttClientSetup.h" - -#if defined(ARDUINO_ARCH_ESP8266) -class espMqttClient : public MqttClientSetup { - public: - espMqttClient(); - - protected: - espMqttClientInternals::ClientSync _client; -}; - -class espMqttClientSecure : public MqttClientSetup { - public: - espMqttClientSecure(); - espMqttClientSecure& setInsecure(); - espMqttClientSecure& setFingerprint(const uint8_t fingerprint[20]); - espMqttClientSecure& setTrustAnchors(const X509List *ta); - espMqttClientSecure& setClientRSACert(const X509List *cert, const PrivateKey *sk); - espMqttClientSecure& setClientECCert(const X509List *cert, const PrivateKey *sk, unsigned allowed_usages, unsigned cert_issuer_key_type); - espMqttClientSecure& setCertStore(CertStoreBase *certStore); - - protected: - espMqttClientInternals::ClientSecureSync _client; -}; -#endif - -#if defined(ARDUINO_ARCH_ESP32) -class espMqttClient : public MqttClientSetup { - public: - explicit espMqttClient(espMqttClientTypes::UseInternalTask useInternalTask); - explicit espMqttClient(uint8_t priority = 1, uint8_t core = 1); - - protected: - espMqttClientInternals::ClientSync _client; -}; - -class espMqttClientSecure : public MqttClientSetup { - public: - explicit espMqttClientSecure(espMqttClientTypes::UseInternalTask useInternalTask); - explicit espMqttClientSecure(uint8_t priority = 1, uint8_t core = 1); - espMqttClientSecure& setInsecure(); - espMqttClientSecure& setCACert(const char* rootCA); - espMqttClientSecure& setCertificate(const char* clientCa); - espMqttClientSecure& setPrivateKey(const char* privateKey); - espMqttClientSecure& setPreSharedKey(const char* pskIdent, const char* psKey); - - protected: - espMqttClientInternals::ClientSecureSync _client; -}; -#endif - -#if defined(__linux__) -class espMqttClient : public MqttClientSetup { - public: - espMqttClient(); - - protected: - espMqttClientInternals::ClientPosix _client; -}; -#endif diff --git a/lib/espMqttClient/src/espMqttClientAsync.cpp b/lib/espMqttClient/src/espMqttClientAsync.cpp deleted file mode 100644 index 98b7f15..0000000 --- a/lib/espMqttClient/src/espMqttClientAsync.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#include "espMqttClientAsync.h" - -espMqttClientAsync::espMqttClientAsync() -: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO) -, _clientAsync() { - _transport = &_clientAsync; - _clientAsync.client.onConnect(onConnectCb, this); - _clientAsync.client.onDisconnect(onDisconnectCb, this); - _clientAsync.client.onData(onDataCb, this); - _clientAsync.client.onPoll(onPollCb, this); -} - -bool espMqttClientAsync::connect() { - bool ret = MqttClient::connect(); - loop(); - return ret; -} - -void espMqttClientAsync::_setupClient(espMqttClientAsync* c) { - (void)c; -} - -void espMqttClientAsync::onConnectCb(void* a, AsyncClient* c) { - c->setNoDelay(true); - espMqttClientAsync* client = reinterpret_cast(a); - client->_state = MqttClient::State::connectingTcp2; - client->loop(); -} - -void espMqttClientAsync::onDataCb(void* a, AsyncClient* c, void* data, size_t len) { - (void)c; - espMqttClientAsync* client = reinterpret_cast(a); - client->_clientAsync.bufData = reinterpret_cast(data); - client->_clientAsync.availableData = len; - client->loop(); -} - -void espMqttClientAsync::onDisconnectCb(void* a, AsyncClient* c) { - (void)c; - espMqttClientAsync* client = reinterpret_cast(a); - client->_state = MqttClient::State::disconnectingTcp2; - client->loop(); -} - -void espMqttClientAsync::onPollCb(void* a, AsyncClient* c) { - (void)c; - espMqttClientAsync* client = reinterpret_cast(a); - client->loop(); -} - -#endif diff --git a/lib/espMqttClient/src/espMqttClientAsync.h b/lib/espMqttClient/src/espMqttClientAsync.h deleted file mode 100644 index 1b9ed8b..0000000 --- a/lib/espMqttClient/src/espMqttClientAsync.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright (c) 2022 Bert Melis. All rights reserved. - -API is based on the original work of Marvin Roger: -https://github.com/marvinroger/async-mqtt-client - -This work is licensed under the terms of the MIT license. -For a copy, see or -the LICENSE file. -*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) - -#include "Transport/ClientAsync.h" - -#include "MqttClientSetup.h" - -class espMqttClientAsync : public MqttClientSetup { - public: - espMqttClientAsync(); - bool connect(); - - protected: - espMqttClientInternals::ClientAsync _clientAsync; - static void _setupClient(espMqttClientAsync* c); - static void _disconnectClient(espMqttClientAsync* c); - - static void onConnectCb(void* a, AsyncClient* c); - static void onDataCb(void* a, AsyncClient* c, void* data, size_t len); - static void onDisconnectCb(void* a, AsyncClient* c); - static void onPollCb(void* a, AsyncClient* c); -}; - -#endif diff --git a/lib/espMqttClient/test-coverage.py b/lib/espMqttClient/test-coverage.py deleted file mode 100644 index 0d83301..0000000 --- a/lib/espMqttClient/test-coverage.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -Import("env", "projenv") - -# Dump build environment (for debug purpose) -print(env.Dump()) - -# access to global build environment -print(env) - -# access to the project build environment -# (used for source files located in the "src" folder) -print(projenv) - -def generateCoverageInfo(source, target, env): - for file in os.listdir("test"): - os.system(".pio/build/native/program test/"+file) - os.system("lcov -d .pio/build/native/ -c -o lcov.info") - os.system("lcov --remove lcov.info '*Unity*' '*unity*' '/usr/include/*' '*/test/*' -o filtered_lcov.info") - os.system("genhtml -o cov/ --demangle-cpp filtered_lcov.info") - -env.AddPostAction(".pio/build/native/program", generateCoverageInfo) \ No newline at end of file diff --git a/lib/espMqttClient/test/test_client_native/test_client_native.cpp b/lib/espMqttClient/test/test_client_native/test_client_native.cpp deleted file mode 100644 index d2eef9d..0000000 --- a/lib/espMqttClient/test/test_client_native/test_client_native.cpp +++ /dev/null @@ -1,405 +0,0 @@ -#include -#include -#include -#include // espMqttClient for Linux also defines millis() - -void setUp() {} -void tearDown() {} - -espMqttClient mqttClient; -uint32_t onConnectCbId = 1; -uint32_t onDisconnectCbId = 2; -uint32_t onSubscribeCbId = 3; -uint32_t onUnsubscribeCbId = 4; -uint32_t onMessageCbId = 5; -uint32_t onPublishCbId = 6; -std::atomic_bool exitProgram(false); -std::thread t; - -//const IPAddress broker(127,0,0,1); -const char* broker = "mqtt"; -//const char* broker = "test.mosquitto.org"; -const uint16_t broker_port = 1883; - -/* - -- setup the client with basic settings -- connect to the broker -- successfully connect - -*/ -void test_connect() { - std::atomic onConnectCalledTest(false); - bool sessionPresentTest = true; - mqttClient.setServer(broker, broker_port) - .setCleanSession(true) - .setKeepAlive(5) - .onConnect([&](bool sessionPresent) mutable { - sessionPresentTest = sessionPresent; - onConnectCalledTest = true; - }, onConnectCbId); - mqttClient.connect(); - uint32_t start = millis(); - while (millis() - start < 2000) { - if (onConnectCalledTest) { - break; - } - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_TRUE(onConnectCalledTest); - TEST_ASSERT_FALSE(sessionPresentTest); - - mqttClient.removeOnConnect(onConnectCbId); -} - -/* - -- keepalive is set at 5 seconds in previous test -- client should stay connected during 2x keepalive period - -*/ - -void test_ping() { - bool pingTest = true; - uint32_t start = millis(); - while (millis() - start < 11000) { - if (mqttClient.disconnected()) { - pingTest = false; - break; - } - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_TRUE(pingTest); -} - -/* - -- client subscribes to topic -- ack is received from broker - -*/ - -void test_subscribe() { - std::atomic subscribeTest(false); - mqttClient.onSubscribe([&](uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* returncodes, size_t len) mutable { - (void) packetId; - if (len == 1 && returncodes[0] == espMqttClientTypes::SubscribeReturncode::QOS0) { - subscribeTest = true; - } - }, onSubscribeCbId); - mqttClient.subscribe("test/test", 0); - uint32_t start = millis(); - while (millis() - start < 2000) { - if (subscribeTest) { - break; - } - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_TRUE(subscribeTest); - - mqttClient.removeOnSubscribe(onSubscribeCbId); -} - -/* - -- client publishes using all three qos levels -- all publish get packetID returned > 0 (equal to 1 for qos 0) -- 2 pubacks are received - -*/ - -void test_publish() { - std::atomic publishSendTest(0); - mqttClient.onPublish([&](uint16_t packetId) mutable { - (void) packetId; - publishSendTest++; - }, onPublishCbId); - std::atomic publishReceiveTest(0); - mqttClient.onMessage([&](const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) mutable { - (void) properties; - (void) topic; - (void) payload; - (void) len; - (void) index; - (void) total; - publishReceiveTest++; - }, onMessageCbId); - uint16_t sendQos0Test = mqttClient.publish("test/test", 0, false, "test0"); - uint16_t sendQos1Test = mqttClient.publish("test/test", 1, false, "test1"); - uint16_t sendQos2Test = mqttClient.publish("test/test", 2, false, "test2"); - uint32_t start = millis(); - while (millis() - start < 6000) { - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_EQUAL_UINT16(1, sendQos0Test); - TEST_ASSERT_GREATER_THAN_UINT16(0, sendQos1Test); - TEST_ASSERT_GREATER_THAN_UINT16(0, sendQos2Test); - TEST_ASSERT_EQUAL_INT(2, publishSendTest); - TEST_ASSERT_EQUAL_INT(3, publishReceiveTest); - - mqttClient.removeOnPublish(onPublishCbId); - mqttClient.removeOnMessage(onMessageCbId); -} - -void test_publish_empty() { - std::atomic publishSendEmptyTest(0); - mqttClient.onPublish([&](uint16_t packetId) mutable { - (void) packetId; - publishSendEmptyTest++; - }, onPublishCbId); - std::atomic publishReceiveEmptyTest(0); - mqttClient.onMessage([&](const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) mutable { - (void) properties; - (void) topic; - (void) payload; - (void) len; - (void) index; - (void) total; - publishReceiveEmptyTest++; - }, onMessageCbId); - uint16_t sendQos0Test = mqttClient.publish("test/test", 0, false, nullptr, 0); - uint16_t sendQos1Test = mqttClient.publish("test/test", 1, false, nullptr, 0); - uint16_t sendQos2Test = mqttClient.publish("test/test", 2, false, nullptr, 0); - uint32_t start = millis(); - while (millis() - start < 6000) { - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_EQUAL_UINT16(1, sendQos0Test); - TEST_ASSERT_GREATER_THAN_UINT16(0, sendQos1Test); - TEST_ASSERT_GREATER_THAN_UINT16(0, sendQos2Test); - TEST_ASSERT_EQUAL_INT(2, publishSendEmptyTest); - TEST_ASSERT_EQUAL_INT(3, publishReceiveEmptyTest); - - mqttClient.removeOnPublish(onPublishCbId); - mqttClient.removeOnMessage(onMessageCbId); -} - -/* - -- subscribe to test/test, qos 1 -- send to test/test, qos 1 -- check if message is received at least once. - -*/ - -void test_receive1() { - std::atomic publishReceive1Test(0); - mqttClient.onMessage([&](const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) mutable { - (void) properties; - (void) topic; - (void) payload; - (void) len; - (void) index; - (void) total; - publishReceive1Test++; - }, onMessageCbId); - mqttClient.onSubscribe([&](uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* returncodes, size_t len) mutable { - (void) packetId; - if (len == 1 && returncodes[0] == espMqttClientTypes::SubscribeReturncode::QOS1) { - mqttClient.publish("test/test", 1, false, ""); - } - }, onSubscribeCbId); - mqttClient.subscribe("test/test", 1); - uint32_t start = millis(); - while (millis() - start < 6000) { - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_GREATER_THAN_INT(0, publishReceive1Test); - - mqttClient.removeOnMessage(onMessageCbId); - mqttClient.removeOnSubscribe(onSubscribeCbId); -} - -/* - -- subscribe to test/test, qos 2 -- send to test/test, qos 2 -- check if message is received exactly once. - -*/ - -void test_receive2() { - std::atomic publishReceive2Test(0); - mqttClient.onMessage([&](const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) mutable { - (void) properties; - (void) topic; - (void) payload; - (void) len; - (void) index; - (void) total; - publishReceive2Test++; - }, onMessageCbId); - mqttClient.onSubscribe([&](uint16_t packetId, const espMqttClientTypes::SubscribeReturncode* returncodes, size_t len) mutable { - (void) packetId; - if (len == 1 && returncodes[0] == espMqttClientTypes::SubscribeReturncode::QOS2) { - mqttClient.publish("test/test", 2, false, ""); - } - }, onSubscribeCbId); - mqttClient.subscribe("test/test", 2); - uint32_t start = millis(); - while (millis() - start < 6000) { - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_EQUAL_INT(1, publishReceive2Test); - - mqttClient.removeOnMessage(onMessageCbId); - mqttClient.removeOnSubscribe(onSubscribeCbId); -} - - -/* - -- client unsibscribes from topic - -*/ - -void test_unsubscribe() { - std::atomic unsubscribeTest(false); - mqttClient.onUnsubscribe([&](uint16_t packetId) mutable { - (void) packetId; - unsubscribeTest = true; - }, onUnsubscribeCbId); - mqttClient.unsubscribe("test/test"); - uint32_t start = millis(); - while (millis() - start < 2000) { - if (unsubscribeTest) { - break; - } - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_TRUE(unsubscribeTest); - - mqttClient.removeOnUnsubscribe(onUnsubscribeCbId); -} - -/* - -- client disconnects cleanly - -*/ - -void test_disconnect() { - std::atomic onDisconnectCalled(false); - espMqttClientTypes::DisconnectReason reasonTest = espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED; - mqttClient.onDisconnect([&](espMqttClientTypes::DisconnectReason reason) mutable { - reasonTest = reason; - onDisconnectCalled = true; - }, onDisconnectCbId); - mqttClient.disconnect(); - uint32_t start = millis(); - while (millis() - start < 2000) { - if (onDisconnectCalled) { - break; - } - std::this_thread::yield(); - } - - TEST_ASSERT_TRUE(onDisconnectCalled); - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::DisconnectReason::USER_OK, reasonTest); - TEST_ASSERT_TRUE(mqttClient.disconnected()); - - mqttClient.removeOnDisconnect(onDisconnectCbId); -} - -void test_pub_before_connect() { - std::atomic onConnectCalledTest(false); - std::atomic publishSendTest(0); - bool sessionPresentTest = true; - mqttClient.setServer(broker, broker_port) - .setCleanSession(true) - .setKeepAlive(5) - .onConnect([&](bool sessionPresent) mutable { - sessionPresentTest = sessionPresent; - onConnectCalledTest = true; - }, onConnectCbId) - .onPublish([&](uint16_t packetId) mutable { - (void) packetId; - publishSendTest++; - }, onPublishCbId); - uint16_t sendQos0Test = mqttClient.publish("test/test", 0, false, "test0"); - uint16_t sendQos1Test = mqttClient.publish("test/test", 1, false, "test1"); - uint16_t sendQos2Test = mqttClient.publish("test/test", 2, false, "test2"); - mqttClient.connect(); - uint32_t start = millis(); - while (millis() - start < 2000) { - if (onConnectCalledTest) { - break; - } - std::this_thread::yield(); - } - TEST_ASSERT_TRUE(mqttClient.connected()); - TEST_ASSERT_TRUE(onConnectCalledTest); - TEST_ASSERT_FALSE(sessionPresentTest); - start = millis(); - while (millis() - start < 10000) { - std::this_thread::yield(); - } - - TEST_ASSERT_EQUAL_UINT16(1, sendQos0Test); - TEST_ASSERT_GREATER_THAN_UINT16(0, sendQos1Test); - TEST_ASSERT_GREATER_THAN_UINT16(0, sendQos2Test); - TEST_ASSERT_EQUAL_INT(2, publishSendTest); - - mqttClient.removeOnConnect(onConnectCbId); - mqttClient.removeOnPublish(onPublishCbId); -} - -void final_disconnect() { - std::atomic onDisconnectCalled(false); - mqttClient.onDisconnect([&](espMqttClientTypes::DisconnectReason reason) mutable { - (void) reason; - onDisconnectCalled = true; - }, onDisconnectCbId); - mqttClient.disconnect(); - uint32_t start = millis(); - while (millis() - start < 2000) { - if (onDisconnectCalled) { - break; - } - std::this_thread::yield(); - } - if (mqttClient.connected()) { - mqttClient.disconnect(true); - } - mqttClient.removeOnDisconnect(onDisconnectCbId); -} - -int main() { - UNITY_BEGIN(); - t = std::thread([] { - while (1) { - mqttClient.loop(); - if (exitProgram) break; - } - }); - RUN_TEST(test_connect); - RUN_TEST(test_ping); - RUN_TEST(test_subscribe); - RUN_TEST(test_publish); - RUN_TEST(test_publish_empty); - RUN_TEST(test_receive1); - RUN_TEST(test_receive2); - RUN_TEST(test_unsubscribe); - RUN_TEST(test_disconnect); - RUN_TEST(test_pub_before_connect); - final_disconnect(); - exitProgram = true; - t.join(); - return UNITY_END(); -} diff --git a/lib/espMqttClient/test/test_outbox/test_outbox.cpp b/lib/espMqttClient/test/test_outbox/test_outbox.cpp deleted file mode 100644 index 0a6a3ef..0000000 --- a/lib/espMqttClient/test/test_outbox/test_outbox.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include - -#include - -using espMqttClientInternals::Outbox; - -void setUp() {} -void tearDown() {} - -void test_outbox_create() { - Outbox outbox; - Outbox::Iterator it = outbox.front(); - TEST_ASSERT_NULL(outbox.getCurrent()); - TEST_ASSERT_NULL(it.get()); - TEST_ASSERT_TRUE(outbox.empty()); -} - -void test_outbox_emplace() { - Outbox outbox; - outbox.emplace(523); - // 523, current points to 523 - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(523, *(outbox.getCurrent())); - TEST_ASSERT_FALSE(outbox.empty()); - - outbox.next(); - // 523, current points to nullptr - TEST_ASSERT_NULL(outbox.getCurrent()); - - outbox.emplace(286); - // 523 286, current points to 286 - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(286, *(outbox.getCurrent())); - - outbox.emplace(364); - // 523 286 364, current points to 286 - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(286, *(outbox.getCurrent())); -} - -void test_outbox_emplaceFront() { - Outbox outbox; - outbox.emplaceFront(1); - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(1, *(outbox.getCurrent())); - - outbox.emplaceFront(2); - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(2, *(outbox.getCurrent())); -} - -void test_outbox_remove1() { - Outbox outbox; - Outbox::Iterator it; - outbox.emplace(1); - outbox.emplace(2); - outbox.emplace(3); - outbox.emplace(4); - outbox.next(); - outbox.next(); - it = outbox.front(); - ++it; - ++it; - ++it; - ++it; - outbox.remove(it); - // 1 2 3 4, it points to nullptr, current points to 3 - TEST_ASSERT_NULL(it.get()); - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(3, *(outbox.getCurrent())); - - it = outbox.front(); - ++it; - ++it; - ++it; - outbox.remove(it); - // 1 2 3, it points to nullptr, current points to 3 - TEST_ASSERT_NULL(it.get()); - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(3, *(outbox.getCurrent())); - - - it = outbox.front(); - outbox.remove(it); - // 2 3, it points to 2, current points to 3 - TEST_ASSERT_NOT_NULL(it.get()); - TEST_ASSERT_EQUAL_UINT32(2, *(it.get())); - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(3, *(outbox.getCurrent())); - - it = outbox.front(); - outbox.remove(it); - // 3, it points to 3, current points to 3 - TEST_ASSERT_NOT_NULL(it.get()); - TEST_ASSERT_EQUAL_UINT32(3, *(it.get())); - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(3, *(outbox.getCurrent())); - - it = outbox.front(); - outbox.remove(it); - TEST_ASSERT_NULL(it.get()); - TEST_ASSERT_NULL(outbox.getCurrent()); -} - -void test_outbox_remove2() { - Outbox outbox; - Outbox::Iterator it; - outbox.emplace(1); - outbox.emplace(2); - outbox.next(); - outbox.next(); - it = outbox.front(); - // 1 2, current points to nullptr - TEST_ASSERT_NULL(outbox.getCurrent()); - TEST_ASSERT_NOT_NULL(it.get()); - TEST_ASSERT_EQUAL_UINT32(1, *(it.get())); - - ++it; - // 1 2, current points to nullptr - TEST_ASSERT_NOT_NULL(it.get()); - TEST_ASSERT_EQUAL_UINT32(2, *(it.get())); - - outbox.remove(it); - // 1, current points to nullptr - TEST_ASSERT_NULL(outbox.getCurrent()); - TEST_ASSERT_NULL(it.get()); - - it = outbox.front(); - TEST_ASSERT_NOT_NULL(it.get()); - TEST_ASSERT_EQUAL_UINT32(1, *(it.get())); - - outbox.remove(it); - TEST_ASSERT_NULL(it.get()); - TEST_ASSERT_TRUE(outbox.empty()); -} - -void test_outbox_removeCurrent() { - Outbox outbox; - outbox.emplace(1); - outbox.emplace(2); - outbox.emplace(3); - outbox.emplace(4); - outbox.removeCurrent(); - // 2 3 4, current points to 2 - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(2, *(outbox.getCurrent())); - - outbox.next(); - outbox.removeCurrent(); - // 2 4, current points to 4 - TEST_ASSERT_NOT_NULL(outbox.getCurrent()); - TEST_ASSERT_EQUAL_UINT32(4, *(outbox.getCurrent())); - - outbox.removeCurrent(); - // 4, current points to nullptr - TEST_ASSERT_NULL(outbox.getCurrent()); - - // outbox will go out of scope and destructor will be called - // Valgrind should not detect a leak here -} - -int main() { - UNITY_BEGIN(); - RUN_TEST(test_outbox_create); - RUN_TEST(test_outbox_emplace); - RUN_TEST(test_outbox_emplaceFront); - RUN_TEST(test_outbox_remove1); - RUN_TEST(test_outbox_remove2); - RUN_TEST(test_outbox_removeCurrent); - return UNITY_END(); -} diff --git a/lib/espMqttClient/test/test_packets/test_packets.cpp b/lib/espMqttClient/test/test_packets/test_packets.cpp deleted file mode 100644 index 3e4c108..0000000 --- a/lib/espMqttClient/test/test_packets/test_packets.cpp +++ /dev/null @@ -1,714 +0,0 @@ -#include - -#include - -using espMqttClientInternals::Packet; -using espMqttClientInternals::PacketType; - -void setUp() {} -void tearDown() {} - -void test_encodeConnect0() { - const uint8_t check[] = { - 0b00010000, // header - 0x0F, // remaining length - 0x00,0x04,'M','Q','T','T', // protocol - 0b00000100, // protocol level - 0b00000010, // connect flags - 0x00,0x10, // keepalive (16) - 0x00,0x03,'c','l','i' // client id - }; - const uint32_t length = 17; - - bool cleanSession = true; - const char* username = nullptr; - const char* password = nullptr; - const char* willTopic = nullptr; - bool willRemain = false; - uint8_t willQoS = 0; - const uint8_t* willPayload = nullptr; - uint16_t willPayloadLength = 0; - uint16_t keepalive = 16; - const char* clientId = "cli"; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, - cleanSession, - username, - password, - willTopic, - willRemain, - willQoS, - willPayload, - willPayloadLength, - keepalive, - clientId); - - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.CONNECT, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(0, packet.packetId()); -} - -void test_encodeConnect1() { - const uint8_t check[] = { - 0b00010000, // header - 0x20, // remaining length - 0x00,0x04,'M','Q','T','T', // protocol - 0b00000100, // protocol level - 0b11101110, // connect flags - 0x00,0x10, // keepalive (16) - 0x00,0x03,'c','l','i', // client id - 0x00,0x03,'t','o','p', // will topic - 0x00,0x02,'p','l', // will payload - 0x00,0x02,'u','n', // username - 0x00,0x02,'p','a' // password - }; - const uint32_t length = 34; - - bool cleanSession = true; - const char* username = "un"; - const char* password = "pa"; - const char* willTopic = "top"; - bool willRemain = true; - uint8_t willQoS = 1; - const uint8_t willPayload[] = {'p', 'l'}; - uint16_t willPayloadLength = 2; - uint16_t keepalive = 16; - const char* clientId = "cli"; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, - cleanSession, - username, - password, - willTopic, - willRemain, - willQoS, - willPayload, - willPayloadLength, - keepalive, - clientId); - - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.CONNECT, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(0, packet.packetId()); -} - -void test_encodeConnect2() { - const uint8_t check[] = { - 0b00010000, // header - 0x20, // remaining length - 0x00,0x04,'M','Q','T','T', // protocol - 0b00000100, // protocol level - 0b11110110, // connect flags - 0x00,0x10, // keepalive (16) - 0x00,0x03,'c','l','i', // client id - 0x00,0x03,'t','o','p', // will topic - 0x00,0x02,'p','l', // will payload - 0x00,0x02,'u','n', // username - 0x00,0x02,'p','a' // password - }; - const uint32_t length = 34; - - bool cleanSession = true; - const char* username = "un"; - const char* password = "pa"; - const char* willTopic = "top"; - bool willRemain = true; - uint8_t willQoS = 2; - const uint8_t willPayload[] = {'p', 'l', '\0'}; - uint16_t willPayloadLength = 0; - uint16_t keepalive = 16; - const char* clientId = "cli"; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, - cleanSession, - username, - password, - willTopic, - willRemain, - willQoS, - willPayload, - willPayloadLength, - keepalive, - clientId); - - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.CONNECT, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(0, packet.packetId()); -} - -void test_encodeConnectFail0() { - bool cleanSession = true; - const char* username = nullptr; - const char* password = nullptr; - const char* willTopic = nullptr; - bool willRemain = false; - uint8_t willQoS = 0; - const uint8_t* willPayload = nullptr; - uint16_t willPayloadLength = 0; - uint16_t keepalive = 16; - const char* clientId = ""; - espMqttClientTypes::Error error = espMqttClientTypes::Error::SUCCESS; - - Packet packet(error, - cleanSession, - username, - password, - willTopic, - willRemain, - willQoS, - willPayload, - willPayloadLength, - keepalive, - clientId); - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::MALFORMED_PARAMETER, error); -} - -void test_encodePublish0() { - const uint8_t check[] = { - 0b00110000, // header, dup, qos, retain - 0x09, - 0x00,0x03,'t','o','p', // topic - 0x01,0x02,0x03,0x04 // payload - }; - const uint32_t length = 11; - - const char* topic = "top"; - uint8_t qos = 0; - bool retain = false; - const uint8_t payload[] = {0x01, 0x02, 0x03, 0x04}; - uint16_t payloadLength = 4; - uint16_t packetId = 22; // any value except 0 for testing - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, - packetId, - topic, - payload, - payloadLength, - qos, - retain); - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PUBLISH, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(0, packet.packetId()); - - packet.setDup(); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); -} - -void test_encodePublish1() { - const uint8_t check[] = { - 0b00110011, // header, dup, qos, retain - 0x0B, - 0x00,0x03,'t','o','p', // topic - 0x00,0x16, // packet Id - 0x01,0x02,0x03,0x04 // payload - }; - const uint32_t length = 13; - - const char* topic = "top"; - uint8_t qos = 1; - bool retain = true; - const uint8_t payload[] = {0x01, 0x02, 0x03, 0x04}; - uint16_t payloadLength = 4; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, - packetId, - topic, - payload, - payloadLength, - qos, - retain); - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PUBLISH, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); - - const uint8_t checkDup[] = { - 0b00111011, // header, dup, qos, retain - 0x0B, - 0x00,0x03,'t','o','p', // topic - 0x00,0x16, // packet Id - 0x01,0x02,0x03,0x04 // payload - }; - - packet.setDup(); - TEST_ASSERT_EQUAL_UINT8_ARRAY(checkDup, packet.data(0), length); -} - -void test_encodePublish2() { - const uint8_t check[] = { - 0b00110101, // header, dup, qos, retain - 0x0B, - 0x00,0x03,'t','o','p', // topic - 0x00,0x16, // packet Id - 0x01,0x02,0x03,0x04 // payload - }; - const uint32_t length = 13; - - const char* topic = "top"; - uint8_t qos = 2; - bool retain = true; - const uint8_t payload[] = {0x01, 0x02, 0x03, 0x04}; - uint16_t payloadLength = 4; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, - packetId, - topic, - payload, - payloadLength, - qos, - retain); - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PUBLISH, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); - - const uint8_t checkDup[] = { - 0b00111101, // header, dup, qos, retain - 0x0B, - 0x00,0x03,'t','o','p', // topic - 0x00,0x16, // packet Id - 0x01,0x02,0x03,0x04 // payload - }; - - packet.setDup(); - TEST_ASSERT_EQUAL_UINT8_ARRAY(checkDup, packet.data(0), length); -} - -void test_encodePubAck() { - const uint8_t check[] = { - 0b01000000, // header - 0x02, - 0x00,0x16, // packet Id - }; - const uint32_t length = 4; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, PacketType.PUBACK, packetId); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PUBACK, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodePubRec() { - const uint8_t check[] = { - 0b01010000, // header - 0x02, - 0x00,0x16, // packet Id - }; - const uint32_t length = 4; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, PacketType.PUBREC, packetId); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PUBREC, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodePubRel() { - const uint8_t check[] = { - 0b01100010, // header - 0x02, - 0x00,0x16, // packet Id - }; - const uint32_t length = 4; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, PacketType.PUBREL, packetId); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PUBREL, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodePubComp() { - const uint8_t check[] = { - 0b01110000, // header - 0x02, // remaining length - 0x00,0x16, // packet Id - }; - const uint32_t length = 4; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, PacketType.PUBCOMP, packetId); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PUBCOMP, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodeSubscribe() { - const uint8_t check[] = { - 0b10000010, // header - 0x08, // remaining length - 0x00,0x16, // packet Id - 0x00, 0x03, 'a', '/', 'b', // topic - 0x02 // qos - }; - const uint32_t length = 10; - const char* topic = "a/b"; - uint8_t qos = 2; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, packetId, topic, qos); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.SUBSCRIBE, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodeMultiSubscribe2() { - const uint8_t check[] = { - 0b10000010, // header - 0x0E, // remaining length - 0x00,0x16, // packet Id - 0x00, 0x03, 'a', '/', 'b', // topic1 - 0x01, // qos1 - 0x00, 0x03, 'c', '/', 'd', // topic2 - 0x02 // qos2 - }; - const uint32_t length = 16; - const char* topic1 = "a/b"; - const char* topic2 = "c/d"; - uint8_t qos1 = 1; - uint8_t qos2 = 2; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, packetId, topic1, qos1, topic2, qos2); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.SUBSCRIBE, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodeMultiSubscribe3() { - const uint8_t check[] = { - 0b10000010, // header - 0x14, // remaining length - 0x00,0x16, // packet Id - 0x00, 0x03, 'a', '/', 'b', // topic1 - 0x01, // qos1 - 0x00, 0x03, 'c', '/', 'd', // topic2 - 0x02, // qos2 - 0x00, 0x03, 'e', '/', 'f', // topic3 - 0x00 // qos3 - }; - const uint32_t length = 22; - const char* topic1 = "a/b"; - const char* topic2 = "c/d"; - const char* topic3 = "e/f"; - uint8_t qos1 = 1; - uint8_t qos2 = 2; - uint8_t qos3 = 0; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, packetId, topic1, qos1, topic2, qos2, topic3, qos3); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.SUBSCRIBE, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodeUnsubscribe() { - const uint8_t check[] = { - 0b10100010, // header - 0x07, // remaining length - 0x00,0x16, // packet Id - 0x00, 0x03, 'a', '/', 'b', // topic - }; - const uint32_t length = 9; - const char* topic = "a/b"; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, packetId, topic); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.UNSUBSCRIBE, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodeMultiUnsubscribe2() { - const uint8_t check[] = { - 0b10100010, // header - 0x0C, // remaining length - 0x00,0x16, // packet Id - 0x00, 0x03, 'a', '/', 'b', // topic1 - 0x00, 0x03, 'c', '/', 'd' // topic2 - }; - const uint32_t length = 14; - const char* topic1 = "a/b"; - const char* topic2 = "c/d"; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, packetId, topic1, topic2); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.UNSUBSCRIBE, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodeMultiUnsubscribe3() { - const uint8_t check[] = { - 0b10100010, // header - 0x11, // remaining length - 0x00,0x16, // packet Id - 0x00, 0x03, 'a', '/', 'b', // topic1 - 0x00, 0x03, 'c', '/', 'd', // topic2 - 0x00, 0x03, 'e', '/', 'f', // topic3 - }; - const uint32_t length = 19; - const char* topic1 = "a/b"; - const char* topic2 = "c/d"; - const char* topic3 = "e/f"; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, packetId, topic1, topic2, topic3); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.UNSUBSCRIBE, packet.packetType()); - TEST_ASSERT_FALSE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); -} - -void test_encodePingReq() { - const uint8_t check[] = { - 0b11000000, // header - 0x00 - }; - const uint32_t length = 2; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, PacketType.PINGREQ); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.PINGREQ, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(0, packet.packetId()); -} - -void test_encodeDisconnect() { - const uint8_t check[] = { - 0b11100000, // header - 0x00 - }; - const uint32_t length = 2; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, PacketType.DISCONNECT); - packet.setDup(); // no effect - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(length, packet.size()); - TEST_ASSERT_EQUAL_UINT8(PacketType.DISCONNECT, packet.packetType()); - TEST_ASSERT_TRUE(packet.removable()); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(0), length); - TEST_ASSERT_EQUAL_UINT16(0, packet.packetId()); -} - -size_t getData(uint8_t* dest, size_t len, size_t index) { - (void) index; - static uint8_t i = 1; - memset(dest, i, len); - ++i; - return len; -} - -void test_encodeChunkedPublish() { - const uint8_t check[] = { - 0b00110011, // header, dup, qos, retain - 0xCF, 0x01, // 7 + 200 = (0x4F * 1) & 0x40 + (0x01 * 128) - 0x00,0x03,'t','o','p', // topic - 0x00,0x16 // packet Id - }; - uint8_t payloadChunk[EMC_TX_BUFFER_SIZE] = {}; - memset(payloadChunk, 0x01, EMC_TX_BUFFER_SIZE); - const char* topic = "top"; - uint8_t qos = 1; - bool retain = true; - size_t headerLength = 10; - size_t payloadLength = 200; - size_t size = headerLength + payloadLength; - uint16_t packetId = 22; - espMqttClientTypes::Error error = espMqttClientTypes::Error::MISC_ERROR; - - Packet packet(error, - packetId, - topic, - getData, - payloadLength, - qos, - retain); - - TEST_ASSERT_EQUAL_UINT8(espMqttClientTypes::Error::SUCCESS, error); - TEST_ASSERT_EQUAL_UINT32(size, packet.size()); - TEST_ASSERT_EQUAL_UINT16(packetId, packet.packetId()); - - size_t available = 0; - size_t index = 0; - - // call 'available' before 'data' - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(headerLength + EMC_TX_BUFFER_SIZE, available); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, packet.data(index), headerLength); - - // index == first payload byte - index = headerLength; - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(EMC_TX_BUFFER_SIZE, available); - TEST_ASSERT_EQUAL_UINT8_ARRAY(payloadChunk, packet.data(index), available); - - // index == first payload byte - index = headerLength + 4; - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(EMC_TX_BUFFER_SIZE - 4, available); - TEST_ASSERT_EQUAL_UINT8_ARRAY(payloadChunk, packet.data(index), available); - - // index == last payload byte in first chunk - index = headerLength + EMC_TX_BUFFER_SIZE - 1; - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(1, available); - - // index == first payloadbyte in second chunk - memset(payloadChunk, 0x02, EMC_TX_BUFFER_SIZE); - index = headerLength + EMC_TX_BUFFER_SIZE; - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(EMC_TX_BUFFER_SIZE, available); - TEST_ASSERT_EQUAL_UINT8_ARRAY(payloadChunk, packet.data(index), available); - - memset(payloadChunk, 0x03, EMC_TX_BUFFER_SIZE); - index = headerLength + EMC_TX_BUFFER_SIZE + EMC_TX_BUFFER_SIZE + 10; - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(EMC_TX_BUFFER_SIZE, available); - TEST_ASSERT_EQUAL_UINT8_ARRAY(payloadChunk, packet.data(index), available); - - const uint8_t checkDup[] = { - 0b00111011, // header, dup, qos, retain - 0xCF, 0x01, // 7 + 200 = (0x4F * 0) + (0x01 * 128) - 0x00,0x03,'t','o','p', // topic - 0x00,0x16, // packet Id - }; - - index = 0; - packet.setDup(); - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(headerLength + EMC_TX_BUFFER_SIZE, available); - TEST_ASSERT_EQUAL_UINT8_ARRAY(checkDup, packet.data(index), headerLength); - - memset(payloadChunk, 0x04, EMC_TX_BUFFER_SIZE); - index = headerLength; - available = packet.available(index); - TEST_ASSERT_EQUAL_UINT32(EMC_TX_BUFFER_SIZE, available); - TEST_ASSERT_EQUAL_UINT8_ARRAY(payloadChunk, packet.data(index), available); -} - -int main() { - UNITY_BEGIN(); - RUN_TEST(test_encodeConnect0); - RUN_TEST(test_encodeConnect1); - RUN_TEST(test_encodeConnect2); - RUN_TEST(test_encodeConnectFail0); - RUN_TEST(test_encodePublish0); - RUN_TEST(test_encodePublish1); - RUN_TEST(test_encodePublish2); - RUN_TEST(test_encodePubAck); - RUN_TEST(test_encodePubRec); - RUN_TEST(test_encodePubRel); - RUN_TEST(test_encodePubComp); - RUN_TEST(test_encodeSubscribe); - RUN_TEST(test_encodeMultiSubscribe2); - RUN_TEST(test_encodeMultiSubscribe3); - RUN_TEST(test_encodeUnsubscribe); - RUN_TEST(test_encodeMultiUnsubscribe2); - RUN_TEST(test_encodeMultiUnsubscribe3); - RUN_TEST(test_encodePingReq); - RUN_TEST(test_encodeDisconnect); - RUN_TEST(test_encodeChunkedPublish); - return UNITY_END(); -} diff --git a/lib/espMqttClient/test/test_parser/test_parser.cpp b/lib/espMqttClient/test/test_parser/test_parser.cpp deleted file mode 100644 index ed51f92..0000000 --- a/lib/espMqttClient/test/test_parser/test_parser.cpp +++ /dev/null @@ -1,355 +0,0 @@ -#include - -#include - -using espMqttClientInternals::Parser; -using espMqttClientInternals::ParserResult; -using espMqttClientInternals::IncomingPacket; - -void setUp() {} -void tearDown() {} - -Parser parser; - -void test_Connack() { - const uint8_t stream[] = { - 0b00100000, // header - 0b00000010, // flags - 0b00000001, // session present - 0b00000000 // reserved - }; - const size_t length = 4; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(4, bytesRead); - TEST_ASSERT_EQUAL_UINT8(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT8(1, parser.getPacket().variableHeader.fixed.connackVarHeader.sessionPresent); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().variableHeader.fixed.connackVarHeader.returnCode); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_Empty() { - const uint8_t stream[] = { - 0x00 - }; - const size_t length = 0; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_UINT8(ParserResult::awaitData, result); - TEST_ASSERT_EQUAL_INT32(0, bytesRead); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_Header() { - const uint8_t stream[] = { - 0x12, - 0x13, - 0x14 - }; - const size_t length = 3; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::protocolError, result); - TEST_ASSERT_EQUAL_UINT32(1, bytesRead); -} - -void test_Publish() { - uint8_t stream[] = { - 0b00110010, // header - 0x0B, // remaining length - 0x00, 0x03, 'a', '/', 'b', // topic - 0x00, 0x0A, // packet id - 0x01, 0x02 // payload - }; - size_t length = 11; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PUBLISH, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_STRING("a/b", parser.getPacket().variableHeader.topic); - TEST_ASSERT_EQUAL_UINT16(10, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT32(0, parser.getPacket().payload.index); - TEST_ASSERT_EQUAL_UINT32(2, parser.getPacket().payload.length); - TEST_ASSERT_EQUAL_UINT32(4, parser.getPacket().payload.total); - TEST_ASSERT_EQUAL_UINT8(1, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); - - stream[0] = 0x03; - stream[1] = 0x04; - length = 2; - - bytesRead = 0; - result = parser.parse(stream, length, &bytesRead); - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_STRING("a/b", parser.getPacket().variableHeader.topic); - TEST_ASSERT_EQUAL_UINT16(10, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT32(2, parser.getPacket().payload.index); - TEST_ASSERT_EQUAL_UINT32(2, parser.getPacket().payload.length); - TEST_ASSERT_EQUAL_UINT32(4, parser.getPacket().payload.total); - TEST_ASSERT_EQUAL_UINT8(1, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_Publish_empty() { - uint8_t stream0[] = { - 0b00110000, // header - 0x05, // remaining length - 0x00, 0x03, 'a', '/', 'b', // topic - }; - size_t length0 = 7; - - size_t bytesRead0 = 0; - ParserResult result0 = parser.parse(stream0, length0, &bytesRead0); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result0); - TEST_ASSERT_EQUAL_UINT32(length0, bytesRead0); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PUBLISH, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_STRING("a/b", parser.getPacket().variableHeader.topic); - TEST_ASSERT_EQUAL_UINT32(0, parser.getPacket().payload.index); - TEST_ASSERT_EQUAL_UINT32(0, parser.getPacket().payload.length); - TEST_ASSERT_EQUAL_UINT32(0, parser.getPacket().payload.total); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); - - uint8_t stream1[] = { - 0b00110000, // header - 0x05, // remaining length - 0x00, 0x03, 'a', '/', 'b', // topic - }; - size_t length1 = 7; - - size_t bytesRead1 = 0; - ParserResult result1 = parser.parse(stream1, length1, &bytesRead1); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result1); - TEST_ASSERT_EQUAL_UINT32(length1, bytesRead1); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PUBLISH, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_STRING("a/b", parser.getPacket().variableHeader.topic); - TEST_ASSERT_EQUAL_UINT32(0, parser.getPacket().payload.index); - TEST_ASSERT_EQUAL_UINT32(0, parser.getPacket().payload.length); - TEST_ASSERT_EQUAL_UINT32(0, parser.getPacket().payload.total); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); - -} - -void test_PubAck() { - const uint8_t stream[] = { - 0b01000000, - 0b00000010, - 0x12, - 0x34 - }; - const size_t length = 4; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PUBACK, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT16(4660, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_PubRec() { - const uint8_t stream[] = { - 0b01010000, - 0b00000010, - 0x56, - 0x78 - }; - const size_t length = 4; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_BITS(0xF0, espMqttClientInternals::PacketType.PUBREC, parser.getPacket().fixedHeader.packetType); - TEST_ASSERT_EQUAL_UINT16(22136, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_PubRel() { - const uint8_t stream[] = { - 0b01100010, - 0b00000010, - 0x9A, - 0xBC - }; - const size_t length = 4; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PUBREL, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT16(0x9ABC, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_PubComp() { - const uint8_t stream[] = { - 0b01110000, - 0b00000010, - 0xDE, - 0xF0 - }; - const size_t length = 4; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PUBCOMP, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT16(0xDEF0, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_SubAck() { - const uint8_t stream[] = { - 0b10010000, - 0b00000100, - 0x00, - 0x0A, - 0x02, - 0x01 - }; - const size_t length = 6; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.SUBACK, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT16(10, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT8_ARRAY(&stream[4], parser.getPacket().payload.data,2); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_UnsubAck() { - const uint8_t stream[] = { - 0b10110000, - 0b00000010, - 0x00, - 0x0A - }; - const size_t length = 4; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.UNSUBACK, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT16(10, parser.getPacket().variableHeader.fixed.packetId); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - - -void test_PingResp() { - const uint8_t stream[] = { - 0b11010000, - 0x00 - }; - const size_t length = 2; - - size_t bytesRead = 0; - ParserResult result = parser.parse(stream, length, &bytesRead); - - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT32(length, bytesRead); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PINGRESP, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -void test_longStream() { - const uint8_t stream[] = { - 0x90, 0x03, 0x00, 0x01, 0x00, 0x31, 0x0F, 0x00, 0x09, 0x66, 0x6F, 0x6F, 0x2F, 0x62, 0x61, 0x72, - 0x2F, 0x30, 0x74, 0x65, 0x73, 0x74, 0x90, 0x03, 0x00, 0x02, 0x01, 0x33, 0x11, 0x00, 0x09, 0x66, - 0x6F, 0x6F, 0x2F, 0x62, 0x61, 0x72, 0x2F, 0x31, 0x00, 0x01, 0x74, 0x65, 0x73, 0x74, 0x90, 0x03, - 0x00, 0x03, 0x02, 0x30, 0x0F, 0x00, 0x09, 0x66, 0x6F, 0x6F, 0x2F, 0x62, 0x61, 0x72, 0x2F, 0x30, - 0x74, 0x65, 0x73, 0x74, 0x32, 0x11, 0x00, 0x09, 0x66, 0x6F, 0x6F, 0x2F, 0x62, 0x61, 0x72, 0x2F, - 0x31, 0x00, 0x02, 0x74, 0x65, 0x73, 0x74, 0x40, 0x02, 0x00, 0x04, 0x50, 0x02, 0x00, 0x05 - }; - const size_t length = 94; - - size_t bytesRead = 0; - ParserResult result = parser.parse(&stream[bytesRead], length - bytesRead, &bytesRead); - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.SUBACK, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT32(5, bytesRead); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); - - result = parser.parse(&stream[bytesRead], length - bytesRead, &bytesRead); - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.PUBLISH, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT32(5 + 17, bytesRead); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_TRUE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); - - result = parser.parse(&stream[bytesRead], length - bytesRead, &bytesRead); - TEST_ASSERT_EQUAL_INT32(ParserResult::packet, result); - TEST_ASSERT_EQUAL_UINT8(espMqttClientInternals::PacketType.SUBACK, parser.getPacket().fixedHeader.packetType & 0xF0); - TEST_ASSERT_EQUAL_UINT32(5 + 17 + 5, bytesRead); - TEST_ASSERT_EQUAL_UINT8(0, parser.getPacket().qos()); - TEST_ASSERT_FALSE(parser.getPacket().retain()); - TEST_ASSERT_FALSE(parser.getPacket().dup()); -} - -int main() { - UNITY_BEGIN(); - RUN_TEST(test_Connack); - RUN_TEST(test_Empty); - RUN_TEST(test_Header); - RUN_TEST(test_Publish); - RUN_TEST(test_Publish_empty); - RUN_TEST(test_PubAck); - RUN_TEST(test_PubRec); - RUN_TEST(test_PubRel); - RUN_TEST(test_PubComp); - RUN_TEST(test_SubAck); - RUN_TEST(test_UnsubAck); - RUN_TEST(test_PingResp); - RUN_TEST(test_longStream); - return UNITY_END(); -} diff --git a/lib/espMqttClient/test/test_remainingLength/test_remainingLength.cpp b/lib/espMqttClient/test/test_remainingLength/test_remainingLength.cpp deleted file mode 100644 index d422b25..0000000 --- a/lib/espMqttClient/test/test_remainingLength/test_remainingLength.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include - -#include - -#include - -void setUp() {} -void tearDown() {} - -// Examples takes from MQTT specification -uint8_t bytes1[] = {0x40}; -uint8_t size1 = 1; -uint32_t length1 = 64; - -uint8_t bytes2[] = {193, 2}; -uint8_t size2 = 2; -uint32_t length2 = 321; - -uint8_t bytes3[] = {0xff, 0xff, 0xff, 0x7f}; -uint8_t size3 = 4; -uint32_t length3 = 268435455; - -void test_remainingLengthDecode() { - TEST_ASSERT_EQUAL_INT32(length1, espMqttClientInternals::decodeRemainingLength(bytes1)); - TEST_ASSERT_EQUAL_INT32(length2, espMqttClientInternals::decodeRemainingLength(bytes2)); - - uint8_t stream[] = {0x80, 0x80, 0x80, 0x01}; - TEST_ASSERT_EQUAL_INT32(2097152 , espMqttClientInternals::decodeRemainingLength(stream)); - - TEST_ASSERT_EQUAL_INT32(length3, espMqttClientInternals::decodeRemainingLength(bytes3)); -} - -void test_remainingLengthEncode() { - uint8_t bytes[4]; - - TEST_ASSERT_EQUAL_UINT8(1, espMqttClientInternals::remainingLengthLength(0)); - - TEST_ASSERT_EQUAL_UINT8(size1, espMqttClientInternals::remainingLengthLength(length1)); - TEST_ASSERT_EQUAL_UINT8(size1, espMqttClientInternals::encodeRemainingLength(length1, bytes)); - TEST_ASSERT_EQUAL_UINT8_ARRAY(bytes1, bytes, size1); - TEST_ASSERT_EQUAL_UINT8(size2, espMqttClientInternals::remainingLengthLength(length2)); - TEST_ASSERT_EQUAL_UINT8(size2, espMqttClientInternals::encodeRemainingLength(length2, bytes)); - TEST_ASSERT_EQUAL_UINT8_ARRAY(bytes2, bytes, size2); - TEST_ASSERT_EQUAL_UINT8(size3, espMqttClientInternals::remainingLengthLength(length3)); - TEST_ASSERT_EQUAL_UINT8(size3, espMqttClientInternals::encodeRemainingLength(length3, bytes)); - TEST_ASSERT_EQUAL_UINT8_ARRAY(bytes3, bytes, size3); -} - -void test_remainingLengthError() { - uint8_t bytes[] = {0xff, 0xff, 0xff, 0x80}; // high bit of last byte is 1 - // this indicates a next byte is coming - // which is a violation of the spec - TEST_ASSERT_EQUAL_UINT8(0, espMqttClientInternals::remainingLengthLength(268435456)); - TEST_ASSERT_EQUAL_INT32(-1, espMqttClientInternals::decodeRemainingLength(bytes)); -} - -int main() { - UNITY_BEGIN(); - RUN_TEST(test_remainingLengthDecode); - RUN_TEST(test_remainingLengthEncode); - RUN_TEST(test_remainingLengthError); - return UNITY_END(); -} diff --git a/lib/espMqttClient/test/test_string/test_string.cpp b/lib/espMqttClient/test/test_string/test_string.cpp deleted file mode 100644 index f171d77..0000000 --- a/lib/espMqttClient/test/test_string/test_string.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include - -#include - -#include - -void setUp() {} -void tearDown() {} - -void test_encodeString() { - const char test[] = "abcd"; - uint8_t buffer[6]; - const uint8_t check[] = {0x00, 0x04, 'a', 'b', 'c', 'd'}; - const uint32_t length = 6; - - TEST_ASSERT_EQUAL_UINT32(length, espMqttClientInternals::encodeString(test, buffer)); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, buffer, length); -} - -void test_emtpyString() { - const char test[] = ""; - uint8_t buffer[2]; - const uint8_t check[] = {0x00, 0x00}; - const uint32_t length = 2; - - TEST_ASSERT_EQUAL_UINT32(length, espMqttClientInternals::encodeString(test, buffer)); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, buffer, length); -} - -void test_longString() { - const size_t maxSize = 65535; - char test[maxSize + 1]; - test[maxSize] = '\0'; - memset(test, 'a', maxSize); - uint8_t buffer[maxSize + 3]; - uint8_t check[maxSize + 2]; - check[0] = 0xFF; - check[1] = 0xFF; - memset(&check[2], 'a', maxSize); - const uint32_t length = 2 + maxSize; - - TEST_ASSERT_EQUAL_UINT32(length, espMqttClientInternals::encodeString(test, buffer)); - TEST_ASSERT_EQUAL_UINT8_ARRAY(check, buffer, length); -} - -void test_tooLongString() { - const size_t maxSize = 65535; - char test[maxSize + 2]; - test[maxSize + 1] = '\0'; - memset(test, 'a', maxSize + 1); - uint8_t buffer[maxSize + 4]; // extra 4 bytes for headroom: test progam, don't test test - const uint32_t length = 0; - - TEST_ASSERT_EQUAL_UINT32(length, espMqttClientInternals::encodeString(test, buffer)); -} - -int main() { - UNITY_BEGIN(); - RUN_TEST(test_encodeString); - RUN_TEST(test_emtpyString); - RUN_TEST(test_longString); - RUN_TEST(test_tooLongString); - return UNITY_END(); -} diff --git a/src/Config.h b/src/Config.h index d3a61df..e161403 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.01" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-08-28" +#define NUKI_HUB_DATE "2024-08-31" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index 8cae45b..40eb868 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -123,18 +123,6 @@ void NukiNetwork::setupDevice() Log->print(F("Network device: ")); Log->print(_device->deviceName()); - - -#ifndef NUKI_HUB_UPDATER - _device->mqttOnConnect([&](bool sessionPresent) - { - onMqttConnect(sessionPresent); - }); - _device->mqttOnDisconnect([&](espMqttClientTypes::DisconnectReason reason) - { - onMqttDisconnect(reason); - }); - #endif } void NukiNetwork::reconfigureDevice() @@ -242,55 +230,128 @@ void NukiNetwork::initialize() } } - Log->print(F("MQTT Broker: ")); - Log->print(_mqttBrokerAddr); - Log->print(F(":")); - Log->println(_mqttPort); - - _device->mqttSetClientId(_hostnameArr); - _device->mqttSetCleanSession(MQTT_CLEAN_SESSIONS); - _device->mqttSetKeepAlive(MQTT_KEEP_ALIVE); - - char gpioPath[250]; - bool rebGpio = rebuildGpio(); - - if(rebGpio) + if(strcmp(_mqttBrokerAddr, "") == 0) { - Log->println(F("Rebuild MQTT GPIO structure")); + Log->println(F("MQTT Broker not configured, aborting connection attempt.")); } - for (const auto &pinEntry: _gpio->pinConfiguration()) + else { - switch (pinEntry.role) + Log->print(F("MQTT Broker: ")); + Log->print(_mqttBrokerAddr); + Log->print(F(":")); + Log->println(_mqttPort); + + esp_mqtt_client_config_t mqtt_cfg = { 0 }; + + mqtt_cfg.credentials.client_id = _hostnameArr; + mqtt_cfg.session.disable_clean_session = !MQTT_CLEAN_SESSIONS; + mqtt_cfg.session.keepalive = MQTT_KEEP_ALIVE; + + size_t caLength = _preferences->getString(preference_mqtt_ca, _ca, TLS_CA_MAX_SIZE); + size_t crtLength = _preferences->getString(preference_mqtt_crt, _cert, TLS_CERT_MAX_SIZE); + size_t keyLength = _preferences->getString(preference_mqtt_key, _key, TLS_KEY_MAX_SIZE); + + if(caLength > 1) { - case PinRole::GeneralInputPullDown: - case PinRole::GeneralInputPullUp: - if(rebGpio) - { - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); - publishString(_lockPath.c_str(), gpioPath, "input", false); - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - publishString(_lockPath.c_str(), gpioPath, std::to_string(digitalRead(pinEntry.pin)).c_str(), false); - } - break; - case PinRole::GeneralOutput: - if(rebGpio) - { - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); - publishString(_lockPath.c_str(), gpioPath, "output", false); - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - publishString(_lockPath.c_str(), gpioPath, "0", false); - } - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - subscribe(_lockPath.c_str(), gpioPath); - break; - default: - break; + Log->println(F("MQTT over TLS.")); + + mqtt_cfg.broker.address.uri = ((String)"mqtts://" + _preferences->getString(preference_mqtt_broker, "") + ":" + _preferences->getString(preference_mqtt_broker_port, "8883")).c_str(); + mqtt_cfg.broker.verification.certificate = _ca; + + if(crtLength > 1 && keyLength > 1) // length is 1 when empty + { + Log->println(F("MQTT with client certificate.")); + mqtt_cfg.credentials.authentication.certificate = _cert; + mqtt_cfg.credentials.authentication.key = _key; + } } + else + { + Log->println(F("MQTT without TLS.")); + mqtt_cfg.broker.address.uri = ((String)"mqtt://" + _preferences->getString(preference_mqtt_broker, "") + ":" + _preferences->getString(preference_mqtt_broker_port, "1883")).c_str(); + } + + if(strlen(_mqttUser) == 0) + { + Log->println(F("MQTT: Connecting without credentials")); + } + else + { + Log->print(F("MQTT: Connecting with user: ")); Log->println(_mqttUser); + mqtt_cfg.credentials.username = _mqttUser; + mqtt_cfg.credentials.authentication.password = _mqttPass; + } + + mqtt_cfg.session.last_will.topic = _mqttConnectionStateTopic; + mqtt_cfg.session.last_will.msg = _lastWillPayload; + mqtt_cfg.session.last_will.msg_len = sizeof(_lastWillPayload); + mqtt_cfg.session.last_will.qos = 1; + mqtt_cfg.session.last_will.retain = true; + + esp_mqtt_client_handle_t _mqttClient = esp_mqtt_client_init(&mqtt_cfg); + esp_mqtt_client_register_event(_mqttClient, (esp_mqtt_event_id_t)ESP_EVENT_ANY_ID, mqtt_event_handler_cb, NULL); + + Log->println(F("Attempting MQTT connection")); + esp_mqtt_client_start(_mqttClient); + + if(_preferences->getBool(preference_mqtt_log_enabled, false) || _preferences->getBool(preference_webserial_enabled, false)) + { + MqttLoggerMode mode; + + if(_preferences->getBool(preference_mqtt_log_enabled, false) && _preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::MqttAndSerialAndWeb; + else if (_preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::SerialAndWeb; + else mode = MqttLoggerMode::MqttAndSerial; + + char* _path = new char[200]; + memset(_path, 0, sizeof(_path)); + + String pathStr = _preferences->getString(preference_mqtt_lock_path); + pathStr.concat(mqtt_topic_log); + strcpy(_path, pathStr.c_str()); + Log = new MqttLogger(_mqttClient, _path, mode); + } + + char gpioPath[250]; + bool rebGpio = rebuildGpio(); + + if(rebGpio) + { + Log->println(F("Rebuild MQTT GPIO structure")); + } + for (const auto &pinEntry: _gpio->pinConfiguration()) + { + switch (pinEntry.role) + { + case PinRole::GeneralInputPullDown: + case PinRole::GeneralInputPullUp: + if(rebGpio) + { + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); + publishString(_lockPath.c_str(), gpioPath, "input", false); + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + publishString(_lockPath.c_str(), gpioPath, std::to_string(digitalRead(pinEntry.pin)).c_str(), false); + } + break; + case PinRole::GeneralOutput: + if(rebGpio) + { + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); + publishString(_lockPath.c_str(), gpioPath, "output", false); + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + publishString(_lockPath.c_str(), gpioPath, "0", false); + } + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + subscribe(_lockPath.c_str(), gpioPath); + break; + default: + break; + } + } + _gpio->addCallback([this](const GpioAction& action, const int& pin) + { + gpioActionCallback(action, pin); + }); } - _gpio->addCallback([this](const GpioAction& action, const int& pin) - { - gpioActionCallback(action, pin); - }); _discoveryTopic = _preferences->getString(preference_mqtt_hass_discovery, ""); _offEnabled = _preferences->getBool(preference_official_hybrid, false); @@ -334,11 +395,6 @@ bool NukiNetwork::update() { _mqttConnectCounter = 0; - if(_firstDisconnected) { - _firstDisconnected = false; - _device->mqttDisconnect(true); - } - if(!_webEnabled) forceEnableWebServer = true; if(_restartOnDisconnect && (esp_timer_get_time() / 1000) > 60000) restartEsp(RestartReason::RestartOnDisconnectWatchdog); @@ -369,18 +425,17 @@ bool NukiNetwork::update() _logIp = false; Log->print(F("IP: ")); Log->println(_device->localIP()); - _firstDisconnected = true; } - if(!_device->mqttConnected() && _device->isConnected()) + while(!_mqttConnected && _device->isConnected()) + { + delay(2000); + _mqttConnectCounter++; + return false; + } + + if(!_mqttConnected && _device->isConnected()) { - bool success = reconnect(); - if(!success) - { - delay(2000); - _mqttConnectCounter++; - return false; - } _mqttConnectCounter = 0; if(forceEnableWebServer && !_webEnabled) { @@ -392,7 +447,7 @@ bool NukiNetwork::update() delay(2000); } - if(!_device->mqttConnected() || !_device->isConnected()) + if(!_mqttConnected || !_device->isConnected()) { if(_networkTimeout > 0 && (ts - _lastConnectedTs > _networkTimeout * 1000) && ts > 60000) { @@ -508,131 +563,91 @@ bool NukiNetwork::update() return true; } - -void NukiNetwork::onMqttConnect(const bool &sessionPresent) +void NukiNetwork::mqtt_event_handler_cb(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { - _connectReplyReceived = true; + _inst->mqtt_event_handler(handler_args, base, event_id, event_data); } -void NukiNetwork::onMqttDisconnect(const espMqttClientTypes::DisconnectReason &reason) +void NukiNetwork::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { - _connectReplyReceived = false; + static const char *MQTT_TAG = "mqtt"; - Log->print("MQTT disconnected. Reason: "); - switch(reason) - { - case espMqttClientTypes::DisconnectReason::USER_OK: - Log->println(F("USER_OK")); - break; - case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: - Log->println(F("MQTT_UNACCEPTABLE_PROTOCOL_VERSION")); - break; - case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED: - Log->println(F("MQTT_IDENTIFIER_REJECTED")); - break; - case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE: - Log->println(F("MQTT_SERVER_UNAVAILABLE")); - break; - case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS: - Log->println(F("MQTT_MALFORMED_CREDENTIALS")); - break; - case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED: - Log->println(F("MQTT_NOT_AUTHORIZED")); - break; - case espMqttClientTypes::DisconnectReason::TLS_BAD_FINGERPRINT: - Log->println(F("TLS_BAD_FINGERPRINT")); - break; - case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED: - Log->println(F("TCP_DISCONNECTED")); - break; - default: - Log->println(F("Unknown")); - break; + ESP_LOGD(MQTT_TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id); + esp_mqtt_event_handle_t event = (esp_mqtt_event_t*)event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(MQTT_TAG, "MQTT_EVENT_CONNECTED"); + _mqttConnected = true; + reconnect(); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(MQTT_TAG, "MQTT_EVENT_DISCONNECTED"); + _mqttConnected = false; + break; + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(MQTT_TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(MQTT_TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGI(MQTT_TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_DATA: + ESP_LOGI(MQTT_TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + onMqttDataReceived((const char*)event->topic, (const uint8_t*)event->data, (size_t&)event->data_len); + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(MQTT_TAG, "MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGI(MQTT_TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); + } + break; + default: + ESP_LOGI(MQTT_TAG, "Other event id:%d", event->event_id); + break; } } bool NukiNetwork::reconnect() { - _mqttConnectionState = 0; - - while (!_device->mqttConnected() && (esp_timer_get_time() / 1000) > _nextReconnect) + if (_mqttConnected) { - if(strcmp(_mqttBrokerAddr, "") == 0) + Log->println(F("MQTT connected")); + _mqttConnectedTs = millis(); + _mqttConnectionState = 1; + delay(100); + for(const String& topic : _subscribedTopics) { - Log->println(F("MQTT Broker not configured, aborting connection attempt.")); - _nextReconnect = (esp_timer_get_time() / 1000) + 5000; - return false; + esp_mqtt_client_subscribe(_mqttClient, topic.c_str(), MQTT_QOS_LEVEL); } - - Log->println(F("Attempting MQTT connection")); - - _connectReplyReceived = false; - - if(strlen(_mqttUser) == 0) + if(_firstConnect) { - Log->println(F("MQTT: Connecting without credentials")); - } - else - { - Log->print(F("MQTT: Connecting with user: ")); Log->println(_mqttUser); - _device->mqttSetCredentials(_mqttUser, _mqttPass); - } - - _device->setWill(_mqttConnectionStateTopic, 1, true, _lastWillPayload); - _device->mqttSetServer(_mqttBrokerAddr, _mqttPort); - _device->mqttConnect(); - - int64_t timeout = (esp_timer_get_time() / 1000) + 60000; - - while(!_connectReplyReceived && (esp_timer_get_time() / 1000) < timeout) - { - delay(50); - _device->update(); - if(_keepAliveCallback != nullptr) + _firstConnect = false; + publishString(_maintenancePathPrefix, mqtt_topic_network_device, _device->deviceName().c_str(), true); + for(const auto& it : _initTopics) { - _keepAliveCallback(); + esp_mqtt_client_publish(_mqttClient, it.first.c_str(), it.second.c_str(), 0, MQTT_QOS_LEVEL, 1); } } - if (_device->mqttConnected()) - { - Log->println(F("MQTT connected")); - _mqttConnectedTs = millis(); - _mqttConnectionState = 1; - delay(100); - _device->mqttOnMessage(NukiNetwork::onMqttDataReceivedCallback); - for(const String& topic : _subscribedTopics) - { - _device->mqttSubscribe(topic.c_str(), MQTT_QOS_LEVEL); - } - if(_firstConnect) - { - _firstConnect = false; - publishString(_maintenancePathPrefix, mqtt_topic_network_device, _device->deviceName().c_str(), true); - for(const auto& it : _initTopics) - { - _device->mqttPublish(it.first.c_str(), MQTT_QOS_LEVEL, true, it.second.c_str()); - } - } + publishString(_maintenancePathPrefix, mqtt_topic_mqtt_connection_state, "online", true); + publishString(_maintenancePathPrefix, mqtt_topic_info_nuki_hub_ip, _device->localIP().c_str(), true); - publishString(_maintenancePathPrefix, mqtt_topic_mqtt_connection_state, "online", true); - publishString(_maintenancePathPrefix, mqtt_topic_info_nuki_hub_ip, _device->localIP().c_str(), true); - - _mqttConnectionState = 2; - for(const auto& callback : _reconnectedCallbacks) - { - callback(); - } - } - else + _mqttConnectionState = 2; + for(const auto& callback : _reconnectedCallbacks) { - Log->print(F("MQTT connect failed, rc=")); - _device->printError(); - _mqttConnectionState = 0; - _nextReconnect = (esp_timer_get_time() / 1000) + 5000; - //_device->mqttDisconnect(true); + callback(); } } + else + { + _mqttConnectionState = 0; + } return _mqttConnectionState > 0; } @@ -683,34 +698,20 @@ void NukiNetwork::registerMqttReceiver(MqttReceiver* receiver) _mqttReceivers.push_back(receiver); } -void NukiNetwork::onMqttDataReceivedCallback(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void NukiNetwork::onMqttDataReceived(const char* topic, const uint8_t* payload, size_t& len) { - uint8_t value[800] = {0}; + if((_mqttConnectedTs == -1 || (millis() - _mqttConnectedTs < 2000)) && topic) return; - size_t l = min(len, sizeof(value)-1); - - for(int i=0; ionMqttDataReceived(properties, topic, value, len, index, total); -} - -void NukiNetwork::onMqttDataReceived(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t& len, size_t& index, size_t& total) -{ - if(_mqttConnectedTs == -1 || (millis() - _mqttConnectedTs < 2000)) return; - - parseGpioTopics(properties, topic, payload, len, index, total); + parseGpioTopics(topic, payload, len); for(auto receiver : _mqttReceivers) { - receiver->onMqttDataReceived(topic, (byte*)payload, index); + receiver->onMqttDataReceived(topic, (byte*)payload, len); } } -void NukiNetwork::parseGpioTopics(const espMqttClientTypes::MessageProperties &properties, const char *topic, const uint8_t *payload, size_t& len, size_t& index, size_t& total) +void NukiNetwork::parseGpioTopics(const char *topic, const uint8_t *payload, size_t& len) { char gpioPath[250]; buildMqttPath(gpioPath, {_lockPath.c_str(), mqtt_topic_gpio_prefix, mqtt_topic_gpio_pin}); @@ -756,11 +757,6 @@ int NukiNetwork::mqttConnectionState() return _mqttConnectionState; } -bool NukiNetwork::encryptionSupported() -{ - return _device->supportsEncryption(); -} - bool NukiNetwork::mqttRecentlyConnected() { return _mqttConnectedTs != -1 && (millis() - _mqttConnectedTs < 6000); @@ -779,7 +775,7 @@ void NukiNetwork::publishFloat(const char* prefix, const char* topic, const floa dtostrf(value, 0, precision, str); char path[200] = {0}; buildMqttPath(path, { prefix, topic }); - _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str); + esp_mqtt_client_publish(_mqttClient, path, str, 0, MQTT_QOS_LEVEL, retain); } void NukiNetwork::publishInt(const char* prefix, const char *topic, const int value, bool retain) @@ -788,7 +784,7 @@ void NukiNetwork::publishInt(const char* prefix, const char *topic, const int va itoa(value, str, 10); char path[200] = {0}; buildMqttPath(path, { prefix, topic }); - _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str); + esp_mqtt_client_publish(_mqttClient, path, str, 0, MQTT_QOS_LEVEL, retain); } void NukiNetwork::publishUInt(const char* prefix, const char *topic, const unsigned int value, bool retain) @@ -797,7 +793,7 @@ void NukiNetwork::publishUInt(const char* prefix, const char *topic, const unsig utoa(value, str, 10); char path[200] = {0}; buildMqttPath(path, { prefix, topic }); - _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str); + esp_mqtt_client_publish(_mqttClient, path, str, 0, MQTT_QOS_LEVEL, retain); } void NukiNetwork::publishULong(const char* prefix, const char *topic, const unsigned long value, bool retain) @@ -806,7 +802,7 @@ void NukiNetwork::publishULong(const char* prefix, const char *topic, const unsi utoa(value, str, 10); char path[200] = {0}; buildMqttPath(path, { prefix, topic }); - _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str); + esp_mqtt_client_publish(_mqttClient, path, str, 0, MQTT_QOS_LEVEL, retain); } void NukiNetwork::publishLongLong(const char* prefix, const char *topic, int64_t value, bool retain) @@ -827,7 +823,7 @@ void NukiNetwork::publishLongLong(const char* prefix, const char *topic, int64_t } char path[200] = {0}; buildMqttPath(path, { prefix, topic }); - _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, result); + esp_mqtt_client_publish(_mqttClient, path, result, 0, MQTT_QOS_LEVEL, retain); } void NukiNetwork::publishBool(const char* prefix, const char *topic, const bool value, bool retain) @@ -836,14 +832,14 @@ void NukiNetwork::publishBool(const char* prefix, const char *topic, const bool str[0] = value ? '1' : '0'; char path[200] = {0}; buildMqttPath(path, { prefix, topic }); - _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str); + esp_mqtt_client_publish(_mqttClient, path, str, 0, MQTT_QOS_LEVEL, retain); } bool NukiNetwork::publishString(const char* prefix, const char *topic, const char *value, bool retain) { char path[200] = {0}; buildMqttPath(path, { prefix, topic }); - return _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, value) > 0; + return esp_mqtt_client_publish(_mqttClient, path, value, 0, MQTT_QOS_LEVEL, retain) > 0; } void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const char *softwareVersion, const char *hardwareVersion, const char* availabilityTopic, const bool& hasKeypad, char* lockAction, char* unlockAction, char* openAction) @@ -899,7 +895,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha path.concat(uidString); path.concat("/smartlock/config"); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); // Battery critical publishHassTopic("binary_sensor", @@ -1755,7 +1751,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons json["options"][4] = "Intelligent"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "fob_action_1", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -1773,7 +1769,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons json["options"][4] = "Intelligent"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "fob_action_2", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -1791,7 +1787,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons json["options"][4] = "Intelligent"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "fob_action_3", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -1808,7 +1804,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons json["options"][3] = "Slowest"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "advertising_mode", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -1869,7 +1865,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "timezone", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2019,7 +2015,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons json["options"][6] = "Show Status"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "single_button_press_action", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2039,7 +2035,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons json["options"][6] = "Show Status"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "double_button_press_action", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2083,7 +2079,7 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons json["options"][2] = "Lithium"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "battery_type", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2516,7 +2512,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["event_types"][1] = "ringlocked"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("event", "ring", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); if((int)basicOpenerConfigAclPrefs[5] == 1) { @@ -2700,7 +2696,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][5] = "Ring"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "fob_action_1", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2719,7 +2715,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][5] = "Ring"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "fob_action_2", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2738,7 +2734,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][5] = "Ring"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "fob_action_3", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2755,7 +2751,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][3] = "Slowest"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "advertising_mode", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2816,7 +2812,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "timezone", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -2845,7 +2841,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][15] = "Spare"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "operating_mode", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3055,7 +3051,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][7] = "CM & RTO & Ring"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "doorbell_suppression", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3099,7 +3095,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][3] = "Sound 3"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "sound_ring", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3116,7 +3112,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][3] = "Sound 3"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "sound_open", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3133,7 +3129,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][3] = "Sound 3"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "sound_rto", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3150,7 +3146,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][3] = "Sound 3"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "sound_cm", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3199,7 +3195,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][7] = "Open"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "single_button_press_action", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3220,7 +3216,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][7] = "Open"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "double_button_press_action", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3236,7 +3232,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co json["options"][2] = "Lithium"; serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath("select", "battery_type", uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } else { @@ -3411,7 +3407,7 @@ void NukiNetwork::publishHassTopic(const String& mqttDeviceType, json = createHassJson(uidString, uidStringPostfix, displayName, name, baseTopic, stateTopic, deviceType, deviceClass, stateClass, entityCat, commandTopic, additionalEntries); serializeJson(json, _buffer, _bufferSize); String path = createHassTopicPath(mqttDeviceType, mqttDeviceName, uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer); + esp_mqtt_client_publish(_mqttClient, path.c_str(), _buffer, 0, MQTT_QOS_LEVEL, 1); } } @@ -3434,7 +3430,7 @@ void NukiNetwork::removeHassTopic(const String& mqttDeviceType, const String& mq if (_discoveryTopic != "") { String path = createHassTopicPath(mqttDeviceType, mqttDeviceName, uidString); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, ""); + esp_mqtt_client_publish(_mqttClient, path.c_str(), "", 0, MQTT_QOS_LEVEL, 1); } } @@ -3442,7 +3438,7 @@ void NukiNetwork::removeTopic(const String& mqttPath, const String& mqttTopic) { String path = mqttPath; path.concat(mqttTopic); - _device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, ""); + esp_mqtt_client_publish(_mqttClient, path.c_str(), "", 0, MQTT_QOS_LEVEL, 1); #ifdef DEBUG_NUKIHUB Log->print(F("Removing MQTT topic: ")); @@ -3812,7 +3808,7 @@ void NukiNetwork::timeZoneIdToString(const Nuki::TimeZoneId timeZoneId, char* st uint16_t NukiNetwork::subscribe(const char *topic, uint8_t qos) { - return _device->mqttSubscribe(topic, qos); + return esp_mqtt_client_subscribe(_mqttClient, topic, qos); } void NukiNetwork::addReconnectedCallback(std::function reconnectedCallback) @@ -3822,7 +3818,7 @@ void NukiNetwork::addReconnectedCallback(std::function reconnectedCallba void NukiNetwork::disableMqtt() { - _device->disableMqtt(); + esp_mqtt_client_disconnect(_mqttClient); _mqttEnabled = false; } diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index afc5ca8..39f0e72 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -10,6 +10,7 @@ #ifndef NUKI_HUB_UPDATER #include "MqttReceiver.h" +#include "mqtt_client.h" #include "MqttTopics.h" #include "Gpio.h" #include @@ -84,7 +85,6 @@ public: void timeZoneIdToString(const Nuki::TimeZoneId timeZoneId, char* str); int mqttConnectionState(); // 0 = not connected; 1 = connected; 2 = connected and mqtt processed - bool encryptionSupported(); bool mqttRecentlyConnected(); bool pathEquals(const char* prefix, const char* path, const char* referencePath); uint16_t subscribe(const char* topic, uint8_t qos); @@ -115,9 +115,10 @@ private: bool _offEnabled = false; #ifndef NUKI_HUB_UPDATER - static void onMqttDataReceivedCallback(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); - void onMqttDataReceived(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t& len, size_t& index, size_t& total); - void parseGpioTopics(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t& len, size_t& index, size_t& total); + static void mqtt_event_handler_cb(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); + void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); + void onMqttDataReceived(const char* topic, const uint8_t* payload, size_t& len); + void parseGpioTopics(const char* topic, const uint8_t* payload, size_t& len); void gpioActionCallback(const GpioAction& action, const int& pin); String createHassTopicPath(const String& mqttDeviceType, const String& mqttDeviceName, const String& uidString); @@ -134,10 +135,6 @@ private: const String& commandTopic = "", std::vector> additionalEntries = {} ); - - void onMqttConnect(const bool& sessionPresent); - void onMqttDisconnect(const espMqttClientTypes::DisconnectReason& reason); - void buildMqttPath(char* outPath, std::initializer_list paths); const char* _lastWillPayload = "offline"; @@ -146,13 +143,18 @@ private: String _discoveryTopic; Gpio* _gpio; - + int _mqttConnectionState = 0; + bool _mqttConnected = false; int _mqttConnectCounter = 0; int _mqttPort = 1883; long _mqttConnectedTs = -1; - bool _connectReplyReceived = false; bool _firstDisconnected = true; + + esp_mqtt_client_handle_t _mqttClient; + char _ca[TLS_CA_MAX_SIZE] = {0}; + char _cert[TLS_CERT_MAX_SIZE] = {0}; + char _key[TLS_KEY_MAX_SIZE] = {0}; int64_t _nextReconnect = 0; char _mqttBrokerAddr[101] = {0}; diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 90f40cf..278a4d8 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -2786,9 +2786,9 @@ esp_err_t WebCfgServer::buildMqttConfigHtml(PsychicRequest *request) printInputField(&response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30, ""); printInputField(&response, "HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261, ""); if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox(&response, "OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), ""); - printTextarea(&response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported(), true); - printTextarea(&response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true); - printTextarea(&response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported(), true); + printTextarea(&response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, true, true); + printTextarea(&response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, true, true); + printTextarea(&response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, true, true); printDropDown(&response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions(), ""); #ifndef CONFIG_IDF_TARGET_ESP32H2 printCheckBox(&response, "NWHWWIFIFB", "Disable fallback to Wi-Fi / Wi-Fi config portal", _preferences->getBool(preference_network_wifi_fallback_disabled), ""); diff --git a/src/networkDevices/EthernetDevice.cpp b/src/networkDevices/EthernetDevice.cpp index 4a9f7cc..466f569 100644 --- a/src/networkDevices/EthernetDevice.cpp +++ b/src/networkDevices/EthernetDevice.cpp @@ -1,10 +1,6 @@ #include "EthernetDevice.h" #include "../PreferencesKeys.h" #include "../Logger.h" -#ifndef NUKI_HUB_UPDATER -#include "../MqttTopics.h" -#include "espMqttClient.h" -#endif #include "../RestartReason.h" EthernetDevice::EthernetDevice(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration, const std::string& deviceName, uint8_t phy_addr, int power, int mdc, int mdio, eth_phy_type_t ethtype, eth_clock_mode_t clock_mode) @@ -50,54 +46,6 @@ EthernetDevice::EthernetDevice(const String &hostname, init(); } -void EthernetDevice::init() -{ -#ifndef NUKI_HUB_UPDATER - size_t caLength = _preferences->getString(preference_mqtt_ca, _ca, TLS_CA_MAX_SIZE); - size_t crtLength = _preferences->getString(preference_mqtt_crt, _cert, TLS_CERT_MAX_SIZE); - size_t keyLength = _preferences->getString(preference_mqtt_key, _key, TLS_KEY_MAX_SIZE); - - _useEncryption = caLength > 1; // length is 1 when empty - - if(_useEncryption) - { - Log->println(F("MQTT over TLS.")); - Log->println(_ca); - _mqttClientSecure = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO); - _mqttClientSecure->setCACert(_ca); - if(crtLength > 1 && keyLength > 1) // length is 1 when empty - { - Log->println(F("MQTT with client certificate.")); - Log->println(_cert); - Log->println(_key); - _mqttClientSecure->setCertificate(_cert); - _mqttClientSecure->setPrivateKey(_key); - } - } else - { - Log->println(F("MQTT without TLS.")); - _mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO); - } - - if(_preferences->getBool(preference_mqtt_log_enabled, false) || _preferences->getBool(preference_webserial_enabled, false)) - { - MqttLoggerMode mode; - - if(_preferences->getBool(preference_mqtt_log_enabled, false) && _preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::MqttAndSerialAndWeb; - else if (_preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::SerialAndWeb; - else mode = MqttLoggerMode::MqttAndSerial; - - _path = new char[200]; - memset(_path, 0, sizeof(_path)); - - String pathStr = _preferences->getString(preference_mqtt_lock_path); - pathStr.concat(mqtt_topic_log); - strcpy(_path, pathStr.c_str()); - Log = new MqttLogger(*getMqttClient(), _path, mode); - } -#endif -} - const String EthernetDevice::deviceName() const { return _deviceName.c_str(); @@ -149,8 +97,6 @@ void EthernetDevice::initialize() void EthernetDevice::update() { - NetworkDevice::update(); - if(_checkIpTs != -1) { if(_ipConfiguration->ipAddress() != ETH.localIP()) @@ -221,19 +167,12 @@ void EthernetDevice::onNetworkEvent(arduino_event_id_t event, arduino_event_info } } - - void EthernetDevice::reconfigure() { delay(200); restartEsp(RestartReason::ReconfigureETH); } -bool EthernetDevice::supportsEncryption() -{ - return true; -} - bool EthernetDevice::isConnected() { return _connected; diff --git a/src/networkDevices/EthernetDevice.h b/src/networkDevices/EthernetDevice.h index dfb45dd..60fb7a2 100644 --- a/src/networkDevices/EthernetDevice.h +++ b/src/networkDevices/EthernetDevice.h @@ -11,9 +11,6 @@ #include #include #include "NetworkDevice.h" -#ifndef NUKI_HUB_UPDATER -#include "espMqttClient.h" -#endif class EthernetDevice : public NetworkDevice { @@ -50,7 +47,6 @@ public: virtual void update(); virtual ReconnectStatus reconnect(bool force = false); - bool supportsEncryption() override; virtual bool isConnected(); @@ -62,12 +58,10 @@ public: private: Preferences* _preferences; - void init(); void onDisconnected(); void onNetworkEvent(arduino_event_id_t event, arduino_event_info_t info); bool _connected = false; - char* _path; bool _hardwareInitialized = false; const std::string _deviceName; @@ -91,10 +85,4 @@ private: eth_phy_type_t _type; eth_clock_mode_t _clock_mode; bool _useSpi = false; - - #ifndef NUKI_HUB_UPDATER - char _ca[TLS_CA_MAX_SIZE] = {0}; - char _cert[TLS_CERT_MAX_SIZE] = {0}; - char _key[TLS_KEY_MAX_SIZE] = {0}; - #endif }; \ No newline at end of file diff --git a/src/networkDevices/NetworkDevice.cpp b/src/networkDevices/NetworkDevice.cpp index 9e65acd..bcea055 100644 --- a/src/networkDevices/NetworkDevice.cpp +++ b/src/networkDevices/NetworkDevice.cpp @@ -1,179 +1,6 @@ #include #include "NetworkDevice.h" -#include "../Logger.h" -void NetworkDevice::printError() -{ - Log->print(F("Free Heap: ")); - Log->println(ESP.getFreeHeap()); -} - -#ifndef NUKI_HUB_UPDATER void NetworkDevice::update() { - if (_mqttEnabled) - { - getMqttClient()->loop(); - } -} - -void NetworkDevice::mqttSetClientId(const char *clientId) -{ - if (_useEncryption) - { - _mqttClientSecure->setClientId(clientId); - } - else - { - _mqttClient->setClientId(clientId); - } -} - -void NetworkDevice::mqttSetCleanSession(bool cleanSession) -{ - if (_useEncryption) - { - _mqttClientSecure->setCleanSession(cleanSession); - } - else - { - _mqttClient->setCleanSession(cleanSession); - } -} - -void NetworkDevice::mqttSetKeepAlive(uint16_t keepAlive) -{ - if (_useEncryption) - { - _mqttClientSecure->setKeepAlive(keepAlive); - } - else - { - _mqttClient->setKeepAlive(keepAlive); - } -} - -uint16_t NetworkDevice::mqttPublish(const char *topic, uint8_t qos, bool retain, const char *payload) -{ - return getMqttClient()->publish(topic, qos, retain, payload); -} - -uint16_t NetworkDevice::mqttPublish(const char *topic, uint8_t qos, bool retain, const uint8_t *payload, size_t length) -{ - return getMqttClient()->publish(topic, qos, retain, payload, length); -} - -bool NetworkDevice::mqttConnected() const -{ - return getMqttClient()->connected(); -} - -void NetworkDevice::mqttSetServer(const char *host, uint16_t port) -{ - if (_useEncryption) - { - _mqttClientSecure->setServer(host, port); - } - else - { - _mqttClient->setServer(host, port); - } -} - -bool NetworkDevice::mqttConnect() -{ - return getMqttClient()->connect(); -} - -bool NetworkDevice::mqttDisconnect(bool force) -{ - return getMqttClient()->disconnect(force); -} - -void NetworkDevice::setWill(const char *topic, uint8_t qos, bool retain, const char *payload) -{ - if (_useEncryption) - { - _mqttClientSecure->setWill(topic, qos, retain, payload); - } - else - { - _mqttClient->setWill(topic, qos, retain, payload); - } -} - -void NetworkDevice::mqttSetCredentials(const char *username, const char *password) -{ - if (_useEncryption) - { - _mqttClientSecure->setCredentials(username, password); - } - else - { - _mqttClient->setCredentials(username, password); - } -} - -void NetworkDevice::mqttOnMessage(espMqttClientTypes::OnMessageCallback callback) -{ - if (_useEncryption) - { - _mqttClientSecure->onMessage(callback); - } - else - { - _mqttClient->onMessage(callback); - } -} - -void NetworkDevice::mqttOnConnect(espMqttClientTypes::OnConnectCallback callback) -{ - if(_useEncryption) - { - _mqttClientSecure->onConnect(callback); - } - else - { - _mqttClient->onConnect(callback); - } -} - -void NetworkDevice::mqttOnDisconnect(espMqttClientTypes::OnDisconnectCallback callback) -{ - if (_useEncryption) - { - _mqttClientSecure->onDisconnect(callback); - } - else - { - _mqttClient->onDisconnect(callback); - } -} - -uint16_t NetworkDevice::mqttSubscribe(const char *topic, uint8_t qos) -{ - return getMqttClient()->subscribe(topic, qos); -} - -void NetworkDevice::disableMqtt() -{ - getMqttClient()->disconnect(); - _mqttEnabled = false; -} - -MqttClient *NetworkDevice::getMqttClient() const -{ - if (_useEncryption) - { - return _mqttClientSecure; - } - else - { - return _mqttClient; - } -} -#else -void NetworkDevice::update() -{ -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/networkDevices/NetworkDevice.h b/src/networkDevices/NetworkDevice.h index fdf9065..2c933de 100644 --- a/src/networkDevices/NetworkDevice.h +++ b/src/networkDevices/NetworkDevice.h @@ -1,9 +1,4 @@ #pragma once - -#ifndef NUKI_HUB_UPDATER -#include "espMqttClient.h" -#include "MqttClientSetup.h" -#endif #include "IPConfiguration.h" enum class ReconnectStatus @@ -26,9 +21,6 @@ public: virtual void initialize() = 0; virtual ReconnectStatus reconnect(bool force = false) = 0; virtual void reconfigure() = 0; - virtual void printError(); - virtual bool supportsEncryption() = 0; - virtual void update(); virtual bool isConnected() = 0; @@ -36,38 +28,7 @@ public: virtual String localIP() = 0; virtual String BSSIDstr() = 0; - - #ifndef NUKI_HUB_UPDATER - virtual void mqttSetClientId(const char* clientId); - virtual void mqttSetCleanSession(bool cleanSession); - virtual void mqttSetKeepAlive(uint16_t keepAlive); - virtual uint16_t mqttPublish(const char* topic, uint8_t qos, bool retain, const char* payload); - virtual uint16_t mqttPublish(const char* topic, uint8_t qos, bool retain, const uint8_t* payload, size_t length); - virtual bool mqttConnected() const; - virtual void mqttSetServer(const char* host, uint16_t port); - virtual bool mqttConnect(); - virtual bool mqttDisconnect(bool force); - virtual void setWill(const char* topic, uint8_t qos, bool retain, const char* payload); - virtual void mqttSetCredentials(const char* username, const char* password); - virtual void mqttOnMessage(espMqttClientTypes::OnMessageCallback callback); - virtual void mqttOnConnect(espMqttClientTypes::OnConnectCallback callback); - virtual void mqttOnDisconnect(espMqttClientTypes::OnDisconnectCallback callback); - virtual void disableMqtt(); - - virtual uint16_t mqttSubscribe(const char* topic, uint8_t qos); - #endif - -protected: - #ifndef NUKI_HUB_UPDATER - espMqttClient *_mqttClient = nullptr; - espMqttClientSecure *_mqttClientSecure = nullptr; - - bool _useEncryption = false; - bool _mqttEnabled = true; - - MqttClient *getMqttClient() const; - #endif - +protected: const String _hostname; const IPConfiguration* _ipConfiguration = nullptr; }; \ No newline at end of file diff --git a/src/networkDevices/WifiDevice.cpp b/src/networkDevices/WifiDevice.cpp index dc32369..c986b5b 100644 --- a/src/networkDevices/WifiDevice.cpp +++ b/src/networkDevices/WifiDevice.cpp @@ -2,10 +2,6 @@ #include "WifiDevice.h" #include "../PreferencesKeys.h" #include "../Logger.h" -#ifndef NUKI_HUB_UPDATER -#include "../MqttTopics.h" -#include "espMqttClient.h" -#endif #include "../RestartReason.h" RTC_NOINIT_ATTR char WiFiDevice_reconfdetect[17]; @@ -16,51 +12,6 @@ WifiDevice::WifiDevice(const String& hostname, Preferences* preferences, const I _wm(preferences->getString(preference_cred_user, "").c_str(), preferences->getString(preference_cred_password, "").c_str()) { _startAp = strcmp(WiFiDevice_reconfdetect, "reconfigure_wifi") == 0; - - #ifndef NUKI_HUB_UPDATER - size_t caLength = preferences->getString(preference_mqtt_ca, _ca, TLS_CA_MAX_SIZE); - size_t crtLength = preferences->getString(preference_mqtt_crt, _cert, TLS_CERT_MAX_SIZE); - size_t keyLength = preferences->getString(preference_mqtt_key, _key, TLS_KEY_MAX_SIZE); - - _useEncryption = caLength > 1; // length is 1 when empty - - if(_useEncryption) - { - Log->println(F("MQTT over TLS.")); - Log->println(_ca); - _mqttClientSecure = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO); - _mqttClientSecure->setCACert(_ca); - if(crtLength > 1 && keyLength > 1) // length is 1 when empty - { - Log->println(F("MQTT with client certificate.")); - Log->println(_cert); - Log->println(_key); - _mqttClientSecure->setCertificate(_cert); - _mqttClientSecure->setPrivateKey(_key); - } - } else - { - Log->println(F("MQTT without TLS.")); - _mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO); - } - - if(preferences->getBool(preference_mqtt_log_enabled, false) || preferences->getBool(preference_webserial_enabled, false)) - { - MqttLoggerMode mode; - - if(preferences->getBool(preference_mqtt_log_enabled, false) && preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::MqttAndSerialAndWeb; - else if (preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::SerialAndWeb; - else mode = MqttLoggerMode::MqttAndSerial; - - _path = new char[200]; - memset(_path, 0, sizeof(_path)); - - String pathStr = preferences->getString(preference_mqtt_lock_path); - pathStr.concat(mqtt_topic_log); - strcpy(_path, pathStr.c_str()); - Log = new MqttLogger(*getMqttClient(), _path, mode); - } - #endif } const String WifiDevice::deviceName() const @@ -145,11 +96,6 @@ void WifiDevice::reconfigure() restartEsp(RestartReason::ReconfigureWifi); } -bool WifiDevice::supportsEncryption() -{ - return true; -} - bool WifiDevice::isConnected() { return WiFi.isConnected(); @@ -212,4 +158,4 @@ String WifiDevice::BSSIDstr() void WifiDevice::clearRtcInitVar(WiFiManager *) { memset(WiFiDevice_reconfdetect, 0, sizeof WiFiDevice_reconfdetect); -} +} \ No newline at end of file diff --git a/src/networkDevices/WifiDevice.h b/src/networkDevices/WifiDevice.h index 26aec2f..5a9787f 100644 --- a/src/networkDevices/WifiDevice.h +++ b/src/networkDevices/WifiDevice.h @@ -5,9 +5,6 @@ #include #include "NetworkDevice.h" #include "WiFiManager.h" -#ifndef NUKI_HUB_UPDATER -#include "espMqttClient.h" -#endif #include "IPConfiguration.h" class WifiDevice : public NetworkDevice @@ -20,7 +17,6 @@ public: virtual void initialize(); virtual void reconfigure(); virtual ReconnectStatus reconnect(bool force = false); - bool supportsEncryption() override; virtual bool isConnected(); @@ -40,12 +36,5 @@ private: bool _startAp = false; bool _isReconnecting = false; - char* _path; int64_t _disconnectTs = 0; - - #ifndef NUKI_HUB_UPDATER - char _ca[TLS_CA_MAX_SIZE] = {0}; - char _cert[TLS_CERT_MAX_SIZE] = {0}; - char _key[TLS_KEY_MAX_SIZE] = {0}; - #endif }; From 18c56de16d811bd9bae9c6eafc46e7ff392a25f5 Mon Sep 17 00:00:00 2001 From: iranl Date: Sat, 31 Aug 2024 22:13:59 +0200 Subject: [PATCH 06/30] ESP-MQTT --- clion/CMakeLists.txt | 9 - lib/MqttLogger/src/MqttLogger.cpp | 4 +- lib/MqttLogger/src/MqttLogger.h | 3 +- src/Config.h | 2 +- src/MqttReceiver.h | 2 +- src/NukiNetwork.cpp | 262 ++++++++++++++++++++---------- src/NukiNetwork.h | 6 +- src/NukiNetworkLock.cpp | 78 +++++---- src/NukiNetworkLock.h | 2 +- src/NukiNetworkOpener.cpp | 64 ++++---- src/NukiNetworkOpener.h | 2 +- src/WebCfgServer.cpp | 54 +++--- src/main.cpp | 7 +- 13 files changed, 288 insertions(+), 207 deletions(-) diff --git a/clion/CMakeLists.txt b/clion/CMakeLists.txt index 2a62b19..83721a7 100644 --- a/clion/CMakeLists.txt +++ b/clion/CMakeLists.txt @@ -52,7 +52,6 @@ set(SRCFILES ../lib/BleScanner/src/BleInterfaces.h ../lib/BleScanner/src/BleScanner.cpp ../lib/MqttLogger/src/MqttLogger.cpp - ../lib/AsyncTCP/src/AsyncTCP.cpp ../src/util/NetworkUtil.cpp ../src/enums/NetworkDeviceType.h ../src/util/NetworkDeviceInstantiator.cpp @@ -62,14 +61,6 @@ file(GLOB_RECURSE SRCFILESREC lib/NimBLE-Arduino/src/*.c lib/NimBLE-Arduino/src/*.cpp lib/NimBLE-Arduino/src/*.h - lib/ESP Async WebServer/src/*.cpp - lib/ESP Async WebServer/src/*.h - lib/espMqttClient/src/*.cpp - lib/espMqttClient/src/*.h - lib/espMqttClient/src/Packets/*.cpp - lib/espMqttClient/src/Packets/*.h - lib/espMqttClient/src/Transport/*.cpp - lib/espMqttClient/src/Transport/*.h lib/ArduinoJson/src/*.h lib/ArduinoJson/src/*.hpp ) diff --git a/lib/MqttLogger/src/MqttLogger.cpp b/lib/MqttLogger/src/MqttLogger.cpp index ea59c0c..3ac5be3 100644 --- a/lib/MqttLogger/src/MqttLogger.cpp +++ b/lib/MqttLogger/src/MqttLogger.cpp @@ -76,9 +76,9 @@ void MqttLogger::sendBuffer() bool doSerial = this->mode==MqttLoggerMode::SerialOnly || this->mode==MqttLoggerMode::MqttAndSerial || this->mode==MqttLoggerMode::MqttAndSerialAndWeb || this->mode==MqttLoggerMode::SerialAndWeb; bool doWebSerial = this->mode==MqttLoggerMode::MqttAndSerialAndWeb || this->mode==MqttLoggerMode::SerialAndWeb; - if (this->mode!=MqttLoggerMode::SerialOnly && this->mode!=MqttLoggerMode::SerialAndWeb && this->client != NULL) + if (this->mode!=MqttLoggerMode::SerialOnly && this->mode!=MqttLoggerMode::SerialAndWeb) { - esp_mqtt_client_publish(this->client, topic, (const char*)this->buffer, this->bufferCnt, 0, 1); + esp_mqtt_client_publish(this->client, topic, (const char*)this->buffer, this->bufferCnt, 1, 1); } else if (this->mode == MqttLoggerMode::MqttAndSerialFallback) { diff --git a/lib/MqttLogger/src/MqttLogger.h b/lib/MqttLogger/src/MqttLogger.h index cb35fcc..56dc300 100644 --- a/lib/MqttLogger/src/MqttLogger.h +++ b/lib/MqttLogger/src/MqttLogger.h @@ -31,7 +31,8 @@ private: const char* topic; uint8_t* buffer; uint8_t* bufferEnd; - uint16_t bufferCnt = 0, bufferSize = 0; + uint16_t bufferCnt = 0; + uint16_t bufferSize = 0; esp_mqtt_client_handle_t client; MqttLoggerMode mode; void sendBuffer(); diff --git a/src/Config.h b/src/Config.h index 1a4d1cb..74e85f5 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.01" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-08-31" +#define NUKI_HUB_DATE "2024-09-01" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/MqttReceiver.h b/src/MqttReceiver.h index 3db350d..5529cfa 100644 --- a/src/MqttReceiver.h +++ b/src/MqttReceiver.h @@ -5,5 +5,5 @@ class MqttReceiver { public: - virtual void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) = 0; + virtual void onMqttDataReceived(char* topic, int topic_len, char* data, int data_len) = 0; }; \ No newline at end of file diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index 40eb868..e56b445 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -241,11 +241,9 @@ void NukiNetwork::initialize() Log->print(F(":")); Log->println(_mqttPort); - esp_mqtt_client_config_t mqtt_cfg = { 0 }; - - mqtt_cfg.credentials.client_id = _hostnameArr; - mqtt_cfg.session.disable_clean_session = !MQTT_CLEAN_SESSIONS; - mqtt_cfg.session.keepalive = MQTT_KEEP_ALIVE; + _mqtt_cfg.credentials.client_id = _hostnameArr; + _mqtt_cfg.session.disable_clean_session = !MQTT_CLEAN_SESSIONS; + _mqtt_cfg.session.keepalive = MQTT_KEEP_ALIVE; size_t caLength = _preferences->getString(preference_mqtt_ca, _ca, TLS_CA_MAX_SIZE); size_t crtLength = _preferences->getString(preference_mqtt_crt, _cert, TLS_CERT_MAX_SIZE); @@ -255,20 +253,38 @@ void NukiNetwork::initialize() { Log->println(F("MQTT over TLS.")); - mqtt_cfg.broker.address.uri = ((String)"mqtts://" + _preferences->getString(preference_mqtt_broker, "") + ":" + _preferences->getString(preference_mqtt_broker_port, "8883")).c_str(); - mqtt_cfg.broker.verification.certificate = _ca; + String uri = "mqtts://"; + uri.concat(_preferences->getString(preference_mqtt_broker, "")); + uri.concat(":"); + uri.concat(_preferences->getInt(preference_mqtt_broker_port, 8883)); + Log->print("URI: "); + Log->println(uri.c_str()); + //_mqtt_cfg.broker.address.uri = uri.c_str(); + _mqtt_cfg.broker.address.hostname = _mqttBrokerAddr; + _mqtt_cfg.broker.address.transport = MQTT_TRANSPORT_OVER_SSL; + _mqtt_cfg.broker.address.port = _preferences->getInt(preference_mqtt_broker_port, 8883); + _mqtt_cfg.broker.verification.certificate = _ca; if(crtLength > 1 && keyLength > 1) // length is 1 when empty { Log->println(F("MQTT with client certificate.")); - mqtt_cfg.credentials.authentication.certificate = _cert; - mqtt_cfg.credentials.authentication.key = _key; + _mqtt_cfg.credentials.authentication.certificate = _cert; + _mqtt_cfg.credentials.authentication.key = _key; } } else { Log->println(F("MQTT without TLS.")); - mqtt_cfg.broker.address.uri = ((String)"mqtt://" + _preferences->getString(preference_mqtt_broker, "") + ":" + _preferences->getString(preference_mqtt_broker_port, "1883")).c_str(); + String uri = "mqtt://"; + uri.concat(_preferences->getString(preference_mqtt_broker, "")); + uri.concat(":"); + uri.concat(_preferences->getInt(preference_mqtt_broker_port, 1883)); + Log->print("URI: "); + Log->println(uri.c_str()); + //_mqtt_cfg.broker.address.uri = uri.c_str(); + _mqtt_cfg.broker.address.hostname = _mqttBrokerAddr; + _mqtt_cfg.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; + _mqtt_cfg.broker.address.port = _preferences->getInt(preference_mqtt_broker_port, 1883); } if(strlen(_mqttUser) == 0) @@ -278,79 +294,18 @@ void NukiNetwork::initialize() else { Log->print(F("MQTT: Connecting with user: ")); Log->println(_mqttUser); - mqtt_cfg.credentials.username = _mqttUser; - mqtt_cfg.credentials.authentication.password = _mqttPass; + _mqtt_cfg.credentials.username = _mqttUser; + _mqtt_cfg.credentials.authentication.password = _mqttPass; } - mqtt_cfg.session.last_will.topic = _mqttConnectionStateTopic; - mqtt_cfg.session.last_will.msg = _lastWillPayload; - mqtt_cfg.session.last_will.msg_len = sizeof(_lastWillPayload); - mqtt_cfg.session.last_will.qos = 1; - mqtt_cfg.session.last_will.retain = true; + _mqtt_cfg.session.last_will.topic = _mqttConnectionStateTopic; + _mqtt_cfg.session.last_will.msg = _lastWillPayload; + _mqtt_cfg.session.last_will.msg_len = sizeof(_lastWillPayload); + _mqtt_cfg.session.last_will.qos = 1; + _mqtt_cfg.session.last_will.retain = true; - esp_mqtt_client_handle_t _mqttClient = esp_mqtt_client_init(&mqtt_cfg); + _mqttClient = esp_mqtt_client_init(&_mqtt_cfg); esp_mqtt_client_register_event(_mqttClient, (esp_mqtt_event_id_t)ESP_EVENT_ANY_ID, mqtt_event_handler_cb, NULL); - - Log->println(F("Attempting MQTT connection")); - esp_mqtt_client_start(_mqttClient); - - if(_preferences->getBool(preference_mqtt_log_enabled, false) || _preferences->getBool(preference_webserial_enabled, false)) - { - MqttLoggerMode mode; - - if(_preferences->getBool(preference_mqtt_log_enabled, false) && _preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::MqttAndSerialAndWeb; - else if (_preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::SerialAndWeb; - else mode = MqttLoggerMode::MqttAndSerial; - - char* _path = new char[200]; - memset(_path, 0, sizeof(_path)); - - String pathStr = _preferences->getString(preference_mqtt_lock_path); - pathStr.concat(mqtt_topic_log); - strcpy(_path, pathStr.c_str()); - Log = new MqttLogger(_mqttClient, _path, mode); - } - - char gpioPath[250]; - bool rebGpio = rebuildGpio(); - - if(rebGpio) - { - Log->println(F("Rebuild MQTT GPIO structure")); - } - for (const auto &pinEntry: _gpio->pinConfiguration()) - { - switch (pinEntry.role) - { - case PinRole::GeneralInputPullDown: - case PinRole::GeneralInputPullUp: - if(rebGpio) - { - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); - publishString(_lockPath.c_str(), gpioPath, "input", false); - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - publishString(_lockPath.c_str(), gpioPath, std::to_string(digitalRead(pinEntry.pin)).c_str(), false); - } - break; - case PinRole::GeneralOutput: - if(rebGpio) - { - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); - publishString(_lockPath.c_str(), gpioPath, "output", false); - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - publishString(_lockPath.c_str(), gpioPath, "0", false); - } - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - subscribe(_lockPath.c_str(), gpioPath); - break; - default: - break; - } - } - _gpio->addCallback([this](const GpioAction& action, const int& pin) - { - gpioActionCallback(action, pin); - }); } _discoveryTopic = _preferences->getString(preference_mqtt_hass_discovery, ""); @@ -420,6 +375,70 @@ bool NukiNetwork::update() } } + if(_device->isConnected() && !_mqttClientInitiated && strcmp(_mqttBrokerAddr, "") != 0) + { + Log->println(F("Attempting MQTT connection")); + esp_mqtt_client_start(_mqttClient); + + if(_preferences->getBool(preference_mqtt_log_enabled, false) || _preferences->getBool(preference_webserial_enabled, false)) + { + MqttLoggerMode mode; + + if(_preferences->getBool(preference_mqtt_log_enabled, false) && _preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::MqttAndSerialAndWeb; + else if (_preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::SerialAndWeb; + else mode = MqttLoggerMode::MqttAndSerial; + + char* _path = new char[200]; + memset(_path, 0, sizeof(_path)); + + String pathStr = _preferences->getString(preference_mqtt_lock_path); + pathStr.concat(mqtt_topic_log); + strcpy(_path, pathStr.c_str()); + Log = new MqttLogger(_mqttClient, _path, mode); + } + + char gpioPath[250]; + bool rebGpio = rebuildGpio(); + + if(rebGpio) + { + Log->println(F("Rebuild MQTT GPIO structure")); + } + for (const auto &pinEntry: _gpio->pinConfiguration()) + { + switch (pinEntry.role) + { + case PinRole::GeneralInputPullDown: + case PinRole::GeneralInputPullUp: + if(rebGpio) + { + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); + publishString(_lockPath.c_str(), gpioPath, "input", false); + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + publishString(_lockPath.c_str(), gpioPath, std::to_string(digitalRead(pinEntry.pin)).c_str(), false); + } + break; + case PinRole::GeneralOutput: + if(rebGpio) + { + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); + publishString(_lockPath.c_str(), gpioPath, "output", false); + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + publishString(_lockPath.c_str(), gpioPath, "0", false); + } + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + subscribe(_lockPath.c_str(), gpioPath); + break; + default: + break; + } + } + _gpio->addCallback([this](const GpioAction& action, const int& pin) + { + gpioActionCallback(action, pin); + }); + } + if(_logIp && device()->isConnected() && !_device->localIP().equals("0.0.0.0")) { _logIp = false; @@ -579,6 +598,7 @@ void NukiNetwork::mqtt_event_handler(void *handler_args, esp_event_base_t base, switch ((esp_mqtt_event_id_t)event_id) { case MQTT_EVENT_CONNECTED: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_CONNECTED"); + _mqttClientInitiated = true; _mqttConnected = true; reconnect(); break; @@ -597,9 +617,9 @@ void NukiNetwork::mqtt_event_handler(void *handler_args, esp_event_base_t base, break; case MQTT_EVENT_DATA: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_DATA"); - printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); - printf("DATA=%.*s\r\n", event->data_len, event->data); - onMqttDataReceived((const char*)event->topic, (const uint8_t*)event->data, (size_t&)event->data_len); + //printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + //printf("DATA=%.*s\r\n", event->data_len, event->data); + onMqttDataReceived(event->topic, event->topic_len, event->data, event->data_len); break; case MQTT_EVENT_ERROR: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_ERROR"); @@ -698,20 +718,26 @@ void NukiNetwork::registerMqttReceiver(MqttReceiver* receiver) _mqttReceivers.push_back(receiver); } -void NukiNetwork::onMqttDataReceived(const char* topic, const uint8_t* payload, size_t& len) +void NukiNetwork::onMqttDataReceived(char* topic, int topic_len, char* data, int data_len) { - if((_mqttConnectedTs == -1 || (millis() - _mqttConnectedTs < 2000)) && topic) return; + char value[800] = {0}; + for(int i=0; ionMqttDataReceived(topic, (byte*)payload, len); + receiver->onMqttDataReceived(topic, topic_len, (char*)value, data_len); } } -void NukiNetwork::parseGpioTopics(const char *topic, const uint8_t *payload, size_t& len) +void NukiNetwork::parseGpioTopics(char* topic, int topic_len, char* data, int data_len) { char gpioPath[250]; buildMqttPath(gpioPath, {_lockPath.c_str(), mqtt_topic_gpio_prefix, mqtt_topic_gpio_pin}); @@ -730,7 +756,7 @@ void NukiNetwork::parseGpioTopics(const char *topic, const uint8_t *payload, siz if(_gpio->getPinRole(pin) == PinRole::GeneralOutput) { - const uint8_t pinState = strcmp((const char*)payload, "1") == 0 ? HIGH : LOW; + const uint8_t pinState = strcmp(data, "1") == 0 ? HIGH : LOW; Log->print(F("GPIO ")); Log->print(pin); Log->print(F(" (Output) --> ")); @@ -771,6 +797,10 @@ bool NukiNetwork::pathEquals(const char* prefix, const char* path, const char* r void NukiNetwork::publishFloat(const char* prefix, const char* topic, const float value, bool retain, const uint8_t precision) { + if(!_mqttClientInitiated) + { + return; + } char str[30]; dtostrf(value, 0, precision, str); char path[200] = {0}; @@ -780,6 +810,10 @@ void NukiNetwork::publishFloat(const char* prefix, const char* topic, const floa void NukiNetwork::publishInt(const char* prefix, const char *topic, const int value, bool retain) { + if(!_mqttClientInitiated) + { + return; + } char str[30]; itoa(value, str, 10); char path[200] = {0}; @@ -789,6 +823,10 @@ void NukiNetwork::publishInt(const char* prefix, const char *topic, const int va void NukiNetwork::publishUInt(const char* prefix, const char *topic, const unsigned int value, bool retain) { + if(!_mqttClientInitiated) + { + return; + } char str[30]; utoa(value, str, 10); char path[200] = {0}; @@ -798,6 +836,10 @@ void NukiNetwork::publishUInt(const char* prefix, const char *topic, const unsig void NukiNetwork::publishULong(const char* prefix, const char *topic, const unsigned long value, bool retain) { + if(!_mqttClientInitiated) + { + return; + } char str[30]; utoa(value, str, 10); char path[200] = {0}; @@ -807,6 +849,10 @@ void NukiNetwork::publishULong(const char* prefix, const char *topic, const unsi void NukiNetwork::publishLongLong(const char* prefix, const char *topic, int64_t value, bool retain) { + if(!_mqttClientInitiated) + { + return; + } static char result[21] = ""; memset(&result[0], 0, sizeof(result)); char temp[21] = ""; @@ -828,6 +874,10 @@ void NukiNetwork::publishLongLong(const char* prefix, const char *topic, int64_t void NukiNetwork::publishBool(const char* prefix, const char *topic, const bool value, bool retain) { + if(!_mqttClientInitiated) + { + return; + } char str[2] = {0}; str[0] = value ? '1' : '0'; char path[200] = {0}; @@ -837,6 +887,10 @@ void NukiNetwork::publishBool(const char* prefix, const char *topic, const bool bool NukiNetwork::publishString(const char* prefix, const char *topic, const char *value, bool retain) { + if(!_mqttClientInitiated) + { + return false; + } char path[200] = {0}; buildMqttPath(path, { prefix, topic }); return esp_mqtt_client_publish(_mqttClient, path, value, 0, MQTT_QOS_LEVEL, retain) > 0; @@ -844,6 +898,10 @@ bool NukiNetwork::publishString(const char* prefix, const char *topic, const cha void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const char *softwareVersion, const char *hardwareVersion, const char* availabilityTopic, const bool& hasKeypad, char* lockAction, char* unlockAction, char* openAction) { + if(!_mqttClientInitiated) + { + return; + } JsonDocument json; json.clear(); JsonObject dev = json["dev"].to(); @@ -1343,6 +1401,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, const char *baseTopic, char *name, char *uidString) { + if(!_mqttClientInitiated) + { + return; + } uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); @@ -2393,6 +2455,10 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons void NukiNetwork::publishHASSConfigDoorSensor(char *deviceType, const char *baseTopic, char *name, char *uidString) { + if(!_mqttClientInitiated) + { + return; + } publishHassTopic("binary_sensor", "door_sensor", uidString, @@ -2413,6 +2479,10 @@ void NukiNetwork::publishHASSConfigDoorSensor(char *deviceType, const char *base void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, const char *baseTopic, char *name, char *uidString) { + if(!_mqttClientInitiated) + { + return; + } uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); uint32_t basicOpenerConfigAclPrefs[14] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; @@ -3401,6 +3471,10 @@ void NukiNetwork::publishHassTopic(const String& mqttDeviceType, std::vector> additionalEntries ) { + if(!_mqttClientInitiated) + { + return; + } if (_discoveryTopic != "") { JsonDocument json; @@ -3427,6 +3501,10 @@ String NukiNetwork::createHassTopicPath(const String& mqttDeviceType, const Stri void NukiNetwork::removeHassTopic(const String& mqttDeviceType, const String& mqttDeviceName, const String& uidString) { + if(!_mqttClientInitiated) + { + return; + } if (_discoveryTopic != "") { String path = createHassTopicPath(mqttDeviceType, mqttDeviceName, uidString); @@ -3436,6 +3514,10 @@ void NukiNetwork::removeHassTopic(const String& mqttDeviceType, const String& mq void NukiNetwork::removeTopic(const String& mqttPath, const String& mqttTopic) { + if(!_mqttClientInitiated) + { + return; + } String path = mqttPath; path.concat(mqttTopic); esp_mqtt_client_publish(_mqttClient, path.c_str(), "", 0, MQTT_QOS_LEVEL, 1); @@ -3808,6 +3890,10 @@ void NukiNetwork::timeZoneIdToString(const Nuki::TimeZoneId timeZoneId, char* st uint16_t NukiNetwork::subscribe(const char *topic, uint8_t qos) { + if(!_mqttClientInitiated) + { + return -1; + } return esp_mqtt_client_subscribe(_mqttClient, topic, qos); } diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index 39f0e72..fad16f3 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -117,8 +117,8 @@ private: #ifndef NUKI_HUB_UPDATER static void mqtt_event_handler_cb(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); - void onMqttDataReceived(const char* topic, const uint8_t* payload, size_t& len); - void parseGpioTopics(const char* topic, const uint8_t* payload, size_t& len); + void onMqttDataReceived(char* topic, int topic_len, char* data, int data_len); + void parseGpioTopics(char* topic, int topic_len, char* data, int data_len); void gpioActionCallback(const GpioAction& action, const int& pin); String createHassTopicPath(const String& mqttDeviceType, const String& mqttDeviceName, const String& uidString); @@ -144,6 +144,8 @@ private: Gpio* _gpio; + esp_mqtt_client_config_t _mqtt_cfg = { 0 }; + bool _mqttClientInitiated = false; int _mqttConnectionState = 0; bool _mqttConnected = false; int _mqttConnectCounter = 0; diff --git a/src/NukiNetworkLock.cpp b/src/NukiNetworkLock.cpp index a5f77af..2c8e84f 100644 --- a/src/NukiNetworkLock.cpp +++ b/src/NukiNetworkLock.cpp @@ -184,24 +184,22 @@ void NukiNetworkLock::initialize() }); } -void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) +void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, int data_len) { - char* value = (char*)payload; - if(_network->mqttRecentlyConnected() && _network->pathEquals(_mqttPath, mqtt_topic_lock_action, topic)) { Log->println("MQTT recently connected, ignoring lock action."); return; } - if(comparePrefixedPath(topic, mqtt_topic_reset) && strcmp(value, "1") == 0) + if(comparePrefixedPath(topic, mqtt_topic_reset) && strcmp(data, "1") == 0) { Log->println(F("Restart requested via MQTT.")); _network->clearWifiFallback(); delay(200); restartEsp(RestartReason::RequestedViaMqtt); } - else if(comparePrefixedPath(topic, mqtt_topic_update) && strcmp(value, "1") == 0 && _preferences->getBool(preference_update_from_mqtt, false)) + else if(comparePrefixedPath(topic, mqtt_topic_update) && strcmp(data, "1") == 0 && _preferences->getBool(preference_update_from_mqtt, false)) { Log->println(F("Update requested via MQTT.")); @@ -303,17 +301,17 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const } else if(comparePrefixedPath(topic, mqtt_topic_webserver_action)) { - if(strcmp(value, "") == 0 || - strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || + strcmp(data, "--") == 0) return; - if(strcmp(value, "1") == 0) + if(strcmp(data, "1") == 0) { if(_preferences->getBool(preference_webserver_enabled, true) || forceEnableWebServer) return; Log->println(F("Webserver enabled, restarting.")); _preferences->putBool(preference_webserver_enabled, true); } - else if (strcmp(value, "0") == 0) + else if (strcmp(data, "0") == 0) { if(!_preferences->getBool(preference_webserver_enabled, true) && !forceEnableWebServer) return; Log->println(F("Webserver disabled, restarting.")); @@ -327,10 +325,10 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const } else if(comparePrefixedPath(topic, mqtt_topic_lock_log_rolling_last)) { - if(strcmp(value, "") == 0 || - strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || + strcmp(data, "--") == 0) return; - if(atoi(value) > 0 && atoi(value) > _lastRollingLog) _lastRollingLog = atoi(value); + if(atoi(data) > 0 && atoi(data) > _lastRollingLog) _lastRollingLog = atoi(data); } if(_offEnabled) @@ -341,7 +339,7 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const { if(_officialUpdateReceivedCallback != nullptr) { - _officialUpdateReceivedCallback(offTopic, value); + _officialUpdateReceivedCallback(offTopic, data); } } } @@ -349,19 +347,19 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const if(comparePrefixedPath(topic, mqtt_topic_lock_action)) { - if(strcmp(value, "") == 0 || - strcmp(value, "--") == 0 || - strcmp(value, "ack") == 0 || - strcmp(value, "unknown_action") == 0 || - strcmp(value, "denied") == 0 || - strcmp(value, "error") == 0) return; + if(strcmp(data, "") == 0 || + strcmp(data, "--") == 0 || + strcmp(data, "ack") == 0 || + strcmp(data, "unknown_action") == 0 || + strcmp(data, "denied") == 0 || + strcmp(data, "error") == 0) return; Log->print(F("Lock action received: ")); - Log->println(value); + Log->println(data); LockActionResult lockActionResult = LockActionResult::Failed; if(_lockActionReceivedCallback != NULL) { - lockActionResult = _lockActionReceivedCallback(value); + lockActionResult = _lockActionReceivedCallback(data); } switch(lockActionResult) @@ -387,16 +385,16 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const { if(_keypadCommandReceivedReceivedCallback != nullptr) { - if(strcmp(value, "--") == 0) return; + if(strcmp(data, "--") == 0) return; - _keypadCommandReceivedReceivedCallback(value, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled); + _keypadCommandReceivedReceivedCallback(data, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled); _keypadCommandId = 0; _keypadCommandName = "--"; _keypadCommandCode = "000000"; _keypadCommandEnabled = 1; - if(strcmp(value, "--") != 0) + if(strcmp(data, "--") != 0) { publishString(mqtt_topic_keypad_command_action, "--", true); } @@ -408,38 +406,38 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_id)) { - _keypadCommandId = atoi(value); + _keypadCommandId = atoi(data); } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_name)) { - _keypadCommandName = value; + _keypadCommandName = data; } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_code)) { - _keypadCommandCode = value; + _keypadCommandCode = data; } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_enabled)) { - _keypadCommandEnabled = atoi(value); + _keypadCommandEnabled = atoi(data); } } - if(comparePrefixedPath(topic, mqtt_topic_query_config) && strcmp(value, "1") == 0) + if(comparePrefixedPath(topic, mqtt_topic_query_config) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_CONFIG; publishString(mqtt_topic_query_config, "0", true); } - else if(comparePrefixedPath(topic, mqtt_topic_query_lockstate) && strcmp(value, "1") == 0) + else if(comparePrefixedPath(topic, mqtt_topic_query_lockstate) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_LOCKSTATE; publishString(mqtt_topic_query_lockstate, "0", true); } - else if(comparePrefixedPath(topic, mqtt_topic_query_keypad) && strcmp(value, "1") == 0) + else if(comparePrefixedPath(topic, mqtt_topic_query_keypad) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_KEYPAD; publishString(mqtt_topic_query_keypad, "0", true); } - else if(comparePrefixedPath(topic, mqtt_topic_query_battery) && strcmp(value, "1") == 0) + else if(comparePrefixedPath(topic, mqtt_topic_query_battery) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_BATTERY; publishString(mqtt_topic_query_battery, "0", true); @@ -447,11 +445,11 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const if(comparePrefixedPath(topic, mqtt_topic_config_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_configUpdateReceivedCallback != NULL) { - _configUpdateReceivedCallback(value); + _configUpdateReceivedCallback(data); } publishString(mqtt_topic_config_action, "--", true); @@ -459,11 +457,11 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const if(comparePrefixedPath(topic, mqtt_topic_keypad_json_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_keypadJsonCommandReceivedReceivedCallback != NULL) { - _keypadJsonCommandReceivedReceivedCallback(value); + _keypadJsonCommandReceivedReceivedCallback(data); } publishString(mqtt_topic_keypad_json_action, "--", true); @@ -471,11 +469,11 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const if(comparePrefixedPath(topic, mqtt_topic_timecontrol_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_timeControlCommandReceivedReceivedCallback != NULL) { - _timeControlCommandReceivedReceivedCallback(value); + _timeControlCommandReceivedReceivedCallback(data); } publishString(mqtt_topic_timecontrol_action, "--", true); @@ -483,11 +481,11 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const if(comparePrefixedPath(topic, mqtt_topic_auth_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_authCommandReceivedReceivedCallback != NULL) { - _authCommandReceivedReceivedCallback(value); + _authCommandReceivedReceivedCallback(data); } publishString(mqtt_topic_auth_action, "--", true); diff --git a/src/NukiNetworkLock.h b/src/NukiNetworkLock.h index d069ccf..34a8cbe 100644 --- a/src/NukiNetworkLock.h +++ b/src/NukiNetworkLock.h @@ -53,7 +53,7 @@ public: void setKeypadJsonCommandReceivedCallback(void (*keypadJsonCommandReceivedReceivedCallback)(const char* value)); void setTimeControlCommandReceivedCallback(void (*timeControlCommandReceivedReceivedCallback)(const char* value)); void setAuthCommandReceivedCallback(void (*authCommandReceivedReceivedCallback)(const char* value)); - void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) override; + void onMqttDataReceived(char* topic, int topic_len, char* data, int data_len) override; void publishFloat(const char* topic, const float value, bool retain, const uint8_t precision = 2); void publishInt(const char* topic, const int value, bool retain); diff --git a/src/NukiNetworkOpener.cpp b/src/NukiNetworkOpener.cpp index da7e868..337068f 100644 --- a/src/NukiNetworkOpener.cpp +++ b/src/NukiNetworkOpener.cpp @@ -136,10 +136,8 @@ void NukiNetworkOpener::update() } } -void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) +void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* data, int data_len) { - char* value = (char*)payload; - if(_network->mqttRecentlyConnected() && _network->pathEquals(_mqttPath, mqtt_topic_lock_action, topic)) { Log->println("MQTT recently connected, ignoring opener action."); @@ -148,27 +146,27 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con if(comparePrefixedPath(topic, mqtt_topic_lock_log_rolling_last)) { - if(strcmp(value, "") == 0 || - strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || + strcmp(data, "--") == 0) return; - if(atoi(value) > 0 && atoi(value) > _lastRollingLog) _lastRollingLog = atoi(value); + if(atoi(data) > 0 && atoi(data) > _lastRollingLog) _lastRollingLog = atoi(data); } if(comparePrefixedPath(topic, mqtt_topic_lock_action)) { - if(strcmp(value, "") == 0 || - strcmp(value, "--") == 0 || - strcmp(value, "ack") == 0 || - strcmp(value, "unknown_action") == 0 || - strcmp(value, "denied") == 0 || - strcmp(value, "error") == 0) return; + if(strcmp(data, "") == 0 || + strcmp(data, "--") == 0 || + strcmp(data, "ack") == 0 || + strcmp(data, "unknown_action") == 0 || + strcmp(data, "denied") == 0 || + strcmp(data, "error") == 0) return; Log->print(F("Opener action received: ")); - Log->println(value); + Log->println(data); LockActionResult lockActionResult = LockActionResult::Failed; if(_lockActionReceivedCallback != NULL) { - lockActionResult = _lockActionReceivedCallback(value); + lockActionResult = _lockActionReceivedCallback(data); } switch(lockActionResult) @@ -194,16 +192,16 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con { if(_keypadCommandReceivedReceivedCallback != nullptr) { - if(strcmp(value, "--") == 0) return; + if(strcmp(data, "--") == 0) return; - _keypadCommandReceivedReceivedCallback(value, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled); + _keypadCommandReceivedReceivedCallback(data, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled); _keypadCommandId = 0; _keypadCommandName = "--"; _keypadCommandCode = "000000"; _keypadCommandEnabled = 1; - if(strcmp(value, "--") != 0) + if(strcmp(data, "--") != 0) { publishString(mqtt_topic_keypad_command_action, "--", true); } @@ -215,38 +213,38 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_id)) { - _keypadCommandId = atoi(value); + _keypadCommandId = atoi(data); } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_name)) { - _keypadCommandName = value; + _keypadCommandName = data; } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_code)) { - _keypadCommandCode = value; + _keypadCommandCode = data; } else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_enabled)) { - _keypadCommandEnabled = atoi(value); + _keypadCommandEnabled = atoi(data); } } - if(comparePrefixedPath(topic, mqtt_topic_query_config) && strcmp(value, "1") == 0) + if(comparePrefixedPath(topic, mqtt_topic_query_config) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_CONFIG; publishString(mqtt_topic_query_config, "0", true); } - else if(comparePrefixedPath(topic, mqtt_topic_query_lockstate) && strcmp(value, "1") == 0) + else if(comparePrefixedPath(topic, mqtt_topic_query_lockstate) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_LOCKSTATE; publishString(mqtt_topic_query_lockstate, "0", true); } - else if(comparePrefixedPath(topic, mqtt_topic_query_keypad) && strcmp(value, "1") == 0) + else if(comparePrefixedPath(topic, mqtt_topic_query_keypad) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_KEYPAD; publishString(mqtt_topic_query_keypad, "0", true); } - else if(comparePrefixedPath(topic, mqtt_topic_query_battery) && strcmp(value, "1") == 0) + else if(comparePrefixedPath(topic, mqtt_topic_query_battery) && strcmp(data, "1") == 0) { _queryCommands = _queryCommands | QUERY_COMMAND_BATTERY; publishString(mqtt_topic_query_battery, "0", true); @@ -254,11 +252,11 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con if(comparePrefixedPath(topic, mqtt_topic_config_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_configUpdateReceivedCallback != NULL) { - _configUpdateReceivedCallback(value); + _configUpdateReceivedCallback(data); } publishString(mqtt_topic_config_action, "--", true); @@ -266,11 +264,11 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con if(comparePrefixedPath(topic, mqtt_topic_keypad_json_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_keypadJsonCommandReceivedReceivedCallback != NULL) { - _keypadJsonCommandReceivedReceivedCallback(value); + _keypadJsonCommandReceivedReceivedCallback(data); } publishString(mqtt_topic_keypad_json_action, "--", true); @@ -278,11 +276,11 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con if(comparePrefixedPath(topic, mqtt_topic_timecontrol_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_timeControlCommandReceivedReceivedCallback != NULL) { - _timeControlCommandReceivedReceivedCallback(value); + _timeControlCommandReceivedReceivedCallback(data); } publishString(mqtt_topic_timecontrol_action, "--", true); @@ -290,11 +288,11 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con if(comparePrefixedPath(topic, mqtt_topic_auth_action)) { - if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; if(_authCommandReceivedReceivedCallback != NULL) { - _authCommandReceivedReceivedCallback(value); + _authCommandReceivedReceivedCallback(data); } publishString(mqtt_topic_auth_action, "--", true); diff --git a/src/NukiNetworkOpener.h b/src/NukiNetworkOpener.h index 498cfe7..df36893 100644 --- a/src/NukiNetworkOpener.h +++ b/src/NukiNetworkOpener.h @@ -47,7 +47,7 @@ public: void setKeypadJsonCommandReceivedCallback(void (*keypadJsonCommandReceivedReceivedCallback)(const char* value)); void setTimeControlCommandReceivedCallback(void (*timeControlCommandReceivedReceivedCallback)(const char* value)); void setAuthCommandReceivedCallback(void (*authCommandReceivedReceivedCallback)(const char* value)); - void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) override; + void onMqttDataReceived(char* topic, int topic_len, char* data, int data_len) override; bool reconnected(); uint8_t queryCommands(); diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 090148b..203c37f 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -289,6 +289,33 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) return response.endSend(); } + #ifndef NUKI_HUB_UPDATER + bool manifestSuccess = false; + JsonDocument doc; + + NetworkClientSecure *clientOTAUpdate = new NetworkClientSecure; + if (clientOTAUpdate) { + clientOTAUpdate->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); + { + HTTPClient httpsOTAClient; + httpsOTAClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + httpsOTAClient.setTimeout(2500); + httpsOTAClient.useHTTP10(true); + + if (httpsOTAClient.begin(*clientOTAUpdate, GITHUB_OTA_MANIFEST_URL)) { + int httpResponseCodeOTA = httpsOTAClient.GET(); + + if (httpResponseCodeOTA == HTTP_CODE_OK || httpResponseCodeOTA == HTTP_CODE_MOVED_PERMANENTLY) + { + DeserializationError jsonError = deserializeJson(doc, httpsOTAClient.getStream()); + if (!jsonError) { manifestSuccess = true; } + } + httpsOTAClient.end(); + } + } + delete clientOTAUpdate; + } + response.print("

      "); response.print("

      Update Nuki Hub

      "); response.print("Click on the button to reboot and automatically update Nuki Hub and the Nuki Hub updater to the latest versions from GitHub"); @@ -317,33 +344,6 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) response.print(NUKI_HUB_DATE); response.print("
      "); - #ifndef NUKI_HUB_UPDATER - bool manifestSuccess = false; - JsonDocument doc; - - NetworkClientSecure *clientOTAUpdate = new NetworkClientSecure; - if (clientOTAUpdate) { - clientOTAUpdate->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); - { - HTTPClient httpsOTAClient; - httpsOTAClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - httpsOTAClient.setTimeout(2500); - httpsOTAClient.useHTTP10(true); - - if (httpsOTAClient.begin(*clientOTAUpdate, GITHUB_OTA_MANIFEST_URL)) { - int httpResponseCodeOTA = httpsOTAClient.GET(); - - if (httpResponseCodeOTA == HTTP_CODE_OK || httpResponseCodeOTA == HTTP_CODE_MOVED_PERMANENTLY) - { - DeserializationError jsonError = deserializeJson(doc, httpsOTAClient.getStream()); - if (!jsonError) { manifestSuccess = true; } - } - httpsOTAClient.end(); - } - } - delete clientOTAUpdate; - } - if(!manifestSuccess) { response.print("currentverlatestverdevverbetaver"); diff --git a/src/main.cpp b/src/main.cpp index 2e82446..a6ca969 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -103,7 +103,11 @@ int _log_vprintf(const char *fmt, va_list args) { void setReroute(){ esp_log_set_vprintf(_log_vprintf); - if(preferences->getBool(preference_mqtt_log_enabled)) esp_log_level_set("*", ESP_LOG_INFO); + if(preferences->getBool(preference_mqtt_log_enabled)) + { + esp_log_level_set("*", ESP_LOG_INFO); + esp_log_level_set("mqtt", ESP_LOG_NONE); + } else { esp_log_level_set("*", ESP_LOG_DEBUG); @@ -376,6 +380,7 @@ void setupTasks(bool ota) void setup() { esp_log_level_set("*", ESP_LOG_ERROR); + esp_log_level_set("mqtt", ESP_LOG_NONE); Serial.begin(115200); Log = &Serial; From 26eace46e4d84fbf0296623f8f3af914e59be8d6 Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 6 Sep 2024 21:01:53 +0200 Subject: [PATCH 07/30] Fix HA discovery errors --- src/NukiNetwork.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index e56b445..53b8e69 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -2990,7 +2990,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co { (char*)"cmd_tpl", (char*)"{ \"electricStrikeDelay\": \"{{ value }}\" }" }, { (char*)"val_tpl", (char*)"{{value_json.electricStrikeDelay}}" }, { (char*)"min", (char*)"0" }, - { (char*)"min", (char*)"30000" }, + { (char*)"max", (char*)"30000" }, { (char*)"step", (char*)"3000" }}); } else @@ -3045,7 +3045,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co { (char*)"cmd_tpl", (char*)"{ \"electricStrikeDuration\": \"{{ value }}\" }" }, { (char*)"val_tpl", (char*)"{{value_json.electricStrikeDuration}}" }, { (char*)"min", (char*)"1000" }, - { (char*)"min", (char*)"30000" }, + { (char*)"max", (char*)"30000" }, { (char*)"step", (char*)"3000" }}); } else @@ -3100,7 +3100,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co { (char*)"cmd_tpl", (char*)"{ \"rtoTimeout\": \"{{ value }}\" }" }, { (char*)"val_tpl", (char*)"{{value_json.rtoTimeout}}" }, { (char*)"min", (char*)"5" }, - { (char*)"min", (char*)"60" }}); + { (char*)"max", (char*)"60" }}); } else { @@ -3147,7 +3147,7 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co { (char*)"cmd_tpl", (char*)"{ \"doorbellSuppressionDuration\": \"{{ value }}\" }" }, { (char*)"val_tpl", (char*)"{{value_json.doorbellSuppressionDuration}}" }, { (char*)"min", (char*)"500" }, - { (char*)"min", (char*)"10000" }, + { (char*)"max", (char*)"10000" }, { (char*)"step", (char*)"1000" }}); } else From c9281d92999844889b9ba067f0a577757cd49dfc Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 6 Sep 2024 21:14:37 +0200 Subject: [PATCH 08/30] Fixes --- lib/WiFiManager/WiFiManager.cpp | 4 ++-- src/Config.h | 2 +- src/NukiNetwork.cpp | 4 +++- src/WebCfgServer.cpp | 1 - 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/WiFiManager/WiFiManager.cpp b/lib/WiFiManager/WiFiManager.cpp index aede273..8f9dd0d 100644 --- a/lib/WiFiManager/WiFiManager.cpp +++ b/lib/WiFiManager/WiFiManager.cpp @@ -1129,7 +1129,7 @@ bool WiFiManager::wifiConnectNew(String ssid, String pass,bool connect){ DEBUG_WM(F("find best RSSI: TRUE")); #endif if (!_numNetworks) - WiFi_scanNetworks(false, _enableCaptivePortal); // scan in case this gets called before any scans + WiFi_scanNetworks(false, false); // scan in case this gets called before any scans int n = _numNetworks; if (n == 0) { @@ -1218,7 +1218,7 @@ bool WiFiManager::wifiConnectDefault(){ DEBUG_WM(F("find best RSSI: TRUE")); #endif if (!_numNetworks) - WiFi_scanNetworks(false, _enableCaptivePortal); // scan in case this gets called before any scans + WiFi_scanNetworks(false, false); // scan in case this gets called before any scans int n = _numNetworks; if (n == 0) { diff --git a/src/Config.h b/src/Config.h index 74e85f5..e56b8ef 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.01" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-09-01" +#define NUKI_HUB_DATE "2024-09-06" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index 53b8e69..aa4b541 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -598,13 +598,16 @@ void NukiNetwork::mqtt_event_handler(void *handler_args, esp_event_base_t base, switch ((esp_mqtt_event_id_t)event_id) { case MQTT_EVENT_CONNECTED: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_CONNECTED"); + Log->println("MQTT Connected"); _mqttClientInitiated = true; _mqttConnected = true; reconnect(); break; case MQTT_EVENT_DISCONNECTED: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_DISCONNECTED"); + Log->println("MQTT Disconnected"); _mqttConnected = false; + reconnect(); break; case MQTT_EVENT_SUBSCRIBED: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); @@ -637,7 +640,6 @@ bool NukiNetwork::reconnect() { if (_mqttConnected) { - Log->println(F("MQTT connected")); _mqttConnectedTs = millis(); _mqttConnectionState = 1; delay(100); diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 203c37f..da0b7c2 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -3234,7 +3234,6 @@ esp_err_t WebCfgServer::buildConfigureWifiHtml(PsychicRequest *request) esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) { - Log->println("INFO HTML"); uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); PsychicStreamResponse response(request, "text/plain"); From 214483445f376b6a6cb50244b73eb42b4cc386ba Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 8 Sep 2024 21:04:22 +0200 Subject: [PATCH 09/30] Update WebCfgServer.cpp --- src/WebCfgServer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index b51a608..14caa77 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -2805,7 +2805,7 @@ esp_err_t WebCfgServer::buildMqttConfigHtml(PsychicRequest *request) printCheckBox(&response, "CHECKUPDATE", "Check for Firmware Updates every 24h", _preferences->getBool(preference_check_updates), ""); printCheckBox(&response, "UPDATEMQTT", "Allow updating using MQTT", _preferences->getBool(preference_update_from_mqtt), ""); printCheckBox(&response, "DISNONJSON", "Disable some extraneous non-JSON topics", _preferences->getBool(preference_disable_non_json), ""); - printCheckBox(&response, "OFFHYBRID", "Enable hybrid official MQTT and Nuki Hub setup", _preferences->getBool(preference_official_hybrid), ""); + printCheckBox(&response, "OFFHYBRID", "Enable hybrid official MQTT and Nuki Hub setup", _preferences->getBool(preference_official_hybrid_enabled), ""); printCheckBox(&response, "HYBRIDACT", "Enable sending actions through official MQTT", _preferences->getBool(preference_official_hybrid_actions), ""); printInputField(&response, "HYBRIDTIMER", "Time between status updates when official MQTT is offline (seconds)", _preferences->getInt(preference_query_interval_hybrid_lockstate), 5, ""); // printCheckBox(&response, "HYBRIDRETRY", "Retry command sent using official MQTT over BLE if failed", _preferences->getBool(preference_official_hybrid_retry), ""); // NOT IMPLEMENTED (YET?) @@ -3494,7 +3494,7 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print("\nRegister as: "); response.print(_preferences->getBool(preference_register_as_app, false) ? "App" : "Bridge"); response.print("\n\n------------ HYBRID MODE ------------"); - if(!_preferences->getBool(preference_official_hybrid, false)) response.print("\nHybrid mode enabled: No"); + if(!_preferences->getBool(preference_official_hybrid_enabled, false)) response.print("\nHybrid mode enabled: No"); else { response.print("\nHybrid mode enabled: Yes"); From 179c3c76bc7e9a9b0e7dfb3402e1c2539474a90b Mon Sep 17 00:00:00 2001 From: iranl Date: Wed, 2 Oct 2024 20:52:01 +0200 Subject: [PATCH 10/30] Fixes --- src/WebCfgServer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 6f665b1..8443950 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -3190,7 +3190,7 @@ esp_err_t WebCfgServer::buildGpioConfigHtml(PsychicRequest *request) { String pinStr = String(pin); String pinDesc = "Gpio " + pinStr; - printDropDown(pinStr.c_str(), pinDesc.c_str(), "", options, "gpioselect"); + printDropDown(&response, pinStr.c_str(), pinDesc.c_str(), "", options, "gpioselect"); if(std::find(disabledPins.begin(), disabledPins.end(), pin) != disabledPins.end()) { gpiopreselects.concat("gpio[" + pinStr + "] = '21';"); @@ -4102,14 +4102,14 @@ void WebCfgServer::printDropDown(PsychicStreamResponse *response, const char *to response->print(""); if(className.length() > 0) { - _response.concat("print(" - -
      -

      -

      2b96i{L#QKAVCOp*ax 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

    - - - - \ No newline at end of file diff --git a/lib/MycilaWebSerial/portal/package-lock.json b/lib/MycilaWebSerial/portal/package-lock.json deleted file mode 100644 index e66710c..0000000 --- a/lib/MycilaWebSerial/portal/package-lock.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "name": "frontend", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@gfx/zopfli": "^1.0.15", - "html-minifier-terser": "^7.1.0" - } - }, - "node_modules/@gfx/zopfli": { - "version": "1.0.15", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/acorn": { - "version": "8.10.0", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/camel-case": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/clean-css": { - "version": "5.3.2", - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/html-minifier-terser": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "~5.3.2", - "commander": "^10.0.0", - "entities": "^4.4.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.15.1" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/terser": { - "version": "5.21.0", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" - } - } -} diff --git a/lib/MycilaWebSerial/portal/package.json b/lib/MycilaWebSerial/portal/package.json deleted file mode 100644 index d6aec35..0000000 --- a/lib/MycilaWebSerial/portal/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "scripts": { - "build": "node finalize.js" - }, - "dependencies": { - "@gfx/zopfli": "^1.0.15", - "html-minifier-terser": "^7.1.0" - } -} diff --git a/lib/MycilaWebSerial/portal/pnpm-lock.yaml b/lib/MycilaWebSerial/portal/pnpm-lock.yaml deleted file mode 100644 index cffcb68..0000000 --- a/lib/MycilaWebSerial/portal/pnpm-lock.yaml +++ /dev/null @@ -1,177 +0,0 @@ -lockfileVersion: 5.3 - -specifiers: - '@gfx/zopfli': ^1.0.15 - html-minifier-terser: ^7.1.0 - -dependencies: - '@gfx/zopfli': 1.0.15 - html-minifier-terser: 7.1.0 - -packages: - - /@gfx/zopfli/1.0.15: - resolution: {integrity: sha512-7mBgpi7UD82fsff5ThQKet0uBTl4BYerQuc+/qA1ELTwWEiIedRTcD3JgiUu9wwZ2kytW8JOb165rSdAt8PfcQ==} - engines: {node: '>= 8'} - dependencies: - base64-js: 1.5.1 - dev: false - - /@jridgewell/gen-mapping/0.3.2: - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.17 - dev: false - - /@jridgewell/resolve-uri/3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - dev: false - - /@jridgewell/set-array/1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: false - - /@jridgewell/source-map/0.3.2: - resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} - dependencies: - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.17 - dev: false - - /@jridgewell/sourcemap-codec/1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: false - - /@jridgewell/trace-mapping/0.3.17: - resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: false - - /acorn/8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false - - /base64-js/1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false - - /buffer-from/1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: false - - /camel-case/4.1.2: - resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - dependencies: - pascal-case: 3.1.2 - tslib: 2.5.0 - dev: false - - /clean-css/5.2.0: - resolution: {integrity: sha512-2639sWGa43EMmG7fn8mdVuBSs6HuWaSor+ZPoFWzenBc6oN+td8YhTfghWXZ25G1NiiSvz8bOFBS7PdSbTiqEA==} - engines: {node: '>= 10.0'} - dependencies: - source-map: 0.6.1 - dev: false - - /commander/2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: false - - /commander/9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - dev: false - - /dot-case/3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - dependencies: - no-case: 3.0.4 - tslib: 2.5.0 - dev: false - - /entities/4.4.0: - resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} - engines: {node: '>=0.12'} - dev: false - - /html-minifier-terser/7.1.0: - resolution: {integrity: sha512-BvPO2S7Ip0Q5qt+Y8j/27Vclj6uHC6av0TMoDn7/bJPhMWHI2UtR2e/zEgJn3/qYAmxumrGp9q4UHurL6mtW9Q==} - engines: {node: ^14.13.1 || >=16.0.0} - hasBin: true - dependencies: - camel-case: 4.1.2 - clean-css: 5.2.0 - commander: 9.5.0 - entities: 4.4.0 - param-case: 3.0.4 - relateurl: 0.2.7 - terser: 5.16.3 - dev: false - - /lower-case/2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - dependencies: - tslib: 2.5.0 - dev: false - - /no-case/3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - dependencies: - lower-case: 2.0.2 - tslib: 2.5.0 - dev: false - - /param-case/3.0.4: - resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} - dependencies: - dot-case: 3.0.4 - tslib: 2.5.0 - dev: false - - /pascal-case/3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - dependencies: - no-case: 3.0.4 - tslib: 2.5.0 - dev: false - - /relateurl/0.2.7: - resolution: {integrity: sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=} - engines: {node: '>= 0.10'} - dev: false - - /source-map-support/0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: false - - /source-map/0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: false - - /terser/5.16.3: - resolution: {integrity: sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==} - engines: {node: '>=10'} - hasBin: true - dependencies: - '@jridgewell/source-map': 0.3.2 - acorn: 8.8.2 - commander: 2.20.3 - source-map-support: 0.5.21 - dev: false - - /tslib/2.5.0: - resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - dev: false diff --git a/lib/MycilaWebSerial/src/MycilaWebSerial.cpp b/lib/MycilaWebSerial/src/MycilaWebSerial.cpp deleted file mode 100644 index d4c331d..0000000 --- a/lib/MycilaWebSerial/src/MycilaWebSerial.cpp +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: MIT -/* - * Copyright (C) 2023-2024 Mathieu Carbou - */ -#include "MycilaWebSerial.h" - -#include "MycilaWebSerialPage.h" - -void WebSerialClass::setAuthentication(const String& username, const String& password) { - _username = username; - _password = password; - _authenticate = !_username.isEmpty() && !_password.isEmpty(); - if (_ws) { - _ws->setAuthentication(_username.c_str(), _password.c_str()); - } -} - -void WebSerialClass::begin(AsyncWebServer* server, const char* url) { - _server = server; - - String backendUrl = url; - backendUrl.concat("ws"); - _ws = new AsyncWebSocket(backendUrl); - - if (_authenticate) { - _ws->setAuthentication(_username.c_str(), _password.c_str()); - } - - _server->on(url, HTTP_GET, [&](AsyncWebServerRequest* request) { - if (_authenticate) { - if (!request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - } - AsyncWebServerResponse* response = request->beginResponse(200, "text/html", WEBSERIAL_HTML, sizeof(WEBSERIAL_HTML)); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - - _ws->onEvent([&](__unused AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, __unused void* arg, uint8_t* data, __unused size_t len) -> void { - if (type == WS_EVT_CONNECT) { - client->setCloseClientOnQueueFull(false); - return; - } - if (type == WS_EVT_DATA) { - AwsFrameInfo* info = (AwsFrameInfo*)arg; - if (info->final && info->index == 0 && info->len == len) { - if (info->opcode == WS_TEXT) { - data[len] = 0; - } - if (strcmp((char*)data, "ping") == 0) - client->text("pong"); - else if (_recv) - _recv(data, len); - } - } - }); - - _server->addHandler(_ws); -} - -void WebSerialClass::onMessage(WSLMessageHandler recv) { - _recv = recv; -} - -void WebSerialClass::onMessage(WSLStringMessageHandler callback) { - _recvString = callback; - _recv = [&](uint8_t* data, size_t len) { - if (data && len) { - String msg; - msg.reserve(len); - msg.concat((char*)data); - _recvString(msg); - } - }; -} - -size_t WebSerialClass::write(uint8_t m) { - if (!_ws) - return 0; - - // We do not support non-buffered write on webserial for the HIGH_PERF version - // we fail with a stack trace allowing the user to change the code to use write(const uint8_t* buffer, size_t size) instead - if (!_initialBufferCapacity) { -#ifdef ESP8266 - ets_printf("'-D WSL_FAIL_ON_NON_BUFFERED_WRITE' is set: non-buffered write is not supported. Please use write(const uint8_t* buffer, size_t size) instead."); -#else - log_e("'-D WSL_FAIL_ON_NON_BUFFERED_WRITE' is set: non-buffered write is not supported. Please use write(const uint8_t* buffer, size_t size) instead."); -#endif - assert(false); - return 0; - } - - write(&m, 1); - return (1); -} - -size_t WebSerialClass::write(const uint8_t* buffer, size_t size) { - if (!_ws || size == 0) - return 0; - - // No buffer, send directly (i.e. use case for log streaming) - if (!_initialBufferCapacity) { - size = buffer[size - 1] == '\n' ? size - 1 : size; - _send(buffer, size); - return size; - } - - // fill the buffer while sending data for each EOL - size_t start = 0, end = 0; - while (end < size) { - if (buffer[end] == '\n') { - if (end > start) { - _buffer.concat(reinterpret_cast(buffer + start), end - start); - } - _send(reinterpret_cast(_buffer.c_str()), _buffer.length()); - start = end + 1; - } - end++; - } - if (end > start) { - _buffer.concat(reinterpret_cast(buffer + start), end - start); - } - return size; -} - -void WebSerialClass::_send(const uint8_t* buffer, size_t size) { - if (_ws && size > 0) { - _ws->cleanupClients(WSL_MAX_WS_CLIENTS); - if (_ws->count()) { - _ws->textAll((const char*)buffer, size); - } - } - - // if buffer grew too much, free it, otherwise clear it - if (_initialBufferCapacity) { - if (_buffer.length() > _initialBufferCapacity) { - setBuffer(_initialBufferCapacity); - } else { - _buffer.clear(); - } - } -} - -void WebSerialClass::setBuffer(size_t initialCapacity) { - assert(initialCapacity <= UINT16_MAX); - _initialBufferCapacity = initialCapacity; - _buffer = String(); - _buffer.reserve(initialCapacity); -} - -WebSerialClass WebSerial; diff --git a/lib/MycilaWebSerial/src/MycilaWebSerial.h b/lib/MycilaWebSerial/src/MycilaWebSerial.h deleted file mode 100644 index 4770618..0000000 --- a/lib/MycilaWebSerial/src/MycilaWebSerial.h +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -/* - * Copyright (C) 2023-2024 Mathieu Carbou - */ -#pragma once - -#if defined(ESP8266) -#include "ESP8266WiFi.h" -#elif defined(ESP32) -#include "WiFi.h" -#endif - -#include -#include -#include - -#define WSL_VERSION "6.3.0" -#define WSL_VERSION_MAJOR 6 -#define WSL_VERSION_MINOR 3 -#define WSL_VERSION_REVISION 0 - -#ifndef WSL_MAX_WS_CLIENTS -#define WSL_MAX_WS_CLIENTS DEFAULT_MAX_WS_CLIENTS -#endif - -// High performance mode: -// - Low memory footprint (no stack allocation, no global buffer by default) -// - Low latency (messages sent immediately to the WebSocket queue) -// - High throughput (up to 20 messages per second, no locking mechanism) -// Also recommended to tweak AsyncTCP and ESPAsyncWebServer settings, for example: -// -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 // AsyncTCP queue size -// -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 // core for the async_task -// -D WS_MAX_QUEUED_MESSAGES=128 // WS message queue size - -typedef std::function WSLMessageHandler; -typedef std::function WSLStringMessageHandler; - -class WebSerialClass : public Print { - public: - void begin(AsyncWebServer* server, const char* url = "/webserial"); - inline void setAuthentication(const char* username, const char* password) { setAuthentication(String(username), String(password)); } - void setAuthentication(const String& username, const String& password); - void onMessage(WSLMessageHandler recv); - void onMessage(WSLStringMessageHandler recv); - size_t write(uint8_t) override; - size_t write(const uint8_t* buffer, size_t size) override; - - // A buffer (shared across cores) can be initialised with an initial capacity to be able to use any Print functions event those that are not buffered and would - // create a performance impact for WS calls. The goal of this buffer is to be used with lines ending with '\n', like log messages. - // The buffer size will eventually grow until a '\n' is found, then the message will be sent to the WS clients and a new buffer will be created. - // Set initialCapacity to 0 to disable buffering. - // Must be called before begin(): calling it after will erase the buffer and its content will be lost. - // The buffer is not enabled by default. - void setBuffer(size_t initialCapacity); - - private: - // Server - AsyncWebServer* _server; - AsyncWebSocket* _ws; - WSLMessageHandler _recv = nullptr; - WSLStringMessageHandler _recvString = nullptr; - bool _authenticate = false; - String _username; - String _password; - size_t _initialBufferCapacity = 0; - String _buffer; - void _send(const uint8_t* buffer, size_t size); -}; - -extern WebSerialClass WebSerial; diff --git a/lib/MycilaWebSerial/src/MycilaWebSerialPage.h b/lib/MycilaWebSerial/src/MycilaWebSerialPage.h deleted file mode 100644 index 8d4bf37..0000000 --- a/lib/MycilaWebSerial/src/MycilaWebSerialPage.h +++ /dev/null @@ -1,84 +0,0 @@ - -// SPDX-License-Identifier: GPL-3.0-or-later -#pragma once -const uint32_t WEBSERIAL_HTML_SIZE = 2320; -const uint8_t WEBSERIAL_HTML[] PROGMEM = { -31,139,8,0,0,0,0,0,2,3,116,85,247,146,179,54,16,127,21,197,95,202,57,99,225,242,117,48,78,239, -189,231,95,33,45,160,156,144,136,36,184,115,24,222,61,139,4,87,231,238,108,107,181,179,229,183,85,199,119,132, -225,254,220,2,169,125,163,78,199,249,23,152,56,29,27,240,140,240,154,89,7,62,239,124,73,223,44,60,163,61, -104,159,175,190,249,34,7,81,193,10,149,125,75,225,223,78,246,249,223,244,143,79,232,103,166,105,153,151,133,130, -211,209,75,143,199,95,80,144,207,140,118,70,193,113,27,89,15,140,93,73,225,235,92,64,47,57,208,112,217,72, -45,189,100,138,58,206,20,228,251,21,209,172,129,188,151,112,213,26,235,79,71,231,207,104,71,200,126,16,210,181, -138,157,211,66,25,126,57,178,161,97,182,146,58,77,94,88,104,50,15,215,158,10,224,198,34,38,163,83,109,52, -140,31,110,210,148,149,30,44,158,5,148,198,194,64,175,160,184,148,158,22,230,154,58,249,159,212,85,42,117,13, -86,250,236,49,107,108,23,39,59,178,35,123,244,51,22,70,156,7,211,131,117,220,26,165,104,1,53,235,165,177, -193,97,86,131,172,106,159,238,119,187,247,178,16,94,36,23,35,89,203,132,64,251,72,149,152,18,90,178,70,170, -115,74,89,219,42,160,238,236,60,52,155,120,208,78,110,62,85,82,95,254,192,248,111,129,243,37,106,108,86,191, -65,101,128,252,241,205,106,243,171,41,140,55,155,213,215,160,122,240,146,51,242,35,116,176,218,56,166,29,117,8, -191,28,147,144,21,142,169,7,59,4,154,41,89,233,52,114,198,164,178,236,60,112,163,16,254,179,87,175,94,239, -223,188,29,19,87,51,97,174,134,82,42,20,73,133,53,45,141,172,139,29,121,209,94,147,231,248,181,85,129,183, -233,127,75,146,221,235,245,154,220,151,59,160,204,225,145,220,171,245,122,76,174,104,217,41,53,220,38,103,66,33, -197,77,113,167,11,178,88,75,15,3,254,166,201,203,41,235,73,169,224,250,70,102,186,76,106,8,115,34,233,68, -165,251,49,249,167,115,94,150,103,42,49,91,142,130,22,195,61,78,10,26,77,91,211,105,1,98,40,140,21,96, -169,101,66,118,110,113,227,128,79,221,51,220,105,133,89,14,57,89,193,248,101,21,244,233,156,180,242,77,249,182, -100,99,210,48,169,239,193,203,2,48,33,109,52,152,162,124,215,232,108,193,51,15,68,16,165,206,51,235,179,80, -153,25,104,172,207,157,110,18,125,29,157,144,164,101,90,131,26,90,227,100,176,108,65,49,47,123,200,34,80,196, -84,150,51,77,195,236,164,206,40,41,22,86,76,124,8,55,187,159,130,169,189,31,133,24,205,69,37,206,20,191, -152,74,70,104,152,133,117,108,98,204,19,164,13,160,141,102,110,116,234,77,155,30,230,148,70,188,164,232,188,199, -196,242,206,58,180,218,26,25,34,92,230,225,13,54,203,126,135,63,72,60,182,106,58,175,164,134,24,78,24,180, -37,216,93,118,7,229,99,236,187,221,171,215,229,139,7,24,82,172,19,43,20,136,225,177,194,75,246,106,255,234, -237,141,194,51,11,28,29,13,13,198,52,23,227,121,114,8,185,91,144,199,76,90,8,120,123,176,211,32,170,236, -169,5,49,62,155,42,143,124,26,177,220,150,145,21,88,166,206,67,134,185,91,140,78,14,35,253,80,111,73,231, -211,17,48,133,88,136,195,64,134,206,77,173,0,10,184,159,147,119,163,53,37,14,246,47,196,77,56,135,185,16, -217,189,101,155,221,228,115,135,163,104,204,180,78,22,141,169,19,8,130,124,170,121,198,132,155,246,28,98,89,116, -112,152,16,125,131,170,113,180,13,243,84,65,233,135,64,166,19,185,176,163,94,228,7,122,60,110,227,139,112,220, -198,23,108,90,200,167,163,144,61,225,138,57,151,175,194,152,204,131,76,238,44,192,21,190,121,123,34,69,30,94, -38,18,172,228,75,152,152,151,7,239,87,189,63,29,25,169,45,148,249,150,156,142,178,169,38,93,101,42,67,80, -214,90,99,115,132,143,133,254,30,89,23,107,226,44,207,183,10,105,68,198,238,1,138,107,145,132,157,70,166,253, -70,98,123,173,162,20,90,189,95,220,211,49,158,139,254,188,177,72,180,179,66,247,92,73,126,153,99,76,216,152, -76,125,166,128,233,139,53,62,149,125,69,152,149,140,214,82,8,208,185,183,29,16,193,60,163,18,61,228,2,20, -120,32,165,84,42,231,157,181,152,149,207,166,50,145,210,240,46,140,68,94,50,245,63,173,85,181,237,214,14,3, -127,69,55,239,210,146,101,89,86,46,115,15,190,148,222,187,92,56,204,252,245,149,189,147,148,185,93,49,206,140, -33,123,76,231,207,96,90,234,191,165,48,182,223,195,127,31,223,252,54,51,133,8,62,183,30,103,48,93,228,161, -248,253,215,120,4,236,192,211,223,102,219,30,2,41,182,86,179,93,161,115,99,204,133,50,138,83,69,211,41,172, -229,34,109,224,192,81,133,5,9,166,87,206,107,201,184,97,170,84,131,204,2,73,41,247,60,203,85,150,198,160, -164,144,201,192,251,111,199,152,244,64,52,196,69,114,75,129,103,165,4,50,39,7,75,96,153,230,145,239,104,209, -22,132,0,131,73,159,142,145,15,18,45,141,230,24,205,215,220,189,245,238,25,28,199,16,232,87,216,7,29,211, -193,197,76,166,112,119,136,194,12,188,150,141,175,176,202,78,102,189,170,114,55,11,231,251,199,136,44,156,136,116, -97,39,124,166,159,179,103,71,221,132,255,15,142,175,183,142,163,254,211,155,245,217,39,13,62,8,213,15,180,55, -11,168,233,14,154,63,22,237,246,86,238,95,175,82,198,36,190,140,107,217,189,13,10,184,215,97,41,128,136,87, -34,163,131,15,185,236,218,24,58,53,208,128,166,176,99,202,109,106,17,53,92,112,209,232,177,206,237,253,30,109, -231,104,57,77,51,51,205,65,10,37,44,6,197,34,221,17,245,54,96,14,96,112,48,184,62,191,181,104,217,255, -224,221,161,26,131,49,175,73,0,37,219,78,9,191,51,235,221,182,186,66,229,116,85,242,59,171,82,249,221,117, -84,242,19,117,226,4,83,202,144,184,79,133,191,215,138,121,184,123,248,44,30,17,135,39,177,100,222,4,62,189, -102,90,95,52,216,118,207,218,193,15,60,26,74,146,208,252,35,166,100,48,233,87,229,224,206,133,57,42,170,254, -122,196,21,138,17,31,212,50,39,93,52,136,236,238,144,193,133,91,168,74,224,140,185,10,38,51,178,81,138,120, -190,170,65,196,101,14,3,133,37,186,172,173,248,149,41,175,77,223,220,168,130,101,39,219,42,170,125,181,228,66, -229,177,184,55,70,37,237,94,163,247,223,154,6,214,33,96,116,24,48,248,149,212,66,125,105,11,25,36,18,40, -193,100,48,42,7,201,130,129,36,76,214,66,61,20,62,84,14,41,164,152,168,30,72,32,152,231,77,122,70,181, -147,232,84,177,231,93,244,161,133,20,133,151,149,88,5,147,235,32,16,254,43,57,158,193,132,161,61,25,57,225, -185,187,187,49,47,91,153,201,145,78,224,180,237,127,127,11,132,54,228,124,164,73,214,89,190,101,151,98,135,185, -189,227,186,160,170,65,117,241,20,134,113,146,97,183,54,114,189,159,197,201,72,198,23,215,156,194,12,148,225,140, -109,228,70,63,243,221,179,33,162,253,104,58,186,26,173,198,183,94,232,204,56,6,141,39,246,118,9,54,195,171, -151,65,180,129,229,19,156,2,28,72,113,178,227,136,66,13,255,108,252,28,42,11,7,234,72,254,61,48,91,189, -222,117,188,41,104,196,188,64,152,250,139,157,24,85,198,230,93,162,91,187,187,55,206,15,204,95,238,66,186,135, -33,201,2,193,187,11,218,138,33,216,71,37,184,215,251,71,207,10,74,66,244,132,241,238,137,219,231,39,42,170, -143,1,238,238,42,93,60,29,27,91,213,71,242,199,175,148,130,22,15,37,94,232,246,116,24,57,218,83,193,244, -204,16,74,228,19,38,130,48,101,252,76,65,159,111,148,244,243,138,104,230,21,73,252,92,70,230,11,175,238,26, -141,243,203,91,53,4,116,235,190,30,36,65,162,139,70,228,131,223,2,202,82,221,174,244,20,183,197,216,12,164, -203,113,249,1,137,104,89,140,2,123,187,147,18,184,243,252,195,155,215,184,11,181,1,10,108,235,212,20,165,20, -81,130,30,218,81,119,1,49,58,244,95,130,170,27,80,208,20,35,140,39,204,22,152,88,27,89,114,101,181,3, -46,105,32,190,82,74,153,188,61,141,51,212,151,122,108,129,146,31,46,2,168,199,170,252,149,61,8,170,153,13, -10,2,225,178,0,176,192,99,211,192,41,57,101,124,29,214,25,191,216,44,134,41,68,98,83,106,56,200,150,182, -154,58,104,59,173,116,53,1,45,43,253,6,31,9,221,135,131,140,7,238,153,112,16,199,70,13,29,239,149,127, -75,185,97,142,173,244,35,247,92,238,69,240,64,225,130,93,184,120,149,80,248,69,182,45,123,87,222,116,152,180, -5,86,66,170,235,37,86,8,135,147,9,79,44,249,197,158,103,41,70,225,196,189,126,33,208,106,98,163,73,138, -179,66,38,149,30,18,41,65,184,238,120,135,94,22,99,108,43,205,67,208,111,121,233,176,177,58,7,180,115,204, -176,41,72,242,131,228,212,97,224,161,178,192,132,173,240,72,82,65,230,131,173,209,37,101,57,249,149,145,28,114, -242,83,147,121,60,230,196,190,42,247,32,151,129,220,57,210,102,51,186,144,11,23,210,143,213,68,118,72,79,125, -83,79,34,78,110,22,211,212,149,36,132,95,214,232,220,128,125,166,209,30,106,54,37,215,202,222,108,182,220,96, -212,44,142,62,88,92,251,138,92,84,129,120,243,246,209,107,92,67,208,185,122,13,191,69,189,171,22,240,42,108, -180,5,13,232,130,18,23,0,97,142,189,230,234,187,233,63,170,40,30,237,131,182,47,199,198,130,134,154,146,135, -111,94,33,232,172,163,85,170,128,130,112,26,179,138,121,76,14,105,180,133,224,217,233,53,13,114,167,231,255,151, -253,15,145,100,45,14,173,21,0,0 -}; diff --git a/lib/WiFiManager/CMakeLists.txt b/lib/WiFiManager/CMakeLists.txt deleted file mode 100644 index c87bb20..0000000 --- a/lib/WiFiManager/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -idf_component_register( - SRCS "WiFiManager.cpp" - INCLUDE_DIRS "." - PRIV_REQUIRES arduino -) - -project(WiFiManager) diff --git a/lib/WiFiManager/LICENSE b/lib/WiFiManager/LICENSE deleted file mode 100644 index 1dabff5..0000000 --- a/lib/WiFiManager/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 tzapu - -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/lib/WiFiManager/README.md b/lib/WiFiManager/README.md deleted file mode 100644 index 29976c6..0000000 --- a/lib/WiFiManager/README.md +++ /dev/null @@ -1,576 +0,0 @@ - -# WiFiManager - -Espressif ESPx WiFi Connection manager with fallback web configuration portal - -:warning: This Documentation is out of date, see notes below - - -[![Release](https://img.shields.io/github/v/release/tzapu/WiFiManager?include_prereleases)](#release) - -[![Build CI Status](https://github.com/tzapu/WiFiManager/actions/workflows/compile_library.yml/badge.svg)](https://github.com/tzapu/WiFiManager/actions/workflows/compile_library.yml) - -[![Build CI Status Examples](https://github.com/tzapu/WiFiManager/actions/workflows/compile_examples.yaml/badge.svg)](https://github.com/tzapu/WiFiManager/actions/workflows/compile_examples.yaml) - -[![arduino-library-badge](https://www.ardu-badge.com/badge/WiFiManager.svg?)](https://www.ardu-badge.com/WiFiManager) - -[![Build with PlatformIO](https://img.shields.io/badge/PlatformIO-Library-orange?)](https://platformio.org/lib/show/567/WiFiManager/installation) - -[![ESP8266](https://img.shields.io/badge/ESP-8266-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp8266) - -[![ESP32](https://img.shields.io/badge/ESP-32-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp32) -[![ESP32](https://img.shields.io/badge/ESP-32S2-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp32-s2) -[![ESP32](https://img.shields.io/badge/ESP-32C3-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp32-c3) -[![ESP32](https://img.shields.io/badge/ESP-32S3-000000.svg?longCache=true&style=flat&colorA=CC101F)](https://www.espressif.com/en/products/socs/esp32-S3) - -Member to Member Support / Chat - - [![Join the chat at https://gitter.im/tablatronix/WiFiManager](https://badges.gitter.im/tablatronix/WiFiManager.svg)](https://gitter.im/tablatronix/WiFiManager?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -[![Discord](https://img.shields.io/badge/Discord-WiFiManager-%237289da.svg?logo=discord)](https://discord.gg/nS5WGkaQH5) -The configuration portal is of the captive variety, so on various devices it will present the configuration dialogue as soon as you connect to the created access point. - -Works with the [ESP8266 Arduino](https://github.com/esp8266/Arduino) and [ESP32 Arduino](https://github.com/espressif/arduino-esp32) platforms. - -### Known Issues - -* Documentation needs to be updated, see [https://github.com/tzapu/WiFiManager/issues/500](https://github.com/tzapu/WiFiManager/issues/500) -------- - -## Contents - - [How it works](#how-it-works) - - [Wishlist](#wishlist) - - [Quick start](#quick-start) - - Installing - - [Arduino - Through Library Manager](#install-through-library-manager) - - [Arduino - From Github](#checkout-from-github) - - [PlatformIO](#install-using-platformio) - - [Using](#using) - - [Documentation](#documentation) - - [Access Point Password](#password-protect-the-configuration-access-point) - - [Callbacks](#callbacks) - - [Configuration Portal Timeout](#configuration-portal-timeout) - - [On Demand Configuration](#on-demand-configuration-portal) - - [Custom Parameters](#custom-parameters) - - [Custom IP Configuration](#custom-ip-configuration) - - [Filter Low Quality Networks](#filter-networks) - - [Debug Output](#debug) - - [Troubleshooting](#troubleshooting) - - [Releases](#releases) - - [Contributors](#contributions-and-thanks) - - -## How It Works -- When your ESP starts up, it sets it up in Station mode and tries to connect to a previously saved Access Point -- if this is unsuccessful (or no previous network saved) it moves the ESP into Access Point mode and spins up a DNS and WebServer (default ip 192.168.4.1) -- using any wifi enabled device with a browser (computer, phone, tablet) connect to the newly created Access Point -- because of the Captive Portal and the DNS server you will either get a 'Join to network' type of popup or get any domain you try to access redirected to the configuration portal -- choose one of the access points scanned, enter password, click save -- ESP will try to connect. If successful, it relinquishes control back to your app. If not, reconnect to AP and reconfigure. -- There are options to change this behavior or manually start the configportal and webportal independantly as well as run them in non blocking mode. - -## How It Looks -![ESP8266 WiFi Captive Portal Homepage](http://i.imgur.com/YPvW9eql.png) ![ESP8266 WiFi Captive Portal Configuration](http://i.imgur.com/oicWJ4gl.png) - -## Wishlist -- [x] remove dependency on EEPROM library -- [x] move HTML Strings to PROGMEM -- [x] cleanup and streamline code (although this is ongoing) -- [x] if timeout is set, extend it when a page is fetched in AP mode -- [x] add ability to configure more parameters than ssid/password -- [x] maybe allow setting ip of ESP after reboot -- [x] add to Arduino Library Manager -- [x] add to PlatformIO -- [ ] add multiple sets of network credentials -- [x] allow users to customize CSS -- [ ] rewrite documentation for simplicity, based on scenarios/goals - -### Development -- [x] ESP32 support -- [x] rely on the SDK's built in auto connect more than forcing a connect -- [x] add non blocking mode -- [x] easy customization of strings -- [x] hostname support -- [x] fix various bugs and workarounds for esp SDK issues -- [x] additional info page items -- [x] last status display / faiilure reason -- [x] customizeable menu -- [x] seperate custom params page -- [x] ondemand webportal -- [x] complete refactor of code to segment functions -- [x] wiif scan icons or percentage display -- [x] invert class for dark mode -- [x] more template tokens -- [x] progmem for all strings -- [ ] new callbacks -- [ ] new callouts / filters -- [ ] shared web server instance -- [x] latest esp idf/sdk support -- [x] wm is now non persistent, will not erase or change stored esp config on esp8266 -- [x] tons of debugging output / levels -- [ ] disable captiveportal -- [ ] preload wiifscans, faster page loads -- [ ] softap stability fixes when sta is not connected - - -## Quick Start - -### Installing -You can either install through the Arduino Library Manager or checkout the latest changes or a release from github - -#### Install through Library Manager -__Currently version 0.8+ works with release 2.4.0 or newer of the [ESP8266 core for Arduino](https://github.com/esp8266/Arduino)__ - - in Arduino IDE got to Sketch/Include Library/Manage Libraries - ![Manage Libraries](http://i.imgur.com/9BkEBkR.png) - - - search for WiFiManager - ![WiFiManager package](http://i.imgur.com/18yIai8.png) - - - click Install and start [using it](#using) - -#### Checkout from github -__Github version works with release 2.4.0 or newer of the [ESP8266 core for Arduino](https://github.com/esp8266/Arduino)__ -- Checkout library to your Arduino libraries folder - -### Using -- Include in your sketch -```cpp -#include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic -``` - -- Initialize library, in your setup function add, NOTEif you are using non blocking you will make sure you create this in global scope or handle appropriatly , it will not work if in setup and using non blocking mode. -```cpp -WiFiManager wifiManager; -``` - -- Also in the setup function add -```cpp -//first parameter is name of access point, second is the password -wifiManager.autoConnect("AP-NAME", "AP-PASSWORD"); -``` -if you just want an unsecured access point -```cpp -wifiManager.autoConnect("AP-NAME"); -``` -or if you want to use and auto generated name from 'ESP' and the esp's Chip ID use -```cpp -wifiManager.autoConnect(); -``` - -After you write your sketch and start the ESP, it will try to connect to WiFi. If it fails it starts in Access Point mode. -While in AP mode, connect to it then open a browser to the gateway IP, default 192.168.4.1, configure wifi, save and it should reboot and connect. - -Also see [examples](https://github.com/tzapu/WiFiManager/tree/master/examples). - -#### Install Using PlatformIO - -[PlatformIO](https://platformio.org/) is an emerging ecosystem for IoT development, and -is an alternative to using the Arduino IDE. Install `WiFiManager` -using the platformio [library manager](https://docs.platformio.org/en/latest/librarymanager/index.html#librarymanager) in your editor, -or using the [PlatformIO Core CLI](https://docs.platformio.org/en/latest/core/index.html), -or by adding it to your `platformio.ini` as shown below (recommended approach). - -The simplest way is to open the `platformio.ini` file at the root of your project, and `WifiManager` to the common top-level env -`lib_deps` key like so: - -``` -[env] -lib_deps = - WiFiManager -``` - - -``` -[env] -lib_deps = - https://github.com/tzapu/WiFiManager.git -``` - -## Documentation - -#### Password protect the configuration Access Point -You can and should password protect the configuration access point. Simply add the password as a second parameter to `autoConnect`. -A short password seems to have unpredictable results so use one that's around 8 characters or more in length. -The guidelines are that a wifi password must consist of 8 to 63 ASCII-encoded characters in the range of 32 to 126 (decimal) -```cpp -wifiManager.autoConnect("AutoConnectAP", "password") -``` - -#### Callbacks -##### Enter Config mode -Use this if you need to do something when your device enters configuration mode on failed WiFi connection attempt. -Before `autoConnect()` -```cpp -wifiManager.setAPCallback(configModeCallback); -``` -`configModeCallback` declaration and example -```cpp -void configModeCallback (WiFiManager *myWiFiManager) { - Serial.println("Entered config mode"); - Serial.println(WiFi.softAPIP()); - - Serial.println(myWiFiManager->getConfigPortalSSID()); -} -``` - -##### Save settings -This gets called when custom parameters have been set **AND** a connection has been established. Use it to set a flag, so when all the configuration finishes, you can save the extra parameters somewhere. - - -IF YOU NEED TO SAVE PARAMETERS EVEN ON WIFI FAIL OR EMPTY, you must set `setBreakAfterConfig` to true, or else saveConfigCallback will not be called. - -```C++ -//if this is set, it will exit after config, even if connection is unsuccessful. - void setBreakAfterConfig(boolean shouldBreak); -``` - -See [AutoConnectWithFSParameters Example](https://github.com/tzapu/WiFiManager/tree/master/examples/Parameters/SPIFFS/AutoConnectWithFSParameters). -```cpp -wifiManager.setSaveConfigCallback(saveConfigCallback); -``` -`saveConfigCallback` declaration and example -```cpp -//flag for saving data -bool shouldSaveConfig = false; - -//callback notifying us of the need to save config -void saveConfigCallback () { - Serial.println("Should save config"); - shouldSaveConfig = true; -} -``` - -#### Configuration Portal Timeout -If you need to set a timeout so the ESP doesn't hang waiting to be configured, for instance after a power failure, you can add -```cpp -wifiManager.setConfigPortalTimeout(180); -``` -which will wait 3 minutes (180 seconds). When the time passes, the autoConnect function will return, no matter the outcome. -Check for connection and if it's still not established do whatever is needed (on some modules I restart them to retry, on others I enter deep sleep) - -#### On Demand Configuration Portal -If you would rather start the configuration portal on demand rather than automatically on a failed connection attempt, then this is for you. - -Instead of calling `autoConnect()` which does all the connecting and failover configuration portal setup for you, you need to use `startConfigPortal()`. __Do not use BOTH.__ - -Example usage -```cpp -void loop() { - // is configuration portal requested? - if ( digitalRead(TRIGGER_PIN) == LOW ) { - WiFiManager wifiManager; - wifiManager.startConfigPortal("OnDemandAP"); - Serial.println("connected...yeey :)"); - } -} -``` -See example for a more complex version. [OnDemandConfigPortal](https://github.com/tzapu/WiFiManager/tree/master/examples/OnDemand/OnDemandConfigPortal) - -#### Exiting from the Configuration Portal -Normally, once entered, the configuration portal will continue to loop until WiFi credentials have been successfully entered or a timeout is reached. -If you'd prefer to exit without joining a WiFi network, say becuase you're going to put the ESP into AP mode, then press the "Exit" button -on the main webpage. -If started via `autoConnect` or `startConfigPortal` then it will return `false (portalAbortResult)` - -#### Custom Parameters -You can use WiFiManager to collect more parameters than just SSID and password. -This could be helpful for configuring stuff like MQTT host and port, [blynk](http://www.blynk.cc) or [emoncms](http://emoncms.org) tokens, just to name a few. -**You are responsible for saving and loading these custom values.** The library just collects and displays the data for you as a convenience. -Usage scenario would be: -- load values from somewhere (EEPROM/FS) or generate some defaults -- add the custom parameters to WiFiManager using -```cpp - // id/name, placeholder/prompt, default, length - WiFiManagerParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40); - wifiManager.addParameter(&custom_mqtt_server); - -``` -- if connection to AP fails, configuration portal starts and you can set /change the values (or use on demand configuration portal) -- once configuration is done and connection is established save config callback() is called -- once WiFiManager returns control to your application, read and save the new values using the `WiFiManagerParameter` object. -```cpp - mqtt_server = custom_mqtt_server.getValue(); -``` -This feature is a lot more involved than all the others, so here are some examples to fully show how it is done. -You should also take a look at adding custom HTML to your form. - -- Save and load custom parameters to file system in json form [AutoConnectWithFSParameters](https://github.com/tzapu/WiFiManager/tree/master/examples/Parameters/SPIFFS/AutoConnectWithFSParameters) -- *Save and load custom parameters to EEPROM* (not done yet) - -#### Custom IP Configuration -You can set a custom IP for both AP (access point, config mode) and STA (station mode, client mode, normal project state) - -##### Custom Access Point IP Configuration -This will set your captive portal to a specific IP should you need/want such a feature. Add the following snippet before `autoConnect()` -```cpp -//set custom ip for portal -wifiManager.setAPStaticIPConfig(IPAddress(10,0,1,1), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); -``` - -##### Custom Station (client) Static IP Configuration -This will make use the specified IP configuration instead of using DHCP in station mode. -```cpp -wifiManager.setSTAStaticIPConfig(IPAddress(192,168,0,99), IPAddress(192,168,0,1), IPAddress(255,255,255,0)); // optional DNS 4th argument -``` -There are a couple of examples in the examples folder that show you how to set a static IP and even how to configure it through the web configuration portal. - -NOTE: You should fill DNS server if you have HTTP requests with hostnames or syncronize time (NTP). It's the same as gateway ip or a popular (Google DNS: 8.8.8.8). - -#### Custom HTML, CSS, Javascript -There are various ways in which you can inject custom HTML, CSS or Javascript into the configuration portal. -The options are: -- inject custom head element -You can use this to any html bit to the head of the configuration portal. If you add a `"); -``` -- inject a custom bit of html in the configuration/param form -```cpp -WiFiManagerParameter custom_text("

    This is just a text paragraph

    "); -wifiManager.addParameter(&custom_text); -``` -- inject a custom bit of html in a configuration form element -Just add the bit you want added as the last parameter to the custom parameter constructor. -```cpp -WiFiManagerParameter custom_mqtt_server("server", "mqtt server", "iot.eclipse", 40, " readonly"); -wifiManager.addParameter(&custom_mqtt_server); -``` - -#### Theming -You can customize certain elements of the default template with some builtin classes -```CPP -wifiManager.setClass("invert"); // dark theme -wifiManager.setScanDispPerc(true); // display percentages instead of graphs for RSSI -``` -There are additional classes in the css you can use in your custom html , see the example template. - -#### Filter Networks -You can filter networks based on signal quality and show/hide duplicate networks. - -- If you would like to filter low signal quality networks you can tell WiFiManager to not show networks below an arbitrary quality %; -```cpp -wifiManager.setMinimumSignalQuality(10); -``` -will not show networks under 10% signal quality. If you omit the parameter it defaults to 8%; - -- You can also remove or show duplicate networks (default is remove). -Use this function to show (or hide) all networks. -```cpp -wifiManager.setRemoveDuplicateAPs(false); -``` - -#### Debug -Debug is enabled by default on `Serial` in non-stable releases. To disable add before autoConnect/startConfigPortal -```cpp -wifiManager.setDebugOutput(false); -``` - -You can pass in a custom stream via constructor -```CPP -WiFiManager wifiManager(Serial1); -``` - -You can customize the debug level by changing `_debugLevel` in source -options are: -* DEBUG_ERROR -* DEBUG_NOTIFY -* DEBUG_VERBOSE -* DEBUG_DEV -* DEBUG_MAX - -## Troubleshooting -If you get compilation errors, more often than not, you may need to install a newer version of the ESP8266 core for Arduino. - -Changes added on 0.8 should make the latest trunk work without compilation errors. Tested down to ESP8266 core 2.0.0. **Please update to version 0.8** - -I am trying to keep releases working with release versions of the core, so they can be installed through boards manager, but if you checkout the latest version directly from github, sometimes, the library will only work if you update the ESP8266 core to the latest version because I am using some newly added function. - -If you connect to the created configuration Access Point but the configuration portal does not show up, just open a browser and type in the IP of the web portal, by default `192.168.4.1`. - -If trying to connect ends up in an endless loop, try to add `setConnectTimeout(60)` before `autoConnect();`. The parameter is timeout to try connecting in seconds. - -I get stuck in ap mode when the power goes out or modem resets, try a setConfigPortalTimeout(seconds). This will cause the configportal to close after no activity, and you can reboot or attempt reconnection in your code. - -## Releases -### 1.0.1 - -### Development Overview - -#### Added Public Methods -`setConfigPortalBlocking` - -`setShowStaticFields` - -`setCaptivePortalEnable` - -`setRestorePersistent` - -`setCaptivePortalClientCheck` - -`setWebPortalClientCheck` - -`startWebPortal` - -`stopWebPortal` - -`process` - -`disconnect` - -`erase` - -` debugSoftAPConfig` - -` debugPlatformInfo` - -`setScanDispPerc` - -`setHostname` - -`setMenu(menu_page_t[])` - -`setWiFiAutoReconnect` - -` setSTAStaticIPConfig(..,dns)` - -`setShowDnsFields` - -`getLastConxResult` - -`getWLStatusString` - -`getModeString` - -`getWiFiIsSaved` - -`setShowInfoErase` - -`setEnableConfigPortal` - -`setCountry` - -`setClass` - -`htmleEtities` - - -#### WiFiManagerParameter -`WiFiManagerParameter(id,label)` - -`WiFiManagerParameter.setValue(value,length)` - -`getParameters` - -`getParametersCount` - - -#### Constructors -`WiFiManager(Stream& consolePort)` - -#### define flags -❗️ **Defines cannot be set in user sketches** -`#define WM_MDNS // use MDNS` - -`#define WM_FIXERASECONFIG // use erase flash fix, esp8266 2.4.0` - -`#define WM_ERASE_NVS // esp32 erase(true) will erase NVS` - -`#include // esp32 info page will show last reset reasons if this file is included` - -#### Changes Overview -- ESP32 support ( fairly stable ) -- complete refactor of strings `strings_en.h` -- adds new tokens for wifiscan, and some classes (left , invert icons, MSG color) -- adds status callout panel default, primary, special colors -- adds tons of info on info page, and erase capability -- adds signal icons, replaces percentage ( has hover titles ) -- adds labels to all inputs (replaces placeholders) -- all html ( and eventually all strings except debug) moved to `strings_en.h` -- added additional debugging, compressed debug lines, debuglevels -- persistent disabled, and restored via de/con-stuctor (uses `setRestorePersistent`) -- should retain all user modes including AP, should not overwrite or persist user modes or configs,even STA (`storeSTAmode`) (BUGGY) -- ⚠️ return values may have changed depending on portal abort, or timeout ( `portalTimeoutResult`,`portalAbortResult`) -- params memory is auto allocated by increment of `WIFI_MANAGER_MAX_PARAMS(5)` when exceeded, user no longer needs to specify this at all. -- addparameter now returns bool, and it returns false if param ID is not alphanum [0-9,A-Z,a-z,_] -- param field ids allow {I} token to use param_n instead of string in case someones wants to change this due to i18n or character issues -- provides `#DEFINE FIXERASECONFIG` to help deal with https://github.com/esp8266/Arduino/pull/3635 -- failure reason reporting on portal -- set esp8266 sta hostname, esp32 sta+ap hostname ( DHCP client id) -- pass in debug stream in constructor WiFiManager(Stream& consolePort) -- you can force ip fields off with showxfields(false) if you set _disableIpFields=true -- param menu/page (setup) added to separate params from wifi page, handled automatically by setMenu -- set custom root menu -- disable configportal on autoconnect -- wm parameters init is now protected, allowing child classes, example included -- wifiscans are precached and async for faster page loads, refresh forces rescan -- adds esp32 gettemperature ( currently commented out, useful for relative measurement only ) - -#### 0.12 -- removed 204 header response -- fixed incompatibility with other libs using isnan and other std:: functions without namespace - -##### 0.11 -- a lot more reliable reconnecting to networks -- custom html in custom parameters (for read only params) -- custom html in custom parameter form (like labels) -- custom head element (like custom css) -- sort networks based on signal quality -- remove duplicate networks - -##### 0.10 -- some css changes -- bug fixes and speed improvements -- added an alternative to waitForConnectResult() for debugging -- changed `setTimeout(seconds)` to `setConfigPortalTimeout(seconds)` - -### Contributions and thanks -The support and help I got from the community has been nothing short of phenomenal. I can't thank you guys enough. This is my first real attept in developing open source stuff and I must say, now I understand why people are so dedicated to it, it is because of all the wonderful people involved. - -__THANK YOU__ - -The esp8266 and esp32 arduino and idf maintainers! - -[Shawn A aka tablatronix](https://github.com/tablatronix) - -[liebman](https://github.com/liebman) - -[Evgeny Dontsov](https://github.com/dontsovcmc) - -[Chris Marrin](https://github.com/cmarrin) - -[bbx10](https://github.com/bbx10) - -[kentaylor](https://github.com/kentaylor) - -[Maximiliano Duarte](https://github.com/domonetic) - -[alltheblinkythings](https://github.com/alltheblinkythings) - -[Niklas Wall](https://github.com/niklaswall) - -[Jakub Piasecki](https://github.com/zaporylie) - -[Peter Allan](https://github.com/alwynallan) - -[John Little](https://github.com/j0hnlittle) - -[markaswift](https://github.com/markaswift) - -[franklinvv](https://github.com/franklinvv) - -[Alberto Ricci Bitti](https://github.com/riccibitti) - -[SebiPanther](https://github.com/SebiPanther) - -[jonathanendersby](https://github.com/jonathanendersby) - -[walthercarsten](https://github.com/walthercarsten) - -And countless others - -#### Inspiration - * http://www.esp8266.com/viewtopic.php?f=29&t=2520 - * https://github.com/chriscook8/esp-arduino-apboot - * https://github.com/esp8266/Arduino/tree/master/libraries/DNSServer/examples/CaptivePortalAdvanced - * Built by AlexT https://github.com/tzapu - diff --git a/lib/WiFiManager/WiFiManager.cpp b/lib/WiFiManager/WiFiManager.cpp deleted file mode 100644 index 8f9dd0d..0000000 --- a/lib/WiFiManager/WiFiManager.cpp +++ /dev/null @@ -1,4187 +0,0 @@ -/** - * WiFiManager.cpp - * - * WiFiManager, a library for the ESP8266/Arduino platform - * for configuration of WiFi credentials using a Captive Portal - * - * @author Creator tzapu - * @author tablatronix - * @version 0.0.0 - * @license MIT - */ - -#include "WiFiManager.h" - -#if defined(ESP8266) || defined(ESP32) - -#ifdef ESP32 -uint8_t WiFiManager::_lastconxresulttmp = WL_IDLE_STATUS; -#endif - -/** - * -------------------------------------------------------------------------------- - * WiFiManagerParameter - * -------------------------------------------------------------------------------- -**/ - -WiFiManagerParameter::WiFiManagerParameter() { - WiFiManagerParameter(""); -} - -WiFiManagerParameter::WiFiManagerParameter(const char *custom) { - _id = NULL; - _label = NULL; - _length = 0; - _value = nullptr; - _labelPlacement = WFM_LABEL_DEFAULT; - _customHTML = custom; -} - -WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label) { - init(id, label, "", 0, "", WFM_LABEL_DEFAULT); -} - -WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length) { - init(id, label, defaultValue, length, "", WFM_LABEL_DEFAULT); -} - -WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom) { - init(id, label, defaultValue, length, custom, WFM_LABEL_DEFAULT); -} - -WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement) { - init(id, label, defaultValue, length, custom, labelPlacement); -} - -void WiFiManagerParameter::init(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement) { - _id = id; - _label = label; - _labelPlacement = labelPlacement; - _customHTML = custom; - _length = 0; - _value = nullptr; - setValue(defaultValue,length); -} - -WiFiManagerParameter::~WiFiManagerParameter() { - if (_value != NULL) { - delete[] _value; - } - _length=0; // setting length 0, ideally the entire parameter should be removed, or added to wifimanager scope so it follows -} - -// WiFiManagerParameter& WiFiManagerParameter::operator=(const WiFiManagerParameter& rhs){ -// Serial.println("copy assignment op called"); -// (*this->_value) = (*rhs._value); -// return *this; -// } - -// @note debug is not available in wmparameter class -void WiFiManagerParameter::setValue(const char *defaultValue, int length) { - if(!_id){ - // Serial.println("cannot set value of this parameter"); - return; - } - - // if(strlen(defaultValue) > length){ - // // Serial.println("defaultValue length mismatch"); - // // return false; //@todo bail - // } - - if(_length != length || _value == nullptr){ - _length = length; - if( _value != nullptr){ - delete[] _value; - } - _value = new char[_length + 1]; - } - - memset(_value, 0, _length + 1); // explicit null - - if (defaultValue != NULL) { - strncpy(_value, defaultValue, _length); - } -} -const char* WiFiManagerParameter::getValue() const { - // Serial.println(printf("Address of _value is %p\n", (void *)_value)); - return _value; -} -const char* WiFiManagerParameter::getID() const { - return _id; -} -const char* WiFiManagerParameter::getPlaceholder() const { - return _label; -} -const char* WiFiManagerParameter::getLabel() const { - return _label; -} -int WiFiManagerParameter::getValueLength() const { - return _length; -} -int WiFiManagerParameter::getLabelPlacement() const { - return _labelPlacement; -} -const char* WiFiManagerParameter::getCustomHTML() const { - return _customHTML; -} - -/** - * [addParameter description] - * @access public - * @param {[type]} WiFiManagerParameter *p [description] - */ -bool WiFiManager::addParameter(WiFiManagerParameter *p) { - - // check param id is valid, unless null - if(p->getID()){ - for (size_t i = 0; i < strlen(p->getID()); i++){ - if(!(isAlphaNumeric(p->getID()[i])) && !(p->getID()[i]=='_')){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] parameter IDs can only contain alpha numeric chars")); - #endif - return false; - } - } - } - - // init params if never malloc - if(_params == NULL){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("allocating params bytes:"),_max_params * sizeof(WiFiManagerParameter*)); - #endif - _params = (WiFiManagerParameter**)malloc(_max_params * sizeof(WiFiManagerParameter*)); - } - - // resize the params array by increment of WIFI_MANAGER_MAX_PARAMS - if(_paramsCount == _max_params){ - _max_params += WIFI_MANAGER_MAX_PARAMS; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Updated _max_params:"),_max_params); - DEBUG_WM(WM_DEBUG_DEV,F("re-allocating params bytes:"),_max_params * sizeof(WiFiManagerParameter*)); - #endif - WiFiManagerParameter** new_params = (WiFiManagerParameter**)realloc(_params, _max_params * sizeof(WiFiManagerParameter*)); - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WIFI_MANAGER_MAX_PARAMS); - // DEBUG_WM(_paramsCount); - // DEBUG_WM(_max_params); - #endif - if (new_params != NULL) { - _params = new_params; - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] failed to realloc params, size not increased!")); - #endif - return false; - } - } - - _params[_paramsCount] = p; - _paramsCount++; - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Added Parameter:"),p->getID()); - #endif - return true; -} - -/** - * [getParameters description] - * @access public - */ -WiFiManagerParameter** WiFiManager::getParameters() { - return _params; -} - -/** - * [getParametersCount description] - * @access public - */ -int WiFiManager::getParametersCount() { - return _paramsCount; -} - -/** - * -------------------------------------------------------------------------------- - * WiFiManager - * -------------------------------------------------------------------------------- -**/ - -// constructors -WiFiManager::WiFiManager(Print& consolePort):_debugPort(consolePort){ - WiFiManagerInit(); -} - -WiFiManager::WiFiManager(const char* user, const char* password) { - WiFiManagerInit(); - - if(strlen(user) > 0) - { - strcpy(_credUser, user); - strcpy(_credPassword, password); - _hasCredentials = true; - } -} - -WiFiManager::WiFiManager() { - WiFiManagerInit(); -} - -void WiFiManager::WiFiManagerInit(){ - setMenu(_menuIdsDefault); - if(_debug && _debugLevel >= WM_DEBUG_DEV) debugPlatformInfo(); - _max_params = WIFI_MANAGER_MAX_PARAMS; -} - -// destructor -WiFiManager::~WiFiManager() { - _end(); - // parameters - // @todo below belongs to wifimanagerparameter - if (_params != NULL){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("freeing allocated params!")); - #endif - free(_params); - _params = NULL; - } - - // remove event - // WiFi.onEvent(std::bind(&WiFiManager::WiFiEvent,this,_1,_2)); - #ifdef ESP32 - WiFi.removeEvent(wm_event_id); - #endif - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("unloading")); - #endif -} - -void WiFiManager::_begin(){ - if(_hasBegun) return; - _hasBegun = true; - // _usermode = WiFi.getMode(); - - #ifndef ESP32 - WiFi.persistent(false); // disable persistent so scannetworks and mode switching do not cause overwrites - #endif -} - -void WiFiManager::_end(){ - _hasBegun = false; - if(_userpersistent) WiFi.persistent(true); // reenable persistent, there is no getter we rely on _userpersistent - // if(_usermode != WIFI_OFF) WiFi.mode(_usermode); -} - -// AUTOCONNECT - -boolean WiFiManager::autoConnect() { - String ssid = getDefaultAPName(); - return autoConnect(ssid.c_str(), NULL); -} - -/** - * [autoConnect description] - * @access public - * @param {[type]} char const *apName [description] - * @param {[type]} char const *apPassword [description] - * @return {[type]} [description] - */ -boolean WiFiManager::autoConnect(char const *apName, char const *apPassword) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("AutoConnect")); - #endif - - // no getter for autoreconnectpolicy before this - // https://github.com/esp8266/Arduino/pull/4359 - // so we must force it on else, if not connectimeout then waitforconnectionresult gets stuck endless loop - WiFi_autoReconnect(); - - // bool wifiIsSaved = getWiFiIsSaved(); - bool wifiIsSaved = true; // workaround until I can check esp32 wifiisinit and has nvs - - #ifdef ESP32 - setupHostname(true); - - if(_hostname != ""){ - // disable wifi if already on - if(WiFi.getMode() & WIFI_STA){ - WiFi.mode(WIFI_OFF); - int timeout = millis()+1200; - // async loop for mode change - while(WiFi.getMode()!= WIFI_OFF && millis()0){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Starting AP on channel:"),channel); - #endif - } - - // start soft AP with password or anonymous - // default channel is 1 here and in esplib, @todo just change to default remove conditionals - if (_apPassword != "") { - if(channel>0){ - ret = WiFi.softAP(_apName.c_str(), _apPassword.c_str(),channel,_apHidden); - } - else{ - ret = WiFi.softAP(_apName.c_str(), _apPassword.c_str(),1,_apHidden);//password option - } - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("AP has anonymous access!")); - #endif - if(channel>0){ - ret = WiFi.softAP(_apName.c_str(),"",channel,_apHidden); - } - else{ - ret = WiFi.softAP(_apName.c_str(),"",1,_apHidden); - } - } - - if(_debugLevel >= WM_DEBUG_DEV) debugSoftAPConfig(); - - // @todo add softAP retry here to dela with unknown failures - - delay(500); // slight delay to make sure we get an AP IP - #ifdef WM_DEBUG_LEVEL - if(!ret) DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] There was a problem starting the AP")); - DEBUG_WM(F("AP IP address:"),WiFi.softAPIP()); - #endif - - // set ap hostname - #ifdef ESP32 - if(ret && _hostname != ""){ - bool res = WiFi.softAPsetHostname(_hostname.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("setting softAP Hostname:"),_hostname); - if(!res)DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] hostname: AP set failed!")); - DEBUG_WM(WM_DEBUG_DEV,F("hostname: AP: "),WiFi.softAPgetHostname()); - #endif - } - #endif - - return ret; -} - -/** - * [startWebPortal description] - * @access public - * @return {[type]} [description] - */ -void WiFiManager::startWebPortal() { - if(configPortalActive || webPortalActive) return; - connect = abort = false; - setupConfigPortal(); - webPortalActive = true; -} - -/** - * [stopWebPortal description] - * @access public - * @return {[type]} [description] - */ -void WiFiManager::stopWebPortal() { - if(!configPortalActive && !webPortalActive) return; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Stopping Web Portal")); - #endif - webPortalActive = false; - shutdownConfigPortal(); -} - -boolean WiFiManager::configPortalHasTimeout(){ - if(!configPortalActive) return false; - uint16_t logintvl = 30000; // how often to emit timeing out counter logging - - // handle timeout portal client check - if(_configPortalTimeout == 0 || (_apClientCheck && (WiFi_softap_num_stations() > 0))){ - // debug num clients every 30s - if(millis() - timer > logintvl){ - timer = millis(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("NUM CLIENTS: "),(String)WiFi_softap_num_stations()); - #endif - } - _configPortalStart = millis(); // kludge, bump configportal start time to skew timeouts - return false; - } - - // handle timeout webclient check - if(_webClientCheck && (_webPortalAccessed>_configPortalStart)>0) _configPortalStart = _webPortalAccessed; - - // handle timed out - if(millis() > _configPortalStart + _configPortalTimeout){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("config portal has timed out")); - #endif - return true; // timeout bail, else do debug logging - } - else if(_debug && _debugLevel > 0) { - // log timeout time remaining every 30s - if((millis() - timer) > logintvl){ - timer = millis(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Portal Timeout In"),(String)((_configPortalStart + _configPortalTimeout-millis())/1000) + (String)F(" seconds")); - #endif - } - } - - return false; -} - -void WiFiManager::setupHTTPServer(){ - server = new PsychicHttpServer; - server->listen(_httpPort); - - /* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */ - server->on(String(FPSTR(R_wifi)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,true)); - server->on(String(FPSTR(R_wifinoscan)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifi, this,std::placeholders::_1,false)); - server->on(String(FPSTR(R_erase)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleErase, this,std::placeholders::_1,false)); - { - using namespace std::placeholders; - server->on(String(FPSTR(R_root)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleRoot, this,_1)); - server->on(String(FPSTR(R_wifisave)).c_str(), HTTP_POST, (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWifiSave, this,_1)); - server->on(String(FPSTR(R_info)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleInfo, this,_1)); - server->on(String(FPSTR(R_param)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParam, this,_1)); - server->on(String(FPSTR(R_paramsave)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleParamSave, this,_1)); - server->on(String(FPSTR(R_restart)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleReset, this,_1)); - server->on(String(FPSTR(R_exit)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleExit, this,_1)); - server->on(String(FPSTR(R_close)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleClose, this,_1)); - server->on(String(FPSTR(R_status)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleWiFiStatus, this,_1)); - server->onNotFound ((PsychicHttpRequestCallback)std::bind(&WiFiManager::handleNotFound, this, _1)); - - server->on(String(FPSTR(R_update)).c_str(), (PsychicHttpRequestCallback)std::bind(&WiFiManager::handleUpdate, this, _1)); - //server->on(String(FPSTR(R_updatedone)).c_str(), HTTP_POST, std::bind(&WiFiManager::handleUpdateDone, this, _1), std::bind(&WiFiManager::handleUpdating, this, _1,_2,_3,_4,_5,_6)); - } -} - -void WiFiManager::teardownHTTPServer(){ - -} - -void WiFiManager::setupDNSD(){ - dnsServer.reset(new DNSServer()); - - if(_httpPort != 80) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("http server starting with custom port: "),_httpPort); // @todo not showing ip - #endif - } - - setupHTTPServer(); - - /* Setup the DNS server redirecting all the domains to the apIP */ - dnsServer->setErrorReplyCode(DNSReplyCode::NoError); - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM("dns server started port: ",DNS_PORT); - DEBUG_WM(WM_DEBUG_DEV,F("dns server started with ip: "),WiFi.softAPIP()); // @todo not showing ip - #endif - dnsServer->start(DNS_PORT, F("*"), WiFi.softAPIP()); -} - -void WiFiManager::setupConfigPortal() { - setupHTTPServer(); - _lastscan = 0; // reset network scan cache - if(_preloadwifiscan) WiFi_scanNetworks(true,true); // preload wifiscan , async -} - -boolean WiFiManager::startConfigPortal() { - String ssid = getDefaultAPName(); - return startConfigPortal(ssid.c_str(), NULL); -} - -/** - * [startConfigPortal description] - * @access public - * @param {[type]} char const *apName [description] - * @param {[type]} char const *apPassword [description] - * @return {[type]} [description] - */ -boolean WiFiManager::startConfigPortal(char const *apName, char const *apPassword) { - _begin(); - - if(configPortalActive){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Starting Config Portal FAILED, is already running")); - #endif - return false; - } - - //setup AP - _apName = apName; // @todo check valid apname ? - _apPassword = apPassword; - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Starting Config Portal")); - #endif - - if(_apName == "") _apName = getDefaultAPName(); - - if(!validApPassword()) return false; - - // HANDLE issues with STA connections, shutdown sta if not connected, or else this will hang channel scanning and softap will not respond - if(_disableSTA || (!WiFi.isConnected() && _disableSTAConn)){ - // this fixes most ap problems, however, simply doing mode(WIFI_AP) does not work if sta connection is hanging, must `wifi_station_disconnect` - #ifdef WM_DISCONWORKAROUND - WiFi.mode(WIFI_AP_STA); - #endif - WiFi_Disconnect(); - WiFi_enableSTA(false); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Disabling STA")); - #endif - } - else { - // WiFi_enableSTA(true); - } - - // init configportal globals to known states - configPortalActive = true; - bool result = connect = abort = false; // loop flags, connect true success, abort true break - uint8_t state; - - _configPortalStart = millis(); - - // start access point - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Enabling AP")); - #endif - startAP(); - WiFiSetCountry(); - - // do AP callback if set - if ( _apcallback != NULL) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] _apcallback calling")); - #endif - _apcallback(this); - } - - // init configportal - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("setupConfigPortal")); - #endif - setupConfigPortal(); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("setupDNSD")); - #endif - setupDNSD(); - - - if(!_configPortalIsBlocking){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Config Portal Running, non blocking (processing)")); - if(_configPortalTimeout > 0) DEBUG_WM(WM_DEBUG_VERBOSE,F("Portal Timeout In"),(String)(_configPortalTimeout/1000) + (String)F(" seconds")); - #endif - return result; // skip blocking loop - } - - // enter blocking loop, waiting for config - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Config Portal Running, blocking, waiting for clients...")); - if(_configPortalTimeout > 0) DEBUG_WM(WM_DEBUG_VERBOSE,F("Portal Timeout In"),(String)(_configPortalTimeout/1000) + (String)F(" seconds")); - #endif - - while(1){ - - // if timed out or abort, break - if(configPortalHasTimeout() || abort){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("configportal loop abort")); - #endif - shutdownConfigPortal(); - result = abort ? portalAbortResult : portalTimeoutResult; // false, false - if (_configportaltimeoutcallback != NULL) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] config portal timeout callback")); - #endif - _configportaltimeoutcallback(); // @CALLBACK - } - break; - } - - state = processConfigPortal(); - - // status change, break - // @todo what is this for, should be moved inside the processor - // I think.. this is to detect autoconnect by esp in background, there are also many open issues about autoreconnect not working - if(state != WL_IDLE_STATUS){ - result = (state == WL_CONNECTED); // true if connected - DEBUG_WM(WM_DEBUG_DEV,F("configportal loop break")); - break; - } - - if(!configPortalActive) break; - - vTaskDelay( 50 / portTICK_PERIOD_MS); - } - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_NOTIFY,F("config portal exiting")); - #endif - return result; -} - -/** - * [process description] - * @access public - * @return bool connected - */ -boolean WiFiManager::process(){ - // process mdns, esp32 not required - #if defined(WM_MDNS) && defined(ESP8266) - MDNS.update(); - #endif - - if(webPortalActive || (configPortalActive && !_configPortalIsBlocking)){ - // if timed out or abort, break - if(_allowExit && (configPortalHasTimeout() || abort)){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("process loop abort")); - #endif - webPortalActive = false; - shutdownConfigPortal(); - if (_configportaltimeoutcallback != NULL) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] config portal timeout callback")); - #endif - _configportaltimeoutcallback(); // @CALLBACK - } - return false; - } - - uint8_t state = processConfigPortal(); // state is WL_IDLE or WL_CONNECTED/FAILED - return state == WL_CONNECTED; - } - return false; -} - -/** - * [processConfigPortal description] - * using esp wl_status enums as returns for now, should be fine - * returns WL_IDLE_STATUS or WL_CONNECTED/WL_CONNECT_FAILED upon connect/save flag - * - * @return {[type]} [description] - */ -uint8_t WiFiManager::processConfigPortal(){ - if(configPortalActive){ - //DNS handler - dnsServer->processNextRequest(); - } - - //HTTP handler - #ifndef WM_ASYNCWEBSERVER - server->handleClient(); - #endif - - if(_rebootNeeded) reboot(); - - // Waiting for save... - if(connect) { - connect = false; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("processing save")); - #endif - if(_enableCaptivePortal) delay(_cpclosedelay); // keeps the captiveportal from closing to fast. - - // skip wifi if no ssid - if(_ssid == ""){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("No ssid, skipping wifi save")); - #endif - } - else{ - // attempt sta connection to submitted _ssid, _pass - uint8_t res = connectWifi(_ssid, _pass, _connectonsave) == WL_CONNECTED; - if (res || (!_connectonsave)) { - #ifdef WM_DEBUG_LEVEL - if(!_connectonsave){ - DEBUG_WM(F("SAVED with no connect to new AP")); - } else { - DEBUG_WM(F("Connect to new AP [SUCCESS]")); - DEBUG_WM(F("Got IP Address:")); - DEBUG_WM(WiFi.localIP()); - reboot(); - } - #endif - - if ( _savewificallback != NULL) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] _savewificallback calling")); - #endif - _savewificallback(); // @CALLBACK - } - if(!_connectonsave) return WL_IDLE_STATUS; - if(_disableConfigPortal) shutdownConfigPortal(); - return WL_CONNECTED; // CONNECT SUCCESS - } - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] Connect to new AP Failed")); - #endif - } - - if (_shouldBreakAfterConfig) { - - // do save callback - // @todo this is more of an exiting callback than a save, clarify when this should actually occur - // confirm or verify data was saved to make this more accurate callback - if ( _savewificallback != NULL) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] WiFi/Param save callback")); - #endif - _savewificallback(); // @CALLBACK - } - if(_disableConfigPortal) shutdownConfigPortal(); - return WL_CONNECT_FAILED; // CONNECT FAIL - } - else if(_configPortalIsBlocking){ - // clear save strings - _ssid = ""; - _pass = ""; - // if connect fails, turn sta off to stabilize AP - WiFi_Disconnect(); - WiFi_enableSTA(false); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Processing - Disabling STA")); - #endif - } - else{ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Portal is non blocking - remaining open")); - #endif - } - } - - return WL_IDLE_STATUS; -} - -/** - * [shutdownConfigPortal description] - * @access public - * @return bool success (softapdisconnect) - */ -bool WiFiManager::shutdownConfigPortal(){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("shutdownConfigPortal")); - #endif - - if(webPortalActive) return false; - - if(configPortalActive){ - //DNS handler - dnsServer->processNextRequest(); - } - - #ifndef WM_ASYNCWEBSERVER - //HTTP handler - server->handleClient(); - #endif - - // @todo what is the proper way to shutdown and free the server up - // debug - many open issues aobut port not clearing for use with other servers - #ifdef WM_ASYNCWEBSERVER - server->stop(); - #else - server->stop(); - #endif - - dnsServer->stop(); // free heap ? - dnsServer.reset(); - - WiFi.scanDelete(); // free wifi scan results - - if(!configPortalActive) return false; - - // turn off AP - // @todo bug workaround - // https://github.com/esp8266/Arduino/issues/3793 - // [APdisconnect] set_config failed! *WM: disconnect configportal - softAPdisconnect failed - // still no way to reproduce reliably - - bool ret = false; - ret = WiFi.softAPdisconnect(false); - - #ifdef WM_DEBUG_LEVEL - if(!ret)DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] disconnect configportal - softAPdisconnect FAILED")); - DEBUG_WM(WM_DEBUG_VERBOSE,F("restoring usermode"),getModeString(_usermode)); - #endif - delay(1000); - WiFi_Mode(_usermode); // restore users wifi mode, BUG https://github.com/esp8266/Arduino/issues/4372 - if(WiFi.status()==WL_IDLE_STATUS){ - WiFi.reconnect(); // restart wifi since we disconnected it in startconfigportal - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Reconnect, was idle")); - #endif - } - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("wifi status:"),getWLStatusString(WiFi.status())); - DEBUG_WM(WM_DEBUG_VERBOSE,F("wifi mode:"),getModeString(WiFi.getMode())); - #endif - configPortalActive = false; - DEBUG_WM(WM_DEBUG_VERBOSE,F("configportal closed")); - _end(); - return ret; -} - -// @todo refactor this up into seperate functions -// one for connecting to flash , one for new client -// clean up, flow is convoluted, and causes bugs -uint8_t WiFiManager::connectWifi(String ssid, String pass, bool connect) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Connecting as wifi client...")); - #endif - uint8_t retry = 1; - uint8_t connRes = (uint8_t)WL_NO_SSID_AVAIL; - - setSTAConfig(); - //@todo catch failures in set_config - - // make sure sta is on before `begin` so it does not call enablesta->mode while persistent is ON ( which would save WM AP state to eeprom !) - // WiFi.setAutoReconnect(false); - if(_cleanConnect) WiFi_Disconnect(); // disconnect before begin, in case anything is hung, this causes a 2 seconds delay for connect - // @todo find out what status is when this is needed, can we detect it and handle it, say in between states or idle_status to avoid these - - // if retry without delay (via begin()), the IDF is still busy even after returning status - // E (5130) wifi:sta is connecting, return error - // [E][WiFiSTA.cpp:221] begin(): connect failed! - - while(retry <= _connectRetries && (connRes!=WL_CONNECTED)){ - if(_connectRetries > 1){ - if(_aggresiveReconn) delay(1000); // add idle time before recon - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("Connect Wifi, ATTEMPT #"),(String)retry+" of "+(String)_connectRetries); - #endif - } - // if ssid argument provided connect to that - // NOTE: this also catches preload() _defaultssid @todo rework - if (ssid != "") { - wifiConnectNew(ssid,pass,connect); - // @todo connect=false seems to disconnect sta in begin() so not sure if _connectonsave is useful at all - // skip wait if not connecting - // if(connect){ - if(_saveTimeout > 0){ - connRes = waitForConnectResult(_saveTimeout); // use default save timeout for saves to prevent bugs in esp->waitforconnectresult loop - } - else { - connRes = waitForConnectResult(); - } - // } - } - else { - // connect using saved ssid if there is one - if (WiFi_hasAutoConnect()) { - wifiConnectDefault(); - connRes = waitForConnectResult(); - } - else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("No wifi saved, skipping")); - #endif - } - } - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Connection result:"),getWLStatusString(connRes)); - #endif - retry++; -} - -// WPS enabled? https://github.com/esp8266/Arduino/pull/4889 -#ifdef NO_EXTRA_4K_HEAP - // do WPS, if WPS options enabled and not connected and no password was supplied - // @todo this seems like wrong place for this, is it a fallback or option? - if (_tryWPS && connRes != WL_CONNECTED && pass == "") { - startWPS(); - // should be connected at the end of WPS - connRes = waitForConnectResult(); - } -#endif - - if(connRes != WL_SCAN_COMPLETED){ - updateConxResult(connRes); - } - - return connRes; -} - -/** - * connect to a new wifi ap - * @since $dev - * @param String ssid - * @param String pass - * @return bool success - * @return connect only save if false - */ -bool WiFiManager::wifiConnectNew(String ssid, String pass,bool connect){ - bool ret = false; - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_DEV,F("CONNECTED: "),WiFi.status() == WL_CONNECTED ? "Y" : "NO"); - DEBUG_WM(F("Connecting to NEW AP:"),ssid); - DEBUG_WM(WM_DEBUG_DEV,F("Using Password:"),pass); - #endif - WiFi_enableSTA(true,storeSTAmode); // storeSTAmode will also toggle STA on in default opmode (persistent) if true (default) - WiFi.persistent(true); - - if (_findBestRSSI) { - WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); - WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("find best RSSI: TRUE")); - #endif - if (!_numNetworks) - WiFi_scanNetworks(false, false); // scan in case this gets called before any scans - - int n = _numNetworks; - if (n == 0) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("No networks found")); - #endif - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(n, F("networks found")); - #endif - int bestConnection = -1; - // Find best RSSI AP for given SSID - for (int i = 0; i < n; i++) { - if (ssid == WiFi.SSID(i)) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(String(F("SSID ")) + ssid + String(F(" found with RSSI: ")) + - String(WiFi.RSSI(i)) + String(F("(")) + - String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + - String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + - String(F(" and channel: ")) + String(WiFi.channel(i))); - #endif - if (bestConnection == -1) { - bestConnection = i; - } else { - if (WiFi.RSSI(i) > WiFi.RSSI(bestConnection)) { - bestConnection = i; - } - } - } - } - if (bestConnection == -1) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("No network found with SSID: "), ssid); - #endif - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(String(F("Trying to connect to SSID ")) + ssid + String(F(" found with RSSI: ")) + - String(WiFi.RSSI(bestConnection)) + String(F("(")) + - String(constrain((100.0 + WiFi.RSSI(bestConnection)) * 2, 0, 100)) + - String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(bestConnection) + - String(F(" and channel: ")) + String(WiFi.channel(bestConnection))); - #endif - ret = WiFi.begin(ssid.c_str(), pass.c_str(), 0, WiFi.BSSID(bestConnection), connect); - } - } - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("find best RSSI: FALSE")); - #endif - ret = WiFi.begin(ssid.c_str(), pass.c_str(), 0, NULL, connect); - } - - WiFi.persistent(false); - #ifdef WM_DEBUG_LEVEL - if(!ret) DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] wifi begin failed")); - #endif - return ret; -} - -/** - * connect to stored wifi - * @since dev - * @return bool success - */ -bool WiFiManager::wifiConnectDefault(){ - bool ret = false; - - String ssid = WiFi_SSID(true); - String pass = WiFi_psk(true); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("Connecting to SAVED AP:"),ssid); - DEBUG_WM(WM_DEBUG_DEV,F("Using Password:"),pass); - #endif - - ret = WiFi_enableSTA(true,storeSTAmode); - delay(500); // THIS DELAY ? - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Mode after delay: "),getModeString(WiFi.getMode())); - if(!ret) DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] wifi enableSta failed")); - #endif - - if (_findBestRSSI) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("find best RSSI: TRUE")); - #endif - if (!_numNetworks) - WiFi_scanNetworks(false, false); // scan in case this gets called before any scans - - int n = _numNetworks; - if (n == 0) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("No networks found")); - #endif - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(n, F("networks found")); - #endif - int bestConnection = -1; - // Find best RSSI AP for given SSID - for (int i = 0; i < n; i++) { - if (ssid == WiFi.SSID(i)) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(String(F("SSID ")) + ssid + String(F(" found with RSSI: ")) + - String(WiFi.RSSI(i)) + String(F("(")) + - String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + - String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + - String(F(" and channel: ")) + String(WiFi.channel(i))); - #endif - if (bestConnection == -1) { - bestConnection = i; - } else { - if (WiFi.RSSI(i) > WiFi.RSSI(bestConnection)) { - bestConnection = i; - } - } - } - } - if (bestConnection == -1) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("No network found with SSID: "), ssid); - #endif - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(String(F("Trying to connect to SSID ")) + ssid + String(F(" found with RSSI: ")) + - String(WiFi.RSSI(bestConnection)) + String(F("(")) + - String(constrain((100.0 + WiFi.RSSI(bestConnection)) * 2, 0, 100)) + - String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(bestConnection) + - String(F(" and channel: ")) + String(WiFi.channel(bestConnection))); - #endif - ret = WiFi.begin(ssid.c_str(), pass.c_str(), 0, WiFi.BSSID(bestConnection), true); - } - } - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("find best RSSI: FALSE")); - #endif - ret = WiFi.begin(); - } - - #ifdef WM_DEBUG_LEVEL - if(!ret) DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] wifi begin failed")); - #endif - - return ret; -} - - -/** - * set sta config if set - * @since $dev - * @return bool success - */ -bool WiFiManager::setSTAConfig(){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("STA static IP:"),_sta_static_ip); - #endif - bool ret = true; - if (_sta_static_ip) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Custom static IP/GW/Subnet/DNS")); - #endif - if(_sta_static_dns) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Custom static DNS")); - #endif - ret = WiFi.config(_sta_static_ip, _sta_static_gw, _sta_static_sn, _sta_static_dns); - } - else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Custom STA IP/GW/Subnet")); - #endif - ret = WiFi.config(_sta_static_ip, _sta_static_gw, _sta_static_sn); - } - - #ifdef WM_DEBUG_LEVEL - if(!ret) DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] wifi config failed")); - else DEBUG_WM(F("STA IP set:"),WiFi.localIP()); - #endif - } - else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("setSTAConfig static ip not set, skipping")); - #endif - } - return ret; -} - -// @todo change to getLastFailureReason and do not touch conxresult -void WiFiManager::updateConxResult(uint8_t status){ - // hack in wrong password detection - _lastconxresult = status; - #ifdef ESP8266 - if(_lastconxresult == WL_CONNECT_FAILED){ - if(wifi_station_get_connect_status() == STATION_WRONG_PASSWORD){ - _lastconxresult = WL_STATION_WRONG_PASSWORD; - } - } - #elif defined(ESP32) - // if(_lastconxresult == WL_CONNECT_FAILED){ - if(_lastconxresult == WL_CONNECT_FAILED || _lastconxresult == WL_DISCONNECTED){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("lastconxresulttmp:"),getWLStatusString(_lastconxresulttmp)); - #endif - if(_lastconxresulttmp != WL_IDLE_STATUS){ - _lastconxresult = _lastconxresulttmp; - // _lastconxresulttmp = WL_IDLE_STATUS; - } - } - DEBUG_WM(WM_DEBUG_DEV,F("lastconxresult:"),getWLStatusString(_lastconxresult)); - #endif -} - - -uint8_t WiFiManager::waitForConnectResult() { - #ifdef WM_DEBUG_LEVEL - if(_connectTimeout > 0) DEBUG_WM(WM_DEBUG_DEV,_connectTimeout,F("ms connectTimeout set")); - #endif - return waitForConnectResult(_connectTimeout); -} - -/** - * waitForConnectResult - * @param uint16_t timeout in seconds - * @return uint8_t WL Status - */ -uint8_t WiFiManager::waitForConnectResult(uint32_t timeout) { - if (timeout == 0){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("connectTimeout not set, ESP waitForConnectResult...")); - #endif - return WiFi.waitForConnectResult(); - } - - unsigned long timeoutmillis = millis() + timeout; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,timeout,F("ms timeout, waiting for connect...")); - #endif - uint8_t status = WiFi.status(); - - while(millis() < timeoutmillis) { - status = WiFi.status(); - // @todo detect additional states, connect happens, then dhcp then get ip, there is some delay here, make sure not to timeout if waiting on IP - if (status == WL_CONNECTED || status == WL_CONNECT_FAILED) { - return status; - } - #ifdef WM_DEBUG_LEVEL - DEBUG_WM (WM_DEBUG_VERBOSE,F(".")); - #endif - delay(100); - } - return status; -} - -// WPS enabled? https://github.com/esp8266/Arduino/pull/4889 -#ifdef NO_EXTRA_4K_HEAP -void WiFiManager::startWPS() { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("START WPS")); - #endif - #ifdef ESP8266 - WiFi.beginWPSConfig(); - #else - // @todo - #endif - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("END WPS")); - #endif -} -#endif - -String WiFiManager::getHTTPHead(String title){ - String page; - page += FPSTR(HTTP_HEAD_START); - page.replace(FPSTR(T_v), title); - page += FPSTR(HTTP_SCRIPT); - page += FPSTR(HTTP_STYLE); - page += _customHeadElement; - - if(_bodyClass != ""){ - String p = FPSTR(HTTP_HEAD_END); - p.replace(FPSTR(T_c), _bodyClass); // add class str - page += p; - } - else { - page += FPSTR(HTTP_HEAD_END); - } - - return page; -} - -esp_err_t WiFiManager::HTTPSend(PsychicRequest *request, String page){ - PsychicResponse response(request); - response.addHeader(HTTP_HEAD_CL, ((String)page.length()).c_str()); - response.setCode(200); - response.setContentType(HTTP_HEAD_CT); - response.setContent(page.c_str()); - return response.send(); -} - -/** - * HTTPD handler for page requests - */ -void WiFiManager::handleRequest() { - _webPortalAccessed = millis(); - - // TESTING HTTPD AUTH RFC 2617 - // BASIC_AUTH will hold onto creds, hard to "logout", but convienent - // DIGEST_AUTH will require new auth often, and nonce is random - // bool authenticate(const char * username, const char * password); - // bool authenticateDigest(const String& username, const String& H1); - // void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String("") ); -} - -/** - * HTTPD CALLBACK root or redirect to captive portal - */ -esp_err_t WiFiManager::handleRoot(PsychicRequest *request) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Root")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page - handleRequest(); - String page = getHTTPHead(_title); // @token options @todo replace options with title - String str = FPSTR(HTTP_ROOT_MAIN); // @todo custom title - str.replace(FPSTR(T_t),_title); - str.replace(FPSTR(T_v),configPortalActive ? _apName : (getWiFiHostname() + " - " + WiFi.localIP().toString())); // use ip if ap is not active for heading @todo use hostname? - page += str; - page += FPSTR(HTTP_PORTAL_OPTIONS); - page += getMenuOut(); - reportStatus(page); - page += FPSTR(HTTP_END); - - //if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime,true); // preload wifiscan throttled, async - // @todo buggy, captive portals make a query on every page load, causing this to run every time in addition to the real page load - // I dont understand why, when you are already in the captive portal, I guess they want to know that its still up and not done or gone - // if we can detect these and ignore them that would be great, since they come from the captive portal redirect maybe there is a refferer - - return HTTPSend(request,page); -} - -/** - * HTTPD CALLBACK Wifi config page handler - */ -esp_err_t WiFiManager::handleWifi(PsychicRequest *request,bool scan = true) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Wifi")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - String page = getHTTPHead(FPSTR(S_titlewifi)); // @token titlewifi - if (scan) { - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_DEV,"refresh flag:",request->hasArg(F("refresh"))); - #endif - WiFi_scanNetworks(request->hasParam("refresh"), true); //wifiscan, force if arg refresh - page += getScanItemOut(); - } - String pitem = ""; - - pitem = FPSTR(HTTP_FORM_START); - pitem.replace(FPSTR(T_v), F("wifisave")); // set form action - page += pitem; - - pitem = FPSTR(HTTP_FORM_WIFI); - pitem.replace(FPSTR(T_v), WiFi_SSID()); - - if(_showPassword){ - pitem.replace(FPSTR(T_p), WiFi_psk()); - } - else if(WiFi_psk() != ""){ - pitem.replace(FPSTR(T_p),FPSTR(S_passph)); - } - else { - pitem.replace(FPSTR(T_p),""); - } - - page += pitem; - - page += getStaticOut(); - page += FPSTR(HTTP_FORM_WIFI_END); - if(_paramsInWifi && _paramsCount>0){ - page += FPSTR(HTTP_FORM_PARAM_HEAD); - page += getParamOut(); - } - page += FPSTR(HTTP_FORM_END); - page += FPSTR(HTTP_SCAN_LINK); - if(_showBack) page += FPSTR(HTTP_BACKBTN); - reportStatus(page); - page += FPSTR(HTTP_END); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Sent config page")); - #endif - - return HTTPSend(request,page); -} - -/** - * HTTPD CALLBACK Wifi param page handler - */ -esp_err_t WiFiManager::handleParam(PsychicRequest *request){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Param")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - String page = getHTTPHead(FPSTR(S_titleparam)); // @token titlewifi - - String pitem = ""; - - pitem = FPSTR(HTTP_FORM_START); - pitem.replace(FPSTR(T_v), F("paramsave")); - page += pitem; - - page += getParamOut(); - page += FPSTR(HTTP_FORM_END); - if(_showBack) page += FPSTR(HTTP_BACKBTN); - reportStatus(page); - page += FPSTR(HTTP_END); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Sent param page")); - #endif - - return HTTPSend(request,page); -} - - -String WiFiManager::getMenuOut(){ - String page; - - for(auto menuId :_menuIds ){ - if((String)_menutokens[menuId] == "param" && _paramsCount == 0) continue; // no params set, omit params from menu, @todo this may be undesired by someone, use only menu to force? - if((String)_menutokens[menuId] == "custom" && _customMenuHTML!=NULL){ - page += _customMenuHTML; - continue; - } - page += HTTP_PORTAL_MENU[menuId]; - delay(0); - } - - return page; -} - -// // is it possible in softap mode to detect aps without scanning -// bool WiFiManager::WiFi_scanNetworksForAP(bool force){ -// WiFi_scanNetworks(force); -// } - -void WiFiManager::WiFi_scanComplete(int networksFound){ - _lastscan = millis(); - _numNetworks = networksFound; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC completed"), "in "+(String)(_lastscan - _startscan)+" ms"); - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC found:"),_numNetworks); - #endif -} - -bool WiFiManager::WiFi_scanNetworks(){ - return WiFi_scanNetworks(false,true); -} - -bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime,bool async){ - return WiFi_scanNetworks(millis()-_lastscan > cachetime,async); -} -bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime){ - return WiFi_scanNetworks(millis()-_lastscan > cachetime,true); -} -bool WiFiManager::WiFi_scanNetworks(bool force,bool async){ - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_DEV,"scanNetworks async:",async == true); - // DEBUG_WM(WM_DEBUG_DEV,_numNetworks,(millis()-_lastscan )); - // DEBUG_WM(WM_DEBUG_DEV,"scanNetworks force:",force == true); - #endif - - if(_numNetworks == 0 && _autoforcerescan){ - DEBUG_WM(WM_DEBUG_DEV,"NO APs found forcing new scan"); - force = true; - } - - // if scan is empty or stale (last scantime > _scancachetime), this avoids fast reloading wifi page and constant scan delayed page loads appearing to freeze. - if(!_lastscan || _lastscan == 0 || (_lastscan>0 && (millis()-_lastscan > _scancachetime))){ - force = true; - } - //force = _lastscan == 0; - - if(force){ - int8_t res; - _startscan = millis(); - if(async && _asyncScan){ - #ifdef ESP8266 - #ifndef WM_NOASYNC // no async available < 2.4.0 - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); - #endif - using namespace std::placeholders; // for `_1` - WiFi.scanNetworksAsync(std::bind(&WiFiManager::WiFi_scanComplete,this,_1)); - #else - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan SYNC started")); - res = WiFi.scanNetworks(); - #endif - #else - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); - #endif - res = WiFi.scanNetworks(true); - #endif - return false; - } - else{ - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan SYNC started")); - res = WiFi.scanNetworks(); - } - if(res == WIFI_SCAN_FAILED){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed")); - #endif - } - else if(res == WIFI_SCAN_RUNNING){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan waiting")); - #endif - while(WiFi.scanComplete() == WIFI_SCAN_RUNNING){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,"."); - #endif - delay(100); - } - _numNetworks = WiFi.scanComplete(); - } - else if(res >=0 ) _numNetworks = res; - _lastscan = millis(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan completed"), "in "+(String)(_lastscan - _startscan)+" ms"); - #endif - return true; - } - else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Scan is cached"),(String)(millis()-_lastscan )+" ms ago"); - #endif - } - return false; -} - -void WiFiManager::resetScan(){ - _numNetworks = 0; -} - -String WiFiManager::WiFiManager::getScanItemOut(){ - String page; - - if(!_numNetworks) WiFi_scanNetworks(true, true); // scan in case this gets called before any scans - - int n = _numNetworks; - if (n == 0) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("No networks found")); - #endif - page += FPSTR(S_nonetworks); // @token nonetworks - page += F("

    "); - } - else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(n,F("networks found")); - #endif - //sort networks - int indices[n]; - for (int i = 0; i < n; i++) { - indices[i] = i; - } - - // RSSI SORT - for (int i = 0; i < n; i++) { - for (int j = i + 1; j < n; j++) { - if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { - std::swap(indices[i], indices[j]); - } - } - } - - /* test std:sort - std::sort(indices, indices + n, [](const int & a, const int & b) -> bool - { - return WiFi.RSSI(a) > WiFi.RSSI(b); - }); - */ - - // remove duplicates ( must be RSSI sorted ) - if (_removeDuplicateAPs) { - String cssid; - for (int i = 0; i < n; i++) { - if (indices[i] == -1) continue; - cssid = WiFi.SSID(indices[i]); - for (int j = i + 1; j < n; j++) { - if (cssid == WiFi.SSID(indices[j])) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("DUP AP:"),WiFi.SSID(indices[j])); - #endif - indices[j] = -1; // set dup aps to index -1 - } - } - } - } - - // token precheck, to speed up replacements on large ap lists - String HTTP_ITEM_STR = FPSTR(HTTP_ITEM); - - // toggle icons with percentage - HTTP_ITEM_STR.replace("{qp}", FPSTR(HTTP_ITEM_QP)); - HTTP_ITEM_STR.replace("{h}",_scanDispOptions ? "" : "h"); - HTTP_ITEM_STR.replace("{qi}", FPSTR(HTTP_ITEM_QI)); - HTTP_ITEM_STR.replace("{h}",_scanDispOptions ? "h" : ""); - - // set token precheck flags - bool tok_r = HTTP_ITEM_STR.indexOf(FPSTR(T_r)) > 0; - bool tok_R = HTTP_ITEM_STR.indexOf(FPSTR(T_R)) > 0; - bool tok_e = HTTP_ITEM_STR.indexOf(FPSTR(T_e)) > 0; - bool tok_q = HTTP_ITEM_STR.indexOf(FPSTR(T_q)) > 0; - bool tok_i = HTTP_ITEM_STR.indexOf(FPSTR(T_i)) > 0; - - //display networks in page - for (int i = 0; i < n; i++) { - if (indices[i] == -1) continue; // skip dups - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("AP: "),(String)WiFi.RSSI(indices[i]) + " " + (String)WiFi.SSID(indices[i])); - #endif - - int rssiperc = getRSSIasQuality(WiFi.RSSI(indices[i])); - uint8_t enc_type = WiFi.encryptionType(indices[i]); - - if (_minimumQuality == -1 || _minimumQuality < rssiperc) { - String item = HTTP_ITEM_STR; - if(WiFi.SSID(indices[i]) == ""){ - // Serial.println(WiFi.BSSIDstr(indices[i])); - continue; // No idea why I am seeing these, lets just skip them for now - } - item.replace(FPSTR(T_V), htmlEntities(WiFi.SSID(indices[i]))); // ssid no encoding - item.replace(FPSTR(T_v), htmlEntities(WiFi.SSID(indices[i]),true)); // ssid no encoding - if(tok_e) item.replace(FPSTR(T_e), encryptionTypeStr(enc_type)); - if(tok_r) item.replace(FPSTR(T_r), (String)rssiperc); // rssi percentage 0-100 - if(tok_R) item.replace(FPSTR(T_R), (String)WiFi.RSSI(indices[i])); // rssi db - if(tok_q) item.replace(FPSTR(T_q), (String)int(round(map(rssiperc,0,100,1,4)))); //quality icon 1-4 - if(tok_i){ - if (enc_type != WM_WIFIOPEN) { - item.replace(FPSTR(T_i), F("l")); - } else { - item.replace(FPSTR(T_i), ""); - } - } - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,item); - #endif - page += item; - delay(0); - } else { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Skipping , does not meet _minimumQuality")); - #endif - } - - } - page += FPSTR(HTTP_BR); - } - - return page; -} - -String WiFiManager::getIpForm(String id, String title, String value){ - String item = FPSTR(HTTP_FORM_LABEL); - item += FPSTR(HTTP_FORM_PARAM); - item.replace(FPSTR(T_i), id); - item.replace(FPSTR(T_n), id); - item.replace(FPSTR(T_p), FPSTR(T_t)); - // item.replace(FPSTR(T_p), default); - item.replace(FPSTR(T_t), title); - item.replace(FPSTR(T_l), F("15")); - item.replace(FPSTR(T_v), value); - item.replace(FPSTR(T_c), ""); - return item; -} - -String WiFiManager::getStaticOut(){ - String page; - if ((_staShowStaticFields || _sta_static_ip) && _staShowStaticFields>=0) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("_staShowStaticFields")); - #endif - page += FPSTR(HTTP_FORM_STATIC_HEAD); - // @todo how can we get these accurate settings from memory , wifi_get_ip_info does not seem to reveal if struct ip_info is static or not - page += getIpForm(FPSTR(S_ip),FPSTR(S_staticip),(_sta_static_ip ? _sta_static_ip.toString() : "")); // @token staticip - // WiFi.localIP().toString(); - page += getIpForm(FPSTR(S_gw),FPSTR(S_staticgw),(_sta_static_gw ? _sta_static_gw.toString() : "")); // @token staticgw - // WiFi.gatewayIP().toString(); - page += getIpForm(FPSTR(S_sn),FPSTR(S_subnet),(_sta_static_sn ? _sta_static_sn.toString() : "")); // @token subnet - // WiFi.subnetMask().toString(); - } - - if((_staShowDns || _sta_static_dns) && _staShowDns>=0){ - page += getIpForm(FPSTR(S_dns),FPSTR(S_staticdns),(_sta_static_dns ? _sta_static_dns.toString() : "")); // @token dns - } - - if(page!="") page += FPSTR(HTTP_BR); // @todo remove these, use css - - return page; -} - -String WiFiManager::getParamOut(){ - String page; - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("getParamOut"),_paramsCount); - #endif - - if(_paramsCount > 0){ - - String HTTP_PARAM_temp = FPSTR(HTTP_FORM_LABEL); - HTTP_PARAM_temp += FPSTR(HTTP_FORM_PARAM); - bool tok_I = HTTP_PARAM_temp.indexOf(FPSTR(T_I)) > 0; - bool tok_i = HTTP_PARAM_temp.indexOf(FPSTR(T_i)) > 0; - bool tok_n = HTTP_PARAM_temp.indexOf(FPSTR(T_n)) > 0; - bool tok_p = HTTP_PARAM_temp.indexOf(FPSTR(T_p)) > 0; - bool tok_t = HTTP_PARAM_temp.indexOf(FPSTR(T_t)) > 0; - bool tok_l = HTTP_PARAM_temp.indexOf(FPSTR(T_l)) > 0; - bool tok_v = HTTP_PARAM_temp.indexOf(FPSTR(T_v)) > 0; - bool tok_c = HTTP_PARAM_temp.indexOf(FPSTR(T_c)) > 0; - - char valLength[5]; - - for (int i = 0; i < _paramsCount; i++) { - //Serial.println((String)_params[i]->_length); - if (_params[i] == NULL || _params[i]->_length > 99999) { - // try to detect param scope issues, doesnt always catch but works ok - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] WiFiManagerParameter is out of scope")); - #endif - return ""; - } - } - - // add the extra parameters to the form - for (int i = 0; i < _paramsCount; i++) { - // label before or after, @todo this could be done via floats or CSS and eliminated - String pitem; - switch (_params[i]->getLabelPlacement()) { - case WFM_LABEL_BEFORE: - pitem = FPSTR(HTTP_FORM_LABEL); - pitem += FPSTR(HTTP_FORM_PARAM); - break; - case WFM_LABEL_AFTER: - pitem = FPSTR(HTTP_FORM_PARAM); - pitem += FPSTR(HTTP_FORM_LABEL); - break; - default: - // WFM_NO_LABEL - pitem = FPSTR(HTTP_FORM_PARAM); - break; - } - - // Input templating - // "
    "; - // if no ID use customhtml for item, else generate from param string - if (_params[i]->getID() != NULL) { - if(tok_I)pitem.replace(FPSTR(T_I), (String)FPSTR(S_parampre)+(String)i); // T_I id number - if(tok_i)pitem.replace(FPSTR(T_i), _params[i]->getID()); // T_i id name - if(tok_n)pitem.replace(FPSTR(T_n), _params[i]->getID()); // T_n id name alias - if(tok_p)pitem.replace(FPSTR(T_p), FPSTR(T_t)); // T_p replace legacy placeholder token - if(tok_t)pitem.replace(FPSTR(T_t), _params[i]->getLabel()); // T_t title/label - snprintf(valLength, 5, "%d", _params[i]->getValueLength()); - if(tok_l)pitem.replace(FPSTR(T_l), valLength); // T_l value length - if(tok_v)pitem.replace(FPSTR(T_v), _params[i]->getValue()); // T_v value - if(tok_c)pitem.replace(FPSTR(T_c), _params[i]->getCustomHTML()); // T_c meant for additional attributes, not html, but can stuff - } else { - pitem = _params[i]->getCustomHTML(); - } - - page += pitem; - } - } - - return page; -} - -esp_err_t WiFiManager::handleWiFiStatus(PsychicRequest *request){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi status ")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - String page; - // String page = "{\"result\":true,\"count\":1}"; - #ifdef WM_JSTEST - page = FPSTR(HTTP_JS); - #endif - - return HTTPSend(request,page); -} - -/** - * HTTPD CALLBACK save form and redirect to WLAN config page again - */ -esp_err_t WiFiManager::handleWifiSave(PsychicRequest *request) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi save ")); - DEBUG_WM(WM_DEBUG_DEV,F("Method:"),request->method() == HTTP_GET ? (String)FPSTR(S_GET) : (String)FPSTR(S_POST)); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - - //SAVE/connect here - if(request->hasParam("s")){ - _ssid = request->getParam("s")->value(); - } - if(request->hasParam("p")){ - _pass = request->getParam("p")->value(); - } - - if(_ssid == "" && _pass != ""){ - _ssid = WiFi_SSID(true); // password change, placeholder ssid, @todo compare pass to old?, confirm ssid is clean - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Detected WiFi password change")); - #endif - } - - #ifdef WM_DEBUG_LEVEL - String requestinfo = "SERVER_REQUEST\n----------------\n"; - requestinfo += "URI: "; - requestinfo += request->url(); - requestinfo += "\nMethod: "; - requestinfo += (request->method() == HTTP_GET) ? "GET" : "POST"; - requestinfo += "\nArguments: "; - requestinfo += request->params(); - requestinfo += "\n"; - for (uint8_t i = 0; i < request->params(); i++) { - requestinfo += " " + request->getParam(i)->name() + ": " + request->getParam(i)->value() + "\n"; - } - - DEBUG_WM(WM_DEBUG_MAX,requestinfo); - #endif - - // set static ips from server args - if(request->hasParam(S_ip)){ - if (request->getParam(S_ip)->value() != "") { - //_sta_static_ip.fromString(request->arg(FPSTR(S_ip)); - String ip = request->getParam(S_ip)->value(); - optionalIPFromString(&_sta_static_ip, ip.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static ip:"),ip); - #endif - } - } - if(request->hasParam(S_gw)){ - if (request->getParam(S_gw)->value() != "") { - String gw = request->getParam(S_gw)->value(); - optionalIPFromString(&_sta_static_gw, gw.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static gateway:"),gw); - #endif - } - } - if(request->hasParam(S_sn)){ - if (request->getParam(S_sn)->value() != "") { - String sn = request->getParam(S_sn)->value(); - optionalIPFromString(&_sta_static_sn, sn.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static netmask:"),sn); - #endif - } - } - if(request->hasParam(S_dns)){ - if (request->getParam(S_dns)->value() != "") { - String dns = request->getParam(S_dns)->value(); - optionalIPFromString(&_sta_static_dns, dns.c_str()); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("static DNS:"),dns); - #endif - } - } - if (_presavewificallback != NULL) { - _presavewificallback(); // @CALLBACK - } - - if(_paramsInWifi) doParamSave(request); - - String page; - - if(_ssid == ""){ - page = getHTTPHead(FPSTR(S_titlewifisettings)); // @token titleparamsaved - page += FPSTR(HTTP_PARAMSAVED); - } - else { - page = getHTTPHead(FPSTR(S_titlewifisaved)); // @token titlewifisaved - page += FPSTR(HTTP_SAVED); - } - - if(_showBack) page += FPSTR(HTTP_BACKBTN); - page += FPSTR(HTTP_END); - - //server->sendHeader(FPSTR(HTTP_HEAD_CORS), FPSTR(HTTP_HEAD_CORS_ALLOW_ALL)); // @HTTPHEAD send cors - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Sent wifi save page")); - #endif - - connect = true; //signal ready to connect/reset process in processConfigPortal - - return HTTPSend(request,page); -} - -esp_err_t WiFiManager::handleParamSave(PsychicRequest *request) { - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Param save ")); - #endif - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Method:"),request->method() == HTTP_GET ? (String)FPSTR(S_GET) : (String)FPSTR(S_POST)); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - - doParamSave(request); - - String page = getHTTPHead(FPSTR(S_titleparamsaved)); // @token titleparamsaved - page += FPSTR(HTTP_PARAMSAVED); - if(_showBack) page += FPSTR(HTTP_BACKBTN); - page += FPSTR(HTTP_END); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Sent param save page")); - #endif - - return HTTPSend(request,page); -} - -void WiFiManager::doParamSave(PsychicRequest *request){ - // @todo use new callback for before paramsaves, is this really needed? - if ( _presaveparamscallback != NULL) { - _presaveparamscallback(); // @CALLBACK - } - - //parameters - if(_paramsCount > 0){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Parameters")); - DEBUG_WM(WM_DEBUG_VERBOSE,FPSTR(D_HR)); - #endif - - for (int i = 0; i < _paramsCount; i++) { - if (_params[i] == NULL || _params[i]->_length > 99999) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] WiFiManagerParameter is out of scope")); - #endif - break; // @todo might not be needed anymore - } - //read parameter from server - String name = (String)FPSTR(S_parampre)+(String)i; - String value; - if(request->hasParam(name.c_str())) { - value = request->getParam(name.c_str())->value(); - } else { - value = request->getParam(_params[i]->getID())->value(); - } - - //store it in params array - value.toCharArray(_params[i]->_value, _params[i]->_length+1); // length+1 null terminated - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,(String)_params[i]->getID() + ":",value); - #endif - } - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,FPSTR(D_HR)); - #endif - } - - if ( _saveparamscallback != NULL) { - _saveparamscallback(); // @CALLBACK - } - -} - -/** - * HTTPD CALLBACK info page - */ -esp_err_t WiFiManager::handleInfo(PsychicRequest *request) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Info")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - //handleRequest(); - String page = getHTTPHead(FPSTR(S_titleinfo)); // @token titleinfo - reportStatus(page); - - uint16_t infos = 0; - - //@todo convert to enum or refactor to strings - //@todo wrap in build flag to remove all info code for memory saving - #ifdef ESP8266 - infos = 28; - String infoids[] = { - F("esphead"), - F("uptime"), - F("chipid"), - F("fchipid"), - F("idesize"), - F("flashsize"), - F("corever"), - F("bootver"), - F("cpufreq"), - F("freeheap"), - F("memsketch"), - F("memsmeter"), - F("lastreset"), - F("wifihead"), - F("conx"), - F("stassid"), - F("staip"), - F("stagw"), - F("stasub"), - F("dnss"), - F("host"), - F("stamac"), - F("autoconx"), - F("wifiaphead"), - F("apssid"), - F("apip"), - F("apbssid"), - F("apmac") - }; - - #elif defined(ESP32) - // add esp_chip_info ? - infos = 27; - String infoids[] = { - F("esphead"), - F("uptime"), - F("chipid"), - F("chiprev"), - F("idesize"), - F("flashsize"), - F("cpufreq"), - F("freeheap"), - F("memsketch"), - F("memsmeter"), - F("lastreset"), - F("temp"), - // F("hall"), - F("wifihead"), - F("conx"), - F("stassid"), - F("staip"), - F("stagw"), - F("stasub"), - F("dnss"), - F("host"), - F("stamac"), - F("apssid"), - F("wifiaphead"), - F("apip"), - F("apmac"), - F("aphost"), - F("apbssid") - }; - #endif - - for(size_t i=0; i"); - - page += F("

    About


    "); - page += getInfoData("aboutver"); - page += getInfoData("aboutarduinover"); - page += getInfoData("aboutidfver"); - page += getInfoData("aboutdate"); - page += F("
    "); - - if(_showInfoUpdate){ - page += HTTP_PORTAL_MENU[8]; - page += HTTP_PORTAL_MENU[9]; - } - if(_showInfoErase) page += FPSTR(HTTP_ERASEBTN); - if(_showBack) page += FPSTR(HTTP_BACKBTN); - page += FPSTR(HTTP_HELP); - page += FPSTR(HTTP_END); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("Sent info page")); - #endif - - return HTTPSend(request,page); -} - -String WiFiManager::getInfoData(String id){ - - String p; - if(id==F("esphead")){ - p = FPSTR(HTTP_INFO_esphead); - #ifdef ESP32 - p.replace(FPSTR(T_1), (String)ESP.getChipModel()); - #endif - } - else if(id==F("wifihead")){ - p = FPSTR(HTTP_INFO_wifihead); - p.replace(FPSTR(T_1),getModeString(WiFi.getMode())); - } - else if(id==F("uptime")){ - // subject to rollover! - p = FPSTR(HTTP_INFO_uptime); - p.replace(FPSTR(T_1),(String)(millis() / 1000 / 60)); - p.replace(FPSTR(T_2),(String)((millis() / 1000) % 60)); - } - else if(id==F("chipid")){ - p = FPSTR(HTTP_INFO_chipid); - p.replace(FPSTR(T_1),String(WIFI_getChipId(),HEX)); - } - #ifdef ESP32 - else if(id==F("chiprev")){ - p = FPSTR(HTTP_INFO_chiprev); - String rev = (String)ESP.getChipRevision(); - #ifdef _SOC_EFUSE_REG_H_ - String revb = (String)(REG_READ(EFUSE_BLK0_RDATA3_REG) >> (EFUSE_RD_CHIP_VER_RESERVE_S)&&EFUSE_RD_CHIP_VER_RESERVE_V); - p.replace(FPSTR(T_1),rev+"
    "+revb); - #else - p.replace(FPSTR(T_1),rev); - #endif - } - #endif - #ifdef ESP8266 - else if(id==F("fchipid")){ - p = FPSTR(HTTP_INFO_fchipid); - p.replace(FPSTR(T_1),(String)ESP.getFlashChipId()); - } - #endif - else if(id==F("idesize")){ - p = FPSTR(HTTP_INFO_idesize); - p.replace(FPSTR(T_1),(String)ESP.getFlashChipSize()); - } - else if(id==F("flashsize")){ - #ifdef ESP8266 - p = FPSTR(HTTP_INFO_flashsize); - p.replace(FPSTR(T_1),(String)ESP.getFlashChipRealSize()); - #elif defined ESP32 - p = FPSTR(HTTP_INFO_psrsize); - p.replace(FPSTR(T_1),(String)ESP.getPsramSize()); - #endif - } - else if(id==F("corever")){ - #ifdef ESP8266 - p = FPSTR(HTTP_INFO_corever); - p.replace(FPSTR(T_1),(String)ESP.getCoreVersion()); - #endif - } - #ifdef ESP8266 - else if(id==F("bootver")){ - p = FPSTR(HTTP_INFO_bootver); - p.replace(FPSTR(T_1),(String)system_get_boot_version()); - } - #endif - else if(id==F("cpufreq")){ - p = FPSTR(HTTP_INFO_cpufreq); - p.replace(FPSTR(T_1),(String)ESP.getCpuFreqMHz()); - } - else if(id==F("freeheap")){ - p = FPSTR(HTTP_INFO_freeheap); - p.replace(FPSTR(T_1),(String)ESP.getFreeHeap()); - } - else if(id==F("memsketch")){ - p = FPSTR(HTTP_INFO_memsketch); - p.replace(FPSTR(T_1),(String)(ESP.getSketchSize())); - p.replace(FPSTR(T_2),(String)(ESP.getSketchSize()+ESP.getFreeSketchSpace())); - } - else if(id==F("memsmeter")){ - p = FPSTR(HTTP_INFO_memsmeter); - p.replace(FPSTR(T_1),(String)(ESP.getSketchSize())); - p.replace(FPSTR(T_2),(String)(ESP.getSketchSize()+ESP.getFreeSketchSpace())); - } - else if(id==F("lastreset")){ - #ifdef ESP8266 - p = FPSTR(HTTP_INFO_lastreset); - p.replace(FPSTR(T_1),(String)ESP.getResetReason()); - #elif defined(ESP32) && defined(_ROM_RTC_H_) - // requires #include - p = FPSTR(HTTP_INFO_lastreset); - for(int i=0;i<2;i++){ - int reason = rtc_get_reset_reason(i); - String tok = (String)T_ss+(String)(i+1)+(String)T_es; - switch (reason) - { - //@todo move to array - case 1 : p.replace(tok,F("Vbat power on reset"));break; - case 3 : p.replace(tok,F("Software reset digital core"));break; - case 4 : p.replace(tok,F("Legacy watch dog reset digital core"));break; - case 5 : p.replace(tok,F("Deep Sleep reset digital core"));break; - case 6 : p.replace(tok,F("Reset by SLC module, reset digital core"));break; - case 7 : p.replace(tok,F("Timer Group0 Watch dog reset digital core"));break; - case 8 : p.replace(tok,F("Timer Group1 Watch dog reset digital core"));break; - case 9 : p.replace(tok,F("RTC Watch dog Reset digital core"));break; - case 10 : p.replace(tok,F("Instrusion tested to reset CPU"));break; - case 11 : p.replace(tok,F("Time Group reset CPU"));break; - case 12 : p.replace(tok,F("Software reset CPU"));break; - case 13 : p.replace(tok,F("RTC Watch dog Reset CPU"));break; - case 14 : p.replace(tok,F("for APP CPU, reseted by PRO CPU"));break; - case 15 : p.replace(tok,F("Reset when the vdd voltage is not stable"));break; - case 16 : p.replace(tok,F("RTC Watch dog reset digital core and rtc module"));break; - default : p.replace(tok,F("NO_MEAN")); - } - } - #endif - } - else if(id==F("apip")){ - p = FPSTR(HTTP_INFO_apip); - p.replace(FPSTR(T_1),WiFi.softAPIP().toString()); - } - else if(id==F("apmac")){ - p = FPSTR(HTTP_INFO_apmac); - p.replace(FPSTR(T_1),(String)WiFi.softAPmacAddress()); - } - #ifdef ESP32 - else if(id==F("aphost")){ - p = FPSTR(HTTP_INFO_aphost); - p.replace(FPSTR(T_1),WiFi.softAPgetHostname()); - } - #endif - #ifndef WM_NOSOFTAPSSID - #ifdef ESP8266 - else if(id==F("apssid")){ - p = FPSTR(HTTP_INFO_apssid); - p.replace(FPSTR(T_1),htmlEntities(WiFi.softAPSSID())); - } - #endif - #endif - else if(id==F("apbssid")){ - p = FPSTR(HTTP_INFO_apbssid); - p.replace(FPSTR(T_1),(String)WiFi.BSSIDstr()); - } - // softAPgetHostname // esp32 - // softAPSubnetCIDR - // softAPNetworkID - // softAPBroadcastIP - - else if(id==F("stassid")){ - p = FPSTR(HTTP_INFO_stassid); - p.replace(FPSTR(T_1),htmlEntities((String)WiFi_SSID())); - } - else if(id==F("staip")){ - p = FPSTR(HTTP_INFO_staip); - p.replace(FPSTR(T_1),WiFi.localIP().toString()); - } - else if(id==F("stagw")){ - p = FPSTR(HTTP_INFO_stagw); - p.replace(FPSTR(T_1),WiFi.gatewayIP().toString()); - } - else if(id==F("stasub")){ - p = FPSTR(HTTP_INFO_stasub); - p.replace(FPSTR(T_1),WiFi.subnetMask().toString()); - } - else if(id==F("dnss")){ - p = FPSTR(HTTP_INFO_dnss); - p.replace(FPSTR(T_1),WiFi.dnsIP().toString()); - } - else if(id==F("host")){ - p = FPSTR(HTTP_INFO_host); - #ifdef ESP32 - p.replace(FPSTR(T_1),WiFi.getHostname()); - #else - p.replace(FPSTR(T_1),WiFi.hostname()); - #endif - } - else if(id==F("stamac")){ - p = FPSTR(HTTP_INFO_stamac); - p.replace(FPSTR(T_1),WiFi.macAddress()); - } - else if(id==F("conx")){ - p = FPSTR(HTTP_INFO_conx); - p.replace(FPSTR(T_1),WiFi.isConnected() ? FPSTR(S_y) : FPSTR(S_n)); - } - #ifdef ESP8266 - else if(id==F("autoconx")){ - p = FPSTR(HTTP_INFO_autoconx); - p.replace(FPSTR(T_1),WiFi.getAutoConnect() ? FPSTR(S_enable) : FPSTR(S_disable)); - } - #endif - #if defined(ESP32) && !defined(WM_NOTEMP) - else if(id==F("temp")){ - // temperature is not calibrated, varying large offsets are present, use for relative temp changes only - p = FPSTR(HTTP_INFO_temp); - p.replace(FPSTR(T_1),(String)temperatureRead()); - p.replace(FPSTR(T_2),(String)((temperatureRead()+32)*1.8f)); - } - // else if(id==F("hall")){ - // p = FPSTR(HTTP_INFO_hall); - // p.replace(FPSTR(T_1),(String)hallRead()); // hall sensor reads can cause issues with adcs - // } - #endif - else if(id==F("aboutver")){ - p = FPSTR(HTTP_INFO_aboutver); - p.replace(FPSTR(T_1),FPSTR(WM_VERSION_STR)); - } - else if(id==F("aboutarduinover")){ - #ifdef VER_ARDUINO_STR - p = FPSTR(HTTP_INFO_aboutarduino); - p.replace(FPSTR(T_1),String(VER_ARDUINO_STR)); - #endif - } - // else if(id==F("aboutidfver")){ - // #ifdef VER_IDF_STR - // p = FPSTR(HTTP_INFO_aboutidf); - // p.replace(FPSTR(T_1),String(VER_IDF_STR)); - // #endif - // } - else if(id==F("aboutsdkver")){ - p = FPSTR(HTTP_INFO_sdkver); - #ifdef ESP32 - p.replace(FPSTR(T_1),(String)esp_get_idf_version()); - // p.replace(FPSTR(T_1),(String)system_get_sdk_version()); // deprecated - #else - p.replace(FPSTR(T_1),(String)system_get_sdk_version()); - #endif - } - else if(id==F("aboutdate")){ - p = FPSTR(HTTP_INFO_aboutdate); - p.replace(FPSTR(T_1),String(__DATE__ " " __TIME__)); - } - return p; -} - -/** - * HTTPD CALLBACK exit, closes configportal if blocking, if non blocking undefined - */ -esp_err_t WiFiManager::handleExit(PsychicRequest *request) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Exit")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - String page = getHTTPHead(FPSTR(S_titleexit)); // @token titleexit - page += FPSTR(S_exiting); // @token exiting - // ('Logout', 401, {'WWW-Authenticate': 'Basic realm="Login required"'}) - delay(2000); - abort = true; - - PsychicResponse response(request); - response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - response.setCode(200); - response.setContentType(HTTP_HEAD_CT); - response.setContent(page.c_str()); - return response.send(); -} - -/** - * HTTPD CALLBACK reset page - */ -esp_err_t WiFiManager::handleReset(PsychicRequest *request) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Reset")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - String page = getHTTPHead(FPSTR(S_titlereset)); //@token titlereset - page += FPSTR(S_resetting); //@token resetting - page += FPSTR(HTTP_END); - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("RESETTING ESP")); - #endif - _rebootNeeded = true; - - return HTTPSend(request,page); -} - -/** - * HTTPD CALLBACK erase page - */ - -// void WiFiManager::handleErase() { -// handleErase(false); -// } -esp_err_t WiFiManager::handleErase(PsychicRequest *request,bool opt = false) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_NOTIFY,F("<- HTTP Erase")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - handleRequest(); - String page = getHTTPHead(FPSTR(S_titleerase)); // @token titleerase - - bool ret = erase(opt); - - if(ret) page += FPSTR(S_resetting); // @token resetting - else { - page += FPSTR(S_error); // @token erroroccur - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] WiFi EraseConfig failed")); - #endif - } - - page += FPSTR(HTTP_END); - - if(ret){ - _rebootNeeded = true; - } - - return HTTPSend(request,page); -} - -/** - * HTTPD CALLBACK 404 - */ -esp_err_t WiFiManager::handleNotFound(PsychicRequest *request) { - if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page - handleRequest(); - String message = FPSTR(S_notfound); // @token notfound - - bool verbose404 = false; // show info in 404 body, uri,method, args - if(verbose404){ - message += FPSTR(S_uri); // @token uri - message += request->url(); - message += FPSTR(S_method); // @token method - message += ( request->method() == HTTP_GET ) ? FPSTR(S_GET) : FPSTR(S_POST); - message += FPSTR(S_args); // @token args - message += request->params(); - message += F("\n"); - - for ( uint8_t i = 0; i < request->params(); i++ ) { - message += " " + request->getParam(i)->name() + ": " + request->getParam(i)->value() + "\n"; - } - } - - PsychicResponse response(request); - response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - response.addHeader("Pragma", "no-cache"); - response.addHeader("Expires", "-1"); - response.setCode(404); - response.setContentType(HTTP_HEAD_CT2); - response.setContent(message.c_str()); - return response.send(); -} - -/** - * HTTPD redirector - * Redirect to captive portal if we got a request for another domain. - * Return true in that case so the page handler do not try to handle the request again. - */ -boolean WiFiManager::captivePortal(PsychicRequest *request) { - - if(!_enableCaptivePortal || !configPortalActive) return false; // skip redirections if cp not enabled or not in ap mode - - String serverLoc = toStringIp(request->client()->localIP()); - - #ifdef WM_DEBUG_LEVEL - //DEBUG_WM(WM_DEBUG_DEV,"-> " + request->host()); - //DEBUG_WM(WM_DEBUG_DEV,"serverLoc " + serverLoc); - #endif - - // fallback for ipv6 bug - if(serverLoc == "0.0.0.0"){ - if ((WiFi.status()) != WL_CONNECTED) - serverLoc = toStringIp(WiFi.softAPIP()); - else - serverLoc = toStringIp(WiFi.localIP()); - } - - if(_httpPort != 80) serverLoc += ":" + (String)_httpPort; // add port if not default - bool doredirect = serverLoc != request->host(); // redirect if hostheader not server ip, prevent redirect loops - - if (doredirect) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- Request redirected to captive portal")); - DEBUG_WM(WM_DEBUG_DEV,"serverLoc " + serverLoc); - #endif - PsychicResponse response(request); - response.addHeader("Location", ((String)("http://") + serverLoc).c_str()); - response.setCode(302); - response.setContentType(HTTP_HEAD_CT2); - response.send(); - return true; - } - return false; -} - -void WiFiManager::stopCaptivePortal(){ - _enableCaptivePortal= false; - // @todo maybe disable configportaltimeout(optional), or just provide callback for user -} - -// HTTPD CALLBACK, handle close, stop captive portal, if not enabled undefined -esp_err_t WiFiManager::handleClose(PsychicRequest *request){ - DEBUG_WM(WM_DEBUG_VERBOSE,F("Disabling Captive Portal")); - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - stopCaptivePortal(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP close")); - #endif - handleRequest(); - String page = getHTTPHead(FPSTR(S_titleclose)); // @token titleclose - page += FPSTR(S_closing); // @token closing - return HTTPSend(request,page); -} - -void WiFiManager::reportStatus(String &page){ - // updateConxResult(WiFi.status()); // @todo: this defeats the purpose of last result, update elsewhere or add logic here - DEBUG_WM(WM_DEBUG_DEV,F("[WIFI] reportStatus prev:"),getWLStatusString(_lastconxresult)); - DEBUG_WM(WM_DEBUG_DEV,F("[WIFI] reportStatus current:"),getWLStatusString(WiFi.status())); - String str; - if (WiFi_SSID() != ""){ - if (WiFi.status()==WL_CONNECTED){ - str = FPSTR(HTTP_STATUS_ON); - str.replace(FPSTR(T_i),WiFi.localIP().toString()); - str.replace(FPSTR(T_v),htmlEntities(WiFi_SSID())); - } - else { - str = FPSTR(HTTP_STATUS_OFF); - str.replace(FPSTR(T_v),htmlEntities(WiFi_SSID())); - if(_lastconxresult == WL_STATION_WRONG_PASSWORD){ - // wrong password - str.replace(FPSTR(T_c),"D"); // class - str.replace(FPSTR(T_r),FPSTR(HTTP_STATUS_OFFPW)); - } - else if(_lastconxresult == WL_NO_SSID_AVAIL){ - // connect failed, or ap not found - str.replace(FPSTR(T_c),"D"); - str.replace(FPSTR(T_r),FPSTR(HTTP_STATUS_OFFNOAP)); - } - else if(_lastconxresult == WL_CONNECT_FAILED){ - // connect failed - str.replace(FPSTR(T_c),"D"); - str.replace(FPSTR(T_r),FPSTR(HTTP_STATUS_OFFFAIL)); - } - else if(_lastconxresult == WL_CONNECTION_LOST){ - // connect failed, MOST likely 4WAY_HANDSHAKE_TIMEOUT/incorrect password, state is ambiguous however - str.replace(FPSTR(T_c),"D"); - str.replace(FPSTR(T_r),FPSTR(HTTP_STATUS_OFFFAIL)); - } - else{ - str.replace(FPSTR(T_c),""); - str.replace(FPSTR(T_r),""); - } - } - } - else { - str = FPSTR(HTTP_STATUS_NONE); - } - page += str; -} - -// PUBLIC - -// METHODS - -/** - * reset wifi settings, clean stored ap password - */ - -/** - * [stopConfigPortal description] - * @return {[type]} [description] - */ -bool WiFiManager::stopConfigPortal(){ - if(_configPortalIsBlocking){ - abort = true; - return true; - } - return shutdownConfigPortal(); -} - -/** - * disconnect - * @access public - * @since $dev - * @return bool success - */ -bool WiFiManager::disconnect(){ - if(WiFi.status() != WL_CONNECTED){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("Disconnecting: Not connected")); - #endif - return false; - } - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("Disconnecting")); - #endif - return WiFi_Disconnect(); -} - -/** - * reboot the device - * @access public - */ -void WiFiManager::reboot(){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("Restarting")); - _debugPort.flush(); - #endif - delay(2000); // time for serial and web flsuh - shutdownConfigPortal(); - ESP.restart(); -} - -/** - * reboot the device - * @access public - */ -bool WiFiManager::erase(){ - return erase(false); -} - -bool WiFiManager::erase(bool opt){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM("Erasing"); - #endif - - #if defined(ESP32) && ((defined(WM_ERASE_NVS) || defined(nvs_flash_h))) - // if opt true, do nvs erase - if(opt){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("Erasing NVS")); - #endif - esp_err_t err; - err = nvs_flash_init(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("nvs_flash_init: "),err!=ESP_OK ? (String)err : "Success"); - #endif - err = nvs_flash_erase(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("nvs_flash_erase: "), err!=ESP_OK ? (String)err : "Success"); - #endif - return err == ESP_OK; - } - #elif defined(ESP8266) && defined(spiffs_api_h) - if(opt){ - bool ret = false; - if(SPIFFS.begin()){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("Erasing SPIFFS")); - #endif - bool ret = SPIFFS.format(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("spiffs erase: "),ret ? "Success" : "ERROR"); - #endif - } else{ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("[ERROR] Could not start SPIFFS")); - #endif - } - return ret; - } - #else - (void)opt; - #endif - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("Erasing WiFi Config")); - #endif - return WiFi_eraseConfig(); -} - -/** - * [resetSettings description] - * ERASES STA CREDENTIALS - * @access public - */ -void WiFiManager::resetSettings() { -#ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("resetSettings")); - #endif - WiFi_enableSTA(true,true); // must be sta to disconnect erase - delay(500); // ensure sta is enabled - if (_resetcallback != NULL){ - _resetcallback(); // @CALLBACK - } - - #ifdef ESP32 - WiFi.disconnect(true,true); - #else - WiFi.persistent(true); - WiFi.disconnect(true); - WiFi.persistent(false); - #endif - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("SETTINGS ERASED")); - #endif -} - -// SETTERS - -/** - * [setTimeout description] - * @access public - * @param {[type]} unsigned long seconds [description] - */ -void WiFiManager::setTimeout(unsigned long seconds) { - setConfigPortalTimeout(seconds); -} - -/** - * [setConfigPortalTimeout description] - * @access public - * @param {[type]} unsigned long seconds [description] - */ -void WiFiManager::setConfigPortalTimeout(unsigned long seconds) { - _configPortalTimeout = seconds * 1000; -} - -/** - * [setConnectTimeout description] - * @access public - * @param {[type]} unsigned long seconds [description] - */ -void WiFiManager::setConnectTimeout(unsigned long seconds) { - _connectTimeout = seconds * 1000; -} - -/** - * [setConnectRetries description] - * @access public - * @param {[type]} uint8_t numRetries [description] - */ -void WiFiManager::setConnectRetries(uint8_t numRetries){ - _connectRetries = constrain(numRetries,1,10); -} - -/** - * toggle _cleanconnect, always disconnect before connecting - * @param {[type]} bool enable [description] - */ -void WiFiManager::setCleanConnect(bool enable){ - _cleanConnect = enable; -} - -/** - * [setConnectTimeout description - * @access public - * @param {[type]} unsigned long seconds [description] - */ -void WiFiManager::setSaveConnectTimeout(unsigned long seconds) { - _saveTimeout = seconds * 1000; -} - -/** - * Set save portal connect on save option, - * if false, will only save credentials not connect - * @access public - * @param {[type]} bool connect [description] - */ -void WiFiManager::setSaveConnect(bool connect) { - _connectonsave = connect; -} - -/** - * [setDebugOutput description] - * @access public - * @param {[type]} boolean debug [description] - */ -void WiFiManager::setDebugOutput(boolean debug) { - _debug = debug; - if(_debug && _debugLevel == WM_DEBUG_DEV) debugPlatformInfo(); - if(_debug && _debugLevel >= WM_DEBUG_NOTIFY)DEBUG_WM((__FlashStringHelper *)WM_VERSION_STR," D:"+String(_debugLevel)); -} - -void WiFiManager::setDebugOutput(boolean debug, String prefix) { - _debugPrefix = prefix; - setDebugOutput(debug); -} - -void WiFiManager::setDebugOutput(boolean debug, wm_debuglevel_t level) { - _debugLevel = level; - // _debugPrefix = prefix; - setDebugOutput(debug); -} - - -/** - * [setAPStaticIPConfig description] - * @access public - * @param {[type]} IPAddress ip [description] - * @param {[type]} IPAddress gw [description] - * @param {[type]} IPAddress sn [description] - */ -void WiFiManager::setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn) { - _ap_static_ip = ip; - _ap_static_gw = gw; - _ap_static_sn = sn; -} - -/** - * [setSTAStaticIPConfig description] - * @access public - * @param {[type]} IPAddress ip [description] - * @param {[type]} IPAddress gw [description] - * @param {[type]} IPAddress sn [description] - */ -void WiFiManager::setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn) { - _sta_static_ip = ip; - _sta_static_gw = gw; - _sta_static_sn = sn; -} - -/** - * [setSTAStaticIPConfig description] - * @since $dev - * @access public - * @param {[type]} IPAddress ip [description] - * @param {[type]} IPAddress gw [description] - * @param {[type]} IPAddress sn [description] - * @param {[type]} IPAddress dns [description] - */ -void WiFiManager::setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn, IPAddress dns) { - setSTAStaticIPConfig(ip,gw,sn); - _sta_static_dns = dns; -} - -/** - * [setMinimumSignalQuality description] - * @access public - * @param {[type]} int quality [description] - */ -void WiFiManager::setMinimumSignalQuality(int quality) { - _minimumQuality = quality; -} - -/** - * [setBreakAfterConfig description] - * @access public - * @param {[type]} boolean shouldBreak [description] - */ -void WiFiManager::setBreakAfterConfig(boolean shouldBreak) { - _shouldBreakAfterConfig = shouldBreak; -} - -/** - * setAPCallback, set a callback when softap is started - * @access public - * @param {[type]} void (*func)(WiFiManager* wminstance) - */ -void WiFiManager::setAPCallback( std::function func ) { - _apcallback = func; -} - -/** - * setWebServerCallback, set a callback after webserver is reset, and before routes are setup - * if we set webserver handlers before wm, they are used and wm is not by esp webserver - * on events cannot be overrided once set, and are not mutiples - * @access public - * @param {[type]} void (*func)(void) - */ -void WiFiManager::setWebServerCallback( std::function func ) { - _webservercallback = func; -} - -/** - * setSaveConfigCallback, set a save config callback after closing configportal - * @note calls only if wifi is saved or changed, or setBreakAfterConfig(true) - * @access public - * @param {[type]} void (*func)(void) - */ -void WiFiManager::setSaveConfigCallback( std::function func ) { - _savewificallback = func; -} - -/** - * setPreSaveConfigCallback, set a callback to fire before saving wifi or params - * @access public - * @param {[type]} void (*func)(void) - */ -void WiFiManager::setPreSaveConfigCallback( std::function func ) { - _presavewificallback = func; -} - -/** - * setConfigResetCallback, set a callback to occur when a resetSettings() occurs - * @access public - * @param {[type]} void(*func)(void) - */ -void WiFiManager::setConfigResetCallback( std::function func ) { - _resetcallback = func; -} - -/** - * setSaveParamsCallback, set a save params callback on params save in wifi or params pages - * @access public - * @param {[type]} void (*func)(void) - */ -void WiFiManager::setSaveParamsCallback( std::function func ) { - _saveparamscallback = func; -} - -/** - * setPreSaveParamsCallback, set a pre save params callback on params save prior to anything else - * @access public - * @param {[type]} void (*func)(void) - */ -void WiFiManager::setPreSaveParamsCallback( std::function func ) { - _presaveparamscallback = func; -} - -/** - * setPreOtaUpdateCallback, set a callback to fire before OTA update - * @access public - * @param {[type]} void (*func)(void) - */ -void WiFiManager::setPreOtaUpdateCallback( std::function func ) { - _preotaupdatecallback = func; -} - -/** - * setConfigPortalTimeoutCallback, set a callback to config portal is timeout - * @access public - * @param {[type]} void (*func)(void) - */ -void WiFiManager::setConfigPortalTimeoutCallback( std::function func ) { - _configportaltimeoutcallback = func; -} - -/** - * set custom head html - * custom element will be added to head, eg. new meta,style,script tag etc. - * @access public - * @param char element - */ -void WiFiManager::setCustomHeadElement(const char* html) { - _customHeadElement = html; -} - -/** - * set custom menu html - * custom element will be added to menu under custom menu item. - * @access public - * @param char element - */ -void WiFiManager::setCustomMenuHTML(const char* html) { - _customMenuHTML = html; -} - -/** - * toggle wifiscan hiding of duplicate ssid names - * if this is false, wifiscan will remove duplicat Access Points - defaut true - * @access public - * @param boolean removeDuplicates [true] - */ -void WiFiManager::setRemoveDuplicateAPs(boolean removeDuplicates) { - _removeDuplicateAPs = removeDuplicates; -} - -/** - * toggle configportal blocking loop - * if enabled, then the configportal will enter a blocking loop and wait for configuration - * if disabled use with process() to manually process webserver - * @since $dev - * @access public - * @param boolean shoudlBlock [false] - */ -void WiFiManager::setConfigPortalBlocking(boolean shouldBlock) { - _configPortalIsBlocking = shouldBlock; -} - -/** - * toggle restore persistent, track internally - * sets ESP wifi.persistent so we can remember it and restore user preference on destruct - * there is no getter in esp8266 platform prior to https://github.com/esp8266/Arduino/pull/3857 - * @since $dev - * @access public - * @param boolean persistent [true] - */ -void WiFiManager::setRestorePersistent(boolean persistent) { - _userpersistent = persistent; - if(!persistent){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("persistent is off")); - #endif - } -} - -/** - * toggle showing static ip form fields - * if enabled, then the static ip, gateway, subnet fields will be visible, even if not set in code - * @since $dev - * @access public - * @param boolean alwaysShow [false] - */ -void WiFiManager::setShowStaticFields(boolean alwaysShow){ - if(_disableIpFields) _staShowStaticFields = alwaysShow ? 1 : -1; - else _staShowStaticFields = alwaysShow ? 1 : 0; -} - -/** - * toggle showing dns fields - * if enabled, then the dns1 field will be visible, even if not set in code - * @since $dev - * @access public - * @param boolean alwaysShow [false] - */ -void WiFiManager::setShowDnsFields(boolean alwaysShow){ - if(_disableIpFields) _staShowDns = alwaysShow ? 1 : -1; - _staShowDns = alwaysShow ? 1 : 0; -} - -/** - * toggle showing password in wifi password field - * if not enabled, placeholder will be S_passph - * @since $dev - * @access public - * @param boolean alwaysShow [false] - */ -void WiFiManager::setShowPassword(boolean show){ - _showPassword = show; -} - -/** - * toggle captive portal - * if enabled, then devices that use captive portal checks will be redirected to root - * if not you will automatically have to navigate to ip [192.168.4.1] - * @since $dev - * @access public - * @param boolean enabled [true] - */ -void WiFiManager::setCaptivePortalEnable(boolean enabled){ - _enableCaptivePortal = enabled; -} - -/** - * toggle connecting to the best AP based on RSSI - * @param boolean enabled [false] - */ -void WiFiManager::setFindBestRSSI(boolean enabled) { - _findBestRSSI = enabled; -} - -/** - * toggle wifi autoreconnect policy - * if enabled, then wifi will autoreconnect automatically always - * On esp8266 we force this on when autoconnect is called, see notes - * On esp32 this is handled on SYSTEM_EVENT_STA_DISCONNECTED since it does not exist in core yet - * @since $dev - * @access public - * @param boolean enabled [true] - */ -void WiFiManager::setWiFiAutoReconnect(boolean enabled){ - _wifiAutoReconnect = enabled; -} - -/** - * toggle configportal timeout wait for station client - * if enabled, then the configportal will start timeout when no stations are connected to softAP - * disabled by default as rogue stations can keep it open if there is no auth - * @since $dev - * @access public - * @param boolean enabled [false] - */ -void WiFiManager::setAPClientCheck(boolean enabled){ - _apClientCheck = enabled; -} - -/** - * toggle configportal timeout wait for web client - * if enabled, then the configportal will restart timeout when client requests come in - * @since $dev - * @access public - * @param boolean enabled [true] - */ -void WiFiManager::setWebPortalClientCheck(boolean enabled){ - _webClientCheck = enabled; -} - -/** - * toggle wifiscan percentages or quality icons - * @since $dev - * @access public - * @param boolean enabled [false] - */ -void WiFiManager::setScanDispPerc(boolean enabled){ - _scanDispOptions = enabled; -} - -/** - * toggle configportal if autoconnect failed - * if enabled, then the configportal will be activated on autoconnect failure - * @since $dev - * @access public - * @param boolean enabled [true] - */ -void WiFiManager::setEnableConfigPortal(boolean enable) -{ - _enableConfigPortal = enable; -} - -/** - * toggle configportal if autoconnect failed - * if enabled, then the configportal will be de-activated on wifi save - * @since $dev - * @access public - * @param boolean enabled [true] - */ -void WiFiManager::setDisableConfigPortal(boolean enable) -{ - _disableConfigPortal = enable; -} - -/** - * set the hostname (dhcp client id) - * @since $dev - * @access public - * @param char* hostname 32 character hostname to use for sta+ap in esp32, sta in esp8266 - * @return bool false if hostname is not valid - */ -bool WiFiManager::setHostname(const char * hostname){ - //@todo max length 32 - _hostname = String(hostname); - return true; -} - -bool WiFiManager::setHostname(String hostname){ - //@todo max length 32 - _hostname = hostname; - return true; -} - -/** - * set the soft ao channel, ignored if channelsync is true and connected - * @param int32_t wifi channel, 0 to disable - */ -void WiFiManager::setWiFiAPChannel(int32_t channel){ - _apChannel = channel; -} - -/** - * set the soft ap hidden - * @param bool wifi ap hidden, default is false - */ -void WiFiManager::setWiFiAPHidden(bool hidden){ - _apHidden = hidden; -} - - -/** - * toggle showing erase wifi config button on info page - * @param boolean enabled - */ -void WiFiManager::setShowInfoErase(boolean enabled){ - _showInfoErase = enabled; -} - -/** - * toggle showing update upload web ota button on info page - * @param boolean enabled - */ -void WiFiManager::setShowInfoUpdate(boolean enabled){ - _showInfoUpdate = enabled; -} - -/** - * check if the config portal is running - * @return bool true if active - */ -bool WiFiManager::getConfigPortalActive(){ - return configPortalActive; -} - -/** - * [getConfigPortalActive description] - * @return bool true if active - */ -bool WiFiManager::getWebPortalActive(){ - return webPortalActive; -} - - -String WiFiManager::getWiFiHostname(){ - #ifdef ESP32 - return (String)WiFi.getHostname(); - #else - return (String)WiFi.hostname(); - #endif -} - -/** - * [setTitle description] - * @param String title, set app title - */ -void WiFiManager::setTitle(String title){ - _title = title; -} - -/** - * set menu items and order - * if param is present in menu , params will be removed from wifi page automatically - * eg. - * const char * menu[] = {"wifi","setup","sep","info","exit"}; - * WiFiManager.setMenu(menu); - * @since $dev - * @param uint8_t menu[] array of menu ids - */ -void WiFiManager::setMenu(const char * menu[], uint8_t size){ -#ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_DEV,"setmenu array"); - #endif - _menuIds.clear(); - for(size_t i = 0; i < size; i++){ - for(size_t j = 0; j < _nummenutokens; j++){ - if((String)menu[i] == (__FlashStringHelper *)(_menutokens[j])){ - if((String)menu[i] == "param") _paramsInWifi = false; // param auto flag - _menuIds.push_back(j); - } - delay(0); - } - delay(0); - } - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(getMenuOut()); - #endif -} - -/** - * setMenu with vector - * eg. - * std::vector menu = {"wifi","setup","sep","info","exit"}; - * WiFiManager.setMenu(menu); - * tokens can be found in _menutokens array in strings_en.h - * @shiftIncrement $dev - * @param {[type]} std::vector& menu [description] - */ -void WiFiManager::setMenu(std::vector& menu){ -#ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_DEV,"setmenu vector"); - #endif - _menuIds.clear(); - for(auto menuitem : menu ){ - for(size_t j = 0; j < _nummenutokens; j++){ - if((String)menuitem == (__FlashStringHelper *)(_menutokens[j])){ - if((String)menuitem == "param") _paramsInWifi = false; // param auto flag - _menuIds.push_back(j); - } - } - } - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_DEV,getMenuOut()); - #endif -} - - -/** - * set params as sperate page not in wifi - * NOT COMPATIBLE WITH setMenu! - * @todo scan menuids and insert param after wifi or something, same for ota - * @param bool enable - * @since $dev - */ -void WiFiManager::setParamsPage(bool enable){ - _paramsInWifi = !enable; - setMenu(enable ? _menuIdsParams : _menuIdsDefault); -} - -// GETTERS - -/** - * get config portal AP SSID - * @since 0.0.1 - * @access public - * @return String the configportal ap name - */ -String WiFiManager::getConfigPortalSSID() { - return _apName; -} - -/** - * return the last known connection result - * logged on autoconnect and wifisave, can be used to check why failed - * get as readable string with getWLStatusString(getLastConxResult); - * @since $dev - * @access public - * @return bool return wl_status codes - */ -uint8_t WiFiManager::getLastConxResult(){ - return _lastconxresult; -} - -/** - * check if wifi has a saved ap or not - * @since $dev - * @access public - * @return bool true if a saved ap config exists - */ -bool WiFiManager::getWiFiIsSaved(){ - return WiFi_hasAutoConnect(); -} - -/** - * getDefaultAPName - * @since $dev - * @return string - */ -String WiFiManager::getDefaultAPName(){ - String hostString = String(WIFI_getChipId(),HEX); - hostString.toUpperCase(); - // char hostString[16] = {0}; - // sprintf(hostString, "%06X", ESP.getChipId()); - return _wifissidprefix + "_" + hostString; -} - -/** - * setCountry - * @since $dev - * @param String cc country code, must be defined in WiFiSetCountry, US, JP, CN - */ -void WiFiManager::setCountry(String cc){ - _wificountry = cc; -} - -/** - * setClass - * @param String str body class string - */ -void WiFiManager::setClass(String str){ - _bodyClass = str; -} - -/** - * setDarkMode - * @param bool enable, enable dark mode via invert class - */ -void WiFiManager::setDarkMode(bool enable){ - _bodyClass = enable ? "invert" : ""; -} - -/** - * setHttpPort - * @param uint16_t port webserver port number default 80 - */ -void WiFiManager::setHttpPort(uint16_t port){ - _httpPort = port; -} - - -bool WiFiManager::preloadWiFi(String ssid, String pass){ - _defaultssid = ssid; - _defaultpass = pass; - return true; -} - -// HELPERS - -/** - * getWiFiSSID - * @since $dev - * @param bool persistent - * @return String - */ -String WiFiManager::getWiFiSSID(bool persistent){ - return WiFi_SSID(persistent); -} - -/** - * getWiFiPass - * @since $dev - * @param bool persistent - * @return String - */ -String WiFiManager::getWiFiPass(bool persistent){ - return WiFi_psk(persistent); -} - -// DEBUG -// @todo fix DEBUG_WM(0,0); -template -void WiFiManager::DEBUG_WM(Generic text) { - DEBUG_WM(WM_DEBUG_NOTIFY,text,""); -} - -template -void WiFiManager::DEBUG_WM(wm_debuglevel_t level,Generic text) { - if(_debugLevel >= level) DEBUG_WM(level,text,""); -} - -template -void WiFiManager::DEBUG_WM(Generic text,Genericb textb) { - DEBUG_WM(WM_DEBUG_NOTIFY,text,textb); -} - -template -void WiFiManager::DEBUG_WM(wm_debuglevel_t level,Generic text,Genericb textb) { - if(!_debug || _debugLevel < level) return; - - if(_debugLevel >= WM_DEBUG_MAX){ - #ifdef ESP8266 - // uint32_t free; - // uint16_t max; - // uint8_t frag; - // ESP.getHeapStats(&free, &max, &frag);// @todo Does not exist in 2.3.0 - // _debugPort.printf("[MEM] free: %5d | max: %5d | frag: %3d%% \n", free, max, frag); - #elif defined ESP32 - // total_free_bytes; ///< Total free bytes in the heap. Equivalent to multi_free_heap_size(). - // total_allocated_bytes; ///< Total bytes allocated to data in the heap. - // largest_free_block; ///< Size of largest free block in the heap. This is the largest malloc-able size. - // minimum_free_bytes; ///< Lifetime minimum free heap size. Equivalent to multi_minimum_free_heap_size(). - // allocated_blocks; ///< Number of (variable size) blocks allocated in the heap. - // free_blocks; ///< Number of (variable size) free blocks in the heap. - // total_blocks; ///< Total number of (variable size) blocks in the heap. - multi_heap_info_t info; - heap_caps_get_info(&info, MALLOC_CAP_INTERNAL); - uint32_t free = info.total_free_bytes; - uint16_t max = info.largest_free_block; - uint8_t frag = 100 - (max * 100) / free; - _debugPort.printf("[MEM] free: %5d | max: %5d | frag: %3d%% \n", free, max, frag); - #endif - } - - _debugPort.print(_debugPrefix); - if(_debugLevel >= debugLvlShow) _debugPort.print("["+(String)level+"] "); - _debugPort.print(text); - if(textb){ - _debugPort.print(" "); - _debugPort.print(textb); - } - _debugPort.println(); -} - -/** - * [debugSoftAPConfig description] - * @access public - * @return {[type]} [description] - */ -void WiFiManager::debugSoftAPConfig(){ - - #ifdef ESP8266 - softap_config config; - wifi_softap_get_config(&config); - #if !defined(WM_NOCOUNTRY) - wifi_country_t country; - wifi_get_country(&country); - #endif - #elif defined(ESP32) - wifi_country_t country; - wifi_config_t conf_config; - esp_wifi_get_config(WIFI_IF_AP, &conf_config); // == ESP_OK - wifi_ap_config_t config = conf_config.ap; - esp_wifi_get_country(&country); - #endif - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("SoftAP Configuration")); - DEBUG_WM(FPSTR(D_HR)); - DEBUG_WM(F("ssid: "),(char *) config.ssid); - DEBUG_WM(F("password: "),(char *) config.password); - DEBUG_WM(F("ssid_len: "),config.ssid_len); - DEBUG_WM(F("channel: "),config.channel); - DEBUG_WM(F("authmode: "),config.authmode); - DEBUG_WM(F("ssid_hidden: "),config.ssid_hidden); - DEBUG_WM(F("max_connection: "),config.max_connection); - #endif - #if !defined(WM_NOCOUNTRY) - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("country: "),(String)country.cc); - #endif - DEBUG_WM(F("beacon_interval: "),(String)config.beacon_interval + "(ms)"); - DEBUG_WM(FPSTR(D_HR)); - #endif -} - -/** - * [debugPlatformInfo description] - * @access public - * @return {[type]} [description] - */ -void WiFiManager::debugPlatformInfo(){ - #ifdef ESP8266 - system_print_meminfo(); - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("[SYS] getCoreVersion(): "),ESP.getCoreVersion()); - DEBUG_WM(F("[SYS] system_get_sdk_version(): "),system_get_sdk_version()); - DEBUG_WM(F("[SYS] system_get_boot_version():"),system_get_boot_version()); - DEBUG_WM(F("[SYS] getFreeHeap(): "),(String)ESP.getFreeHeap()); - #endif - #elif defined(ESP32) - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("[SYS] WM version: "), String((__FlashStringHelper *)WM_VERSION_STR) +" D:"+String(_debugLevel)); - DEBUG_WM(F("[SYS] Arduino version: "), VER_ARDUINO_STR); - DEBUG_WM(F("[SYS] ESP SDK version: "), ESP.getSdkVersion()); - DEBUG_WM(F("[SYS] Free heap: "), ESP.getFreeHeap()); - #endif - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("[SYS] Chip ID:"),WIFI_getChipId()); - DEBUG_WM(F("[SYS] Chip Model:"), ESP.getChipModel()); - DEBUG_WM(F("[SYS] Chip Cores:"), ESP.getChipCores()); - DEBUG_WM(F("[SYS] Chip Rev:"), ESP.getChipRevision()); - #endif - #endif -} - -int WiFiManager::getRSSIasQuality(int RSSI) { - int quality = 0; - - if (RSSI <= -100) { - quality = 0; - } else if (RSSI >= -50) { - quality = 100; - } else { - quality = 2 * (RSSI + 100); - } - return quality; -} - -/** Is this an IP? */ -boolean WiFiManager::isIp(String str) { - for (size_t i = 0; i < str.length(); i++) { - int c = str.charAt(i); - if (c != '.' && (c < '0' || c > '9')) { - return false; - } - } - return true; -} - -/** IP to String? */ -String WiFiManager::toStringIp(IPAddress ip) { - String res = ""; - for (int i = 0; i < 3; i++) { - res += String((ip >> (8 * i)) & 0xFF) + "."; - } - res += String(((ip >> 8 * 3)) & 0xFF); - return res; -} - -boolean WiFiManager::validApPassword(){ - // check that ap password is valid, return false - if (_apPassword == NULL) _apPassword = ""; - if (_apPassword != "") { - if (_apPassword.length() < 8 || _apPassword.length() > 63) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(F("AccessPoint set password is INVALID or <8 chars")); - #endif - _apPassword = ""; - return false; // @todo FATAL or fallback to empty , currently fatal, fail secure. - } - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("AccessPoint set password is VALID")); - DEBUG_WM(WM_DEBUG_DEV,"ap pass",_apPassword); - #endif - } - return true; -} - -/** - * encode htmlentities - * @since $dev - * @param string str string to replace entities - * @return string encoded string - */ -String WiFiManager::htmlEntities(String str, bool whitespace) { - str.replace("&","&"); - str.replace("<","<"); - str.replace(">",">"); - str.replace("'","'"); - if(whitespace) str.replace(" "," "); - // str.replace("-","–"); - // str.replace("\"","""); - // str.replace("/": "/"); - // str.replace("`": "`"); - // str.replace("=": "="); -return str; -} - -/** - * [getWLStatusString description] - * @access public - * @param {[type]} uint8_t status [description] - * @return {[type]} [description] - */ -String WiFiManager::getWLStatusString(uint8_t status){ - if(status <= 7) return WIFI_STA_STATUS[status]; - return FPSTR(S_NA); -} - -String WiFiManager::getWLStatusString(){ - uint8_t status = WiFi.status(); - if(status <= 7) return WIFI_STA_STATUS[status]; - return FPSTR(S_NA); -} - -String WiFiManager::encryptionTypeStr(uint8_t authmode) { -#ifdef WM_DEBUG_LEVEL - // DEBUG_WM("enc_tye: ",authmode); - #endif - return AUTH_MODE_NAMES[authmode]; -} - -String WiFiManager::getModeString(uint8_t mode){ - if(mode <= 3) return WIFI_MODES[mode]; - return FPSTR(S_NA); -} - -bool WiFiManager::WiFiSetCountry(){ - if(_wificountry == "") return false; // skip not set - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFiSetCountry to"),_wificountry); - #endif - -/* - * @return - * - ESP_OK: succeed - * - ESP_ERR_WIFI_NOT_INIT: WiFi is not initialized by eps_wifi_init - * - ESP_ERR_WIFI_IF: invalid interface - * - ESP_ERR_WIFI_ARG: invalid argument - * - others: refer to error codes in esp_err.h - */ - - // @todo move these definitions, and out of cpp `esp_wifi_set_country(&WM_COUNTRY_US)` - bool ret = true; - // ret = esp_wifi_set_bandwidth(WIFI_IF_AP,WIFI_BW_HT20); // WIFI_BW_HT40 - #ifdef ESP32 - esp_err_t err = ESP_OK; - // @todo check if wifi is init, no idea how, doesnt seem to be exposed atm ( check again it might be now! ) - if(WiFi.getMode() == WIFI_MODE_NULL){ - DEBUG_WM(WM_DEBUG_ERROR,"[ERROR] cannot set country, wifi not init"); - } // exception if wifi not init! - // Assumes that _wificountry is set to one of the supported country codes : "01"(world safe mode) "AT","AU","BE","BG","BR", - // "CA","CH","CN","CY","CZ","DE","DK","EE","ES","FI","FR","GB","GR","HK","HR","HU", - // "IE","IN","IS","IT","JP","KR","LI","LT","LU","LV","MT","MX","NL","NO","NZ","PL","PT", - // "RO","SE","SI","SK","TW","US" - // If an invalid country code is passed, ESP_ERR_WIFI_ARG will be returned - // This also uses 802.11d mode, which matches the STA to the country code of the AP it connects to (meaning - // that the country code will be overridden if connecting to a "foreign" AP) - else { - #ifndef WM_NOCOUNTRY - err = esp_wifi_set_country_code(_wificountry.c_str(), true); - #else - DEBUG_WM(WM_DEBUG_ERROR,"[ERROR] esp wifi set country is not available"); - err = true; - #endif - } - #ifdef WM_DEBUG_LEVEL - if(err){ - if(err == ESP_ERR_WIFI_NOT_INIT) DEBUG_WM(WM_DEBUG_ERROR,"[ERROR] ESP_ERR_WIFI_NOT_INIT"); - else if(err == ESP_ERR_INVALID_ARG) DEBUG_WM(WM_DEBUG_ERROR,"[ERROR] ESP_ERR_WIFI_ARG (invalid country code)"); - else if(err != ESP_OK)DEBUG_WM(WM_DEBUG_ERROR,"[ERROR] unknown error",(String)err); - } - #endif - ret = err == ESP_OK; - - #elif defined(ESP8266) && !defined(WM_NOCOUNTRY) - // if(WiFi.getMode() == WIFI_OFF); // exception if wifi not init! - if(_wificountry == "US") ret = wifi_set_country((wifi_country_t*)&WM_COUNTRY_US); - else if(_wificountry == "JP") ret = wifi_set_country((wifi_country_t*)&WM_COUNTRY_JP); - else if(_wificountry == "CN") ret = wifi_set_country((wifi_country_t*)&WM_COUNTRY_CN); - #ifdef WM_DEBUG_LEVEL - else DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] country code not found")); - #endif - #endif - - #ifdef WM_DEBUG_LEVEL - if(ret) DEBUG_WM(WM_DEBUG_VERBOSE,F("[OK] esp_wifi_set_country: "),_wificountry); - else DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] esp_wifi_set_country failed")); - #endif - return ret; -} - -// set mode ignores WiFi.persistent -bool WiFiManager::WiFi_Mode(WiFiMode_t m,bool persistent) { - bool ret; - #ifdef ESP8266 - if((wifi_get_opmode() == (uint8) m ) && !persistent) { - return true; - } - ETS_UART_INTR_DISABLE(); - if(persistent) ret = wifi_set_opmode(m); - else ret = wifi_set_opmode_current(m); - ETS_UART_INTR_ENABLE(); - return ret; - #elif defined(ESP32) - if(persistent && esp32persistent) WiFi.persistent(true); - ret = WiFi.mode(m); // @todo persistent check persistant mode, was eventually added to esp lib, but have to add version checking probably - if(persistent && esp32persistent) WiFi.persistent(false); - return ret; - #endif -} -bool WiFiManager::WiFi_Mode(WiFiMode_t m) { - return WiFi_Mode(m,false); -} - -// sta disconnect without persistent -bool WiFiManager::WiFi_Disconnect() { - #ifdef ESP8266 - if((WiFi.getMode() & WIFI_STA) != 0) { - bool ret; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("WiFi station disconnect")); - #endif - ETS_UART_INTR_DISABLE(); // @todo possibly not needed - ret = wifi_station_disconnect(); - ETS_UART_INTR_ENABLE(); - return ret; - } - #elif defined(ESP32) - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("WiFi station disconnect")); - #endif - return WiFi.disconnect(); // not persistent atm - #endif - return false; -} - -// toggle STA without persistent -bool WiFiManager::WiFi_enableSTA(bool enable,bool persistent) { -#ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("WiFi_enableSTA"),(String) enable? "enable" : "disable"); - #endif - #ifdef ESP8266 - WiFiMode_t newMode; - WiFiMode_t currentMode = WiFi.getMode(); - bool isEnabled = (currentMode & WIFI_STA) != 0; - if(enable) newMode = (WiFiMode_t)(currentMode | WIFI_STA); - else newMode = (WiFiMode_t)(currentMode & (~WIFI_STA)); - - if((isEnabled != enable) || persistent) { - if(enable) { - #ifdef WM_DEBUG_LEVEL - if(persistent) DEBUG_WM(WM_DEBUG_DEV,F("enableSTA PERSISTENT ON")); - #endif - return WiFi_Mode(newMode,persistent); - } - else { - return WiFi_Mode(newMode,persistent); - } - } else { - return true; - } - #elif defined(ESP32) - bool ret; - if(persistent && esp32persistent) WiFi.persistent(true); - ret = WiFi.enableSTA(enable); // @todo handle persistent when it is implemented in platform - if(persistent && esp32persistent) WiFi.persistent(false); - return ret; - #endif -} - -bool WiFiManager::WiFi_enableSTA(bool enable) { - return WiFi_enableSTA(enable,false); -} - -bool WiFiManager::WiFi_eraseConfig() { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_DEV,F("WiFi_eraseConfig")); - #endif - - #ifdef ESP8266 - #ifndef WM_FIXERASECONFIG - return ESP.eraseConfig(); - #else - // erase config BUG replacement - // https://github.com/esp8266/Arduino/pull/3635 - const size_t cfgSize = 0x4000; - size_t cfgAddr = ESP.getFlashChipSize() - cfgSize; - - for (size_t offset = 0; offset < cfgSize; offset += SPI_FLASH_SEC_SIZE) { - if (!ESP.flashEraseSector((cfgAddr + offset) / SPI_FLASH_SEC_SIZE)) { - return false; - } - } - return true; - #endif - #elif defined(ESP32) - - bool ret; - WiFi.mode(WIFI_AP_STA); // cannot erase if not in STA mode ! - WiFi.persistent(true); - ret = WiFi.disconnect(true,true); // disconnect(bool wifioff, bool eraseap) - delay(500); - WiFi.persistent(false); - return ret; - #endif -} - -uint8_t WiFiManager::WiFi_softap_num_stations(){ - #ifdef ESP8266 - return wifi_softap_get_station_num(); - #elif defined(ESP32) - return WiFi.softAPgetStationNum(); - #endif -} - -bool WiFiManager::WiFi_hasAutoConnect(){ - return WiFi_SSID(true) != ""; -} - -String WiFiManager::WiFi_SSID(bool persistent) const{ - - #ifdef ESP8266 - struct station_config conf; - if(persistent) wifi_station_get_config_default(&conf); - else wifi_station_get_config(&conf); - - char tmp[33]; //ssid can be up to 32chars, => plus null term - memcpy(tmp, conf.ssid, sizeof(conf.ssid)); - tmp[32] = 0; //nullterm in case of 32 char ssid - return String(reinterpret_cast(tmp)); - - #elif defined(ESP32) - // bool res = WiFi.wifiLowLevelInit(true); // @todo fix for S3, not found - // wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - if(persistent){ - wifi_config_t conf; - esp_wifi_get_config(WIFI_IF_STA, &conf); - return String(reinterpret_cast(conf.sta.ssid)); - } - else { - if(WiFiGenericClass::getMode() == WIFI_MODE_NULL){ - return String(); - } - wifi_ap_record_t info; - if(!esp_wifi_sta_get_ap_info(&info)) { - return String(reinterpret_cast(info.ssid)); - } - return String(); - } - #endif -} - -String WiFiManager::WiFi_psk(bool persistent) const { - #ifdef ESP8266 - struct station_config conf; - - if(persistent) wifi_station_get_config_default(&conf); - else wifi_station_get_config(&conf); - - char tmp[65]; //psk is 64 bytes hex => plus null term - memcpy(tmp, conf.password, sizeof(conf.password)); - tmp[64] = 0; //null term in case of 64 byte psk - return String(reinterpret_cast(tmp)); - - #elif defined(ESP32) - // only if wifi is init - if(WiFiGenericClass::getMode() == WIFI_MODE_NULL){ - return String(); - } - wifi_config_t conf; - esp_wifi_get_config(WIFI_IF_STA, &conf); - return String(reinterpret_cast(conf.sta.password)); - #endif -} - -#ifdef ESP32 - #ifdef WM_ARDUINOEVENTS - void WiFiManager::WiFiEvent(WiFiEvent_t event,arduino_event_info_t info){ - #else - void WiFiManager::WiFiEvent(WiFiEvent_t event,system_event_info_t info){ - #define wifi_sta_disconnected disconnected - #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED SYSTEM_EVENT_STA_DISCONNECTED - #define ARDUINO_EVENT_WIFI_SCAN_DONE SYSTEM_EVENT_SCAN_DONE - #endif - if(!_hasBegun){ - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_VERBOSE,"[ERROR] WiFiEvent, not ready"); - #endif - // Serial.println(F("\n[EVENT] WiFiEvent logging (wm debug not available)")); - // Serial.print(F("[EVENT] ID: ")); - // Serial.println(event); - return; - } - #ifdef WM_DEBUG_LEVEL - // DEBUG_WM(WM_DEBUG_VERBOSE,"[EVENT]",event); - #endif - if(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("[EVENT] WIFI_REASON: "),info.wifi_sta_disconnected.reason); - #endif - if(info.wifi_sta_disconnected.reason == WIFI_REASON_AUTH_EXPIRE || info.wifi_sta_disconnected.reason == WIFI_REASON_AUTH_FAIL){ - _lastconxresulttmp = 7; // hack in wrong password internally, sdk emit WIFI_REASON_AUTH_EXPIRE on some routers on auth_fail - } else _lastconxresulttmp = WiFi.status(); - #ifdef WM_DEBUG_LEVEL - if(info.wifi_sta_disconnected.reason == WIFI_REASON_NO_AP_FOUND) DEBUG_WM(WM_DEBUG_VERBOSE,F("[EVENT] WIFI_REASON: NO_AP_FOUND")); - if(info.wifi_sta_disconnected.reason == WIFI_REASON_ASSOC_FAIL){ - if(_aggresiveReconn && _connectRetries<4) _connectRetries=4; - DEBUG_WM(WM_DEBUG_VERBOSE,F("[EVENT] WIFI_REASON: AUTH FAIL")); - } - #endif - #ifdef esp32autoreconnect - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("[Event] SYSTEM_EVENT_STA_DISCONNECTED, reconnecting")); - #endif - //WiFi.reconnect(); - #endif - } - else if(event == ARDUINO_EVENT_WIFI_SCAN_DONE && _asyncScan){ - uint16_t scans = WiFi.scanComplete(); - WiFi_scanComplete(scans); - } -} -#endif - -void WiFiManager::WiFi_autoReconnect(){ - #ifdef ESP8266 - WiFi.setAutoReconnect(_wifiAutoReconnect); - #elif defined(ESP32) - // if(_wifiAutoReconnect){ - // @todo move to seperate method, used for event listener now - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("ESP32 event handler enabled")); - #endif - using namespace std::placeholders; - if(wm_event_id == 0) wm_event_id = WiFi.onEvent(std::bind(&WiFiManager::WiFiEvent,this,_1,_2)); - // } - #endif -} - -// Called when /update is requested -esp_err_t WiFiManager::handleUpdate(PsychicRequest *request) { - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("<- Handle update")); - #endif - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - if (captivePortal(request)) return 0; // If captive portal redirect instead of displaying the page - String page = getHTTPHead(_title); // @token options - String str = FPSTR(HTTP_ROOT_MAIN); - str.replace(FPSTR(T_t), _title); - str.replace(FPSTR(T_v), configPortalActive ? _apName : (getWiFiHostname() + " - " + WiFi.localIP().toString())); // use ip if ap is not active for heading - page += str; - - page += FPSTR(HTTP_UPDATE); - page += FPSTR(HTTP_END); - - return HTTPSend(request,page); - -} - -// upload via /u POST -void WiFiManager::handleUpdating(String filename, size_t index, uint8_t *data, size_t len, bool final){ - // @todo - // cannot upload files in captive portal, file select is not allowed, show message with link or hide - // cannot upload if softreset after upload, maybe check for hard reset at least for dev, ERROR[11]: Invalid bootstrapping state, reset ESP8266 before updating - // add upload status to webpage somehow - // abort upload if error detected ? - // [x] supress cp timeout on upload, so it doesnt keep uploading? - // add progress handler for debugging - // combine route handlers into one callback and use argument or post checking instead of mutiple functions maybe, if POST process else server upload page? - // [x] add upload checking, do we need too check file? - // convert output to debugger if not moving to example - - // if (captivePortal()) return; // If captive portal redirect instead of displaying the page - bool error = false; - unsigned long _configPortalTimeoutSAV = _configPortalTimeout; // store cp timeout - _configPortalTimeout = 0; // disable timeout - bool otadebug = false; - - // UPLOAD START - if (!index) { - // if(_debug) Serial.setDebugOutput(true); - uint32_t maxSketchSpace; - - // Use new callback for before OTA update - if (_preotaupdatecallback != NULL) { - _preotaupdatecallback(); // @CALLBACK - } - #ifdef ESP8266 - WiFiUDP::stopAll(); - maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; - #elif defined(ESP32) - // Think we do not need to stop WiFIUDP because we haven't started a listener - // maxSketchSpace = (ESP.getFlashChipSize() - 0x1000) & 0xFFFFF000; - // #define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF // include update.h - maxSketchSpace = UPDATE_SIZE_UNKNOWN; - #endif - - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,"[OTA] Update file: ", filename.c_str()); - #endif - - // Update.onProgress(THandlerFunction_Progress fn); - // Update.onProgress([](unsigned int progress, unsigned int total) { - // Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - // }); - - if (!Update.begin(maxSketchSpace)) { // start with max available size - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] OTA Update ERROR"), Update.getError()); - #endif - error = true; - Update.end(); // Not sure the best way to abort, I think client will keep sending.. - } - #ifdef ESP8266 - Update.runAsync(true); // tell the updaterClass to run in async mode - #endif - } - // UPLOAD WRITE - if (index 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - String page = getHTTPHead(FPSTR(S_options)); // @token options - String str = FPSTR(HTTP_ROOT_MAIN); - str.replace(FPSTR(T_t),_title); - str.replace(FPSTR(T_v), configPortalActive ? _apName : WiFi.localIP().toString()); // use ip if ap is not active for heading - page += str; - - if (Update.hasError()) { - page += FPSTR(HTTP_UPDATE_FAIL); - #ifdef ESP32 - page += "OTA Error: " + (String)Update.errorString(); - #else - page += "OTA Error: " + (String)Update.getError(); - #endif - DEBUG_WM(F("[OTA] update failed")); - } - else { - page += FPSTR(HTTP_UPDATE_SUCCESS); - DEBUG_WM(F("[OTA] update ok")); - } - page += FPSTR(HTTP_END); - - // delay(1000); // send page - if (!Update.hasError()) { - //ESP.restart(); - _rebootNeeded = true; - } - - return HTTPSend(request,page); -} - -#endif diff --git a/lib/WiFiManager/WiFiManager.h b/lib/WiFiManager/WiFiManager.h deleted file mode 100644 index c48ec7f..0000000 --- a/lib/WiFiManager/WiFiManager.h +++ /dev/null @@ -1,870 +0,0 @@ -/** - * WiFiManager.h - * - * WiFiManager, a library for the ESP8266/Arduino platform - * for configuration of WiFi credentials using a Captive Portal - * - * @author Creator tzapu - * @author tablatronix - * @version 0.0.0 - * @license MIT - */ - - -#ifndef WiFiManager_h -#define WiFiManager_h - -#if defined(ESP8266) || defined(ESP32) - -#ifdef ESP8266 -#include -#endif - -#include - -// #define WM_MDNS // includes MDNS, also set MDNS with sethostname -// #define WM_FIXERASECONFIG // use erase flash fix -// #define WM_ERASE_NVS // esp32 erase(true) will erase NVS -// #define WM_RTC // esp32 info page will include reset reasons - -// #define WM_JSTEST // build flag for enabling js xhr tests -// #define WIFI_MANAGER_OVERRIDE_STRINGS // build flag for using own strings include - -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -#warning "ARDUINO_ESP8266_RELEASE_2_3_0, some WM features disabled" -// @todo check failing on platform = espressif8266@1.7.3 -#define WM_NOASYNC // esp8266 no async scan wifi -#define WM_NOCOUNTRY // esp8266 no country -#define WM_NOAUTH // no httpauth -#define WM_NOSOFTAPSSID // no softapssid() @todo shim -#endif - -// #ifdef CONFIG_IDF_TARGET_ESP32S2 -// #warning ESP32S2 -// #endif - -// #ifdef CONFIG_IDF_TARGET_ESP32C3 -// #warning ESP32C3 -// #endif - -// #ifdef CONFIG_IDF_TARGET_ESP32S3 -// #warning ESP32S3 -// #endif - -// #if defined(ARDUINO_ESP32S3_DEV) || defined(CONFIG_IDF_TARGET_ESP32S3) -// #warning "WM_NOTEMP" -// #define WM_NOTEMP // disabled temp sensor, have to determine which chip we are on -// #endif - -// #include "soc/efuse_reg.h" // include to add efuse chip rev to info, getChipRevision() is almost always the same though, so not sure why it matters. - -// #define esp32autoreconnect // implement esp32 autoreconnect event listener kludge, @DEPRECATED -// autoreconnect is WORKING https://github.com/espressif/arduino-esp32/issues/653#issuecomment-405604766 - -#define WM_WEBSERVERSHIM // use webserver shim lib -#define WM_ASYNCWEBSERVER // use async webserver - -#define WM_G(string_literal) (String(FPSTR(string_literal)).c_str()) - -#ifdef ESP8266 - - extern "C" { - #include "user_interface.h" - } - #include - - #ifdef WM_ASYNCWEBSERVER - #include - #include - #else - #include - #endif - - #ifdef WM_MDNS - #include - #endif - - #define WIFI_getChipId() ESP.getChipId() - #define WM_WIFIOPEN ENC_TYPE_NONE - -#elif defined(ESP32) - - #include - #include - #include - - #define WIFI_getChipId() (uint32_t)ESP.getEfuseMac() - #define WM_WIFIOPEN WIFI_AUTH_OPEN - - #ifdef WM_ASYNCWEBSERVER - #include - #include - #else - #ifndef WEBSERVER_H - #ifdef WM_WEBSERVERSHIM - #include - #else - #include - // Forthcoming official ? probably never happening - // https://github.com/esp8266/ESPWebServer - #endif - #endif - #endif - - #ifdef WM_ERASE_NVS - #include - #include - #endif - - #ifdef WM_MDNS - #include - #endif - - #ifdef WM_RTC - #ifdef ESP_IDF_VERSION_MAJOR // IDF 4+ - #if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4 - #include "esp32/rom/rtc.h" - #elif CONFIG_IDF_TARGET_ESP32S2 - #include "esp32s2/rom/rtc.h" - #elif CONFIG_IDF_TARGET_ESP32C3 - #include "esp32c3/rom/rtc.h" - #elif CONFIG_IDF_TARGET_ESP32S3 - #include "esp32s3/rom/rtc.h" - #else - #error Target CONFIG_IDF_TARGET is not supported - #endif - #else // ESP32 Before IDF 4.0 - #include "rom/rtc.h" - #endif - #endif - -#else -#endif - -#include -#include - - -// Include wm strings vars -// Pass in strings env override via WM_STRINGS_FILE -#ifndef WM_STRINGS_FILE -#define WM_STRINGS_FILE "wm_strings_en.h" // this includes constants as dependency -#endif -#include WM_STRINGS_FILE - -// prep string concat vars -#define WM_STRING2(x) #x -#define WM_STRING(x) WM_STRING2(x) - -// #include -#ifdef ESP_IDF_VERSION - // #pragma message "ESP_IDF_VERSION_MAJOR = " WM_STRING(ESP_IDF_VERSION_MAJOR) - // #pragma message "ESP_IDF_VERSION_MINOR = " WM_STRING(ESP_IDF_VERSION_MINOR) - // #pragma message "ESP_IDF_VERSION_PATCH = " WM_STRING(ESP_IDF_VERSION_PATCH) - #define VER_IDF_STR WM_STRING(ESP_IDF_VERSION_MAJOR) "." WM_STRING(ESP_IDF_VERSION_MINOR) "." WM_STRING(ESP_IDF_VERSION_PATCH) -#else - #define VER_IDF_STR "Unknown" -#endif - -#ifdef Arduino_h - #ifdef ESP32 - // #include "esp_arduino_version.h" // esp32 arduino > 2.x - #endif - // esp_get_idf_version - #ifdef ESP_ARDUINO_VERSION - // #pragma message "ESP_ARDUINO_VERSION_MAJOR = " WM_STRING(ESP_ARDUINO_VERSION_MAJOR) - // #pragma message "ESP_ARDUINO_VERSION_MINOR = " WM_STRING(ESP_ARDUINO_VERSION_MINOR) - // #pragma message "ESP_ARDUINO_VERSION_PATCH = " WM_STRING(ESP_ARDUINO_VERSION_PATCH) - #ifdef ESP_ARDUINO_VERSION_MAJOR - #define VER_ARDUINO_STR WM_STRING(ESP_ARDUINO_VERSION_MAJOR) "." WM_STRING(ESP_ARDUINO_VERSION_MINOR) "." WM_STRING(ESP_ARDUINO_VERSION_PATCH) - #else - #define VER_ARDUINO_STR "Unknown" - #endif - #else - #include - // #pragma message "ESP_ARDUINO_VERSION_GIT = " WM_STRING(ARDUINO_ESP32_GIT_VER)// 0x46d5afb1 - // #pragma message "ESP_ARDUINO_VERSION_DESC = " WM_STRING(ARDUINO_ESP32_GIT_DESC) // 1.0.6 - // #pragma message "ESP_ARDUINO_VERSION_REL = " WM_STRING(ARDUINO_ESP32_RELEASE) //"1_0_6" - #ifdef ESP_ARDUINO_VERSION_MAJOR - #define VER_ARDUINO_STR WM_STRING(ESP_ARDUINO_VERSION_MAJOR) "." WM_STRING(ESP_ARDUINO_VERSION_MINOR) "." WM_STRING(ESP_ARDUINO_VERSION_PATCH) - #else - #define VER_ARDUINO_STR "Unknown" - #endif - #endif -#else -#define VER_ARDUINO_STR "Unknown" -#endif - -// #pragma message "VER_IDF_STR = " WM_STRING(VER_IDF_STR) -// #pragma message "VER_ARDUINO_STR = " WM_STRING(VER_ARDUINO_STR) - -#ifndef WIFI_MANAGER_MAX_PARAMS - #define WIFI_MANAGER_MAX_PARAMS 5 // params will autoincrement and realloc by this amount when max is reached -#endif - -#define WFM_LABEL_BEFORE 1 -#define WFM_LABEL_AFTER 2 -#define WFM_NO_LABEL 0 -#define WFM_LABEL_DEFAULT 1 - -class WiFiManagerParameter { - public: - /** - Create custom parameters that can be added to the WiFiManager setup web page - @id is used for HTTP queries and must not contain spaces nor other special characters - */ - WiFiManagerParameter(); - WiFiManagerParameter(const char *custom); - WiFiManagerParameter(const char *id, const char *label); - WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length); - WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom); - WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement); - ~WiFiManagerParameter(); - // WiFiManagerParameter& operator=(const WiFiManagerParameter& rhs); - - const char *getID() const; - const char *getValue() const; - const char *getLabel() const; - const char *getPlaceholder() const; // @deprecated, use getLabel - int getValueLength() const; - int getLabelPlacement() const; - virtual const char *getCustomHTML() const; - void setValue(const char *defaultValue, int length); - - protected: - void init(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement); - - WiFiManagerParameter& operator=(const WiFiManagerParameter&); - const char *_id; - const char *_label; - char *_value; - int _length; - int _labelPlacement; - - const char *_customHTML; - friend class WiFiManager; -}; - - - // debugging - typedef enum { - WM_DEBUG_SILENT = 0, // debug OFF but still compiled for runtime - WM_DEBUG_ERROR = 1, // error only - WM_DEBUG_NOTIFY = 2, // default stable,INFO - WM_DEBUG_VERBOSE = 3, // move verbose info - WM_DEBUG_DEV = 4, // development useful debugging info - WM_DEBUG_MAX = 5 // MAX extra dev auditing, var dumps etc (MAX+1 will print timing,mem and frag info) - } wm_debuglevel_t; - -class WiFiManager -{ - public: - WiFiManager(Print& consolePort); - WiFiManager(const char* user, const char* password); - WiFiManager(); - ~WiFiManager(); - void WiFiManagerInit(); - - // auto connect to saved wifi, or custom, and start config portal on failures - boolean autoConnect(); - boolean autoConnect(char const *apName, char const *apPassword = NULL); - - //manually start the config portal, autoconnect does this automatically on connect failure - boolean startConfigPortal(); // auto generates apname - boolean startConfigPortal(char const *apName, char const *apPassword = NULL); - - //manually stop the config portal if started manually, stop immediatly if non blocking, flag abort if blocking - bool stopConfigPortal(); - - //manually start the web portal, autoconnect does this automatically on connect failure - void startWebPortal(); - - //manually stop the web portal if started manually - void stopWebPortal(); - - // Run webserver processing, if setConfigPortalBlocking(false) - boolean process(); - - // get the AP name of the config portal, so it can be used in the callback - String getConfigPortalSSID(); - int getRSSIasQuality(int RSSI); - - // erase wifi credentials - void resetSettings(); - - // reset wifi scan - void resetScan(); - - // reboot esp - void reboot(); - - // disconnect wifi, without persistent saving or erasing - bool disconnect(); - - // erase esp - bool erase(); - bool erase(bool opt); - - //adds a custom parameter, returns false on failure - bool addParameter(WiFiManagerParameter *p); - - //returns the list of Parameters - WiFiManagerParameter** getParameters(); - - // returns the Parameters Count - int getParametersCount(); - - // SET CALLBACKS - - //called after AP mode and config portal has started - void setAPCallback( std::function func ); - - //called after webserver has started - void setWebServerCallback( std::function func ); - - //called when settings reset have been triggered - void setConfigResetCallback( std::function func ); - - //called when wifi settings have been changed and connection was successful ( or setBreakAfterConfig(true) ) - void setSaveConfigCallback( std::function func ); - - //called when saving params-in-wifi or params before anything else happens (eg wifi) - void setPreSaveConfigCallback( std::function func ); - - //called when saving params before anything else happens - void setPreSaveParamsCallback( std::function func ); - - //called when saving either params-in-wifi or params page - void setSaveParamsCallback( std::function func ); - - //called just before doing OTA update - void setPreOtaUpdateCallback( std::function func ); - - //called when config portal is timeout - void setConfigPortalTimeoutCallback( std::function func ); - - //sets timeout before AP,webserver loop ends and exits even if there has been no setup. - //useful for devices that failed to connect at some point and got stuck in a webserver loop - //in seconds setConfigPortalTimeout is a new name for setTimeout, ! not used if setConfigPortalBlocking - void setConfigPortalTimeout(unsigned long seconds); - void setTimeout(unsigned long seconds); // @deprecated, alias - - //sets timeout for which to attempt connecting, useful if you get a lot of failed connects - void setConnectTimeout(unsigned long seconds); - - // sets number of retries for autoconnect, force retry after wait failure exit - void setConnectRetries(uint8_t numRetries); // default 1 - - //sets timeout for which to attempt connecting on saves, useful if there are bugs in esp waitforconnectloop - void setSaveConnectTimeout(unsigned long seconds); - - // lets you disable automatically connecting after save from webportal - void setSaveConnect(bool connect = true); - - // toggle debug output - void setDebugOutput(boolean debug); - void setDebugOutput(boolean debug, String prefix); // log line prefix, default "*wm:" - void setDebugOutput(boolean debug, wm_debuglevel_t level ); // log line prefix, default "*wm:" - - //set min quality percentage to include in scan, defaults to 8% if not specified - void setMinimumSignalQuality(int quality = 8); - - //sets a custom ip /gateway /subnet configuration - void setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn); - - //sets config for a static IP - void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn); - - //sets config for a static IP with DNS - void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn, IPAddress dns); - - //if this is set, it will exit after config, even if connection is unsuccessful. - void setBreakAfterConfig(boolean shouldBreak); - - // if this is set, portal will be blocking and wait until save or exit, - // is false user must manually `process()` to handle config portal, - // setConfigPortalTimeout is ignored in this mode, user is responsible for closing configportal - void setConfigPortalBlocking(boolean shouldBlock); - - //add custom html at inside for all pages - void setCustomHeadElement(const char* html); - - //if this is set, customise style - void setCustomMenuHTML(const char* html); - - //if this is true, remove duplicated Access Points - defaut true - void setRemoveDuplicateAPs(boolean removeDuplicates); - - //setter for ESP wifi.persistent so we can remember it and restore user preference, as WIFi._persistent is protected - void setRestorePersistent(boolean persistent); - - //if true, always show static net inputs, IP, subnet, gateway, else only show if set via setSTAStaticIPConfig - void setShowStaticFields(boolean alwaysShow); - - //if true, always show static dns, esle only show if set via setSTAStaticIPConfig - void setShowDnsFields(boolean alwaysShow); - - // toggle showing the saved wifi password in wifi form, could be a security issue. - void setShowPassword(boolean show); - - //if false, disable captive portal redirection - void setCaptivePortalEnable(boolean enabled); - - //if false, timeout captive portal even if a STA client connected to softAP (false), suggest disabling if captiveportal is open - void setAPClientCheck(boolean enabled); - - //if true, reset timeout when webclient connects (true), suggest disabling if captiveportal is open - void setWebPortalClientCheck(boolean enabled); - - // if true, enable autoreconnecting - void setWiFiAutoReconnect(boolean enabled); - - // if true, wifiscan will show percentage instead of quality icons, until we have better templating - void setScanDispPerc(boolean enabled); - - // if true (default) then start the config portal from autoConnect if connection failed - void setEnableConfigPortal(boolean enable); - - // if true (default) then stop the config portal from autoConnect when wifi is saved - void setDisableConfigPortal(boolean enable); - - // if true then find the AP with the best RSSI for the given SSID - void setFindBestRSSI(boolean enabled); - - // set a custom hostname, sets sta and ap dhcp client id for esp32, and sta for esp8266 - bool setHostname(const char * hostname); - bool setHostname(String hostname); - - // show erase wifi onfig button on info page, true - void setShowInfoErase(boolean enabled); - - // show OTA upload button on info page - void setShowInfoUpdate(boolean enabled); - - // set ap channel - void setWiFiAPChannel(int32_t channel); - - // set ap hidden - void setWiFiAPHidden(bool hidden); // default false - - // clean connect, always disconnect before connecting - void setCleanConnect(bool enable); // default false - - // set custom menu items and order, vector or arr - // see _menutokens for ids - void setMenu(std::vector& menu); - void setMenu(const char* menu[], uint8_t size); - - // set the webapp title, default WiFiManager - void setTitle(String title); - - // add params to its own menu page and remove from wifi, NOT TO BE COMBINED WITH setMenu! - void setParamsPage(bool enable); - - // get last connection result, includes autoconnect and wifisave - uint8_t getLastConxResult(); - - // get a status as string - String getWLStatusString(uint8_t status); - String getWLStatusString(); - - // get wifi mode as string - String getModeString(uint8_t mode); - - // check if the module has a saved ap to connect to - bool getWiFiIsSaved(); - - // helper to get saved password, if persistent get stored, else get current if connected - String getWiFiPass(bool persistent = true); - - // helper to get saved ssid, if persistent get stored, else get current if connected - String getWiFiSSID(bool persistent = true); - - // debug output the softap config - void debugSoftAPConfig(); - - // debug output platform info and versioning - void debugPlatformInfo(); - - // helper for html - String htmlEntities(String str, bool whitespace = false); - - // set the country code for wifi settings, CN - void setCountry(String cc); - - // set body class (invert), may be used for hacking in alt classes - void setClass(String str); - - // set dark mode via invert class - void setDarkMode(bool enable); - - // get default ap esp uses , esp_chipid etc - String getDefaultAPName(); - - // set port of webserver, 80 - void setHttpPort(uint16_t port); - - // check if config portal is active (true) - bool getConfigPortalActive(); - - // check if web portal is active (true) - bool getWebPortalActive(); - - // to preload autoconnect for test fixtures or other uses that skip esp sta config - bool preloadWiFi(String ssid, String pass); - - // get hostname helper - String getWiFiHostname(); - - - std::unique_ptr dnsServer; - PsychicHttpServer* server; - - protected: - // vars - std::vector _menuIds; - std::vector _menuIdsParams = {"wifi","param","info","exit"}; - std::vector _menuIdsUpdate = {"wifi","param","info","update","exit"}; - std::vector _menuIdsDefault = {"wifi","info","exit","sep","update"}; - - // ip configs @todo struct ? - IPAddress _ap_static_ip; - IPAddress _ap_static_gw; - IPAddress _ap_static_sn; - IPAddress _sta_static_ip; - IPAddress _sta_static_gw; - IPAddress _sta_static_sn; - IPAddress _sta_static_dns; - - unsigned long _configPortalStart = 0; // ms config portal start time (updated for timeouts) - unsigned long _webPortalAccessed = 0; // ms last web access time - uint8_t _lastconxresult = WL_IDLE_STATUS; // store last result when doing connect operations - int _numNetworks = 0; // init index for numnetworks wifiscans - unsigned long _lastscan = 0; // ms for timing wifi scans - unsigned long _startscan = 0; // ms for timing wifi scans - unsigned long _startconn = 0; // ms for timing wifi connects - - // defaults - const byte DNS_PORT = 53; - String _apName = "no-net"; - String _apPassword = ""; - String _ssid = ""; // var temp ssid - String _pass = ""; // var temp psk - String _defaultssid = ""; // preload ssid - String _defaultpass = ""; // preload pass - - // options flags - unsigned long _configPortalTimeout = 0; // ms close config portal loop if set (depending on _cp/webClientCheck options) - unsigned long _connectTimeout = 0; // ms stop trying to connect to ap if set - unsigned long _saveTimeout = 0; // ms stop trying to connect to ap on saves, in case bugs in esp waitforconnectresult - - WiFiMode_t _usermode = WIFI_STA; // Default user mode - String _wifissidprefix = FPSTR(S_ssidpre); // auto apname prefix prefix+chipid - int _cpclosedelay = 2000; // delay before wifisave, prevents captive portal from closing to fast. - bool _cleanConnect = false; // disconnect before connect in connectwifi, increases stability on connects - bool _connectonsave = true; // connect to wifi when saving creds - bool _disableSTA = false; // disable sta when starting ap, always - bool _disableSTAConn = true; // disable sta when starting ap, if sta is not connected ( stability ) - bool _channelSync = false; // use same wifi sta channel when starting ap - int32_t _apChannel = 0; // default channel to use for ap, 0 for auto - bool _apHidden = false; // store softap hidden value - uint16_t _httpPort = 80; // port for webserver - // uint8_t _retryCount = 0; // counter for retries, probably not needed if synchronous - uint8_t _connectRetries = 1; // number of sta connect retries, force reconnect, wait loop (connectimeout) does not always work and first disconnect bails - bool _aggresiveReconn = false; // use an agrressive reconnect strategy, WILL delay conxs - // on some conn failure modes will add delays and many retries to work around esp and ap bugs, ie, anti de-auth protections - // https://github.com/tzapu/WiFiManager/issues/1067 - bool _allowExit = true; // allow exit in nonblocking, else user exit/abort calls will be ignored including cptimeout - - #ifdef ESP32 - wifi_event_id_t wm_event_id = 0; - static uint8_t _lastconxresulttmp; // tmp var for esp32 callback - #endif - - #ifndef WL_STATION_WRONG_PASSWORD - uint8_t WL_STATION_WRONG_PASSWORD = 7; // @kludge define a WL status for wrong password - #endif - - // parameter options - int _minimumQuality = -1; // filter wifiscan ap by this rssi - int _staShowStaticFields = 0; // ternary 1=always show static ip fields, 0=only if set, -1=never(cannot change ips via web!) - int _staShowDns = 0; // ternary 1=always show dns, 0=only if set, -1=never(cannot change dns via web!) - boolean _removeDuplicateAPs = true; // remove dup aps from wifiscan - boolean _showPassword = false; // show or hide saved password on wifi form, might be a security issue! - boolean _shouldBreakAfterConfig = false; // stop configportal on save failure - boolean _configPortalIsBlocking = true; // configportal enters blocking loop - boolean _enableCaptivePortal = true; // enable captive portal redirection - boolean _userpersistent = true; // users preffered persistence to restore - boolean _wifiAutoReconnect = true; // there is no platform getter for this, we must assume its true and make it so - boolean _apClientCheck = false; // keep cp alive if ap have station - boolean _webClientCheck = true; // keep cp alive if web have client - boolean _scanDispOptions = false; // show percentage in scans not icons - boolean _paramsInWifi = true; // show custom parameters on wifi page - boolean _showInfoErase = true; // info page erase button - boolean _showInfoUpdate = true; // info page update button - boolean _showBack = false; // show back button - boolean _enableConfigPortal = true; // FOR autoconnect - start config portal if autoconnect failed - boolean _disableConfigPortal = true; // FOR autoconnect - stop config portal if cp wifi save - boolean _findBestRSSI = false; // find best rssi ap in wifiscan - String _hostname = ""; // hostname for esp8266 for dhcp, and or MDNS - - const char* _customHeadElement = ""; // store custom head element html from user isnide - const char* _customMenuHTML = ""; // store custom head element html from user inside <> - String _bodyClass = ""; // class to add to body - String _title = FPSTR(S_brand); // app title - default WiFiManager - - // internal options - - bool _rebootNeeded = false; // async reboot flag - - // wifiscan notes - // currently disabled due to issues with caching, sometimes first scan is empty esp32 wifi not init yet race, or portals hit server nonstop flood - // The following are background wifi scanning optimizations - // experimental to make scans faster, preload scans after starting cp, and visiting home page, so when you click wifi its already has your list - // ideally we would add async and xhr here but I am holding off on js requirements atm - // might be slightly buggy since captive portals hammer the home page, @todo workaround this somehow. - // cache time helps throttle this - // async enables asyncronous scans, so they do not block anything - // the refresh button bypasses cache - // no aps found is problematic as scans are always going to want to run, leading to page load delays - // - // These settings really only make sense with _preloadwifiscan true - // but not limited to, we could run continuous background scans on various page hits, or xhr hits - // which would be better coupled with asyncscan - // atm preload is only done on root hit and startcp - // - // preload scanning causes AP to delay showing for users, but also caches and lets the cp load faster once its open - // my scan takes 7-10 seconds -public: - boolean _preloadwifiscan = true; // preload wifiscan if true - unsigned int _scancachetime = 30000; // ms cache time for preload scans - boolean _asyncScan = true; // perform wifi network scan async - bool wifiConnectDefault(); -protected: - - boolean _autoforcerescan = false; // automatically force rescan if scan networks is 0, ignoring cache - - boolean _disableIpFields = false; // modify function of setShow_X_Fields(false), forces ip fields off instead of default show if set, eg. _staShowStaticFields=-1 - - String _wificountry = ""; // country code, @todo define in strings lang - - // wrapper functions for handling setting and unsetting persistent for now. - bool esp32persistent = false; - bool _hasBegun = false; // flag wm loaded,unloaded - void _begin(); - void _end(); - - void setupConfigPortal(); - bool shutdownConfigPortal(); - bool setupHostname(bool restart); - void setupHTTPServer(); - void teardownHTTPServer(); - -#ifdef NO_EXTRA_4K_HEAP - boolean _tryWPS = false; // try WPS on save failure, unsupported - void startWPS(); -#endif - - bool startAP(); - void setupDNSD(); - - uint8_t connectWifi(String ssid, String pass, bool connect = true); - bool setSTAConfig(); - bool wifiConnectNew(String ssid, String pass,bool connect = true); - - uint8_t waitForConnectResult(); - uint8_t waitForConnectResult(uint32_t timeout); - void updateConxResult(uint8_t status); -public: - bool WiFi_scanNetworks(bool force,bool async); -protected: - // webserver handlers - esp_err_t handleRoot(PsychicRequest *request); - esp_err_t handleWifi(PsychicRequest *request,bool scan); - esp_err_t handleWifiSave(PsychicRequest *request); - esp_err_t handleInfo(PsychicRequest *request); - esp_err_t handleReset(PsychicRequest *request); - esp_err_t handleNotFound(PsychicRequest *request); - esp_err_t handleExit(PsychicRequest *request); - esp_err_t handleClose(PsychicRequest *request); - // esp_err_t handleErase(PsychicRequest *request); - esp_err_t handleErase(PsychicRequest *request, bool opt); - esp_err_t handleParam(PsychicRequest *request); - esp_err_t handleWiFiStatus(PsychicRequest *request); - esp_err_t handleParamSave(PsychicRequest *request); - void doParamSave(PsychicRequest *request); - - void handleRequest(); - esp_err_t HTTPSend(PsychicRequest *request, String page); - - boolean captivePortal(PsychicRequest *request); - boolean configPortalHasTimeout(); - uint8_t processConfigPortal(); - void stopCaptivePortal(); - // OTA Update handler - esp_err_t handleUpdate(PsychicRequest *request); - void handleUpdating(String filename, size_t index, uint8_t *data, size_t len, bool final); - esp_err_t handleUpdateDone(PsychicRequest *request); - - // wifi platform abstractions - bool WiFi_Mode(WiFiMode_t m); - bool WiFi_Mode(WiFiMode_t m,bool persistent); - bool WiFi_Disconnect(); - bool WiFi_enableSTA(bool enable); - bool WiFi_enableSTA(bool enable,bool persistent); - bool WiFi_eraseConfig(); - uint8_t WiFi_softap_num_stations(); - bool WiFi_hasAutoConnect(); - void WiFi_autoReconnect(); - String WiFi_SSID(bool persistent = true) const; - String WiFi_psk(bool persistent = true) const; - bool WiFi_scanNetworks(); - bool WiFi_scanNetworks(unsigned int cachetime,bool async); - bool WiFi_scanNetworks(unsigned int cachetime); - void WiFi_scanComplete(int networksFound); - bool WiFiSetCountry(); - - #ifdef ESP32 - - // check for arduino or system event system, handle esp32 arduino v2 and IDF - #if defined(ESP_ARDUINO_VERSION) && defined(ESP_ARDUINO_VERSION_VAL) - - #define WM_ARDUINOVERCHECK ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) - #define WM_ARDUINOVERCHECK_204 ESP_ARDUINO_VERSION <= ESP_ARDUINO_VERSION_VAL(2, 0, 5) - - #ifdef WM_ARDUINOVERCHECK - #define WM_ARDUINOEVENTS - #else - #define WM_NOSOFTAPSSID - #define WM_NOCOUNTRY - #endif - - #ifdef WM_ARDUINOVERCHECK_204 - #define WM_DISCONWORKAROUND - #endif - - #else - #define WM_NOCOUNTRY - #endif - - #ifdef WM_NOCOUNTRY - #warning "ESP32 set country unavailable" - #endif - - - #ifdef WM_ARDUINOEVENTS - void WiFiEvent(WiFiEvent_t event, arduino_event_info_t info); - #else - void WiFiEvent(WiFiEvent_t event, system_event_info_t info); - #endif - #endif - - // output helpers - String getParamOut(); - String getIpForm(String id, String title, String value); - String getScanItemOut(); - String getStaticOut(); - String getHTTPHead(String title); - String getMenuOut(); - //helpers - boolean isIp(String str); - String toStringIp(IPAddress ip); - boolean validApPassword(); - String encryptionTypeStr(uint8_t authmode); - void reportStatus(String &page); - String getInfoData(String id); - - // flags - boolean connect = false; - boolean abort = false; - boolean reset = false; - boolean configPortalActive = false; - - - // these are state flags for portal mode, we are either in webportal mode(STA) or configportal mode(AP) - // these are mutually exclusive as STA+AP mode is not supported due to channel restrictions and stability - // if we decide to support this, these checks will need to be replaced with something client aware to check if client origin is ap or web - // These state checks are critical and used for internal function checks - boolean webPortalActive = false; - boolean portalTimeoutResult = false; - - boolean portalAbortResult = false; - boolean storeSTAmode = true; // option store persistent STA mode in connectwifi - int timer = 0; // timer for debug throttle for numclients, and portal timeout messages - - // WiFiManagerParameter - int _paramsCount = 0; - int _max_params; - WiFiManagerParameter** _params = NULL; - - boolean _debug = true; - String _debugPrefix = FPSTR(S_debugPrefix); - - wm_debuglevel_t debugLvlShow = WM_DEBUG_VERBOSE; // at which level start showing [n] level tags - - // build debuglevel support - // @todo use DEBUG_ESP_x? - - // Set default debug level - #ifndef WM_DEBUG_LEVEL - #define WM_DEBUG_LEVEL WM_DEBUG_NOTIFY - #endif - - // override debug level OFF - #ifdef WM_NODEBUG - #undef WM_DEBUG_LEVEL - #endif - - #ifdef WM_DEBUG_LEVEL - uint8_t _debugLevel = (uint8_t)WM_DEBUG_LEVEL; - #else - uint8_t _debugLevel = 0; // default debug level - #endif - - // @todo use DEBUG_ESP_PORT ? - #ifdef WM_DEBUG_PORT - Print& _debugPort = WM_DEBUG_PORT; - #else - Print& _debugPort = Serial; // debug output stream ref - #endif - - template - void DEBUG_WM(Generic text); - - template - void DEBUG_WM(wm_debuglevel_t level,Generic text); - template - void DEBUG_WM(Generic text,Genericb textb); - template - void DEBUG_WM(wm_debuglevel_t level, Generic text,Genericb textb); - - // callbacks - // @todo use cb list (vector) maybe event ids, allow no return value - std::function _apcallback; - std::function _webservercallback; - std::function _savewificallback; - std::function _presavewificallback; - std::function _presaveparamscallback; - std::function _saveparamscallback; - std::function _resetcallback; - std::function _preotaupdatecallback; - std::function _configportaltimeoutcallback; - - bool _hasCredentials = false; - char _credUser[31] = {0}; - char _credPassword[31] = {0}; - - template - auto optionalIPFromString(T *obj, const char *s) -> decltype( obj->fromString(s) ) { - return obj->fromString(s); - } - auto optionalIPFromString(...) -> bool { - // DEBUG_WM("NO fromString METHOD ON IPAddress, you need ESP8266 core 2.1.0 or newer for Custom IP configuration to work."); - return false; - } - -}; - -#endif - -#endif diff --git a/lib/WiFiManager/examples/Advanced/Advanced.ino b/lib/WiFiManager/examples/Advanced/Advanced.ino deleted file mode 100644 index 3834e56..0000000 --- a/lib/WiFiManager/examples/Advanced/Advanced.ino +++ /dev/null @@ -1,141 +0,0 @@ -/** - * WiFiManager advanced demo, contains advanced configurartion options - * Implements TRIGGEN_PIN button press, press for ondemand configportal, hold for 3 seconds for reset settings. - */ -#include // https://github.com/tzapu/WiFiManager - -#define TRIGGER_PIN 0 - -// wifimanager can run in a blocking mode or a non blocking mode -// Be sure to know how to process loops with no delay() if using non blocking -bool wm_nonblocking = false; // change to true to use non blocking - -WiFiManager wm; // global wm instance -WiFiManagerParameter custom_field; // global param ( for non blocking w params ) - -void setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - Serial.begin(115200); - Serial.setDebugOutput(true); - delay(3000); - Serial.println("\n Starting"); - - pinMode(TRIGGER_PIN, INPUT); - - // wm.resetSettings(); // wipe settings - - if(wm_nonblocking) wm.setConfigPortalBlocking(false); - - // add a custom input field - int customFieldLength = 40; - - - // new (&custom_field) WiFiManagerParameter("customfieldid", "Custom Field Label", "Custom Field Value", customFieldLength,"placeholder=\"Custom Field Placeholder\""); - - // test custom html input type(checkbox) - // new (&custom_field) WiFiManagerParameter("customfieldid", "Custom Field Label", "Custom Field Value", customFieldLength,"placeholder=\"Custom Field Placeholder\" type=\"checkbox\""); // custom html type - - // test custom html(radio) - const char* custom_radio_str = "
    One
    Two
    Three"; - new (&custom_field) WiFiManagerParameter(custom_radio_str); // custom html input - - wm.addParameter(&custom_field); - wm.setSaveParamsCallback(saveParamCallback); - - // custom menu via array or vector - // - // menu tokens, "wifi","wifinoscan","info","param","close","sep","erase","restart","exit" (sep is seperator) (if param is in menu, params will not show up in wifi page!) - // const char* menu[] = {"wifi","info","param","sep","restart","exit"}; - // wm.setMenu(menu,6); - std::vector menu = {"wifi","info","param","sep","restart","exit"}; - wm.setMenu(menu); - - // set dark theme - wm.setClass("invert"); - - - //set static ip - // wm.setSTAStaticIPConfig(IPAddress(10,0,1,99), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); // set static ip,gw,sn - // wm.setShowStaticFields(true); // force show static ip fields - // wm.setShowDnsFields(true); // force show dns field always - - // wm.setConnectTimeout(20); // how long to try to connect for before continuing - wm.setConfigPortalTimeout(30); // auto close configportal after n seconds - // wm.setCaptivePortalEnable(false); // disable captive portal redirection - // wm.setAPClientCheck(true); // avoid timeout if client connected to softap - - // wifi scan settings - // wm.setRemoveDuplicateAPs(false); // do not remove duplicate ap names (true) - // wm.setMinimumSignalQuality(20); // set min RSSI (percentage) to show in scans, null = 8% - // wm.setShowInfoErase(false); // do not show erase button on info page - // wm.setScanDispPerc(true); // show RSSI as percentage not graph icons - - // wm.setBreakAfterConfig(true); // always exit configportal even if wifi save fails - - bool res; - // res = wm.autoConnect(); // auto generated AP name from chipid - // res = wm.autoConnect("AutoConnectAP"); // anonymous ap - res = wm.autoConnect("AutoConnectAP","password"); // password protected ap - - if(!res) { - Serial.println("Failed to connect or hit timeout"); - // ESP.restart(); - } - else { - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - } -} - -void checkButton(){ - // check for button press - if ( digitalRead(TRIGGER_PIN) == LOW ) { - // poor mans debounce/press-hold, code not ideal for production - delay(50); - if( digitalRead(TRIGGER_PIN) == LOW ){ - Serial.println("Button Pressed"); - // still holding button for 3000 ms, reset settings, code not ideaa for production - delay(3000); // reset delay hold - if( digitalRead(TRIGGER_PIN) == LOW ){ - Serial.println("Button Held"); - Serial.println("Erasing Config, restarting"); - wm.resetSettings(); - ESP.restart(); - } - - // start portal w delay - Serial.println("Starting config portal"); - wm.setConfigPortalTimeout(120); - - if (!wm.startConfigPortal("OnDemandAP","password")) { - Serial.println("failed to connect or hit timeout"); - delay(3000); - // ESP.restart(); - } else { - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - } - } - } -} - - -String getParam(String name){ - //read parameter from server, for customhmtl input - String value; - if(wm.server->hasArg(name)) { - value = wm.server->arg(name); - } - return value; -} - -void saveParamCallback(){ - Serial.println("[CALLBACK] saveParamCallback fired"); - Serial.println("PARAM customfieldid = " + getParam("customfieldid")); -} - -void loop() { - if(wm_nonblocking) wm.process(); // avoid delays() in loop when non-blocking and other long running code - checkButton(); - // put your main code here, to run repeatedly: -} diff --git a/lib/WiFiManager/examples/Basic/Basic.ino b/lib/WiFiManager/examples/Basic/Basic.ino deleted file mode 100644 index bf1e263..0000000 --- a/lib/WiFiManager/examples/Basic/Basic.ino +++ /dev/null @@ -1,41 +0,0 @@ -#include // https://github.com/tzapu/WiFiManager - - -void setup() { - // WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - // it is a good practice to make sure your code sets wifi mode how you want it. - - // put your setup code here, to run once: - Serial.begin(115200); - - //WiFiManager, Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wm; - - // reset settings - wipe stored credentials for testing - // these are stored by the esp library - // wm.resetSettings(); - - // Automatically connect using saved credentials, - // if connection fails, it starts an access point with the specified name ( "AutoConnectAP"), - // if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect()) - // then goes into a blocking loop awaiting configuration and will return success result - - bool res; - // res = wm.autoConnect(); // auto generated AP name from chipid - // res = wm.autoConnect("AutoConnectAP"); // anonymous ap - res = wm.autoConnect("AutoConnectAP","password"); // password protected ap - - if(!res) { - Serial.println("Failed to connect"); - // ESP.restart(); - } - else { - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - } - -} - -void loop() { - // put your main code here, to run repeatedly: -} diff --git a/lib/WiFiManager/examples/NonBlocking/AutoConnectNonBlocking/AutoConnectNonBlocking.ino b/lib/WiFiManager/examples/NonBlocking/AutoConnectNonBlocking/AutoConnectNonBlocking.ino deleted file mode 100644 index ab52396..0000000 --- a/lib/WiFiManager/examples/NonBlocking/AutoConnectNonBlocking/AutoConnectNonBlocking.ino +++ /dev/null @@ -1,27 +0,0 @@ -#include // https://github.com/tzapu/WiFiManager -WiFiManager wm; - -void setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - // put your setup code here, to run once: - Serial.begin(115200); - - //reset settings - wipe credentials for testing - //wm.resetSettings(); - - wm.setConfigPortalBlocking(false); - wm.setConfigPortalTimeout(60); - //automatically connect using saved credentials if they exist - //If connection fails it starts an access point with the specified name - if(wm.autoConnect("AutoConnectAP")){ - Serial.println("connected...yeey :)"); - } - else { - Serial.println("Configportal running"); - } -} - -void loop() { - wm.process(); - // put your main code here, to run repeatedly: -} diff --git a/lib/WiFiManager/examples/NonBlocking/AutoConnectNonBlockingwParams/AutoConnectNonBlockingwParams.ino b/lib/WiFiManager/examples/NonBlocking/AutoConnectNonBlockingwParams/AutoConnectNonBlockingwParams.ino deleted file mode 100644 index 3af79f0..0000000 --- a/lib/WiFiManager/examples/NonBlocking/AutoConnectNonBlockingwParams/AutoConnectNonBlockingwParams.ino +++ /dev/null @@ -1,36 +0,0 @@ -#include // https://github.com/tzapu/WiFiManager -WiFiManager wm; -WiFiManagerParameter custom_mqtt_server("server", "mqtt server", "", 40); - -void setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - // put your setup code here, to run once: - Serial.begin(115200); - - //reset settings - wipe credentials for testing - //wm.resetSettings(); - wm.addParameter(&custom_mqtt_server); - wm.setConfigPortalBlocking(false); - wm.setSaveParamsCallback(saveParamsCallback); - - //automatically connect using saved credentials if they exist - //If connection fails it starts an access point with the specified name - if(wm.autoConnect("AutoConnectAP")){ - Serial.println("connected...yeey :)"); - } - else { - Serial.println("Configportal running"); - } -} - -void loop() { - wm.process(); - // put your main code here, to run repeatedly: -} - -void saveParamsCallback () { - Serial.println("Get Params:"); - Serial.print(custom_mqtt_server.getID()); - Serial.print(" : "); - Serial.println(custom_mqtt_server.getValue()); -} diff --git a/lib/WiFiManager/examples/NonBlocking/OnDemandNonBlocking/onDemandNonBlocking.ino b/lib/WiFiManager/examples/NonBlocking/OnDemandNonBlocking/onDemandNonBlocking.ino deleted file mode 100644 index 0bc3992..0000000 --- a/lib/WiFiManager/examples/NonBlocking/OnDemandNonBlocking/onDemandNonBlocking.ino +++ /dev/null @@ -1,85 +0,0 @@ -/** - * OnDemandNonBlocking.ino - * example of running the webportal or configportal manually and non blocking - * trigger pin will start a webportal for 120 seconds then turn it off. - * startAP = true will start both the configportal AP and webportal - */ -#include // https://github.com/tzapu/WiFiManager - -// include MDNS -#ifdef ESP8266 -#include -#elif defined(ESP32) -#include -#endif - -// select which pin will trigger the configuration portal when set to LOW -#define TRIGGER_PIN 0 - -WiFiManager wm; - -unsigned int timeout = 120; // seconds to run for -unsigned int startTime = millis(); -bool portalRunning = false; -bool startAP = false; // start AP and webserver if true, else start only webserver - -void setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - // put your setup code here, to run once - Serial.begin(115200); - Serial.setDebugOutput(true); - delay(1000); - Serial.println("\n Starting"); - - pinMode(TRIGGER_PIN, INPUT_PULLUP); - - // wm.resetSettings(); - wm.setHostname("MDNSEXAMPLE"); - // wm.setEnableConfigPortal(false); - // wm.setConfigPortalBlocking(false); - wm.autoConnect(); -} - -void loop() { - #ifdef ESP8266 - MDNS.update(); - #endif - doWiFiManager(); - // put your main code here, to run repeatedly: -} - -void doWiFiManager(){ - // is auto timeout portal running - if(portalRunning){ - wm.process(); // do processing - - // check for timeout - if((millis()-startTime) > (timeout*1000)){ - Serial.println("portaltimeout"); - portalRunning = false; - if(startAP){ - wm.stopConfigPortal(); - } - else{ - wm.stopWebPortal(); - } - } - } - - // is configuration portal requested? - if(digitalRead(TRIGGER_PIN) == LOW && (!portalRunning)) { - if(startAP){ - Serial.println("Button Pressed, Starting Config Portal"); - wm.setConfigPortalBlocking(false); - wm.startConfigPortal(); - } - else{ - Serial.println("Button Pressed, Starting Web Portal"); - wm.startWebPortal(); - } - portalRunning = true; - startTime = millis(); - } -} - - diff --git a/lib/WiFiManager/examples/Old_examples/AutoConnectWithFeedback/AutoConnectWithFeedback.ino b/lib/WiFiManager/examples/Old_examples/AutoConnectWithFeedback/AutoConnectWithFeedback.ino deleted file mode 100644 index d3c4ed8..0000000 --- a/lib/WiFiManager/examples/Old_examples/AutoConnectWithFeedback/AutoConnectWithFeedback.ino +++ /dev/null @@ -1,42 +0,0 @@ -#include // https://github.com/tzapu/WiFiManager - -void configModeCallback (WiFiManager *myWiFiManager) { - Serial.println("Entered config mode"); - Serial.println(WiFi.softAPIP()); - //if you used auto generated SSID, print it - Serial.println(myWiFiManager->getConfigPortalSSID()); -} - -void setup() { - // put your setup code here, to run once: - Serial.begin(115200); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - //reset settings - for testing - //wifiManager.resetSettings(); - - //set callback that gets called when connecting to previous WiFi fails, and enters Access Point mode - wifiManager.setAPCallback(configModeCallback); - - //fetches ssid and pass and tries to connect - //if it does not connect it starts an access point with the specified name - //here "AutoConnectAP" - //and goes into a blocking loop awaiting configuration - if(!wifiManager.autoConnect()) { - Serial.println("failed to connect and hit timeout"); - //reset and try again, or maybe put it to deep sleep - ESP.restart(); - delay(1000); - } - - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - -} - -void loop() { - // put your main code here, to run repeatedly: - -} diff --git a/lib/WiFiManager/examples/Old_examples/AutoConnectWithReset/AutoConnectWithReset.ino b/lib/WiFiManager/examples/Old_examples/AutoConnectWithReset/AutoConnectWithReset.ino deleted file mode 100644 index 53a0d13..0000000 --- a/lib/WiFiManager/examples/Old_examples/AutoConnectWithReset/AutoConnectWithReset.ino +++ /dev/null @@ -1,43 +0,0 @@ -#include // this needs to be first, or it all crashes and burns... -#include // https://github.com/tzapu/WiFiManager - -void setup() { - // put your setup code here, to run once: - Serial.begin(115200); - Serial.println(); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - - //exit after config instead of connecting - wifiManager.setBreakAfterConfig(true); - - //reset settings - for testing - //wifiManager.resetSettings(); - - - //tries to connect to last known settings - //if it does not connect it starts an access point with the specified name - //here "AutoConnectAP" with password "password" - //and goes into a blocking loop awaiting configuration - if (!wifiManager.autoConnect("AutoConnectAP", "password")) { - Serial.println("failed to connect, we should reset as see if it connects"); - delay(3000); - ESP.restart(); - delay(5000); - } - - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - - - Serial.println("local ip"); - Serial.println(WiFi.localIP()); -} - -void loop() { - // put your main code here, to run repeatedly: - - -} diff --git a/lib/WiFiManager/examples/Old_examples/AutoConnectWithStaticIP/AutoConnectWithStaticIP.ino b/lib/WiFiManager/examples/Old_examples/AutoConnectWithStaticIP/AutoConnectWithStaticIP.ino deleted file mode 100644 index 9f88e47..0000000 --- a/lib/WiFiManager/examples/Old_examples/AutoConnectWithStaticIP/AutoConnectWithStaticIP.ino +++ /dev/null @@ -1,71 +0,0 @@ -#include // this needs to be first, or it all crashes and burns... -#include // https://github.com/tzapu/WiFiManager - -/************************************************************************************** - * this example shows how to set a static IP configuration for the ESP - * although the IP shows in the config portal, the changes will revert - * to the IP set in the source file. - * if you want the ability to configure and persist the new IP configuration - * look at the FS examples, which save the config to file - *************************************************************************************/ - -//default custom static IP -//char static_ip[16] = "10.0.1.59"; -//char static_gw[16] = "10.0.1.1"; -//char static_sn[16] = "255.255.255.0"; - -void setup() { - // put your setup code here, to run once: - Serial.begin(115200); - Serial.println(); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - - //reset settings - for testing - //wifiManager.resetSettings(); - - //set static ip - //block1 should be used for ESP8266 core 2.1.0 or newer, otherwise use block2 - - //start-block1 - //IPAddress _ip,_gw,_sn; - //_ip.fromString(static_ip); - //_gw.fromString(static_gw); - //_sn.fromString(static_sn); - //end-block1 - - //start-block2 - IPAddress _ip = IPAddress(10, 0, 1, 78); - IPAddress _gw = IPAddress(10, 0, 1, 1); - IPAddress _sn = IPAddress(255, 255, 255, 0); - //end-block2 - - wifiManager.setSTAStaticIPConfig(_ip, _gw, _sn); - - - //tries to connect to last known settings - //if it does not connect it starts an access point with the specified name - //here "AutoConnectAP" with password "password" - //and goes into a blocking loop awaiting configuration - if (!wifiManager.autoConnect("AutoConnectAP", "password")) { - Serial.println("failed to connect, we should reset as see if it connects"); - delay(3000); - ESP.restart(); - delay(5000); - } - - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - - - Serial.println("local ip"); - Serial.println(WiFi.localIP()); -} - -void loop() { - // put your main code here, to run repeatedly: - - -} diff --git a/lib/WiFiManager/examples/Old_examples/AutoConnectWithTimeout/AutoConnectWithTimeout.ino b/lib/WiFiManager/examples/Old_examples/AutoConnectWithTimeout/AutoConnectWithTimeout.ino deleted file mode 100644 index 9df428d..0000000 --- a/lib/WiFiManager/examples/Old_examples/AutoConnectWithTimeout/AutoConnectWithTimeout.ino +++ /dev/null @@ -1,38 +0,0 @@ -#include // https://github.com/tzapu/WiFiManager - -void setup() { - // put your setup code here, to run once: - Serial.begin(115200); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - //reset settings - for testing - //wifiManager.resetSettings(); - - //sets timeout until configuration portal gets turned off - //useful to make it all retry or go to sleep - //in seconds - wifiManager.setConfigPortalTimeout(180); - - //fetches ssid and pass and tries to connect - //if it does not connect it starts an access point with the specified name - //here "AutoConnectAP" - //and goes into a blocking loop awaiting configuration - if(!wifiManager.autoConnect("AutoConnectAP")) { - Serial.println("failed to connect and hit timeout"); - delay(3000); - //reset and try again, or maybe put it to deep sleep - ESP.restart(); - delay(5000); - } - - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - -} - -void loop() { - // put your main code here, to run repeatedly: - -} diff --git a/lib/WiFiManager/examples/OnDemand/OnDemandConfigPortal/OnDemandConfigPortal.ino b/lib/WiFiManager/examples/OnDemand/OnDemandConfigPortal/OnDemandConfigPortal.ino deleted file mode 100644 index a45122a..0000000 --- a/lib/WiFiManager/examples/OnDemand/OnDemandConfigPortal/OnDemandConfigPortal.ino +++ /dev/null @@ -1,47 +0,0 @@ -/** - * OnDemandConfigPortal.ino - * example of running the configPortal AP manually, independantly from the captiveportal - * trigger pin will start a configPortal AP for 120 seconds then turn it off. - * - */ -#include // https://github.com/tzapu/WiFiManager - -// select which pin will trigger the configuration portal when set to LOW -#define TRIGGER_PIN 0 - -int timeout = 120; // seconds to run for - -void setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - // put your setup code here, to run once: - Serial.begin(115200); - Serial.println("\n Starting"); - pinMode(TRIGGER_PIN, INPUT_PULLUP); -} - -void loop() { - // is configuration portal requested? - if ( digitalRead(TRIGGER_PIN) == LOW) { - WiFiManager wm; - - //reset settings - for testing - //wm.resetSettings(); - - // set configportal timeout - wm.setConfigPortalTimeout(timeout); - - if (!wm.startConfigPortal("OnDemandAP")) { - Serial.println("failed to connect and hit timeout"); - delay(3000); - //reset and try again, or maybe put it to deep sleep - ESP.restart(); - delay(5000); - } - - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - - } - - // put your main code here, to run repeatedly: -} diff --git a/lib/WiFiManager/examples/OnDemand/OnDemandWebPortal/onDemandWebPortal.ino b/lib/WiFiManager/examples/OnDemand/OnDemandWebPortal/onDemandWebPortal.ino deleted file mode 100644 index 33fa384..0000000 --- a/lib/WiFiManager/examples/OnDemand/OnDemandWebPortal/onDemandWebPortal.ino +++ /dev/null @@ -1,51 +0,0 @@ -/** - * OnDemandWebPortal.ino - * example of running the webportal (always NON blocking) - */ -#include // https://github.com/tzapu/WiFiManager - -// select which pin will trigger the configuration portal when set to LOW -#define TRIGGER_PIN 0 - -WiFiManager wm; - -bool portalRunning = false; - -void setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - // put your setup code here, to run once - Serial.begin(115200); - Serial.println("\n Starting"); - pinMode(TRIGGER_PIN, INPUT_PULLUP); -} - -void loop() { - checkButton(); - // put your main code here, to run repeatedly: -} - -void checkButton(){ - // is auto timeout portal running - if(portalRunning){ - wm.process(); - } - - // is configuration portal requested? - if(digitalRead(TRIGGER_PIN) == LOW) { - delay(50); - if(digitalRead(TRIGGER_PIN) == LOW) { - if(!portalRunning){ - Serial.println("Button Pressed, Starting Portal"); - wm.startWebPortal(); - portalRunning = true; - } - else{ - Serial.println("Button Pressed, Stopping Portal"); - wm.stopWebPortal(); - portalRunning = false; - } - } - } -} - - diff --git a/lib/WiFiManager/examples/Parameters/LittleFS/LittleFSParameters.ino b/lib/WiFiManager/examples/Parameters/LittleFS/LittleFSParameters.ino deleted file mode 100644 index 188b3c1..0000000 --- a/lib/WiFiManager/examples/Parameters/LittleFS/LittleFSParameters.ino +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Basic example using LittleFS to store data - */ - -#include -#include -#include - -String readFile(fs::FS &fs, const char * path){ - Serial.printf("Reading file: %s\r\n", path); - File file = fs.open(path, "r"); - if(!file || file.isDirectory()){ - Serial.println("- empty file or failed to open file"); - return String(); - } - Serial.println("- read from file:"); - String fileContent; - while(file.available()){ - fileContent+=String((char)file.read()); - } - file.close(); - Serial.println(fileContent); - return fileContent; -} -void writeFile(fs::FS &fs, const char * path, const char * message){ - Serial.printf("Writing file: %s\r\n", path); - File file = fs.open(path, "w"); - if(!file){ - Serial.println("- failed to open file for writing"); - return; - } - if(file.print(message)){ - Serial.println("- file written"); - } else { - Serial.println("- write failed"); - } - file.close(); -} - -int data = 4; - -#include -#define TRIGGER_PIN 2 -int timeout = 120; // seconds to run for - -void setup() { -if (!LittleFS.begin()) { //to start littlefs -Serial.println("LittleFS mount failed"); -return; -} -data = readFile(LittleFS, "/data.txt").toInt(); -WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - // put your setup code here, to run once: - pinMode(TRIGGER_PIN, INPUT_PULLUP); - WiFiManager wm; - //wm.resetSettings(); - bool res; - res = wm.autoConnect("Setup"); - if(!res) { - Serial.println("Failed to connect"); - // ESP.restart(); - } - -} - -void loop() { -if ( digitalRead(TRIGGER_PIN) == LOW) { - WiFiManager wm; - //wm.resetSettings(); - wm.setConfigPortalTimeout(timeout); - if (!wm.startConfigPortal("Sharmander")) { - Serial.println("failed to connect and hit timeout"); - delay(3000); - ESP.restart(); - delay(5000); - } - Serial.println("connected...yeey :)"); -} -} \ No newline at end of file diff --git a/lib/WiFiManager/examples/Parameters/SPIFFS/AutoConnectWithFSParameters/AutoConnectWithFSParameters.ino b/lib/WiFiManager/examples/Parameters/SPIFFS/AutoConnectWithFSParameters/AutoConnectWithFSParameters.ino deleted file mode 100644 index a9c7b79..0000000 --- a/lib/WiFiManager/examples/Parameters/SPIFFS/AutoConnectWithFSParameters/AutoConnectWithFSParameters.ino +++ /dev/null @@ -1,169 +0,0 @@ -#include //this needs to be first, or it all crashes and burns... -#include //https://github.com/tzapu/WiFiManager - -#ifdef ESP32 - #include -#endif - -#include //https://github.com/bblanchon/ArduinoJson - -//define your default values here, if there are different values in config.json, they are overwritten. -char mqtt_server[40]; -char mqtt_port[6] = "8080"; -char api_token[34] = "YOUR_API_TOKEN"; - -//flag for saving data -bool shouldSaveConfig = false; - -//callback notifying us of the need to save config -void saveConfigCallback () { - Serial.println("Should save config"); - shouldSaveConfig = true; -} - -void setup() { - // put your setup code here, to run once: - Serial.begin(115200); - Serial.println(); - - //clean FS, for testing - //SPIFFS.format(); - - //read configuration from FS json - Serial.println("mounting FS..."); - - if (SPIFFS.begin()) { - Serial.println("mounted file system"); - if (SPIFFS.exists("/config.json")) { - //file exists, reading and loading - Serial.println("reading config file"); - File configFile = SPIFFS.open("/config.json", "r"); - if (configFile) { - Serial.println("opened config file"); - size_t size = configFile.size(); - // Allocate a buffer to store contents of the file. - std::unique_ptr buf(new char[size]); - - configFile.readBytes(buf.get(), size); - - #if defined(ARDUINOJSON_VERSION_MAJOR) && ARDUINOJSON_VERSION_MAJOR >= 6 - DynamicJsonDocument json(1024); - auto deserializeError = deserializeJson(json, buf.get()); - serializeJson(json, Serial); - if ( ! deserializeError ) { -#else - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.parseObject(buf.get()); - json.printTo(Serial); - if (json.success()) { -#endif - Serial.println("\nparsed json"); - strcpy(mqtt_server, json["mqtt_server"]); - strcpy(mqtt_port, json["mqtt_port"]); - strcpy(api_token, json["api_token"]); - } else { - Serial.println("failed to load json config"); - } - configFile.close(); - } - } - } else { - Serial.println("failed to mount FS"); - } - //end read - - // The extra parameters to be configured (can be either global or just in the setup) - // After connecting, parameter.getValue() will get you the configured value - // id/name placeholder/prompt default length - WiFiManagerParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40); - WiFiManagerParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6); - WiFiManagerParameter custom_api_token("apikey", "API token", api_token, 32); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - - //set config save notify callback - wifiManager.setSaveConfigCallback(saveConfigCallback); - - //set static ip - wifiManager.setSTAStaticIPConfig(IPAddress(10, 0, 1, 99), IPAddress(10, 0, 1, 1), IPAddress(255, 255, 255, 0)); - - //add all your parameters here - wifiManager.addParameter(&custom_mqtt_server); - wifiManager.addParameter(&custom_mqtt_port); - wifiManager.addParameter(&custom_api_token); - - //reset settings - for testing - //wifiManager.resetSettings(); - - //set minimu quality of signal so it ignores AP's under that quality - //defaults to 8% - //wifiManager.setMinimumSignalQuality(); - - //sets timeout until configuration portal gets turned off - //useful to make it all retry or go to sleep - //in seconds - //wifiManager.setTimeout(120); - - //fetches ssid and pass and tries to connect - //if it does not connect it starts an access point with the specified name - //here "AutoConnectAP" - //and goes into a blocking loop awaiting configuration - if (!wifiManager.autoConnect("AutoConnectAP", "password")) { - Serial.println("failed to connect and hit timeout"); - delay(3000); - //reset and try again, or maybe put it to deep sleep - ESP.restart(); - delay(5000); - } - - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - - //read updated parameters - strcpy(mqtt_server, custom_mqtt_server.getValue()); - strcpy(mqtt_port, custom_mqtt_port.getValue()); - strcpy(api_token, custom_api_token.getValue()); - Serial.println("The values in the file are: "); - Serial.println("\tmqtt_server : " + String(mqtt_server)); - Serial.println("\tmqtt_port : " + String(mqtt_port)); - Serial.println("\tapi_token : " + String(api_token)); - - //save the custom parameters to FS - if (shouldSaveConfig) { - Serial.println("saving config"); - #if defined(ARDUINOJSON_VERSION_MAJOR) && ARDUINOJSON_VERSION_MAJOR >= 6 - DynamicJsonDocument json(1024); -#else - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); -#endif - json["mqtt_server"] = mqtt_server; - json["mqtt_port"] = mqtt_port; - json["api_token"] = api_token; - - File configFile = SPIFFS.open("/config.json", "w"); - if (!configFile) { - Serial.println("failed to open config file for writing"); - } - -#if defined(ARDUINOJSON_VERSION_MAJOR) && ARDUINOJSON_VERSION_MAJOR >= 6 - serializeJson(json, Serial); - serializeJson(json, configFile); -#else - json.printTo(Serial); - json.printTo(configFile); -#endif - configFile.close(); - //end save - } - - Serial.println("local ip"); - Serial.println(WiFi.localIP()); -} - -void loop() { - // put your main code here, to run repeatedly: - -} diff --git a/lib/WiFiManager/examples/Parameters/SPIFFS/AutoConnectWithFSParametersAndCustomIP/AutoConnectWithFSParametersAndCustomIP.ino b/lib/WiFiManager/examples/Parameters/SPIFFS/AutoConnectWithFSParametersAndCustomIP/AutoConnectWithFSParametersAndCustomIP.ino deleted file mode 100644 index 63523e9..0000000 --- a/lib/WiFiManager/examples/Parameters/SPIFFS/AutoConnectWithFSParametersAndCustomIP/AutoConnectWithFSParametersAndCustomIP.ino +++ /dev/null @@ -1,194 +0,0 @@ -#include //this needs to be first, or it all crashes and burns... - -#include //https://github.com/tzapu/WiFiManager - -#ifdef ESP32 - #include -#endif - -#include //https://github.com/bblanchon/ArduinoJson - -//define your default values here, if there are different values in config.json, they are overwritten. -//length should be max size + 1 -char mqtt_server[40]; -char mqtt_port[6] = "8080"; -char api_token[34] = "YOUR_APITOKEN"; -//default custom static IP -char static_ip[16] = "10.0.1.56"; -char static_gw[16] = "10.0.1.1"; -char static_sn[16] = "255.255.255.0"; - -//flag for saving data -bool shouldSaveConfig = false; - -//callback notifying us of the need to save config -void saveConfigCallback () { - Serial.println("Should save config"); - shouldSaveConfig = true; -} - -void setup() { - // put your setup code here, to run once: - Serial.begin(115200); - Serial.println(); - - //clean FS, for testing - //SPIFFS.format(); - - //read configuration from FS json - Serial.println("mounting FS..."); - - if (SPIFFS.begin()) { - Serial.println("mounted file system"); - if (SPIFFS.exists("/config.json")) { - //file exists, reading and loading - Serial.println("reading config file"); - File configFile = SPIFFS.open("/config.json", "r"); - if (configFile) { - Serial.println("opened config file"); - size_t size = configFile.size(); - // Allocate a buffer to store contents of the file. - std::unique_ptr buf(new char[size]); - - configFile.readBytes(buf.get(), size); - #if defined(ARDUINOJSON_VERSION_MAJOR) && ARDUINOJSON_VERSION_MAJOR >= 6 - DynamicJsonDocument json(1024); - auto deserializeError = deserializeJson(json, buf.get()); - serializeJson(json, Serial); - if ( ! deserializeError ) { -#else - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.parseObject(buf.get()); - json.printTo(Serial); - if (json.success()) { -#endif - Serial.println("\nparsed json"); - - strcpy(mqtt_server, json["mqtt_server"]); - strcpy(mqtt_port, json["mqtt_port"]); - strcpy(api_token, json["api_token"]); - - if (json["ip"]) { - Serial.println("setting custom ip from config"); - strcpy(static_ip, json["ip"]); - strcpy(static_gw, json["gateway"]); - strcpy(static_sn, json["subnet"]); - Serial.println(static_ip); - } else { - Serial.println("no custom ip in config"); - } - } else { - Serial.println("failed to load json config"); - } - } - } - } else { - Serial.println("failed to mount FS"); - } - //end read - Serial.println(static_ip); - Serial.println(api_token); - Serial.println(mqtt_server); - - - // The extra parameters to be configured (can be either global or just in the setup) - // After connecting, parameter.getValue() will get you the configured value - // id/name placeholder/prompt default length - WiFiManagerParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40); - WiFiManagerParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 5); - WiFiManagerParameter custom_api_token("apikey", "API token", api_token, 34); - - //WiFiManager - //Local intialization. Once its business is done, there is no need to keep it around - WiFiManager wifiManager; - - //set config save notify callback - wifiManager.setSaveConfigCallback(saveConfigCallback); - - //set static ip - IPAddress _ip, _gw, _sn; - _ip.fromString(static_ip); - _gw.fromString(static_gw); - _sn.fromString(static_sn); - - wifiManager.setSTAStaticIPConfig(_ip, _gw, _sn); - - //add all your parameters here - wifiManager.addParameter(&custom_mqtt_server); - wifiManager.addParameter(&custom_mqtt_port); - wifiManager.addParameter(&custom_api_token); - - //reset settings - for testing - //wifiManager.resetSettings(); - - //set minimu quality of signal so it ignores AP's under that quality - //defaults to 8% - wifiManager.setMinimumSignalQuality(); - - //sets timeout until configuration portal gets turned off - //useful to make it all retry or go to sleep - //in seconds - //wifiManager.setTimeout(120); - - //fetches ssid and pass and tries to connect - //if it does not connect it starts an access point with the specified name - //here "AutoConnectAP" - //and goes into a blocking loop awaiting configuration - if (!wifiManager.autoConnect("AutoConnectAP", "password")) { - Serial.println("failed to connect and hit timeout"); - delay(3000); - //reset and try again, or maybe put it to deep sleep - ESP.restart(); - delay(5000); - } - - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - - //read updated parameters - strcpy(mqtt_server, custom_mqtt_server.getValue()); - strcpy(mqtt_port, custom_mqtt_port.getValue()); - strcpy(api_token, custom_api_token.getValue()); - - //save the custom parameters to FS - if (shouldSaveConfig) { - Serial.println("saving config"); - #if defined(ARDUINOJSON_VERSION_MAJOR) && ARDUINOJSON_VERSION_MAJOR >= 6 - DynamicJsonDocument json(1024); -#else - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); -#endif - json["mqtt_server"] = mqtt_server; - json["mqtt_port"] = mqtt_port; - json["api_token"] = api_token; - - json["ip"] = WiFi.localIP().toString(); - json["gateway"] = WiFi.gatewayIP().toString(); - json["subnet"] = WiFi.subnetMask().toString(); - - File configFile = SPIFFS.open("/config.json", "w"); - if (!configFile) { - Serial.println("failed to open config file for writing"); - } - - #if defined(ARDUINOJSON_VERSION_MAJOR) && ARDUINOJSON_VERSION_MAJOR >= 6 - serializeJson(json, Serial); - serializeJson(json, configFile); -#else - json.printTo(Serial); - json.printTo(configFile); -#endif - configFile.close(); - //end save - } - - Serial.println("local ip"); - Serial.println(WiFi.localIP()); - Serial.println(WiFi.gatewayIP()); - Serial.println(WiFi.subnetMask()); -} - -void loop() { - // put your main code here, to run repeatedly: -} diff --git a/lib/WiFiManager/examples/ParamsChildClass/ParamsChildClass.ino b/lib/WiFiManager/examples/ParamsChildClass/ParamsChildClass.ino deleted file mode 100644 index 8739a05..0000000 --- a/lib/WiFiManager/examples/ParamsChildClass/ParamsChildClass.ino +++ /dev/null @@ -1,143 +0,0 @@ -/** - * WiFiManagerParameter child class example - */ -#include // https://github.com/tzapu/WiFiManager -#include -#include - -#define SETUP_PIN 0 - -class IPAddressParameter : public WiFiManagerParameter { -public: - IPAddressParameter(const char *id, const char *placeholder, IPAddress address) - : WiFiManagerParameter("") { - init(id, placeholder, address.toString().c_str(), 16, "", WFM_LABEL_BEFORE); - } - - bool getValue(IPAddress &ip) { - return ip.fromString(WiFiManagerParameter::getValue()); - } -}; - -class IntParameter : public WiFiManagerParameter { -public: - IntParameter(const char *id, const char *placeholder, long value, const uint8_t length = 10) - : WiFiManagerParameter("") { - init(id, placeholder, String(value).c_str(), length, "", WFM_LABEL_BEFORE); - } - - long getValue() { - return String(WiFiManagerParameter::getValue()).toInt(); - } -}; - -class FloatParameter : public WiFiManagerParameter { -public: - FloatParameter(const char *id, const char *placeholder, float value, const uint8_t length = 10) - : WiFiManagerParameter("") { - init(id, placeholder, String(value).c_str(), length, "", WFM_LABEL_BEFORE); - } - - float getValue() { - return String(WiFiManagerParameter::getValue()).toFloat(); - } -}; - -struct Settings { - float f; - int i; - char s[20]; - uint32_t ip; -} sett; - - -void setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - pinMode(SETUP_PIN, INPUT_PULLUP); - Serial.begin(115200); - - //Delay to push SETUP button - Serial.println("Press setup button"); - for (int sec = 3; sec > 0; sec--) { - Serial.print(sec); - Serial.print(".."); - delay(1000); - } - - // warning for example only, this will initialize empty memory into your vars - // always init flash memory or add some checksum bits - EEPROM.begin( 512 ); - EEPROM.get(0, sett); - Serial.println("Settings loaded"); - - if (digitalRead(SETUP_PIN) == LOW) { - // Button pressed - Serial.println("SETUP"); - - WiFiManager wm; - - sett.s[19] = '\0'; //add null terminator at the end cause overflow - WiFiManagerParameter param_str( "str", "param_string", sett.s, 20); - FloatParameter param_float( "float", "param_float", sett.f); - IntParameter param_int( "int", "param_int", sett.i); - - IPAddress ip(sett.ip); - IPAddressParameter param_ip("ip", "param_ip", ip); - - wm.addParameter( ¶m_str ); - wm.addParameter( ¶m_float ); - wm.addParameter( ¶m_int ); - wm.addParameter( ¶m_ip ); - - //SSID & password parameters already included - wm.startConfigPortal(); - - strncpy(sett.s, param_str.getValue(), 20); - sett.s[19] = '\0'; - sett.f = param_float.getValue(); - sett.i = param_int.getValue(); - - Serial.print("String param: "); - Serial.println(sett.s); - Serial.print("Float param: "); - Serial.println(sett.f); - Serial.print("Int param: "); - Serial.println(sett.i, DEC); - - if (param_ip.getValue(ip)) { - sett.ip = ip; - - Serial.print("IP param: "); - Serial.println(ip); - } else { - Serial.println("Incorrect IP"); - } - - EEPROM.put(0, sett); - if (EEPROM.commit()) { - Serial.println("Settings saved"); - } else { - Serial.println("EEPROM error"); - } - } - else { - Serial.println("WORK"); - - //connect to saved SSID - WiFi.begin(); - - //do smth - Serial.print("String param: "); - Serial.println(sett.s); - Serial.print("Float param: "); - Serial.println(sett.f); - Serial.print("Int param: "); - Serial.println(sett.i, DEC); - Serial.print("IP param: "); - IPAddress ip(sett.ip); - Serial.println(ip); - } -} - -void loop() { -} diff --git a/lib/WiFiManager/examples/Super/OnDemandConfigPortal/OnDemandConfigPortal.ino b/lib/WiFiManager/examples/Super/OnDemandConfigPortal/OnDemandConfigPortal.ino deleted file mode 100644 index 7962d62..0000000 --- a/lib/WiFiManager/examples/Super/OnDemandConfigPortal/OnDemandConfigPortal.ino +++ /dev/null @@ -1,444 +0,0 @@ -/** - * This is a kind of unit test for DEV for now - * It contains many of the public methods - * - */ -#include // https://github.com/tzapu/WiFiManager -#include -#include - -#define USEOTA -// enable OTA -#ifdef USEOTA -#include -#include -#endif - -const char* modes[] = { "NULL", "STA", "AP", "STA+AP" }; - -unsigned long mtime = 0; - - -WiFiManager wm; - - -// TEST OPTION FLAGS -bool TEST_CP = false; // always start the configportal, even if ap found -int TESP_CP_TIMEOUT = 90; // test cp timeout - -bool TEST_NET = true; // do a network test after connect, (gets ntp time) -bool ALLOWONDEMAND = true; // enable on demand -int ONDDEMANDPIN = 0; // gpio for button -bool WMISBLOCKING = true; // use blocking or non blocking mode, non global params wont work in non blocking - -uint8_t BUTTONFUNC = 1; // 0 resetsettings, 1 configportal, 2 autoconnect - -// char ssid[] = "*************"; // your network SSID (name) -// char pass[] = "********"; // your network password - - -//callbacks - // called after AP mode and config portal has started - // setAPCallback( std::function func ); - // called after webserver has started - // setWebServerCallback( std::function func ); - // called when settings reset have been triggered - // setConfigResetCallback( std::function func ); - // called when wifi settings have been changed and connection was successful ( or setBreakAfterConfig(true) ) - // setSaveConfigCallback( std::function func ); - // called when saving either params-in-wifi or params page - // setSaveParamsCallback( std::function func ); - // called when saving params-in-wifi or params before anything else happens (eg wifi) - // setPreSaveConfigCallback( std::function func ); - // called just before doing OTA update - // setPreOtaUpdateCallback( std::function func ); - -void saveWifiCallback(){ - Serial.println("[CALLBACK] saveCallback fired"); -} - -//gets called when WiFiManager enters configuration mode -void configModeCallback (WiFiManager *myWiFiManager) { - Serial.println("[CALLBACK] configModeCallback fired"); - // myWiFiManager->setAPStaticIPConfig(IPAddress(10,0,1,1), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); - // Serial.println(WiFi.softAPIP()); - //if you used auto generated SSID, print it - // Serial.println(myWiFiManager->getConfigPortalSSID()); - // - // esp_wifi_set_bandwidth(WIFI_IF_AP, WIFI_BW_HT20); -} - -void saveParamCallback(){ - Serial.println("[CALLBACK] saveParamCallback fired"); - // wm.stopConfigPortal(); -} - -void bindServerCallback(){ - wm.server->on("/custom",handleRoute); - - // you can override wm route endpoints, I have not found a way to remove handlers, but this would let you disable them or add auth etc. - // wm.server->on("/info",handleNotFound); - // wm.server->on("/update",handleNotFound); - wm.server->on("/erase",handleNotFound); // disable erase -} - -void handleRoute(){ - Serial.println("[HTTP] handle custom route"); - wm.server->send(200, "text/plain", "hello from user code"); -} - -void handleNotFound(){ - Serial.println("[HTTP] override handle route"); - wm.handleNotFound(); -} - -void handlePreOtaUpdateCallback(){ - Update.onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("CUSTOM Progress: %u%%\r", (progress / (total / 100))); - }); -} - -void setup() { - // WiFi.mode(WIFI_STA); // explicitly set mode, esp can default to STA+AP - - // put your setup code here, to run once: - Serial.begin(115200); - delay(3000); - // Serial.setDebugOutput(true); - - // WiFi.setTxPower(WIFI_POWER_8_5dBm); - - Serial.println("\n Starting"); - // WiFi.setSleepMode(WIFI_NONE_SLEEP); // disable sleep, can improve ap stability - - Serial.println("Error - TEST"); - Serial.println("Information- - TEST"); - - Serial.println("[ERROR] TEST"); - Serial.println("[INFORMATION] TEST"); - - - // WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // wifi_scan_method_t scanMethod - // WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); // wifi_sort_method_t sortMethod - WIFI_CONNECT_AP_BY_SIGNAL,WIFI_CONNECT_AP_BY_SECURITY - // WiFi.setMinSecurity(WIFI_AUTH_WPA2_PSK); - - wm.setDebugOutput(true, WM_DEBUG_DEV); - wm.debugPlatformInfo(); - - //reset settings - for testing - // wm.resetSettings(); - // wm.erase(); - - // setup some parameters - - WiFiManagerParameter custom_html("

    This Is Custom HTML

    "); // only custom html - WiFiManagerParameter custom_mqtt_server("server", "mqtt server", "", 40); - WiFiManagerParameter custom_mqtt_port("port", "mqtt port", "", 6); - WiFiManagerParameter custom_token("api_token", "api token", "", 16); - WiFiManagerParameter custom_tokenb("invalid token", "invalid token", "", 0); // id is invalid, cannot contain spaces - WiFiManagerParameter custom_ipaddress("input_ip", "input IP", "", 15,"pattern='\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}'"); // custom input attrs (ip mask) - WiFiManagerParameter custom_input_type("input_pwd", "input pass", "", 15,"type='password'"); // custom input attrs (ip mask) - - const char _customHtml_checkbox[] = "type=\"checkbox\""; - WiFiManagerParameter custom_checkbox("my_checkbox", "My Checkbox", "T", 2, _customHtml_checkbox, WFM_LABEL_AFTER); - - const char *bufferStr = R"( - -
    -

    Select Choice

    - -
    - -
    - - -
    - - - )"; - - WiFiManagerParameter custom_html_inputs(bufferStr); - - // callbacks - wm.setAPCallback(configModeCallback); - wm.setWebServerCallback(bindServerCallback); - wm.setSaveConfigCallback(saveWifiCallback); - wm.setSaveParamsCallback(saveParamCallback); - wm.setPreOtaUpdateCallback(handlePreOtaUpdateCallback); - - // add all your parameters here - wm.addParameter(&custom_html); - wm.addParameter(&custom_mqtt_server); - wm.addParameter(&custom_mqtt_port); - wm.addParameter(&custom_token); - wm.addParameter(&custom_tokenb); - wm.addParameter(&custom_ipaddress); - wm.addParameter(&custom_checkbox); - wm.addParameter(&custom_input_type); - - wm.addParameter(&custom_html_inputs); - - // set values later if you want - custom_html.setValue("test",4); - custom_token.setValue("test",4); - - // const char* icon = " - // "; - - - // set custom html head content , inside - // examples of favicon, or meta tags etc - // const char* headhtml = ""; - // const char* headhtml = ""; - // wm.setCustomHeadElement(headhtml); - - // set custom html menu content , inside menu item "custom", see setMenu() - const char* menuhtml = "

    \n"; - wm.setCustomMenuHTML(menuhtml); - - // invert theme, dark - wm.setDarkMode(true); - - // show scan RSSI as percentage, instead of signal stength graphic - // wm.setScanDispPerc(true); - -/* - Set cutom menu via menu[] or vector - const char* menu[] = {"wifi","wifinoscan","info","param","close","sep","erase","restart","exit"}; - wm.setMenu(menu,9); // custom menu array must provide length -*/ - - std::vector menu = {"wifi","wifinoscan","info","param","custom","close","sep","erase","update","restart","exit"}; - // wm.setMenu(menu); // custom menu, pass vector - - // wm.setParamsPage(true); // move params to seperate page, not wifi, do not combine with setmenu! - - // set STA static ip - // wm.setSTAStaticIPConfig(IPAddress(10,0,1,99), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); - // wm.setShowStaticFields(false); - // wm.setShowDnsFields(false); - - // set AP static ip - // wm.setAPStaticIPConfig(IPAddress(10,0,1,1), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); - // wm.setAPStaticIPConfig(IPAddress(10,0,1,99), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); - - // set country - // setting wifi country seems to improve OSX soft ap connectivity, - // may help others as well, default is CN which has different channels - - // wm.setCountry("US"); // crashing on esp32 2.0 - - // set Hostname - - // wm.setHostname(("WM_"+wm.getDefaultAPName()).c_str()); - // wm.setHostname("WM_RANDO_1234"); - - // set custom channel - // wm.setWiFiAPChannel(13); - - // set AP hidden - // wm.setAPHidden(true); - - // show password publicly in form - // wm.setShowPassword(true); - - // sets wether wm configportal is a blocking loop(legacy) or not, use wm.process() in loop if false - // wm.setConfigPortalBlocking(false); - - if(!WMISBLOCKING){ - wm.setConfigPortalBlocking(false); - } - - - //sets timeout until configuration portal gets turned off - //useful to make it all retry or go to sleep in seconds - wm.setConfigPortalTimeout(TESP_CP_TIMEOUT); - - // set min quality to show in web list, default 8% - // wm.setMinimumSignalQuality(50); - - // set connection timeout - // wm.setConnectTimeout(20); - - // set wifi connect retries - // wm.setConnectRetries(2); - - // connect after portal save toggle - // wm.setSaveConnect(false); // do not connect, only save - - // show static ip fields - // wm.setShowStaticFields(true); - - // wm.startConfigPortal("AutoConnectAP", "password"); - - // This is sometimes necessary, it is still unknown when and why this is needed but it may solve some race condition or bug in esp SDK/lib - // wm.setCleanConnect(true); // disconnect before connect, clean connect - - wm.setBreakAfterConfig(true); // needed to use saveWifiCallback - - // set custom webserver port, automatic captive portal does not work with custom ports! - // wm.setHttpPort(8080); - - //fetches ssid and pass and tries to connect - //if it does not connect it starts an access point with the specified name - //here "AutoConnectAP" - //and goes into a blocking loop awaiting configuration - - // use autoconnect, but prevent configportal from auto starting - // wm.setEnableConfigPortal(false); - - wifiInfo(); - - // to preload autoconnect with credentials - // wm.preloadWiFi("ssid","password"); - - if(!wm.autoConnect("WM_AutoConnectAP","12345678")) { - Serial.println("failed to connect and hit timeout"); - } - else if(TEST_CP) { - // start configportal always - delay(1000); - Serial.println("TEST_CP ENABLED"); - wm.setConfigPortalTimeout(TESP_CP_TIMEOUT); - wm.startConfigPortal("WM_ConnectAP","12345678"); - } - else { - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - } - - wifiInfo(); - pinMode(ONDDEMANDPIN, INPUT_PULLUP); - - #ifdef USEOTA - ArduinoOTA.begin(); - #endif - -} - -void wifiInfo(){ - // can contain gargbage on esp32 if wifi is not ready yet - Serial.println("[WIFI] WIFI_INFO DEBUG"); - WiFi.printDiag(Serial); - Serial.println("[WIFI] MODE: " + (String)(wm.getModeString(WiFi.getMode()))); - Serial.println("[WIFI] SAVED: " + (String)(wm.getWiFiIsSaved() ? "YES" : "NO")); - Serial.println("[WIFI] SSID: " + (String)wm.getWiFiSSID()); - Serial.println("[WIFI] PASS: " + (String)wm.getWiFiPass()); - // Serial.println("[WIFI] HOSTNAME: " + (String)WiFi.getHostname()); -} - -void loop() { - - if(!WMISBLOCKING){ - wm.process(); - } - - - #ifdef USEOTA - ArduinoOTA.handle(); - #endif - // is configuration portal requested? - if (ALLOWONDEMAND && digitalRead(ONDDEMANDPIN) == LOW ) { - delay(100); - if ( digitalRead(ONDDEMANDPIN) == LOW || BUTTONFUNC == 2){ - Serial.println("BUTTON PRESSED"); - - // button reset/reboot - if(BUTTONFUNC == 0){ - wm.resetSettings(); - wm.reboot(); - delay(200); - return; - } - - // start configportal - if(BUTTONFUNC == 1){ - if (!wm.startConfigPortal("OnDemandAP","12345678")) { - Serial.println("failed to connect and hit timeout"); - delay(3000); - } - return; - } - - //test autoconnect as reconnect etc. - if(BUTTONFUNC == 2){ - wm.setConfigPortalTimeout(TESP_CP_TIMEOUT); - wm.autoConnect(); - return; - } - - } - else { - //if you get here you have connected to the WiFi - Serial.println("connected...yeey :)"); - getTime(); - } - } - - // every 10 seconds - if(millis()-mtime > 10000 ){ - if(WiFi.status() == WL_CONNECTED){ - getTime(); - } - else Serial.println("No Wifi"); - mtime = millis(); - } - // put your main code here, to run repeatedly: - delay(100); -} - -void getTime() { - int tz = -5; - int dst = 0; - time_t now = time(nullptr); - unsigned timeout = 5000; // try for timeout - unsigned start = millis(); - configTime(tz * 3600, dst * 3600, "pool.ntp.org", "time.nist.gov"); - Serial.print("Waiting for NTP time sync: "); - while (now < 8 * 3600 * 2 ) { // what is this ? - delay(100); - Serial.print("."); - now = time(nullptr); - if((millis() - start) > timeout){ - Serial.println("\n[ERROR] Failed to get NTP time."); - return; - } - } - Serial.println(""); - struct tm timeinfo; - gmtime_r(&now, &timeinfo); - Serial.print("Current time: "); - Serial.print(asctime(&timeinfo)); -} - -void debugchipid(){ - // WiFi.mode(WIFI_STA); - // WiFi.printDiag(Serial); - // Serial.println(modes[WiFi.getMode()]); - - // ESP.eraseConfig(); - // wm.resetSettings(); - // wm.erase(true); - WiFi.mode(WIFI_AP); - // WiFi.softAP(); - WiFi.enableAP(true); - delay(500); - // esp_wifi_start(); - delay(1000); - WiFi.printDiag(Serial); - delay(60000); - ESP.restart(); - - // AP esp_267751 - // 507726A4AE30 - // ESP32 Chip ID = 507726A4AE30 -} diff --git a/lib/WiFiManager/examples/Tests/wifi_softap/wifi_softap.ino b/lib/WiFiManager/examples/Tests/wifi_softap/wifi_softap.ino deleted file mode 100644 index aa3e45c..0000000 --- a/lib/WiFiManager/examples/Tests/wifi_softap/wifi_softap.ino +++ /dev/null @@ -1,51 +0,0 @@ -// wifi_basic.ino - -#include -#include - -// #define NVSERASE -#ifdef NVSERASE -#include -#include -#endif - -void setup(){ - Serial.begin(115200); - delay(2000); - Serial.println("Startup...."); - - #ifdef NVSERASE - esp_err_t err; - err = nvs_flash_init(); - err = nvs_flash_erase(); - #endif - - Serial.setDebugOutput(true); - - WiFi.begin("hellowifi","noonehere"); - - while (WiFi.status() != WL_CONNECTED && millis()<15000) { - delay(500); - Serial.print("."); - } - - if(WiFi.status() == WL_CONNECTED){ - Serial.println(""); - Serial.println("WiFi connected."); - Serial.println("IP address: "); - // Serial.println(WiFi.localIP()); - } - else { - Serial.println("WiFi NOT CONNECTED, starting ap"); - /////////////// - /// BUG - // WiFi.enableSTA(false); // BREAKS softap start, says ok BUT no ap found - - delay(2000); - WiFi.softAP("espsoftap","12345678"); - } -} - -void loop(){ - -} \ No newline at end of file diff --git a/lib/WiFiManager/examples/Unique/cb/AnonymousCB.ino b/lib/WiFiManager/examples/Unique/cb/AnonymousCB.ino deleted file mode 100644 index f34d80f..0000000 --- a/lib/WiFiManager/examples/Unique/cb/AnonymousCB.ino +++ /dev/null @@ -1,26 +0,0 @@ -#include // https://github.com/tzapu/WiFiManager - -bool _enteredConfigMode = false; - -void setup(){ - Serial.begin(115200); - WiFiManager wifiManager; - - // wifiManager.setAPCallback([this](WiFiManager* wifiManager) { - wifiManager.setAPCallback([&](WiFiManager* wifiManager) { - Serial.printf("Entered config mode:ip=%s, ssid='%s'\n", - WiFi.softAPIP().toString().c_str(), - wifiManager->getConfigPortalSSID().c_str()); - _enteredConfigMode = true; - }); - wifiManager.resetSettings(); - if (!wifiManager.autoConnect()) { - Serial.printf("*** Failed to connect and hit timeout\n"); - ESP.restart(); - delay(1000); - } -} - -void loop(){ - -} diff --git a/lib/WiFiManager/extras/WiFiManager.template.html b/lib/WiFiManager/extras/WiFiManager.template.html deleted file mode 100644 index 934c033..0000000 --- a/lib/WiFiManager/extras/WiFiManager.template.html +++ /dev/null @@ -1,400 +0,0 @@ - - - - - - {v} - - - - - - - - - - -
    - - - -

    /


    - - - - -

    -

    -

    -

    -

    -

    -

    -

    -

    -

    -

    - - -

    /wifi


    - - - - - - - - - - - - - - - - - - - - -



    - - -

    custom parameter


    -
    -
    - - -
    - - -
    - - -
    - - -
    - - -

    Saving Credentials

    Trying to connect ESP to network.
    If it fails reconnect to AP to try again
    - - -
    Connected to {v}
    with IP {i}
    - - -
    Not Connected to {v}{r}
    - - -
    Not Connected to apname - - -
    Authentication Failure - - -
    AP not found - - -
    Could not Connect - -
    - -
    No AP set
    - - -

    H4 Color Header P

    content
    - - -

    H4 Color Header S

    content
    - - -

    Heading 1

    -

    Heading 2

    -

    Heading 3

    -

    Heading 4

    -

    WIFI HEAD (WIFI_OFF)


    -
    -
    Chip ID
    123456
    -
    Flash Chip ID
    1234556
    -
    IDE Flash Size
    4194304 bytes
    -
    Real Flash Size
    4194304 bytes
    -
    Empty
    -
    Soft AP IP
    192.168.4.1
    -
    Soft AP MAC
    00:00:00:00:00:00
    -
    Station MAC
    00:00:00:00:00:00
    -
    - - -

    Available Pages


    - - - - - - - - - - - - - - - - - - - - -
    PageFunction
    /Menu page.
    /wifiShow WiFi scan results and enter WiFi configuration.(/0wifi noscan)
    /wifisaveSave WiFi configuration information and configure device. Needs variables supplied.
    /closeClose the configuration server and configuration WiFi network.
    /infoInformation page
    /closeClose the captiveportal popup,configportal will remain active
    /exitExit Config Portal, configportal will close
    /restartReboot the device
    /eraseErase WiFi configuration and reboot Device. Device will not reconnect to a network until new WiFi configuration data is entered.
    -

    About


    - Version v1.x.x-xxxxx
    - Build_date
    - Build_file
    - Arduino_version
    -

    Github https://github.com/tzapu/WiFiManager - - -

    Form UPLOAD
    -

    - - - * Upload may not function inside captive portal, Open in browser - http://192.168.4.1 - - - - -



    -
    - -
    - -
    - - - - - -
    - - -

    Select Choice

    - -
    - -
    - - -
    - - - -
    - - - - -
    - - - diff --git a/lib/WiFiManager/extras/parse.js b/lib/WiFiManager/extras/parse.js deleted file mode 100644 index 97a3e38..0000000 --- a/lib/WiFiManager/extras/parse.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const fs = require('fs'); - -console.log('starting'); - -const inFile = 'WiFiManager.template.html'; -const outFile = 'template.h'; - -const defineRegEx = //gm; -console.log('parsing', inFile); - -fs.readFile(inFile, 'utf8', function (err,data) { - if (err) { - return console.log(err); - } - //console.log(data); - - let defines = data.match(defineRegEx); - - //console.log(defines); - var stream = fs.createWriteStream(outFile); - stream.once('open', function(fd) { - for (const i in defines) { - - const start = defines[i]; - const end = start.replace(' - - - - diff --git a/lib/WiFiManager/keywords.txt b/lib/WiFiManager/keywords.txt deleted file mode 100644 index 7159e74..0000000 --- a/lib/WiFiManager/keywords.txt +++ /dev/null @@ -1,39 +0,0 @@ -####################################### -# Syntax Coloring Map For WifiManager -####################################### - -####################################### -# Datatypes (KEYWORD1) -####################################### - -WiFiManager KEYWORD1 -WiFiManagerParameter KEYWORD1 - - -####################################### -# Methods and Functions (KEYWORD2) -####################################### -autoConnect KEYWORD2 -getSSID KEYWORD2 -getPassword KEYWORD2 -getConfigPortalSSID KEYWORD2 -resetSettings KEYWORD2 -setConfigPortalTimeout KEYWORD2 -setConnectTimeout KEYWORD2 -setDebugOutput KEYWORD2 -setMinimumSignalQuality KEYWORD2 -setAPStaticIPConfig KEYWORD2 -setSTAStaticIPConfig KEYWORD2 -setAPCallback KEYWORD2 -setSaveConfigCallback KEYWORD2 -addParameter KEYWORD2 -getID KEYWORD2 -getValue KEYWORD2 -getPlaceholder KEYWORD2 -getValueLength KEYWORD2 - -####################################### -# Constants (LITERAL1) -####################################### - -# LITERAL1 diff --git a/lib/WiFiManager/library.json b/lib/WiFiManager/library.json deleted file mode 100644 index a04050a..0000000 --- a/lib/WiFiManager/library.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "WiFiManager", - "version": "2.0.17", - "keywords": "wifi,wi-fi,esp,esp8266,esp32,espressif8266,espressif32,nodemcu,wemos,arduino", - "description": "WiFi Configuration manager with web configuration portal for ESP boards", - "authors": - [ - { - "name": "tzapu", - "url": "https://github.com/tzapu" - }, - { - "name": "tablatronix", - "url": "https://github.com/tablatronix", - "maintainer": true - } - ], - "repository": - { - "type": "git", - "url": "https://github.com/tzapu/WiFiManager.git" - }, - "frameworks": "arduino", - "platforms": - [ - "espressif8266", - "espressif32" - ] -} \ No newline at end of file diff --git a/lib/WiFiManager/library.properties b/lib/WiFiManager/library.properties deleted file mode 100644 index 559ff72..0000000 --- a/lib/WiFiManager/library.properties +++ /dev/null @@ -1,9 +0,0 @@ -name=WiFiManager -version=2.0.17 -author=tzapu -maintainer=tablatronix -sentence=WiFi Configuration manager with web configuration portal for Espressif ESPx boards, by tzapu -paragraph=Library for configuring ESP8266/ESP32 modules WiFi credentials and custom parameters at runtime with captive portal. -category=Communication -url=https://github.com/tzapu/WiFiManager.git -architectures=esp8266,esp32 diff --git a/lib/WiFiManager/strings_en.h b/lib/WiFiManager/strings_en.h deleted file mode 100644 index cabeb58..0000000 --- a/lib/WiFiManager/strings_en.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Contents of this file have moved to 2 new locations - * wm_strings_nn.h - * wm_consts_nn.h - */ - -#warning "This file is deprecated" - -#ifndef _STRINGS_EN_H_ -#define _STRINGS_EN_H_ - -// strings files must include a consts file! -#include "wm_strings_en.h" // include constants, tokens, routes - -#endif \ No newline at end of file diff --git a/lib/WiFiManager/travis/common.sh b/lib/WiFiManager/travis/common.sh deleted file mode 100644 index 4b3e655..0000000 --- a/lib/WiFiManager/travis/common.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -function build_examples() -{ - excludes=("$@") - # track the exit code for this platform - local exit_code=0 - # loop through results and add them to the array - examples=($(find $PWD/examples/ -name "*.pde" -o -name "*.ino")) - - # get the last example in the array - local last="${examples[@]:(-1)}" - - # loop through example sketches - for example in "${examples[@]}"; do - - # store the full path to the example's sketch directory - local example_dir=$(dirname $example) - - # store the filename for the example without the path - local example_file=$(basename $example) - - # skip files listed as excludes - for exclude in "${excludes[@]}"; do - if [ "${example_file}" == "${exclude}" ] ; then - echo ">>>>>>>>>>>>>>>>>>>>>>>> Skipping ${example_file} <<<<<<<<<<<<<<<<<<<<<<<<<<" - continue 2 - fi - done - - echo "$example_file: " - local sketch="$example_dir/$example_file" - echo "$sketch" - #arduino -v --verbose-build --verify $sketch - - # verify the example, and save stdout & stderr to a variable - # we have to avoid reading the exit code of local: - # "when declaring a local variable in a function, the local acts as a command in its own right" - local build_stdout - build_stdout=$(arduino --verify $sketch 2>&1) - - # echo output if the build failed - if [ $? -ne 0 ]; then - # heavy X - echo -e "\xe2\x9c\x96" - echo -e "----------------------------- DEBUG OUTPUT -----------------------------\n" - echo "$build_stdout" - echo -e "\n------------------------------------------------------------------------\n" - - # mark as fail - exit_code=1 - - else - # heavy checkmark - echo -e "\xe2\x9c\x93" - fi - done - - return $exit_code -} diff --git a/lib/WiFiManager/wm_consts_en.h b/lib/WiFiManager/wm_consts_en.h deleted file mode 100644 index dac35e4..0000000 --- a/lib/WiFiManager/wm_consts_en.h +++ /dev/null @@ -1,265 +0,0 @@ -/** - * wm_consts.h - * internal const strings/tokens - * WiFiManager, a library for the ESP8266/Arduino platform - * for configuration of WiFi credentials using a Captive Portal - * - * @author Creator tzapu - * @author tablatronix - * @version 0.0.0 - * @license MIT - */ - -#ifndef _WM_CONSTS_H -#define _WM_CONSTS_H - - -// ----------------------------------------------------------------------------------------------- -// TOKENS - -const char WM_VERSION_STR[] PROGMEM = "v2.0.17"; - -static const char _wifi_token[] PROGMEM = "wifi"; -static const char _wifinoscan_token[] PROGMEM = "wifinoscan"; -static const char _info_token[] PROGMEM = "info"; -static const char _param_token[] PROGMEM = "param"; -static const char _close_token[] PROGMEM = "close"; -static const char _restart_token[] PROGMEM = "restart"; -static const char _exit_token[] PROGMEM = "exit"; -static const char _erase_token[] PROGMEM = "erase"; -static const char _update_token[] PROGMEM = "update"; -static const char _sep_token[] PROGMEM = "sep"; -static const char _custom_token[] PROGMEM = "custom"; -static PGM_P _menutokens[] PROGMEM = { - _wifi_token, - _wifinoscan_token, - _info_token, - _param_token, - _close_token, - _restart_token, - _exit_token, - _erase_token, - _update_token, - _sep_token, - _custom_token -}; -const uint8_t _nummenutokens = (sizeof(_menutokens) / sizeof(PGM_P)); - - -const char R_root[] PROGMEM = "/"; -const char R_wifi[] PROGMEM = "/wifi"; -const char R_wifinoscan[] PROGMEM = "/0wifi"; -const char R_wifisave[] PROGMEM = "/wifisave"; -const char R_info[] PROGMEM = "/info"; -const char R_param[] PROGMEM = "/param"; -const char R_paramsave[] PROGMEM = "/paramsave"; -const char R_restart[] PROGMEM = "/restart"; -const char R_exit[] PROGMEM = "/exit"; -const char R_close[] PROGMEM = "/close"; -const char R_erase[] PROGMEM = "/erase"; -const char R_status[] PROGMEM = "/status"; -const char R_update[] PROGMEM = "/update"; -const char R_updatedone[] PROGMEM = "/u"; - - -//Strings -const char S_ip[] PROGMEM = "ip"; -const char S_gw[] PROGMEM = "gw"; -const char S_sn[] PROGMEM = "sn"; -const char S_dns[] PROGMEM = "dns"; - - - -//Tokens -//@todo consolidate and reduce -const char T_ss[] PROGMEM = "{"; // token start sentinel -const char T_es[] PROGMEM = "}"; // token end sentinel -const char T_1[] PROGMEM = "{1}"; // @token 1 -const char T_2[] PROGMEM = "{2}"; // @token 2 -const char T_3[] PROGMEM = "{3}"; // @token 2 -const char T_v[] PROGMEM = "{v}"; // @token v -const char T_V[] PROGMEM = "{V}"; // @token v -const char T_I[] PROGMEM = "{I}"; // @token I -const char T_i[] PROGMEM = "{i}"; // @token i -const char T_n[] PROGMEM = "{n}"; // @token n -const char T_p[] PROGMEM = "{p}"; // @token p -const char T_t[] PROGMEM = "{t}"; // @token t -const char T_l[] PROGMEM = "{l}"; // @token l -const char T_c[] PROGMEM = "{c}"; // @token c -const char T_e[] PROGMEM = "{e}"; // @token e -const char T_q[] PROGMEM = "{q}"; // @token q -const char T_r[] PROGMEM = "{r}"; // @token r -const char T_R[] PROGMEM = "{R}"; // @token R -const char T_h[] PROGMEM = "{h}"; // @token h - -// http -const char HTTP_HEAD_CL[] PROGMEM = "Content-Length"; -const char HTTP_HEAD_CT[] PROGMEM = "text/html"; -const char HTTP_HEAD_CT2[] PROGMEM = "text/plain"; -const char HTTP_HEAD_CORS[] PROGMEM = "Access-Control-Allow-Origin"; -const char HTTP_HEAD_CORS_ALLOW_ALL[] PROGMEM = "*"; - -const char * const WIFI_STA_STATUS[] PROGMEM -{ - "WL_IDLE_STATUS", // 0 STATION_IDLE - "WL_NO_SSID_AVAIL", // 1 STATION_NO_AP_FOUND - "WL_SCAN_COMPLETED", // 2 - "WL_CONNECTED", // 3 STATION_GOT_IP - "WL_CONNECT_FAILED", // 4 STATION_CONNECT_FAIL, STATION_WRONG_PASSWORD(NI) - "WL_CONNECTION_LOST", // 5 - "WL_DISCONNECTED", // 6 - "WL_STATION_WRONG_PASSWORD" // 7 KLUDGE -}; - -#ifdef ESP32 -const char * const AUTH_MODE_NAMES[] PROGMEM -{ - "OPEN", - "WEP", - "WPA_PSK", - "WPA2_PSK", - "WPA_WPA2_PSK", - "WPA2_ENTERPRISE", - "MAX" -}; -#elif defined(ESP8266) -const char * const AUTH_MODE_NAMES[] PROGMEM -{ - "", - "", - "WPA_PSK", // 2 ENC_TYPE_TKIP - "", - "WPA2_PSK", // 4 ENC_TYPE_CCMP - "WEP", // 5 ENC_TYPE_WEP - "", - "OPEN", //7 ENC_TYPE_NONE - "WPA_WPA2_PSK", // 8 ENC_TYPE_AUTO -}; -#endif - -const char* const WIFI_MODES[] PROGMEM = { "NULL", "STA", "AP", "STA+AP" }; - - -#ifdef ESP32 -// as 2.5.2 -// typedef struct { -// char cc[3]; /**< country code string */ -// uint8_t schan; /**< start channel */ -// uint8_t nchan; /**< total channel number */ -// int8_t max_tx_power; /**< This field is used for getting WiFi maximum transmitting power, call esp_wifi_set_max_tx_power to set the maximum transmitting power. */ -// wifi_country_policy_t policy; /**< country policy */ -// } wifi_country_t; -const wifi_country_t WM_COUNTRY_US{"US",1,11,CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER,WIFI_COUNTRY_POLICY_AUTO}; -const wifi_country_t WM_COUNTRY_CN{"CN",1,13,CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER,WIFI_COUNTRY_POLICY_AUTO}; -const wifi_country_t WM_COUNTRY_JP{"JP",1,14,CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER,WIFI_COUNTRY_POLICY_AUTO}; -#elif defined(ESP8266) && !defined(WM_NOCOUNTRY) -// typedef struct { -// char cc[3]; /**< country code string */ -// uint8_t schan; /**< start channel */ -// uint8_t nchan; /**< total channel number */ -// uint8_t policy; /**< country policy */ -// } wifi_country_t; -const wifi_country_t WM_COUNTRY_US{"US",1,11,WIFI_COUNTRY_POLICY_AUTO}; -const wifi_country_t WM_COUNTRY_CN{"CN",1,13,WIFI_COUNTRY_POLICY_AUTO}; -const wifi_country_t WM_COUNTRY_JP{"JP",1,14,WIFI_COUNTRY_POLICY_AUTO}; -#endif - - -/* -* ESP32 WiFi Events - -0 SYSTEM_EVENT_WIFI_READY < ESP32 WiFi ready -1 SYSTEM_EVENT_SCAN_DONE < ESP32 finish scanning AP -2 SYSTEM_EVENT_STA_START < ESP32 station start -3 SYSTEM_EVENT_STA_STOP < ESP32 station stop -4 SYSTEM_EVENT_STA_CONNECTED < ESP32 station connected to AP -5 SYSTEM_EVENT_STA_DISCONNECTED < ESP32 station disconnected from AP -6 SYSTEM_EVENT_STA_AUTHMODE_CHANGE < the auth mode of AP connected by ESP32 station changed -7 SYSTEM_EVENT_STA_GOT_IP < ESP32 station got IP from connected AP -8 SYSTEM_EVENT_STA_LOST_IP < ESP32 station lost IP and the IP is reset to 0 -9 SYSTEM_EVENT_STA_WPS_ER_SUCCESS < ESP32 station wps succeeds in enrollee mode -10 SYSTEM_EVENT_STA_WPS_ER_FAILED < ESP32 station wps fails in enrollee mode -11 SYSTEM_EVENT_STA_WPS_ER_TIMEOUT < ESP32 station wps timeout in enrollee mode -12 SYSTEM_EVENT_STA_WPS_ER_PIN < ESP32 station wps pin code in enrollee mode -13 SYSTEM_EVENT_AP_START < ESP32 soft-AP start -14 SYSTEM_EVENT_AP_STOP < ESP32 soft-AP stop -15 SYSTEM_EVENT_AP_STACONNECTED < a station connected to ESP32 soft-AP -16 SYSTEM_EVENT_AP_STADISCONNECTED < a station disconnected from ESP32 soft-AP -17 SYSTEM_EVENT_AP_STAIPASSIGNED < ESP32 soft-AP assign an IP to a connected station -18 SYSTEM_EVENT_AP_PROBEREQRECVED < Receive probe request packet in soft-AP interface -19 SYSTEM_EVENT_GOT_IP6 < ESP32 station or ap or ethernet interface v6IP addr is preferred -20 SYSTEM_EVENT_ETH_START < ESP32 ethernet start -21 SYSTEM_EVENT_ETH_STOP < ESP32 ethernet stop -22 SYSTEM_EVENT_ETH_CONNECTED < ESP32 ethernet phy link up -23 SYSTEM_EVENT_ETH_DISCONNECTED < ESP32 ethernet phy link down -24 SYSTEM_EVENT_ETH_GOT_IP < ESP32 ethernet got IP from connected AP -25 SYSTEM_EVENT_MAX - - -typedef enum { - ARDUINO_EVENT_WIFI_READY = 0, - ARDUINO_EVENT_WIFI_SCAN_DONE, - ARDUINO_EVENT_WIFI_STA_START, - ARDUINO_EVENT_WIFI_STA_STOP, - ARDUINO_EVENT_WIFI_STA_CONNECTED, - ARDUINO_EVENT_WIFI_STA_DISCONNECTED, - ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE, - ARDUINO_EVENT_WIFI_STA_GOT_IP, - ARDUINO_EVENT_WIFI_STA_GOT_IP6, - ARDUINO_EVENT_WIFI_STA_LOST_IP, - ARDUINO_EVENT_WIFI_AP_START, - ARDUINO_EVENT_WIFI_AP_STOP, - ARDUINO_EVENT_WIFI_AP_STACONNECTED, - ARDUINO_EVENT_WIFI_AP_STADISCONNECTED, - ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED, - ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED, - ARDUINO_EVENT_WIFI_AP_GOT_IP6, - ARDUINO_EVENT_WIFI_FTM_REPORT, - ARDUINO_EVENT_ETH_START, - ARDUINO_EVENT_ETH_STOP, - ARDUINO_EVENT_ETH_CONNECTED, - ARDUINO_EVENT_ETH_DISCONNECTED, - ARDUINO_EVENT_ETH_GOT_IP, - ARDUINO_EVENT_ETH_GOT_IP6, - ARDUINO_EVENT_WPS_ER_SUCCESS, - ARDUINO_EVENT_WPS_ER_FAILED, - ARDUINO_EVENT_WPS_ER_TIMEOUT, - ARDUINO_EVENT_WPS_ER_PIN, - ARDUINO_EVENT_WPS_ER_PBC_OVERLAP, - ARDUINO_EVENT_SC_SCAN_DONE, - ARDUINO_EVENT_SC_FOUND_CHANNEL, - ARDUINO_EVENT_SC_GOT_SSID_PSWD, - ARDUINO_EVENT_SC_SEND_ACK_DONE, - ARDUINO_EVENT_PROV_INIT, - ARDUINO_EVENT_PROV_DEINIT, - ARDUINO_EVENT_PROV_START, - ARDUINO_EVENT_PROV_END, - ARDUINO_EVENT_PROV_CRED_RECV, - ARDUINO_EVENT_PROV_CRED_FAIL, - ARDUINO_EVENT_PROV_CRED_SUCCESS, - ARDUINO_EVENT_MAX -} arduino_event_id_t; - -typedef union { - wifi_event_sta_scan_done_t wifi_scan_done; - wifi_event_sta_authmode_change_t wifi_sta_authmode_change; - wifi_event_sta_connected_t wifi_sta_connected; - wifi_event_sta_disconnected_t wifi_sta_disconnected; - wifi_event_sta_wps_er_pin_t wps_er_pin; - wifi_event_sta_wps_fail_reason_t wps_fail_reason; - wifi_event_ap_probe_req_rx_t wifi_ap_probereqrecved; - wifi_event_ap_staconnected_t wifi_ap_staconnected; - wifi_event_ap_stadisconnected_t wifi_ap_stadisconnected; - wifi_event_ftm_report_t wifi_ftm_report; - ip_event_ap_staipassigned_t wifi_ap_staipassigned; - ip_event_got_ip_t got_ip; - ip_event_got_ip6_t got_ip6; - smartconfig_event_got_ssid_pswd_t sc_got_ssid_pswd; - esp_eth_handle_t eth_connected; - wifi_sta_config_t prov_cred_recv; - wifi_prov_sta_fail_reason_t prov_fail_reason; -} arduino_event_info_t; - -*/ - -#endif \ No newline at end of file diff --git a/lib/WiFiManager/wm_strings_en.h b/lib/WiFiManager/wm_strings_en.h deleted file mode 100644 index 4b53160..0000000 --- a/lib/WiFiManager/wm_strings_en.h +++ /dev/null @@ -1,275 +0,0 @@ -/** - * wm_strings_en.h - * engligh strings for - * WiFiManager, a library for the ESP8266/Arduino platform - * for configuration of WiFi credentials using a Captive Portal - * - * @author Creator tzapu - * @author tablatronix - * @version 0.0.0 - * @license MIT - */ - -#ifndef _WM_STRINGS_EN_H_ -#define _WM_STRINGS_EN_H_ - - -#ifndef WIFI_MANAGER_OVERRIDE_STRINGS -// !!! ABOVE WILL NOT WORK if you define in your sketch, must be build flag, if anyone one knows how to order includes to be able to do this it would be neat.. I have seen it done.. - -// strings files must include a consts file! -#include "wm_consts_en.h" // include constants, tokens, routes - -const char WM_LANGUAGE[] PROGMEM = "en-US"; // i18n lang code - -const char HTTP_HEAD_START[] PROGMEM = "" -"" -"" -"" -"" -"{v}"; - -const char HTTP_SCRIPT[] PROGMEM = ""; // @todo add button states, disable on click , show ack , spinner etc - -const char HTTP_HEAD_END[] PROGMEM = "
    "; // {c} = _bodyclass -// example of embedded logo, base64 encoded inline, No styling here -// const char HTTP_ROOT_MAIN[] PROGMEM = "

    {v}

    WiFiManager

    "; -const char HTTP_ROOT_MAIN[] PROGMEM = "

    {t}

    {v}

    "; - -const char * const HTTP_PORTAL_MENU[] PROGMEM = { -"

    \n", // MENU_WIFI -"

    \n", // MENU_WIFINOSCAN -"

    \n", // MENU_INFO -"

    \n",//MENU_PARAM -"

    \n", // MENU_CLOSE -"

    \n",// MENU_RESTART -"

    \n", // MENU_EXIT -"

    \n", // MENU_ERASE -"

    \n",// MENU_UPDATE -"

    " // MENU_SEP -}; - -// const char HTTP_PORTAL_OPTIONS[] PROGMEM = strcat(HTTP_PORTAL_MENU[0] , HTTP_PORTAL_MENU[3] , HTTP_PORTAL_MENU[7]); -const char HTTP_PORTAL_OPTIONS[] PROGMEM = ""; -const char HTTP_ITEM_QI[] PROGMEM = ""; // rssi icons -const char HTTP_ITEM_QP[] PROGMEM = "
    {r}%
    "; // rssi percentage {h} = hidden showperc pref -const char HTTP_ITEM[] PROGMEM = "
    {v}{qi}{qp}
    "; // {q} = HTTP_ITEM_QI, {r} = HTTP_ITEM_QP -// const char HTTP_ITEM[] PROGMEM = "
    {v} {R} {r}% {q} {e}
    "; // test all tokens - -const char HTTP_FORM_START[] PROGMEM = "
    "; -const char HTTP_FORM_WIFI[] PROGMEM = "

    "; -const char HTTP_FORM_WIFI_END[] PROGMEM = ""; -const char HTTP_FORM_STATIC_HEAD[] PROGMEM = "

    "; -const char HTTP_FORM_END[] PROGMEM = "

    "; -const char HTTP_FORM_LABEL[] PROGMEM = ""; -const char HTTP_FORM_PARAM_HEAD[] PROGMEM = "

    "; -const char HTTP_FORM_PARAM[] PROGMEM = "
    \n"; // do not remove newline! - -const char HTTP_SCAN_LINK[] PROGMEM = "
    "; -const char HTTP_SAVED[] PROGMEM = "
    Saving Credentials
    Trying to connect ESP to network.
    If it fails reconnect to AP to try again
    "; -const char HTTP_PARAMSAVED[] PROGMEM = "
    Saved
    "; -const char HTTP_END[] PROGMEM = "
    "; -const char HTTP_ERASEBTN[] PROGMEM = "
    "; -const char HTTP_UPDATEBTN[] PROGMEM = "
    "; -const char HTTP_BACKBTN[] PROGMEM = "

    "; - -const char HTTP_STATUS_ON[] PROGMEM = "
    Connected to {v}
    with IP {i}
    "; -const char HTTP_STATUS_OFF[] PROGMEM = "
    Not connected to {v}{r}
    "; // {c=class} {v=ssid} {r=status_off} -const char HTTP_STATUS_OFFPW[] PROGMEM = "
    Authentication failure"; // STATION_WRONG_PASSWORD, no eps32 -const char HTTP_STATUS_OFFNOAP[] PROGMEM = "
    AP not found"; // WL_NO_SSID_AVAIL -const char HTTP_STATUS_OFFFAIL[] PROGMEM = "
    Could not connect"; // WL_CONNECT_FAILED -const char HTTP_STATUS_NONE[] PROGMEM = "
    No AP set
    "; -const char HTTP_BR[] PROGMEM = "
    "; - -const char HTTP_STYLE[] PROGMEM = ""; - -#ifndef WM_NOHELP -const char HTTP_HELP[] PROGMEM = - "

    Available pages


    " - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "
    PageFunction
    /Menu page.
    /wifiShow WiFi scan results and enter WiFi configuration.(/0wifi noscan)
    /wifisaveSave WiFi configuration information and configure device. Needs variables supplied.
    /paramParameter page
    /infoInformation page
    /uOTA Update
    /closeClose the captiveportal popup, config portal will remain active
    /exitExit Config portal, config portal will close
    /restartReboot the device
    /eraseErase WiFi configuration and reboot device. Device will not reconnect to a network until new WiFi configuration data is entered.
    " - "

    Github https://github.com/tzapu/WiFiManager."; -#else -const char HTTP_HELP[] PROGMEM = ""; -#endif - -const char HTTP_UPDATE[] PROGMEM = "Upload new firmware

    * May not function inside captive portal, open in browser http://192.168.4.1"; -const char HTTP_UPDATE_FAIL[] PROGMEM = "
    Update failed!
    Reboot device and try again
    "; -const char HTTP_UPDATE_SUCCESS[] PROGMEM = "
    Update successful.
    Device rebooting now...
    "; - -#ifdef WM_JSTEST -const char HTTP_JS[] PROGMEM = -""; -#endif - -// Info html -// @todo remove html elements from progmem, repetetive strings -#ifdef ESP32 - const char HTTP_INFO_esphead[] PROGMEM = "

    esp32


    "; - const char HTTP_INFO_chiprev[] PROGMEM = "
    Chip rev
    {1}
    "; - const char HTTP_INFO_lastreset[] PROGMEM = "
    Last reset reason
    CPU0: {1}
    CPU1: {2}
    "; - const char HTTP_INFO_aphost[] PROGMEM = "
    Access point hostname
    {1}
    "; - const char HTTP_INFO_psrsize[] PROGMEM = "
    PSRAM Size
    {1} bytes
    "; - const char HTTP_INFO_temp[] PROGMEM = "
    Temperature
    {1} C° / {2} F°
    "; - const char HTTP_INFO_hall[] PROGMEM = "
    Hall
    {1}
    "; -#else - const char HTTP_INFO_esphead[] PROGMEM = "

    esp8266


    "; - const char HTTP_INFO_fchipid[] PROGMEM = "
    Flash chip ID
    {1}
    "; - const char HTTP_INFO_corever[] PROGMEM = "
    Core version
    {1}
    "; - const char HTTP_INFO_bootver[] PROGMEM = "
    Boot version
    {1}
    "; - const char HTTP_INFO_lastreset[] PROGMEM = "
    Last reset reason
    {1}
    "; - const char HTTP_INFO_flashsize[] PROGMEM = "
    Real flash size
    {1} bytes
    "; -#endif - -const char HTTP_INFO_memsmeter[] PROGMEM = "
    "; -const char HTTP_INFO_memsketch[] PROGMEM = "
    Memory - Sketch size
    Used / Total bytes
    {1} / {2}"; -const char HTTP_INFO_freeheap[] PROGMEM = "
    Memory - Free heap
    {1} bytes available
    "; -const char HTTP_INFO_wifihead[] PROGMEM = "

    WiFi


    "; -const char HTTP_INFO_uptime[] PROGMEM = "
    Uptime
    {1} mins {2} secs
    "; -const char HTTP_INFO_chipid[] PROGMEM = "
    Chip ID
    {1}
    "; -const char HTTP_INFO_idesize[] PROGMEM = "
    Flash size
    {1} bytes
    "; -const char HTTP_INFO_sdkver[] PROGMEM = "
    SDK version
    {1}
    "; -const char HTTP_INFO_cpufreq[] PROGMEM = "
    CPU frequency
    {1}MHz
    "; -const char HTTP_INFO_apip[] PROGMEM = "
    Access point IP
    {1}
    "; -const char HTTP_INFO_apmac[] PROGMEM = "
    Access point MAC
    {1}
    "; -const char HTTP_INFO_apssid[] PROGMEM = "
    Access point SSID
    {1}
    "; -const char HTTP_INFO_apbssid[] PROGMEM = "
    BSSID
    {1}
    "; -const char HTTP_INFO_stassid[] PROGMEM = "
    Station SSID
    {1}
    "; -const char HTTP_INFO_staip[] PROGMEM = "
    Station IP
    {1}
    "; -const char HTTP_INFO_stagw[] PROGMEM = "
    Station gateway
    {1}
    "; -const char HTTP_INFO_stasub[] PROGMEM = "
    Station subnet
    {1}
    "; -const char HTTP_INFO_dnss[] PROGMEM = "
    DNS Server
    {1}
    "; -const char HTTP_INFO_host[] PROGMEM = "
    Hostname
    {1}
    "; -const char HTTP_INFO_stamac[] PROGMEM = "
    Station MAC
    {1}
    "; -const char HTTP_INFO_conx[] PROGMEM = "
    Connected
    {1}
    "; -const char HTTP_INFO_autoconx[] PROGMEM = "
    Autoconnect
    {1}
    "; - -const char HTTP_INFO_aboutver[] PROGMEM = "
    WiFiManager
    {1}
    "; -const char HTTP_INFO_aboutarduino[] PROGMEM = "
    Arduino
    {1}
    "; -const char HTTP_INFO_aboutsdk[] PROGMEM = "
    ESP-SDK/IDF
    {1}
    "; -const char HTTP_INFO_aboutdate[] PROGMEM = "
    Build date
    {1}
    "; - -const char S_brand[] PROGMEM = "WiFiManager"; -const char S_debugPrefix[] PROGMEM = "*wm:"; -const char S_y[] PROGMEM = "Yes"; -const char S_n[] PROGMEM = "No"; -const char S_enable[] PROGMEM = "Enabled"; -const char S_disable[] PROGMEM = "Disabled"; -const char S_GET[] PROGMEM = "GET"; -const char S_POST[] PROGMEM = "POST"; -const char S_NA[] PROGMEM = "Unknown"; -const char S_passph[] PROGMEM = "********"; -const char S_titlewifisaved[] PROGMEM = "Credentials saved"; -const char S_titlewifisettings[] PROGMEM = "Settings saved"; -const char S_titlewifi[] PROGMEM = "Config ESP"; -const char S_titleinfo[] PROGMEM = "Info"; -const char S_titleparam[] PROGMEM = "Setup"; -const char S_titleparamsaved[] PROGMEM = "Setup saved"; -const char S_titleexit[] PROGMEM = "Exit"; -const char S_titlereset[] PROGMEM = "Reset"; -const char S_titleerase[] PROGMEM = "Erase"; -const char S_titleclose[] PROGMEM = "Close"; -const char S_options[] PROGMEM = "options"; -const char S_nonetworks[] PROGMEM = "No networks found. Refresh to scan again."; -const char S_staticip[] PROGMEM = "Static IP"; -const char S_staticgw[] PROGMEM = "Static gateway"; -const char S_staticdns[] PROGMEM = "Static DNS"; -const char S_subnet[] PROGMEM = "Subnet"; -const char S_exiting[] PROGMEM = "Exiting"; -const char S_resetting[] PROGMEM = "Module will reset in a few seconds."; -const char S_closing[] PROGMEM = "You can close the page, portal will continue to run"; -const char S_error[] PROGMEM = "An error occured"; -const char S_notfound[] PROGMEM = "File not found\n\n"; -const char S_uri[] PROGMEM = "URI: "; -const char S_method[] PROGMEM = "\nMethod: "; -const char S_args[] PROGMEM = "\nArguments: "; -const char S_parampre[] PROGMEM = "param_"; - -// debug strings -const char D_HR[] PROGMEM = "--------------------"; - - -// softap ssid default prefix -#ifdef ESP8266 - const char S_ssidpre[] PROGMEM = "ESP"; -#elif defined(ESP32) - const char S_ssidpre[] PROGMEM = "ESP32"; -#else - const char S_ssidpre[] PROGMEM = "WM"; -#endif - -// END WIFI_MANAGER_OVERRIDE_STRINGS -#endif - -#endif diff --git a/lib/WiFiManager/wm_strings_es.h b/lib/WiFiManager/wm_strings_es.h deleted file mode 100644 index 781d055..0000000 --- a/lib/WiFiManager/wm_strings_es.h +++ /dev/null @@ -1,282 +0,0 @@ -/** - * SAMPLE SAMPLE SAMPLE - * - * wm_strings_es.h - * spanish strings for - * WiFiManager, a library for the ESPX/Arduino platform - * for configuration of WiFi credentials using a Captive Portal - * - * @author Creator tzapu - * @author tablatronix - * @version 0.0.0 - * @license MIT - */ - -#ifndef _WM_STRINGS_EN_H_ -#define _WM_STRINGS_EN_H_ - - -/** - * ADD TO BUILD FLAGS - * -DWM_STRINGS_FILE="\"wm_strings_es.h\"" - */ - -#ifndef WIFI_MANAGER_OVERRIDE_STRINGS -// !!! ABOVE WILL NOT WORK if you define in your sketch, must be build flag, if anyone one knows how to order includes to be able to do this it would be neat.. I have seen it done.. - -// strings files must include a consts file! -// Copy and change to custom locale tokens if necessary, but strings should be good enough -#include "wm_consts_en.h" // include constants, tokens, routes - -const char WM_LANGUAGE[] PROGMEM = "es-ES"; // i18n lang code - -const char HTTP_HEAD_START[] PROGMEM = "" -"" -"" -"" -"" -"{v}"; - -const char HTTP_SCRIPT[] PROGMEM = ""; // @todo add button states, disable on click , show ack , spinner etc - -const char HTTP_HEAD_END[] PROGMEM = "
    "; // {c} = _bodyclass -// example of embedded logo, base64 encoded inline, No styling here -// const char HTTP_ROOT_MAIN[] PROGMEM = "

    {v}

    WiFiManager

    "; -const char HTTP_ROOT_MAIN[] PROGMEM = "

    {t}

    {v}

    "; - -const char * const HTTP_PORTAL_MENU[] PROGMEM = { -"

    \n", // MENU_WIFI -"

    \n", // MENU_WIFINOSCAN -"

    \n", // MENU_INFO -"

    \n",//MENU_PARAM -"

    \n", // MENU_CLOSE -"

    \n",// MENU_RESTART -"

    \n", // MENU_EXIT -"

    \n", // MENU_ERASE -"

    \n",// MENU_UPDATE -"

    " // MENU_SEP -}; - -// const char HTTP_PORTAL_OPTIONS[] PROGMEM = strcat(HTTP_PORTAL_MENU[0] , HTTP_PORTAL_MENU[3] , HTTP_PORTAL_MENU[7]); -const char HTTP_PORTAL_OPTIONS[] PROGMEM = ""; -const char HTTP_ITEM_QI[] PROGMEM = ""; // rssi icons -const char HTTP_ITEM_QP[] PROGMEM = "
    {r}%
    "; // rssi percentage {h} = hidden showperc pref -const char HTTP_ITEM[] PROGMEM = "
    {v}{qi}{qp}
    "; // {q} = HTTP_ITEM_QI, {r} = HTTP_ITEM_QP -// const char HTTP_ITEM[] PROGMEM = "
    {v} {R} {r}% {q} {e}
    "; // test all tokens - -const char HTTP_FORM_START[] PROGMEM = "
    "; -const char HTTP_FORM_WIFI[] PROGMEM = "
    Mostrar contraseña"; -const char HTTP_FORM_WIFI_END[] PROGMEM = ""; -const char HTTP_FORM_STATIC_HEAD[] PROGMEM = "

    "; -const char HTTP_FORM_END[] PROGMEM = "

    "; -const char HTTP_FORM_LABEL[] PROGMEM = ""; -const char HTTP_FORM_PARAM_HEAD[] PROGMEM = "

    "; -const char HTTP_FORM_PARAM[] PROGMEM = "
    \n"; // do not remove newline! - -const char HTTP_SCAN_LINK[] PROGMEM = "
    "; -const char HTTP_SAVED[] PROGMEM = "
    Saving Credentials
    Trying to connect ESP to network.
    If it fails reconnect to AP to try again
    "; -const char HTTP_PARAMSAVED[] PROGMEM = "
    Saved
    "; -const char HTTP_END[] PROGMEM = "
    "; -const char HTTP_ERASEBTN[] PROGMEM = "
    "; -const char HTTP_UPDATEBTN[] PROGMEM = "
    "; -const char HTTP_BACKBTN[] PROGMEM = "

    "; - -const char HTTP_STATUS_ON[] PROGMEM = "
    Conectado a {v}
    con IP {i}
    "; -const char HTTP_STATUS_OFF[] PROGMEM = "
    No conectado a {v}{r}
    "; // {c=class} {v=ssid} {r=status_off} -const char HTTP_STATUS_OFFPW[] PROGMEM = "
    Authentication Failure"; // STATION_WRONG_PASSWORD, no eps32 -const char HTTP_STATUS_OFFNOAP[] PROGMEM = "
    No Encontrado"; // WL_NO_SSID_AVAIL -const char HTTP_STATUS_OFFFAIL[] PROGMEM = "
    No se pudo conectar"; // WL_CONNECT_FAILED -const char HTTP_STATUS_NONE[] PROGMEM = "
    Sin AP establecido
    "; -const char HTTP_BR[] PROGMEM = "
    "; - -const char HTTP_STYLE[] PROGMEM = ""; - -#ifndef WM_NOHELP -const char HTTP_HELP[] PROGMEM = - "

    Available Pages


    " - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "
    PageFunction
    /Menu page.
    /wifiShow WiFi scan results and enter WiFi configuration.(/0wifi noscan)
    /wifisaveSave WiFi configuration information and configure device. Needs variables supplied.
    /paramParameter page
    /infoInformation page
    /uOTA Update
    /closeClose the captiveportal popup,configportal will remain active
    /exitExit Config Portal, configportal will close
    /restartReboot the device
    /eraseErase WiFi configuration and reboot Device. Device will not reconnect to a network until new WiFi configuration data is entered.
    " - "

    Github https://github.com/tzapu/WiFiManager."; -#else -const char HTTP_HELP[] PROGMEM = ""; -#endif - -const char HTTP_UPDATE[] PROGMEM = "Upload New Firmware

    * May not function inside captive portal, Open in browser http://192.168.4.1"; -const char HTTP_UPDATE_FAIL[] PROGMEM = "
    Update Failed!
    Reboot device and try again
    "; -const char HTTP_UPDATE_SUCCESS[] PROGMEM = "
    Update Successful.
    Device Rebooting now...
    "; - -#ifdef WM_JSTEST -const char HTTP_JS[] PROGMEM = -""; -#endif - -// Info html -// @todo remove html elements from progmem, repetetive strings -#ifdef ESP32 - const char HTTP_INFO_esphead[] PROGMEM = "

    esp32


    "; - const char HTTP_INFO_chiprev[] PROGMEM = "
    Chip Rev
    {1}
    "; - const char HTTP_INFO_lastreset[] PROGMEM = "
    Last reset reason
    CPU0: {1}
    CPU1: {2}
    "; - const char HTTP_INFO_aphost[] PROGMEM = "
    Access Point Hostname
    {1}
    "; - const char HTTP_INFO_psrsize[] PROGMEM = "
    PSRAM Size
    {1} bytes
    "; - const char HTTP_INFO_temp[] PROGMEM = "
    Temperature
    {1} C° / {2} F°
    Hall
    {3}
    "; -#else - const char HTTP_INFO_esphead[] PROGMEM = "

    esp8266


    "; - const char HTTP_INFO_fchipid[] PROGMEM = "
    Flash Chip ID
    {1}
    "; - const char HTTP_INFO_corever[] PROGMEM = "
    Core Version
    {1}
    "; - const char HTTP_INFO_bootver[] PROGMEM = "
    Boot Version
    {1}
    "; - const char HTTP_INFO_lastreset[] PROGMEM = "
    Last reset reason
    {1}
    "; - const char HTTP_INFO_flashsize[] PROGMEM = "
    Real Flash Size
    {1} bytes
    "; -#endif - -const char HTTP_INFO_memsmeter[] PROGMEM = "
    "; -const char HTTP_INFO_memsketch[] PROGMEM = "
    Memory - Sketch Size
    Used / Total bytes
    {1} / {2}"; -const char HTTP_INFO_freeheap[] PROGMEM = "
    Memory - Free Heap
    {1} bytes available
    "; -const char HTTP_INFO_wifihead[] PROGMEM = "

    WiFi


    "; -const char HTTP_INFO_uptime[] PROGMEM = "
    Uptime
    {1} Mins {2} Secs
    "; -const char HTTP_INFO_chipid[] PROGMEM = "
    Chip ID
    {1}
    "; -const char HTTP_INFO_idesize[] PROGMEM = "
    Flash Size
    {1} bytes
    "; -const char HTTP_INFO_sdkver[] PROGMEM = "
    SDK Version
    {1}
    "; -const char HTTP_INFO_cpufreq[] PROGMEM = "
    CPU Frequency
    {1}MHz
    "; -const char HTTP_INFO_apip[] PROGMEM = "
    Access Point IP
    {1}
    "; -const char HTTP_INFO_apmac[] PROGMEM = "
    Access Point MAC
    {1}
    "; -const char HTTP_INFO_apssid[] PROGMEM = "
    Access Point SSID
    {1}
    "; -const char HTTP_INFO_apbssid[] PROGMEM = "
    BSSID
    {1}
    "; -const char HTTP_INFO_stassid[] PROGMEM = "
    Station SSID
    {1}
    "; -const char HTTP_INFO_staip[] PROGMEM = "
    Station IP
    {1}
    "; -const char HTTP_INFO_stagw[] PROGMEM = "
    Station Gateway
    {1}
    "; -const char HTTP_INFO_stasub[] PROGMEM = "
    Station Subnet
    {1}
    "; -const char HTTP_INFO_dnss[] PROGMEM = "
    DNS Server
    {1}
    "; -const char HTTP_INFO_host[] PROGMEM = "
    Hostname
    {1}
    "; -const char HTTP_INFO_stamac[] PROGMEM = "
    Station MAC
    {1}
    "; -const char HTTP_INFO_conx[] PROGMEM = "
    Connected
    {1}
    "; -const char HTTP_INFO_autoconx[] PROGMEM = "
    Autoconnect
    {1}
    "; - -const char HTTP_INFO_aboutver[] PROGMEM = "
    WiFiManager
    {1}
    "; -const char HTTP_INFO_aboutarduino[] PROGMEM = "
    Arduino
    {1}
    "; -const char HTTP_INFO_aboutsdk[] PROGMEM = "
    ESP-SDK/IDF
    {1}
    "; -const char HTTP_INFO_aboutdate[] PROGMEM = "
    Build Date
    {1}
    "; - -const char S_brand[] PROGMEM = "WiFiManager"; -const char S_debugPrefix[] PROGMEM = "*wm:"; -const char S_y[] PROGMEM = "Yes"; -const char S_n[] PROGMEM = "No"; -const char S_enable[] PROGMEM = "Enabled"; -const char S_disable[] PROGMEM = "Disabled"; -const char S_GET[] PROGMEM = "GET"; -const char S_POST[] PROGMEM = "POST"; -const char S_NA[] PROGMEM = "Unknown"; -const char S_passph[] PROGMEM = "********"; -const char S_titlewifisaved[] PROGMEM = "Credentials Saved"; -const char S_titlewifisettings[] PROGMEM = "Settings Saved"; -const char S_titlewifi[] PROGMEM = "Config ESP"; -const char S_titleinfo[] PROGMEM = "Info"; -const char S_titleparam[] PROGMEM = "Setup"; -const char S_titleparamsaved[] PROGMEM = "Setup Saved"; -const char S_titleexit[] PROGMEM = "Exit"; -const char S_titlereset[] PROGMEM = "Reset"; -const char S_titleerase[] PROGMEM = "Erase"; -const char S_titleclose[] PROGMEM = "Close"; -const char S_options[] PROGMEM = "options"; -const char S_nonetworks[] PROGMEM = "No networks found. Refresh to scan again."; -const char S_staticip[] PROGMEM = "Static IP"; -const char S_staticgw[] PROGMEM = "Static Gateway"; -const char S_staticdns[] PROGMEM = "Static DNS"; -const char S_subnet[] PROGMEM = "Subnet"; -const char S_exiting[] PROGMEM = "Exiting"; -const char S_resetting[] PROGMEM = "Module will reset in a few seconds."; -const char S_closing[] PROGMEM = "You can close the page, portal will continue to run"; -const char S_error[] PROGMEM = "An Error Occured"; -const char S_notfound[] PROGMEM = "File Not Found\n\n"; -const char S_uri[] PROGMEM = "URI: "; -const char S_method[] PROGMEM = "\nMethod: "; -const char S_args[] PROGMEM = "\nArguments: "; -const char S_parampre[] PROGMEM = "param_"; - -// debug strings -const char D_HR[] PROGMEM = "--------------------"; - - -// softap ssid default prefix -#ifdef ESP8266 - const char S_ssidpre[] PROGMEM = "ESP"; -#elif defined(ESP32) - const char S_ssidpre[] PROGMEM = "ESP32"; -#else - const char S_ssidpre[] PROGMEM = "WM"; -#endif - -// END WIFI_MANAGER_OVERRIDE_STRINGS -#endif - -#endif From 06e96b5ea7f54598c6d1cd2eff10fbece14b22e4 Mon Sep 17 00:00:00 2001 From: iranl Date: Wed, 9 Oct 2024 21:33:18 +0200 Subject: [PATCH 12/30] WiFi portal --- README.md | 6 +- platformio.ini | 1 + src/Config.h | 2 +- src/NukiNetwork.cpp | 42 +- src/NukiNetwork.h | 3 +- src/NukiOpenerWrapper.cpp | 46 +- src/NukiWrapper.cpp | 69 ++- src/PreferencesKeys.h | 33 +- src/RestartReason.h | 3 - src/WebCfgServer.cpp | 692 ++++++++++++++++---------- src/WebCfgServer.h | 19 +- src/main.cpp | 41 +- src/networkDevices/EthernetDevice.cpp | 37 +- src/networkDevices/EthernetDevice.h | 8 +- src/networkDevices/NetworkDevice.h | 10 +- src/networkDevices/WifiDevice.cpp | 457 +++++++++++++---- src/networkDevices/WifiDevice.h | 22 +- updater/platformio.ini | 5 +- 18 files changed, 999 insertions(+), 497 deletions(-) diff --git a/README.md b/README.md index 483d664..421e18f 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,8 @@ Unpack the zip archive and read the included how-to-flash.txt for installation i ## Initial setup (Network and MQTT) -Power up the ESP32 and a new Wi-Fi access point named "ESP32_(8 CHARACTER ALPHANUMERIC)" should appear.
    +Power up the ESP32 and a new Wi-Fi access point named "NukiHub" should appear.
    +The password of the access point is "NukiHubESP32".
    Connect a client device to this access point and in a browser navigate to "http://192.168.4.1".
    Use the web interface to connect the ESP to your preferred Wi-Fi network.

    @@ -138,7 +139,7 @@ PSRAM is usually 2, 4 or 8MB in size and thus greatly enlarges the 320kb of inte It is basically impossible to run out of RAM when PSRAM is available. You can check on the info page of the Web configurator if PSRAM is available. -Note that there are two build of Nuki Hub for the ESP32-S3 available.
    +Note that there are two builds of Nuki Hub for the ESP32-S3 available.
    One for devices with no or Quad SPI PSRAM and one for devices with Octal SPI PSRAM.
    If your ESP32-S3 device has PSRAM but it is not detected please flash the other S3 binary. @@ -165,7 +166,6 @@ In a browser navigate to the IP address assigned to the ESP32. - MQTT SSL Client Certificate: Optionally set to the Client SSL certificate of the MQTT broker, see the "[MQTT Encryption](#mqtt-encryption-optional)" section of this README. - MQTT SSL Client Key: Optionally set to the Client SSL key of the MQTT broker, see the "[MQTT Encryption](#mqtt-encryption-optional)" section of this README. - Network hardware: "Wi-Fi only" by default, set to one of the specified ethernet modules if available, see the "Supported Ethernet devices" and "[Connecting via Ethernet](#connecting-via-ethernet-optional)" section of this README. -- Disable fallback to Wi-Fi / Wi-Fi config portal: By default the Nuki Hub will fallback to Wi-Fi and open the Wi-Fi configuration portal when the network connection fails. Enable this setting to disable this fallback. - Connect to AP with the best signal in an environment with multiple APs with the same SSID: Enable to perform a scan for the Access Point with the best signal strenght for the specified SSID in a multi AP/Mesh environment. - RSSI Publish interval: Set to a positive integer to set the amount of seconds between updates to the maintenance/wifiRssi MQTT topic with the current Wi-Fi RSSI, set to -1 to disable, default 60. - MQTT Timeout until restart: Set to a positive integer to restart the Nuki Hub after the set amount of seconds has passed without an active connection to the MQTT broker, set to -1 to disable, default 60. diff --git a/platformio.ini b/platformio.ini index a4c1aa0..a357b42 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,6 +27,7 @@ board_build.partitions = partitions.csv build_unflags = -DCONFIG_BT_NIMBLE_LOG_LEVEL -DCONFIG_BTDM_BLE_SCAN_DUPL + -DESP32 -Werror=all -Wall build_flags = diff --git a/src/Config.h b/src/Config.h index e56b8ef..6c6c438 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.01" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-09-06" +#define NUKI_HUB_DATE "2024-10-14" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index f564089..e1d33a5 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -69,7 +69,7 @@ void NukiNetwork::setupDevice() { _ipConfiguration = new IPConfiguration(_preferences); int hardwareDetect = _preferences->getInt(preference_network_hardware, 0); - Log->print(F("Hardware detect : ")); + Log->print(F("Hardware detect: ")); Log->println(hardwareDetect); _firstBootAfterDeviceChange = _preferences->getBool(preference_ntw_reconfigure, false); @@ -95,7 +95,7 @@ void NukiNetwork::setupDevice() if(strcmp(WiFi_fallbackDetect, "wifi_fallback") == 0) { #ifndef CONFIG_IDF_TARGET_ESP32H2 - if(_preferences->getBool(preference_network_wifi_fallback_disabled) && !_firstBootAfterDeviceChange) + if(!_firstBootAfterDeviceChange) { Log->println(F("Failed to connect to network. Wi-Fi fallback is disabled, rebooting.")); memset(WiFi_fallbackDetect, 0, sizeof(WiFi_fallbackDetect)); @@ -122,7 +122,7 @@ void NukiNetwork::setupDevice() _device = NetworkDeviceInstantiator::Create(_networkDeviceType, _hostname, _preferences, _ipConfiguration); Log->print(F("Network device: ")); - Log->print(_device->deviceName()); + Log->println(_device->deviceName()); } void NukiNetwork::reconfigureDevice() @@ -130,6 +130,16 @@ void NukiNetwork::reconfigureDevice() _device->reconfigure(); } +void NukiNetwork::scan(bool passive, bool async) +{ + _device->scan(passive, async); +} + +bool NukiNetwork::isApOpen() +{ + return _device->isApOpen(); +} + const String NukiNetwork::networkDeviceName() const { return _device->deviceName(); @@ -317,7 +327,6 @@ void NukiNetwork::readSettings() { _restartOnDisconnect = _preferences->getBool(preference_restart_on_disconnect, false); _checkUpdates = _preferences->getBool(preference_check_updates, false); - _reconnectNetworkOnMqttDisconnect = _preferences->getBool(preference_recon_netw_on_mqtt_discon, false); _rssiPublishInterval = _preferences->getInt(preference_rssi_publish_interval, 0) * 1000; if(_rssiPublishInterval == 0) @@ -341,38 +350,17 @@ bool NukiNetwork::update() int64_t ts = (esp_timer_get_time() / 1000); _device->update(); - if(!_mqttEnabled) + if(!_mqttEnabled || _device->isApOpen()) { return true; } - if(!_device->isConnected() || (_mqttConnectCounter > 15 && _reconnectNetworkOnMqttDisconnect && !_firstConnect)) + if(!_device->isConnected() || (_mqttConnectCounter > 15 && !_firstConnect)) { _mqttConnectCounter = 0; if(!_webEnabled) forceEnableWebServer = true; if(_restartOnDisconnect && (esp_timer_get_time() / 1000) > 60000) restartEsp(RestartReason::RestartOnDisconnectWatchdog); - - Log->println(F("Network not connected. Trying reconnect.")); - ReconnectStatus reconnectStatus = _device->reconnect(true); - - switch(reconnectStatus) - { - case ReconnectStatus::CriticalFailure: - strcpy(WiFi_fallbackDetect, "wifi_fallback"); - Log->println("Network device has a critical failure, enable fallback to Wi-Fi and reboot."); - delay(200); - restartEsp(RestartReason::NetworkDeviceCriticalFailure); - break; - case ReconnectStatus::Success: - memset(WiFi_fallbackDetect, 0, sizeof(WiFi_fallbackDetect)); - Log->print(F("Reconnect successful: IP: ")); - Log->println(_device->localIP()); - break; - case ReconnectStatus::Failure: - Log->println(F("Reconnect failed")); - break; - } } if(_device->isConnected() && !_mqttClientInitiated && strcmp(_mqttBrokerAddr, "") != 0) diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index fad16f3..602d74b 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -24,6 +24,8 @@ public: void readSettings(); bool update(); void reconfigureDevice(); + void scan(bool passive = false, bool async = true); + bool isApOpen(); void clearWifiFallback(); const String networkDeviceName() const; @@ -168,7 +170,6 @@ private: std::vector _mqttReceivers; bool _restartOnDisconnect = false; bool _checkUpdates = false; - bool _reconnectNetworkOnMqttDisconnect = false; bool _firstConnect = true; bool _publishDebugInfo = false; bool _logIp = true; diff --git a/src/NukiOpenerWrapper.cpp b/src/NukiOpenerWrapper.cpp index 3ae17a7..c78e0ed 100644 --- a/src/NukiOpenerWrapper.cpp +++ b/src/NukiOpenerWrapper.cpp @@ -1772,21 +1772,21 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json.containsKey("code")) code = json["code"].as(); + if(json["code"].is()) code = json["code"].as(); else code = 12; - if(json.containsKey("enabled")) enabled = json["enabled"].as(); + if(json["enabled"].is()) enabled = json["enabled"].as(); else enabled = 2; - if(json.containsKey("timeLimited")) timeLimited = json["timeLimited"].as(); + if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); else timeLimited = 2; - if(json.containsKey("name")) name = json["name"].as(); - if(json.containsKey("allowedFrom")) allowedFrom = json["allowedFrom"].as(); - if(json.containsKey("allowedUntil")) allowedUntil = json["allowedUntil"].as(); - if(json.containsKey("allowedWeekdays")) allowedWeekdays = json["allowedWeekdays"].as(); - if(json.containsKey("allowedFromTime")) allowedFromTime = json["allowedFromTime"].as(); - if(json.containsKey("allowedUntilTime")) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["name"].is()) name = json["name"].as(); + if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); + if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); + if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); + if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); + if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); if(action) { @@ -2209,12 +2209,12 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) String lockAction; NukiOpener::LockAction timeControlLockAction; - if(json.containsKey("enabled")) enabled = json["enabled"].as(); + if(json["enabled"].is()) enabled = json["enabled"].as(); else enabled = 2; - if(json.containsKey("weekdays")) weekdays = json["weekdays"].as(); - if(json.containsKey("time")) time = json["time"].as(); - if(json.containsKey("lockAction")) lockAction = json["lockAction"].as(); + if(json["weekdays"].is()) weekdays = json["weekdays"].as(); + if(json["time"].is()) time = json["time"].as(); + if(json["lockAction"].is()) lockAction = json["lockAction"].as(); if(lockAction.length() > 0) { @@ -2443,22 +2443,22 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json.containsKey("remoteAllowed")) remoteAllowed = json["remoteAllowed"].as(); + if(json["remoteAllowed"].is()) remoteAllowed = json["remoteAllowed"].as(); else remoteAllowed = 2; - if(json.containsKey("enabled")) enabled = json["enabled"].as(); + if(json["enabled"].is()) enabled = json["enabled"].as(); else enabled = 2; - if(json.containsKey("timeLimited")) timeLimited = json["timeLimited"].as(); + if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); else timeLimited = 2; - if(json.containsKey("name")) name = json["name"].as(); - //if(json.containsKey("sharedKey")) sharedKey = json["sharedKey"].as(); - if(json.containsKey("allowedFrom")) allowedFrom = json["allowedFrom"].as(); - if(json.containsKey("allowedUntil")) allowedUntil = json["allowedUntil"].as(); - if(json.containsKey("allowedWeekdays")) allowedWeekdays = json["allowedWeekdays"].as(); - if(json.containsKey("allowedFromTime")) allowedFromTime = json["allowedFromTime"].as(); - if(json.containsKey("allowedUntilTime")) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["name"].is()) name = json["name"].as(); + //if(json["sharedKey"].is()) sharedKey = json["sharedKey"].as(); + if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); + if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); + if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); + if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); + if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); if(action) { diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index 7cc1092..8ed2900 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -1,3 +1,6 @@ +#ifndef CONFIG_IDF_TARGET_ESP32H2 +#include "esp_wifi.h" +#endif #include "NukiWrapper.h" #include "PreferencesKeys.h" #include "MqttTopics.h" @@ -58,8 +61,24 @@ void NukiWrapper::initialize(const bool& firstStart) if(firstStart) { Log->println("First start, setting preference defaults"); - _preferences->putBool(preference_network_wifi_fallback_disabled, false); - _preferences->putBool(preference_find_best_rssi, false); + + #ifndef CONFIG_IDF_TARGET_ESP32H2 + wifi_config_t wifi_cfg; + if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to get Wi-Fi configuration in RAM"); + } + + if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { + Log->println("Failed to set storage Wi-Fi"); + } + + memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); + memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); + + if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to clear NVS Wi-Fi configuration"); + } + #endif _preferences->putBool(preference_check_updates, true); _preferences->putBool(preference_opener_continuous_mode, false); _preferences->putBool(preference_official_hybrid_enabled, false); @@ -1894,21 +1913,21 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json.containsKey("code")) code = json["code"].as(); + if(json["code"].is()) code = json["code"].as(); else code = 12; - if(json.containsKey("enabled")) enabled = json["enabled"].as(); + if(json["enabled"].is()) enabled = json["enabled"].as(); else enabled = 2; - if(json.containsKey("timeLimited")) timeLimited = json["timeLimited"].as(); + if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); else timeLimited = 2; - if(json.containsKey("name")) name = json["name"].as(); - if(json.containsKey("allowedFrom")) allowedFrom = json["allowedFrom"].as(); - if(json.containsKey("allowedUntil")) allowedUntil = json["allowedUntil"].as(); - if(json.containsKey("allowedWeekdays")) allowedWeekdays = json["allowedWeekdays"].as(); - if(json.containsKey("allowedFromTime")) allowedFromTime = json["allowedFromTime"].as(); - if(json.containsKey("allowedUntilTime")) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["name"].is()) name = json["name"].as(); + if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); + if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); + if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); + if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); + if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); if(action) { @@ -2331,12 +2350,12 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) String lockAction; NukiLock::LockAction timeControlLockAction; - if(json.containsKey("enabled")) enabled = json["enabled"].as(); + if(json["enabled"].is()) enabled = json["enabled"].as(); else enabled = 2; - if(json.containsKey("weekdays")) weekdays = json["weekdays"].as(); - if(json.containsKey("time")) time = json["time"].as(); - if(json.containsKey("lockAction")) lockAction = json["lockAction"].as(); + if(json["weekdays"].is()) weekdays = json["weekdays"].as(); + if(json["time"].is()) time = json["time"].as(); + if(json["lockAction"].is()) lockAction = json["lockAction"].as(); if(lockAction.length() > 0) { @@ -2567,22 +2586,22 @@ void NukiWrapper::onAuthCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json.containsKey("remoteAllowed")) remoteAllowed = json["remoteAllowed"].as(); + if(json["remoteAllowed"].is()) remoteAllowed = json["remoteAllowed"].as(); else remoteAllowed = 2; - if(json.containsKey("enabled")) enabled = json["enabled"].as(); + if(json["enabled"].is()) enabled = json["enabled"].as(); else enabled = 2; - if(json.containsKey("timeLimited")) timeLimited = json["timeLimited"].as(); + if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); else timeLimited = 2; - if(json.containsKey("name")) name = json["name"].as(); - //if(json.containsKey("sharedKey")) sharedKey = json["sharedKey"].as(); - if(json.containsKey("allowedFrom")) allowedFrom = json["allowedFrom"].as(); - if(json.containsKey("allowedUntil")) allowedUntil = json["allowedUntil"].as(); - if(json.containsKey("allowedWeekdays")) allowedWeekdays = json["allowedWeekdays"].as(); - if(json.containsKey("allowedFromTime")) allowedFromTime = json["allowedFromTime"].as(); - if(json.containsKey("allowedUntilTime")) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["name"].is()) name = json["name"].as(); + //if(json["sharedKey"].is()) sharedKey = json["sharedKey"].as(); + if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); + if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); + if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); + if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); + if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); if(action) { diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index fc0cb16..63a7986 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -52,9 +52,10 @@ #define preference_update_from_mqtt (char*)"updMqtt" #define preference_disable_non_json (char*)"disnonjson" #define preference_official_hybrid_enabled (char*)"offHybrid" +#define preference_wifi_ssid (char*)"wifiSSID" +#define preference_wifi_pass (char*)"wifiPass" // CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT -#define preference_find_best_rssi (char*)"nwbestrssi" #define preference_ntw_reconfigure (char*)"ntwRECONF" #define preference_auth_max_entries (char*)"authmaxentry" #define preference_auth_info_enabled (char*)"authInfoEna" @@ -88,14 +89,12 @@ #define preference_command_retry_delay (char*)"rtryDelay" #define preference_query_interval_hybrid_lockstate (char*)"hybridTimer" #define preference_mqtt_hass_cu_url (char*)"hassConfigUrl" -#define preference_network_wifi_fallback_disabled (char*)"nwwififb" #define preference_check_updates (char*)"checkupdates" #define preference_opener_continuous_mode (char*)"openercont" #define preference_rssi_publish_interval (char*)"rssipb" #define preference_network_timeout (char*)"nettmout" #define preference_restart_on_disconnect (char*)"restdisc" #define preference_publish_debug_info (char*)"pubdbg" -#define preference_recon_netw_on_mqtt_discon (char*)"recNtwMqttDis" #define preference_official_hybrid_actions (char*)"hybridAct" #define preference_official_hybrid_retry (char*)"hybridRtry" @@ -118,12 +117,14 @@ #define preference_lock_max_timecontrol_entry_count (char*)"maxtc" #define preference_opener_max_timecontrol_entry_count (char*)"opmaxtc" #define preference_latest_version (char*)"latest" +#define preference_wifi_converted (char*)"wifiConv" //OBSOLETE #define preference_access_level (char*)"accLvl" #define preference_gpio_locking_enabled (char*)"gpiolck" #define preference_network_hardware_gpio (char*)"nwhwdt" #define preference_presence_detection_timeout (char*)"prdtimeout" +#define preference_network_wifi_fallback_disabled (char*)"nwwififb" inline bool initPreferences(Preferences* preferences) { @@ -248,9 +249,15 @@ inline bool initPreferences(Preferences* preferences) if (configVer < 901) { #if defined(CONFIG_IDF_TARGET_ESP32S3) - if (preferences->getInt(preference_network_hardware) == 3) preferences->putInt(preference_network_hardware, 10); + if (preferences->getInt(preference_network_hardware) == 3) + { + preferences->putInt(preference_network_hardware, 10); + } #endif - if (preferences->getInt(preference_network_hardware) == 2) preferences->putInt(preference_network_hardware, 3); + if (preferences->getInt(preference_network_hardware) == 2) + { + preferences->putInt(preference_network_hardware, 3); + } } preferences->putInt(preference_config_version, atof(NUKI_HUB_VERSION) * 100); @@ -271,8 +278,8 @@ private: preference_opener_continuous_mode, preference_mqtt_opener_path, preference_lock_max_keypad_code_count, preference_opener_max_keypad_code_count, preference_lock_max_timecontrol_entry_count, preference_opener_max_timecontrol_entry_count, preference_enable_bootloop_reset, preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, preference_mqtt_hass_discovery, preference_mqtt_hass_cu_url, preference_buffer_size, preference_ip_dhcp_enabled, preference_ip_address, - preference_ip_subnet, preference_ip_gateway, preference_ip_dns_server, preference_network_hardware, preference_network_wifi_fallback_disabled, - preference_rssi_publish_interval, preference_hostname, preference_find_best_rssi, preference_network_timeout, preference_restart_on_disconnect, + preference_ip_subnet, preference_ip_gateway, preference_ip_dns_server, preference_network_hardware, + preference_rssi_publish_interval, preference_hostname, preference_network_timeout, preference_restart_on_disconnect, preference_restart_ble_beacon_lost, preference_query_interval_lockstate, preference_timecontrol_topic_per_entry, preference_keypad_topic_per_entry, preference_query_interval_configuration, preference_query_interval_battery, preference_query_interval_keypad, preference_keypad_control_enabled, preference_keypad_info_enabled, preference_keypad_publish_code, preference_timecontrol_control_enabled, preference_timecontrol_info_enabled, preference_conf_info_enabled, @@ -280,26 +287,26 @@ private: preference_cred_password, preference_disable_non_json, preference_publish_authdata, preference_publish_debug_info, preference_official_hybrid_enabled, preference_query_interval_hybrid_lockstate, preference_official_hybrid_actions, preference_official_hybrid_retry, preference_task_size_network, preference_task_size_nuki, preference_authlog_max_entries, preference_keypad_max_entries, preference_timecontrol_max_entries, - preference_update_from_mqtt, preference_show_secrets, preference_ble_tx_power, preference_recon_netw_on_mqtt_discon, preference_webserial_enabled, + preference_update_from_mqtt, preference_show_secrets, preference_ble_tx_power, preference_webserial_enabled, preference_network_custom_mdc, preference_network_custom_clk, preference_network_custom_phy, preference_network_custom_addr, preference_network_custom_irq, preference_network_custom_rst, preference_network_custom_cs, preference_network_custom_sck, preference_network_custom_miso, preference_network_custom_mosi, preference_network_custom_pwr, preference_network_custom_mdio, preference_ntw_reconfigure, preference_lock_max_auth_entry_count, preference_opener_max_auth_entry_count, - preference_auth_control_enabled, preference_auth_topic_per_entry, preference_auth_info_enabled, preference_auth_max_entries, + preference_auth_control_enabled, preference_auth_topic_per_entry, preference_auth_info_enabled, preference_auth_max_entries, preference_wifi_ssid, preference_wifi_pass }; std::vector _redact = { preference_mqtt_user, preference_mqtt_password, preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, preference_cred_user, preference_cred_password, - preference_nuki_id_lock, preference_nuki_id_opener, + preference_nuki_id_lock, preference_nuki_id_opener, preference_wifi_pass }; std::vector _boolPrefs = { preference_started_before, preference_mqtt_log_enabled, preference_check_updates, preference_lock_enabled, preference_opener_enabled, preference_opener_continuous_mode, - preference_timecontrol_topic_per_entry, preference_keypad_topic_per_entry, preference_enable_bootloop_reset, preference_webserver_enabled, preference_find_best_rssi, + preference_timecontrol_topic_per_entry, preference_keypad_topic_per_entry, preference_enable_bootloop_reset, preference_webserver_enabled, preference_restart_on_disconnect, preference_keypad_control_enabled, preference_keypad_info_enabled, preference_keypad_publish_code, preference_show_secrets, preference_timecontrol_control_enabled, preference_timecontrol_info_enabled, preference_register_as_app, preference_register_opener_as_app, preference_ip_dhcp_enabled, - preference_publish_authdata, preference_publish_debug_info, preference_network_wifi_fallback_disabled, preference_official_hybrid_enabled, + preference_publish_authdata, preference_publish_debug_info, preference_official_hybrid_enabled, preference_official_hybrid_actions, preference_official_hybrid_retry, preference_conf_info_enabled, preference_disable_non_json, preference_update_from_mqtt, - preference_auth_control_enabled, preference_auth_topic_per_entry, preference_auth_info_enabled, preference_recon_netw_on_mqtt_discon, preference_webserial_enabled, + preference_auth_control_enabled, preference_auth_topic_per_entry, preference_auth_info_enabled, preference_webserial_enabled, preference_ntw_reconfigure }; std::vector _bytePrefs = diff --git a/src/RestartReason.h b/src/RestartReason.h index de62376..9d7857d 100644 --- a/src/RestartReason.h +++ b/src/RestartReason.h @@ -33,12 +33,9 @@ enum class RestartReason extern int restartReason; extern uint64_t restartReasonValidDetect; extern bool rebuildGpioRequested; - extern RestartReason currentRestartReason; - extern bool restartReason_isValid; - inline static void restartEsp(RestartReason reason) { if(reason == RestartReason::GpioConfigurationUpdated) diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 8443950..eb72821 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -9,6 +9,7 @@ #endif #ifndef CONFIG_IDF_TARGET_ESP32H2 #include +#include #endif #include @@ -77,12 +78,22 @@ void WebCfgServer::initialize() { _psychicServer->on("/", HTTP_GET, [&](PsychicRequest *request){ if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - #ifndef NUKI_HUB_UPDATER - return buildHtml(request); - #else - return buildOtaHtml(request); + if(!_network->isApOpen()) + { + #ifndef NUKI_HUB_UPDATER + return buildHtml(request); + #else + return buildOtaHtml(request); + #endif + } + #ifndef CONFIG_IDF_TARGET_ESP32H2 + else + { + return buildWifiConnectHtml(request); + } #endif }); + _psychicServer->on("/style.css", HTTP_GET, [&](PsychicRequest *request){ if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); return sendCss(request); @@ -91,128 +102,6 @@ void WebCfgServer::initialize() if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); return sendFavicon(request); }); - #ifndef NUKI_HUB_UPDATER - _psychicServer->on("/import", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - String message = ""; - bool restart = processImport(request, message); - return buildConfirmHtml(request, message, 3, true); - }); - _psychicServer->on("/export", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return sendSettings(request); - }); - _psychicServer->on("/impexpcfg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildImportExportHtml(request); - }); - _psychicServer->on("/status", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildStatusHtml(request); - }); - _psychicServer->on("/acclvl", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildAccLvlHtml(request); - }); - _psychicServer->on("/custntw", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildCustomNetworkConfigHtml(request); - }); - _psychicServer->on("/advanced", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildAdvancedConfigHtml(request); - }); - _psychicServer->on("/cred", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildCredHtml(request); - }); - _psychicServer->on("/mqttconfig", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildMqttConfigHtml(request); - }); - _psychicServer->on("/nukicfg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildNukiConfigHtml(request); - }); - _psychicServer->on("/gpiocfg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildGpioConfigHtml(request); - }); - #ifndef CONFIG_IDF_TARGET_ESP32H2 - _psychicServer->on("/wifi", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildConfigureWifiHtml(request); - }); - _psychicServer->on("/wifimanager", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - if(_allowRestartToPortal) - { - esp_err_t res = buildConfirmHtml(request, "Restarting. Connect to ESP access point to reconfigure Wi-Fi.", 0); - waitAndProcess(false, 1000); - _network->reconfigureDevice(); - return res; - } - return(ESP_OK); - }); - #endif - _psychicServer->on("/unpairlock", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return processUnpair(request, false); - }); - _psychicServer->on("/unpairopener", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return processUnpair(request, true); - }); - _psychicServer->on("/factoryreset", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return processFactoryReset(request); - }); - _psychicServer->on("/infopg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildInfoHtml(request); - }); - _psychicServer->on("/debugon", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - _preferences->putBool(preference_publish_debug_info, true); - return buildConfirmHtml(request, "Debug On", 3, true); - }); - _psychicServer->on("/debugoff", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - _preferences->putBool(preference_publish_debug_info, false); - return buildConfirmHtml(request, "Debug Off", 3, true); - }); - _psychicServer->on("/savecfg", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - String message = ""; - bool restart = processArgs(request, message); - return buildConfirmHtml(request, message, 3, true); - }); - _psychicServer->on("/savegpiocfg", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - processGpioArgs(request); - esp_err_t res = buildConfirmHtml(request, "Saving GPIO configuration. Restarting.", 3, true); - Log->println(F("Restarting")); - waitAndProcess(true, 1000); - restartEsp(RestartReason::GpioConfigurationUpdated); - return res; - }); - #endif - _psychicServer->on("/ota", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildOtaHtml(request); - }); - _psychicServer->on("/otadebug", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return buildOtaHtml(request, true); - }); - _psychicServer->on("/reboottoota", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - esp_err_t res = buildConfirmHtml(request, "Rebooting to other partition", 2, true); - waitAndProcess(true, 1000); - esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)); - restartEsp(RestartReason::OTAReboot); - return res; - }); _psychicServer->on("/reboot", HTTP_GET, [&](PsychicRequest *request){ if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); esp_err_t res = buildConfirmHtml(request, "Rebooting", 2, true); @@ -220,50 +109,371 @@ void WebCfgServer::initialize() restartEsp(RestartReason::RequestedViaWebServer); return res; }); - _psychicServer->on("/autoupdate", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - #ifndef NUKI_HUB_UPDATER - return processUpdate(request); - #else - return request->redirect("/"); - #endif - }); - PsychicUploadHandler *updateHandler = new PsychicUploadHandler(); - updateHandler->onUpload([&](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final) - { + if(_network->isApOpen()) + { + #ifndef CONFIG_IDF_TARGET_ESP32H2 + _psychicServer->on("/ssidlist", HTTP_GET, [&](PsychicRequest *request){ if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return handleOtaUpload(request, filename, index, data, len, final); - } - ); + return buildSSIDListHtml(request); + }); + _psychicServer->on("/savewifi", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + String message = ""; + bool connected = processWiFi(request, message); + esp_err_t res = buildConfirmHtml(request, message, 10, true); - updateHandler->onRequest([&](PsychicRequest *request) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - - String result; - if (!Update.hasError()) - { - Log->print("Update code or data OK Update.errorString() "); - Log->println(Update.errorString()); - result = "Update OK."; - esp_err_t res = request->reply(200,"text/html",result.c_str()); - restartEsp(RestartReason::OTACompleted); + if(connected) + { + waitAndProcess(true, 3000); + restartEsp(RestartReason::ReconfigureWifi); + //abort(); + } return res; - } - else { - result = " Update.errorString() " + String(Update.errorString()); - Log->print("ERROR : error "); - Log->println(result.c_str()); - esp_err_t res = request->reply(500, "text/html", result.c_str()); - restartEsp(RestartReason::OTAAborted); + }); + #endif + } + else + { + #ifndef NUKI_HUB_UPDATER + _psychicServer->on("/import", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + String message = ""; + bool restart = processImport(request, message); + return buildConfirmHtml(request, message, 3, true); + }); + _psychicServer->on("/export", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return sendSettings(request); + }); + _psychicServer->on("/impexpcfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildImportExportHtml(request); + }); + _psychicServer->on("/status", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildStatusHtml(request); + }); + _psychicServer->on("/acclvl", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildAccLvlHtml(request); + }); + _psychicServer->on("/custntw", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildCustomNetworkConfigHtml(request); + }); + _psychicServer->on("/advanced", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildAdvancedConfigHtml(request); + }); + _psychicServer->on("/cred", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildCredHtml(request); + }); + _psychicServer->on("/mqttconfig", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildMqttConfigHtml(request); + }); + _psychicServer->on("/nukicfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildNukiConfigHtml(request); + }); + _psychicServer->on("/gpiocfg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildGpioConfigHtml(request); + }); + #ifndef CONFIG_IDF_TARGET_ESP32H2 + _psychicServer->on("/wifi", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildConfigureWifiHtml(request); + }); + _psychicServer->on("/wifimanager", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + if(_allowRestartToPortal) + { + esp_err_t res = buildConfirmHtml(request, "Restarting. Connect to ESP access point (\"NukiHub\" with password \"NukiHubESP32\") to reconfigure Wi-Fi.", 0); + waitAndProcess(false, 1000); + _network->reconfigureDevice(); + return res; + } + return(ESP_OK); + }); + #endif + _psychicServer->on("/unpairlock", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processUnpair(request, false); + }); + _psychicServer->on("/unpairopener", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processUnpair(request, true); + }); + _psychicServer->on("/factoryreset", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return processFactoryReset(request); + }); + _psychicServer->on("/infopg", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildInfoHtml(request); + }); + _psychicServer->on("/debugon", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _preferences->putBool(preference_publish_debug_info, true); + return buildConfirmHtml(request, "Debug On", 3, true); + }); + _psychicServer->on("/debugoff", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _preferences->putBool(preference_publish_debug_info, false); + return buildConfirmHtml(request, "Debug Off", 3, true); + }); + _psychicServer->on("/savecfg", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + String message = ""; + bool restart = processArgs(request, message); + return buildConfirmHtml(request, message, 3, true); + }); + _psychicServer->on("/savegpiocfg", HTTP_POST, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + processGpioArgs(request); + esp_err_t res = buildConfirmHtml(request, "Saving GPIO configuration. Restarting.", 3, true); + Log->println(F("Restarting")); + waitAndProcess(true, 1000); + restartEsp(RestartReason::GpioConfigurationUpdated); return res; - } - }); + }); + #endif + _psychicServer->on("/ota", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildOtaHtml(request); + }); + _psychicServer->on("/otadebug", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return buildOtaHtml(request, true); + }); + _psychicServer->on("/reboottoota", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + esp_err_t res = buildConfirmHtml(request, "Rebooting to other partition", 2, true); + waitAndProcess(true, 1000); + esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)); + restartEsp(RestartReason::OTAReboot); + return res; + }); + _psychicServer->on("/autoupdate", HTTP_GET, [&](PsychicRequest *request){ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + #ifndef NUKI_HUB_UPDATER + return processUpdate(request); + #else + return request->redirect("/"); + #endif + }); - _psychicServer->on("/uploadota", HTTP_POST, updateHandler); - //Update.onProgress(printProgress); + PsychicUploadHandler *updateHandler = new PsychicUploadHandler(); + updateHandler->onUpload([&](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + return handleOtaUpload(request, filename, index, data, len, final); + } + ); + + updateHandler->onRequest([&](PsychicRequest *request) { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + + String result; + if (!Update.hasError()) + { + Log->print("Update code or data OK Update.errorString() "); + Log->println(Update.errorString()); + result = "Update OK."; + esp_err_t res = request->reply(200,"text/html",result.c_str()); + restartEsp(RestartReason::OTACompleted); + return res; + } + else { + result = " Update.errorString() " + String(Update.errorString()); + Log->print("ERROR : error "); + Log->println(result.c_str()); + esp_err_t res = request->reply(500, "text/html", result.c_str()); + restartEsp(RestartReason::OTAAborted); + return res; + } + }); + + _psychicServer->on("/uploadota", HTTP_POST, updateHandler); + //Update.onProgress(printProgress); + } } +#ifndef CONFIG_IDF_TARGET_ESP32H2 +esp_err_t WebCfgServer::buildSSIDListHtml(PsychicRequest *request) +{ + _network->scan(true, false); + createSsidList(); + + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + + for (int i = 0; i < _ssidList.size(); i++) + { + response.print("" + _ssidList[i] + String(F(" (")) + String(_rssiList[i]) + String(F(" %)")) + ""); + } + return response.endSend(); +} + +void WebCfgServer::createSsidList() +{ + int _foundNetworks = WiFi.scanComplete(); + std::vector _tmpSsidList; + std::vector _tmpRssiList; + + for (int i = 0; i < _foundNetworks; i++) + { + int rssi = constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100); + auto it1 = std::find(_ssidList.begin(), _ssidList.end(), WiFi.SSID(i)); + auto it2 = std::find(_tmpSsidList.begin(), _tmpSsidList.end(), WiFi.SSID(i)); + + if(it1 == _ssidList.end()) + { + _ssidList.push_back(WiFi.SSID(i)); + _rssiList.push_back(rssi); + _tmpSsidList.push_back(WiFi.SSID(i)); + _tmpRssiList.push_back(rssi); + } + else if (it2 == _tmpSsidList.end()) + { + _tmpSsidList.push_back(WiFi.SSID(i)); + _tmpRssiList.push_back(rssi); + int index = it1 - _ssidList.begin(); + _rssiList[index] = rssi; + } + else + { + int index = it1 - _ssidList.begin(); + int index2 = it2 - _tmpSsidList.begin(); + if (_tmpRssiList[index2] < rssi) + { + _tmpRssiList[index2] = rssi; + _rssiList[index] = rssi; + } + } + } +} + +esp_err_t WebCfgServer::buildWifiConnectHtml(PsychicRequest *request) +{ + String header = ""; + PsychicStreamResponse response(request, "text/plain"); + response.beginSend(); + buildHtmlHeader(&response, header); + response.print("

    Available WiFi networks

    "); + response.print(""); + createSsidList(); + for (int i = 0; i < _ssidList.size(); i++) + { + response.print(""); + } + response.print("
    " + _ssidList[i] + String(F(" (")) + String(_rssiList[i]) + String(F(" %)")) + "
    "); + response.print("
    "); + response.print("

    WiFi credentials

    "); + response.print(""); + printInputField(&response, "WIFISSID", "SSID", "", 32, "id=\"inputssid\"", false, true); + printInputField(&response, "WIFIPASS", "Secret key", "", 63, "id=\"inputpass\"", false, true); + response.print("
    "); + response.print("
    "); + response.print("
    "); + response.print("

    "); + response.print(""); + return response.endSend(); +} + +bool WebCfgServer::processWiFi(PsychicRequest *request, String& message) +{ + bool res = false; + int params = request->params(); + String ssid; + String pass; + + for(int index = 0; index < params; index++) + { + const PsychicWebParameter* p = request->getParam(index); + String key = p->name(); + String value = p->value(); + + + if(index < params -1) + { + const PsychicWebParameter* next = request->getParam(index+1); + if(key == next->name()) continue; + } + + if(key == "WIFISSID") + { + ssid = value; + } + else if(key == "WIFIPASS") + { + pass = value; + } + } + + ssid.trim(); + pass.trim(); + + if (ssid.length() > 0 && pass.length() > 0) + { + WiFi.begin(ssid, pass); + + int loop = 0; + while(WiFi.status() != WL_CONNECTED && loop < 150) + { + delay(100); + loop++; + } + + if (WiFi.status() != WL_CONNECTED) + { + message = "Failed to connect to the given SSID with the given secret key, credentials not saved
    "; + } + else + { + message = "Connection successful. Rebooting Nuki Hub.
    "; + if(WiFi.isConnected()) + { + esp_wifi_disconnect(); + } + + wifi_config_t wifi_cfg; + if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to get Wi-Fi configuration in RAM"); + return res; + } + + if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { + Log->println("Failed to set storage Wi-Fi"); + return res; + } + + memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); + memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); + + if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to set Wi-Fi configuration"); + return res; + } + + _preferences->putString(preference_wifi_ssid, ssid); + _preferences->putString(preference_wifi_pass, pass); + + res = true; + } + } + else + { + message = "No SSID or secret key entered, credentials not saved
    "; + } + + return res; +} +#endif + esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) { PsychicStreamResponse response(request, "text/plain"); @@ -648,6 +858,62 @@ String WebCfgServer::generateConfirmCode() return String(code); } +void WebCfgServer::printInputField(PsychicStreamResponse *response, + const char *token, + const char *description, + const char *value, + const size_t& maxLength, + const char *args, + const bool& isPassword, + const bool& showLengthRestriction) +{ + char maxLengthStr[20]; + + itoa(maxLength, maxLengthStr, 10); + + response->print(""); + response->print(description); + + if(showLengthRestriction) + { + response->print(" (Max. "); + response->print(maxLength); + response->print(" characters)"); + } + + response->print(""); + response->print("print(" "); + response->print(args); + } + if(strcmp(value, "") != 0) + { + response->print(" value=\""); + response->print(value); + } + response->print("\" name=\""); + response->print(token); + response->print("\" size=\"25\" maxlength=\""); + response->print(maxLengthStr); + response->print("\"/>"); + response->print(""); +} + +void WebCfgServer::printInputField(PsychicStreamResponse *response, + const char *token, + const char *description, + const int value, + size_t maxLength, + const char *args) +{ + char valueStr[20]; + itoa(value, valueStr, 10); + printInputField(response, token, description, valueStr, maxLength, args); +} + #ifndef NUKI_HUB_UPDATER esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) { @@ -1162,16 +1428,6 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) //configChanged = true; } } - else if(key == "BESTRSSI") - { - if(_preferences->getBool(preference_find_best_rssi, false) != (value == "1")) - { - _preferences->putBool(preference_find_best_rssi, (value == "1")); - Log->print(F("Setting changed: ")); - Log->println(key); - //configChanged = true; - } - } else if(key == "HOSTNAME") { if(_preferences->getString(preference_hostname, "") != value) @@ -1202,16 +1458,6 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) //configChanged = true; } } - else if(key == "RECNWTMQTTDIS") - { - if(_preferences->getBool(preference_recon_netw_on_mqtt_discon, false) != (value == "1")) - { - _preferences->putBool(preference_recon_netw_on_mqtt_discon, (value == "1")); - Log->print(F("Setting changed: ")); - Log->println(key); - //configChanged = true; - } - } else if(key == "MQTTLOG") { if(_preferences->getBool(preference_mqtt_log_enabled, false) != (value == "1")) @@ -2794,13 +3040,10 @@ esp_err_t WebCfgServer::buildMqttConfigHtml(PsychicRequest *request) printTextarea(&response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, true, true); printDropDown(&response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions(), ""); #ifndef CONFIG_IDF_TARGET_ESP32H2 - printCheckBox(&response, "NWHWWIFIFB", "Disable fallback to Wi-Fi / Wi-Fi config portal", _preferences->getBool(preference_network_wifi_fallback_disabled), ""); - printCheckBox(&response, "BESTRSSI", "Connect to AP with the best signal in an environment with multiple APs with the same SSID", _preferences->getBool(preference_find_best_rssi), ""); printInputField(&response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6, ""); #endif printInputField(&response, "NETTIMEOUT", "MQTT Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5, ""); printCheckBox(&response, "RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect), ""); - printCheckBox(&response, "RECNWTMQTTDIS", "Reconnect network on MQTT connection failure", _preferences->getBool(preference_recon_netw_on_mqtt_discon), ""); printCheckBox(&response, "MQTTLOG", "Enable MQTT logging", _preferences->getBool(preference_mqtt_log_enabled), ""); printCheckBox(&response, "CHECKUPDATE", "Check for Firmware Updates every 24h", _preferences->getBool(preference_check_updates), ""); printCheckBox(&response, "UPDATEMQTT", "Allow updating using MQTT", _preferences->getBool(preference_update_from_mqtt), ""); @@ -2945,13 +3188,13 @@ String WebCfgServer::pinStateToString(uint8_t value) { switch(value) { case 0: - return (String)"PIN not set"; + return String("PIN not set"); case 1: - return (String)"PIN valid"; + return String("PIN valid"); case 2: - return (String)"PIN set but invalid";; + return String("PIN set but invalid"); default: - return (String)"Unknown"; + return String("Unknown"); } } @@ -3230,7 +3473,7 @@ esp_err_t WebCfgServer::buildConfigureWifiHtml(PsychicRequest *request) response.beginSend(); buildHtmlHeader(&response); response.print("

    Wi-Fi

    "); - response.print("Click confirm to restart ESP into Wi-Fi configuration mode. After restart, connect to ESP access point to reconfigure Wi-Fi.

    "); + response.print("Click confirm to remove saved WiFi settings and restart ESP into Wi-Fi configuration mode. After restart, connect to ESP access point to reconfigure Wi-Fi.

    "); buildNavigationButton(&response, "Confirm", "/wifimanager"); response.print(""); return response.endSend(); @@ -3366,12 +3609,8 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) } #ifndef CONFIG_IDF_TARGET_ESP32H2 - response.print("\nFallback to Wi-Fi / Wi-Fi config portal disabled: "); - response.print(_preferences->getBool(preference_network_wifi_fallback_disabled, false) ? "Yes" : "No"); if(_network->networkDeviceName() == "Built-in Wi-Fi") { - response.print("\nConnect to AP with the best signal enabled: "); - response.print(_preferences->getBool(preference_find_best_rssi, false) ? "Yes" : "No"); response.print("\nRSSI Publish interval (s): "); if(_preferences->getInt(preference_rssi_publish_interval, 60) < 0) response.print("Disabled"); @@ -3380,8 +3619,6 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) #endif response.print("\nRestart ESP32 on network disconnect enabled: "); response.print(_preferences->getBool(preference_restart_on_disconnect, false) ? "Yes" : "No"); - response.print("\nReconnect network on MQTT connection failure enabled: "); - response.print(_preferences->getBool(preference_recon_netw_on_mqtt_discon, false) ? "Yes" : "No"); response.print("\nMQTT Timeout until restart (s): "); if(_preferences->getInt(preference_network_timeout, 60) < 0) response.print("Disabled"); else response.print(_preferences->getInt(preference_network_timeout, 60)); @@ -3967,11 +4204,6 @@ esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) #ifndef CONFIG_IDF_TARGET_ESP32H2 if(resetWifi) { - wifi_config_t current_conf; - esp_wifi_get_config((wifi_interface_t)ESP_IF_WIFI_STA, ¤t_conf); - memset(current_conf.sta.ssid, 0, sizeof(current_conf.sta.ssid)); - memset(current_conf.sta.password, 0, sizeof(current_conf.sta.password)); - esp_wifi_set_config((wifi_interface_t)ESP_IF_WIFI_STA, ¤t_conf); _network->reconfigureDevice(); } #endif @@ -3981,62 +4213,6 @@ esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) return res; } -void WebCfgServer::printInputField(PsychicStreamResponse *response, - const char *token, - const char *description, - const char *value, - const size_t& maxLength, - const char *args, - const bool& isPassword, - const bool& showLengthRestriction) -{ - char maxLengthStr[20]; - - itoa(maxLength, maxLengthStr, 10); - - response->print(""); - response->print(description); - - if(showLengthRestriction) - { - response->print(" (Max. "); - response->print(maxLength); - response->print(" characters)"); - } - - response->print(""); - response->print("print(" "); - response->print(args); - } - if(strcmp(value, "") != 0) - { - response->print(" value=\""); - response->print(value); - } - response->print("\" name=\""); - response->print(token); - response->print("\" size=\"25\" maxlength=\""); - response->print(maxLengthStr); - response->print("\"/>"); - response->print(""); -} - -void WebCfgServer::printInputField(PsychicStreamResponse *response, - const char *token, - const char *description, - const int value, - size_t maxLength, - const char *args) -{ - char valueStr[20]; - itoa(value, valueStr, 10); - printInputField(response, token, description, valueStr, maxLength, args); -} - void WebCfgServer::printCheckBox(PsychicStreamResponse *response, const char *token, const char *description, const bool value, const char *htmlClass) { response->print(""); @@ -4183,7 +4359,7 @@ const std::vector> WebCfgServer::getNetworkDetectionOp { std::vector> options; - options.push_back(std::make_pair("1", "Wi-Fi only")); + options.push_back(std::make_pair("1", "Wi-Fi")); options.push_back(std::make_pair("2", "Generic W5500")); options.push_back(std::make_pair("3", "M5Stack Atom POE (W5500)")); options.push_back(std::make_pair("10", "M5Stack Atom POE S3 (W5500)")); diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index f9fe598..0a6e75d 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -55,7 +55,7 @@ private: esp_err_t buildCredHtml(PsychicRequest *request); esp_err_t buildImportExportHtml(PsychicRequest *request); esp_err_t buildMqttConfigHtml(PsychicRequest *request); - esp_err_t buildStatusHtml(PsychicRequest *request); + esp_err_t buildStatusHtml(PsychicRequest *request); esp_err_t buildAdvancedConfigHtml(PsychicRequest *request); esp_err_t buildNukiConfigHtml(PsychicRequest *request); esp_err_t buildGpioConfigHtml(PsychicRequest *request); @@ -67,8 +67,6 @@ private: esp_err_t processUnpair(PsychicRequest *request, bool opener); esp_err_t processUpdate(PsychicRequest *request); esp_err_t processFactoryReset(PsychicRequest *request); - void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const char* value, const size_t& maxLength, const char* args, const bool& isPassword = false, const bool& showLengthRestriction = false); - void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const int value, size_t maxLength, const char* args); void printCheckBox(PsychicStreamResponse *response, const char* token, const char* description, const bool value, const char* htmlClass); void printTextarea(PsychicStreamResponse *response, const char *token, const char *description, const char *value, const size_t& maxLength, const bool& enabled = true, const bool& showLengthRestriction = false); void printDropDown(PsychicStreamResponse *response, const char *token, const char *description, const String preselectedValue, std::vector> options, const String className); @@ -94,19 +92,30 @@ private: bool _brokerConfigured = false; bool _rebootRequired = false; #endif - + + std::vector _ssidList; + std::vector _rssiList; String generateConfirmCode(); String _confirmCode = "----"; + esp_err_t buildSSIDListHtml(PsychicRequest *request); esp_err_t buildConfirmHtml(PsychicRequest *request, const String &message, uint32_t redirectDelay = 5, bool redirect = false); esp_err_t buildOtaHtml(PsychicRequest *request, bool debug = false); esp_err_t buildOtaCompletedHtml(PsychicRequest *request); esp_err_t sendCss(PsychicRequest *request); esp_err_t sendFavicon(PsychicRequest *request); + void createSsidList(); void buildHtmlHeader(PsychicStreamResponse *response, String additionalHeader = ""); void waitAndProcess(const bool blocking, const uint32_t duration); esp_err_t handleOtaUpload(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final); void printProgress(size_t prg, size_t sz); - + #ifndef CONFIG_IDF_TARGET_ESP32H2 + esp_err_t buildWifiConnectHtml(PsychicRequest *request); + bool processWiFi(PsychicRequest *request, String& message); + + #endif + void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const char* value, const size_t& maxLength, const char* args, const bool& isPassword = false, const bool& showLengthRestriction = false); + void printInputField(PsychicStreamResponse *response, const char* token, const char* description, const int value, size_t maxLength, const char* args); + PsychicHttpServer* _psychicServer = nullptr; NukiNetwork* _network = nullptr; Preferences* _preferences = nullptr; diff --git a/src/main.cpp b/src/main.cpp index ab5702a..32d6b4e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,7 @@ #include "esp_ota_ops.h" #include "esp_http_client.h" #include "esp_https_ota.h" -#include +#include "esp_task_wdt.h" #include "Config.h" #ifndef NUKI_HUB_UPDATER @@ -78,7 +78,6 @@ TaskHandle_t networkTaskHandle = nullptr; ssize_t write_fn(void* cookie, const char* buf, ssize_t size) { Log->write((uint8_t *)buf, (size_t)size); - return size; } @@ -104,7 +103,7 @@ int _log_vprintf(const char *fmt, va_list args) { void setReroute(){ esp_log_set_vprintf(_log_vprintf); - if(preferences->getBool(preference_mqtt_log_enabled)) + if(preferences->getBool(preference_mqtt_log_enabled)) { esp_log_level_set("*", ESP_LOG_INFO); esp_log_level_set("mqtt", ESP_LOG_NONE); @@ -377,8 +376,11 @@ void setupTasks(bool ota) xTaskCreatePinnedToCore(networkTask, "ntw", preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), NULL, 3, &networkTaskHandle, 1); esp_task_wdt_add(networkTaskHandle); #ifndef NUKI_HUB_UPDATER - xTaskCreatePinnedToCore(nukiTask, "nuki", preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), NULL, 2, &nukiTaskHandle, 0); - esp_task_wdt_add(nukiTaskHandle); + if(!network->isApOpen()) + { + xTaskCreatePinnedToCore(nukiTask, "nuki", preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), NULL, 2, &nukiTaskHandle, 0); + esp_task_wdt_add(nukiTaskHandle); + } #endif } } @@ -434,7 +436,7 @@ void setup() webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); psychicServer->listen(80); - psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); + psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); } #else Log->print(F("Nuki Hub version ")); @@ -453,7 +455,6 @@ void setup() } char16_t buffer_size = preferences->getInt(preference_buffer_size, 4096); - CharBuffer::initialize(buffer_size); gpio = new Gpio(preferences); @@ -461,22 +462,30 @@ void setup() gpio->getConfigurationText(gpioDesc, gpio->pinConfiguration(), "\n\r"); Log->print(gpioDesc.c_str()); + const String mqttLockPath = preferences->getString(preference_mqtt_lock_path); + + network = new NukiNetwork(preferences, gpio, mqttLockPath, CharBuffer::get(), buffer_size); + network->initialize(); + + lockEnabled = preferences->getBool(preference_lock_enabled); + openerEnabled = preferences->getBool(preference_opener_enabled); + + if(network->isApOpen()) + { + forceEnableWebServer = true; + doOta = false; + lockEnabled = false; + openerEnabled = false; + } + bleScanner = new BleScanner::Scanner(); // Scan interval and window according to Nuki recommendations: // https://developer.nuki.io/t/bluetooth-specification-questions/1109/27 bleScanner->initialize("NukiHub", true, 40, 40); bleScanner->setScanDuration(0); - lockEnabled = preferences->getBool(preference_lock_enabled); - openerEnabled = preferences->getBool(preference_opener_enabled); - - const String mqttLockPath = preferences->getString(preference_mqtt_lock_path); - nukiOfficial = new NukiOfficial(preferences); - network = new NukiNetwork(preferences, gpio, mqttLockPath, CharBuffer::get(), buffer_size); - network->initialize(); - networkLock = new NukiNetworkLock(network, nukiOfficial, preferences, CharBuffer::get(), buffer_size); networkLock->initialize(); @@ -533,7 +542,7 @@ void setup() if(doOta) setupTasks(true); else setupTasks(false); - + #ifdef DEBUG_NUKIHUB Log->print("Task Name\tStatus\tPrio\tHWM\tTask\tAffinity\n"); char stats_buffer[1024]; diff --git a/src/networkDevices/EthernetDevice.cpp b/src/networkDevices/EthernetDevice.cpp index 466f569..9a31c7f 100644 --- a/src/networkDevices/EthernetDevice.cpp +++ b/src/networkDevices/EthernetDevice.cpp @@ -3,6 +3,9 @@ #include "../Logger.h" #include "../RestartReason.h" +RTC_NOINIT_ATTR bool criticalEthFailure; +extern char WiFi_fallbackDetect[14]; + EthernetDevice::EthernetDevice(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration, const std::string& deviceName, uint8_t phy_addr, int power, int mdc, int mdio, eth_phy_type_t ethtype, eth_clock_mode_t clock_mode) : NetworkDevice(hostname, ipConfiguration), _deviceName(deviceName), @@ -54,20 +57,34 @@ const String EthernetDevice::deviceName() const void EthernetDevice::initialize() { delay(250); + if(criticalEthFailure) + { + criticalEthFailure = false; + Log->println(F("Failed to initialize ethernet hardware")); + Log->println("Network device has a critical failure, enable fallback to Wi-Fi and reboot."); + strcpy(WiFi_fallbackDetect, "wifi_fallback"); + delay(200); + restartEsp(RestartReason::NetworkDeviceCriticalFailure); + return; + } Log->println(F("Init Ethernet")); if(_useSpi) { Log->println(F("Use SPI")); + criticalEthFailure = true; SPI.begin(_spi_sck, _spi_miso, _spi_mosi); _hardwareInitialized = ETH.begin(_type, _phy_addr, _cs, _irq, _rst, SPI); + criticalEthFailure = false; } #ifdef CONFIG_IDF_TARGET_ESP32 else { Log->println(F("Use RMII")); + criticalEthFailure = true; _hardwareInitialized = ETH.begin(_type, _phy_addr, _mdc, _mdio, _power, _clock_mode); + criticalEthFailure = false; if(!_ipConfiguration->dhcpEnabled()) { _checkIpTs = (esp_timer_get_time() / 1000) + 2000; @@ -78,6 +95,7 @@ void EthernetDevice::initialize() if(_hardwareInitialized) { Log->println(F("Ethernet hardware Initialized")); + memset(WiFi_fallbackDetect, 0, sizeof(WiFi_fallbackDetect)); if(_useSpi && !_ipConfiguration->dhcpEnabled()) { @@ -92,6 +110,11 @@ void EthernetDevice::initialize() else { Log->println(F("Failed to initialize ethernet hardware")); + Log->println("Network device has a critical failure, enable fallback to Wi-Fi and reboot."); + strcpy(WiFi_fallbackDetect, "wifi_fallback"); + delay(200); + restartEsp(RestartReason::NetworkDeviceCriticalFailure); + return; } } @@ -173,25 +196,23 @@ void EthernetDevice::reconfigure() restartEsp(RestartReason::ReconfigureETH); } +void EthernetDevice::scan(bool passive, bool async) +{ +} + bool EthernetDevice::isConnected() { return _connected; } -ReconnectStatus EthernetDevice::reconnect(bool force) +bool EthernetDevice::isApOpen() { - if(!_hardwareInitialized) - { - return ReconnectStatus::CriticalFailure; - } - delay(200); - return isConnected() ? ReconnectStatus::Success : ReconnectStatus::Failure; + return false; } void EthernetDevice::onDisconnected() { if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); - reconnect(); } int8_t EthernetDevice::signalStrength() diff --git a/src/networkDevices/EthernetDevice.h b/src/networkDevices/EthernetDevice.h index 60fb7a2..c68a22e 100644 --- a/src/networkDevices/EthernetDevice.h +++ b/src/networkDevices/EthernetDevice.h @@ -45,19 +45,19 @@ public: virtual void initialize(); virtual void reconfigure(); virtual void update(); - - virtual ReconnectStatus reconnect(bool force = false); + virtual void scan(bool passive = false, bool async = true); virtual bool isConnected(); + virtual bool isApOpen(); int8_t signalStrength() override; - + String localIP() override; String BSSIDstr() override; private: Preferences* _preferences; - + void onDisconnected(); void onNetworkEvent(arduino_event_id_t event, arduino_event_info_t info); diff --git a/src/networkDevices/NetworkDevice.h b/src/networkDevices/NetworkDevice.h index 2c933de..7c32fc4 100644 --- a/src/networkDevices/NetworkDevice.h +++ b/src/networkDevices/NetworkDevice.h @@ -1,13 +1,6 @@ #pragma once #include "IPConfiguration.h" -enum class ReconnectStatus -{ - Failure = 0, - Success = 1, - CriticalFailure = 2 -}; - class NetworkDevice { public: @@ -19,11 +12,12 @@ public: virtual const String deviceName() const = 0; virtual void initialize() = 0; - virtual ReconnectStatus reconnect(bool force = false) = 0; virtual void reconfigure() = 0; virtual void update(); + virtual void scan(bool passive = false, bool async = true) = 0; virtual bool isConnected() = 0; + virtual bool isApOpen() = 0; virtual int8_t signalStrength() = 0; virtual String localIP() = 0; diff --git a/src/networkDevices/WifiDevice.cpp b/src/networkDevices/WifiDevice.cpp index c986b5b..3012fa1 100644 --- a/src/networkDevices/WifiDevice.cpp +++ b/src/networkDevices/WifiDevice.cpp @@ -1,17 +1,14 @@ +#include "esp_wifi.h" #include #include "WifiDevice.h" #include "../PreferencesKeys.h" #include "../Logger.h" #include "../RestartReason.h" -RTC_NOINIT_ATTR char WiFiDevice_reconfdetect[17]; - WifiDevice::WifiDevice(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration) : NetworkDevice(hostname, ipConfiguration), - _preferences(preferences), - _wm(preferences->getString(preference_cred_user, "").c_str(), preferences->getString(preference_cred_password, "").c_str()) + _preferences(preferences) { - _startAp = strcmp(WiFiDevice_reconfdetect, "reconfigure_wifi") == 0; } const String WifiDevice::deviceName() const @@ -21,123 +18,399 @@ const String WifiDevice::deviceName() const void WifiDevice::initialize() { - std::vector wm_menu; - wm_menu.push_back("wifi"); - wm_menu.push_back("exit"); - _wm.setEnableConfigPortal(_startAp || !_preferences->getBool(preference_network_wifi_fallback_disabled, false)); - // reduced timeout if ESP is set to restart on disconnect - _wm.setFindBestRSSI(_preferences->getBool(preference_find_best_rssi)); - _wm.setConnectTimeout(20); - _wm.setConfigPortalTimeout(_preferences->getBool(preference_restart_on_disconnect, false) ? 60 * 3 : 60 * 30); - _wm.setShowInfoUpdate(false); - _wm.setMenu(wm_menu); - _wm.setHostname(_hostname); + String ssid = _preferences->getString(preference_wifi_ssid, ""); + String pass = _preferences->getString(preference_wifi_pass, ""); + WiFi.setHostname(_hostname.c_str()); if(!_ipConfiguration->dhcpEnabled()) { - _wm.setSTAStaticIPConfig(_ipConfiguration->ipAddress(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet(), _ipConfiguration->dnsServer()); - } - - _wm.setAPCallback(clearRtcInitVar); - - bool res = false; - bool connectedFromPortal = false; - - if(_startAp) - { - Log->println(F("Opening Wi-Fi configuration portal.")); - res = _wm.startConfigPortal(); - connectedFromPortal = true; - } - else - { - res = _wm.autoConnect(); // password protected ap - } - - if(!res) - { - esp_wifi_disconnect(); - esp_wifi_stop(); - esp_wifi_deinit(); - - Log->println(F("Failed to connect. Wait for ESP restart.")); - delay(1000); - restartEsp(RestartReason::WifiInitFailed); - } - else { - Log->print(F("Wi-Fi connected: ")); - Log->println(WiFi.localIP().toString()); - - if(connectedFromPortal) - { - Log->println(F("Connected using WifiManager portal. Wait for ESP restart.")); - delay(1000); - restartEsp(RestartReason::ConfigurationUpdated); - } + WiFi.config(_ipConfiguration->ipAddress(), _ipConfiguration->dnsServer(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet()); } WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) { if(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - onDisconnected(); + if(!_openAP && !_connecting && _connected) + { + onDisconnected(); + } } - else if(event == ARDUINO_EVENT_WIFI_STA_GOT_IP) + else if(event == ARDUINO_EVENT_WIFI_STA_CONNECTED) { onConnected(); } + else if(event == ARDUINO_EVENT_WIFI_SCAN_DONE) + { + Log->println(F("Wi-Fi scan done")); + _foundNetworks = WiFi.scanComplete(); + + for (int i = 0; i < _foundNetworks; i++) + { + Log->println(String(F("SSID ")) + WiFi.SSID(i) + String(F(" found with RSSI: ")) + + String(WiFi.RSSI(i)) + String(F("(")) + + String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + + String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + + String(F(" and channel: ")) + String(WiFi.channel(i))); + } + + if (_connectOnScanDone && _foundNetworks > 0) + { + connect(); + } + else if (_connectOnScanDone) + { + Log->println("No networks found, restarting scan"); + scan(false, true); + } + else if (_openAP) + { + openAP(); + } + else if(_convertOldWiFi) + { + _convertOldWiFi = false; + _preferences->putBool(preference_wifi_converted, true); + + wifi_config_t wifi_cfg; + if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to get Wi-Fi configuration in RAM"); + } + + if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { + Log->println("Failed to set storage Wi-Fi"); + } + + String tempSSID = String(reinterpret_cast(wifi_cfg.sta.ssid)); + String tempPass = String(reinterpret_cast(wifi_cfg.sta.password)); + tempSSID.trim(); + tempPass.trim(); + bool found = false; + + for (int i = 0; i < _foundNetworks; i++) + { + if(tempSSID.length() > 0 && tempSSID == WiFi.SSID(i) && tempPass.length() > 0) + { + ssid = tempSSID; + pass = tempPass; + _preferences->putString(preference_wifi_ssid, ssid); + _preferences->putString(preference_wifi_pass, pass); + found = true; + break; + } + } + + memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); + memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); + + if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to clear NVS Wi-Fi configuration"); + } + + if(found) + { + Log->println(String("Attempting to connect to saved SSID ") + String(ssid)); + _connectOnScanDone = true; + _openAP = false; + scan(false, true); + } + else + { + Log->println("No SSID or Wifi password saved, opening AP"); + _connectOnScanDone = false; + _openAP = true; + scan(false, true); + } + } + } }); + + ssid.trim(); + pass.trim(); + + if(ssid.length() > 0 && ssid != "~" && pass.length() > 0) + { + Log->println(String("Attempting to connect to saved SSID ") + String(ssid)); + _connectOnScanDone = true; + _openAP = false; + scan(false, true); + } + else + { + if(!_preferences->getBool(preference_wifi_converted, false)) + { + _connectOnScanDone = false; + _openAP = false; + _convertOldWiFi = true; + scan(false, true); + } + + ssid.trim(); + pass.trim(); + + if(ssid.length() > 0 && ssid != "~" && pass.length() > 0) + { + Log->println(String("Attempting to connect to saved SSID ") + String(ssid)); + _connectOnScanDone = true; + _openAP = false; + scan(false, true); + } + else + { + Log->println("No SSID or Wifi password saved, opening AP"); + _connectOnScanDone = false; + _openAP = true; + scan(false, true); + } + } +} + +void WifiDevice::scan(bool passive, bool async) +{ + WiFi.scanDelete(); + + if(async) + { + Log->println(F("Wi-Fi async scan started")); + } + else + { + Log->println(F("Wi-Fi sync scan started")); + } + if(passive) + { + WiFi.scanNetworks(async,false,true,75U); + } + else + { + WiFi.scanNetworks(async); + } +} + +void WifiDevice::openAP() +{ + if(_startAP) + { + WiFi.persistent(false); + WiFi.mode(WIFI_AP_STA); + WiFi.persistent(false); + WiFi.softAPsetHostname(_hostname.c_str()); + WiFi.softAP("NukiHub", "NukiHubESP32"); + WiFi.persistent(false); + _startAP = false; + } +} + +bool WifiDevice::connect() +{ + bool ret = false; + String ssid = _preferences->getString(preference_wifi_ssid, ""); + String pass = _preferences->getString(preference_wifi_pass, ""); + WiFi.persistent(false); + WiFi.mode(WIFI_STA); + WiFi.setHostname(_hostname.c_str()); + delay(500); + + int bestConnection = -1; + for (int i = 0; i < _foundNetworks; i++) + { + if (ssid == WiFi.SSID(i)) + { + Log->println(String(F("Saved SSID ")) + ssid + String(F(" found with RSSI: ")) + + String(WiFi.RSSI(i)) + String(F("(")) + + String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + + String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + + String(F(" and channel: ")) + String(WiFi.channel(i))); + if (bestConnection == -1) + { + bestConnection = i; + } + else + { + if (WiFi.RSSI(i) > WiFi.RSSI(bestConnection)) + { + bestConnection = i; + } + } + } + } + + if (bestConnection == -1) + { + Log->print("No network found with SSID: "); + Log->println(ssid); + if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); + _connectOnScanDone = true; + _openAP = false; + scan(false, true); + return false; + } + else + { + _connecting = true; + Log->println(String(F("Trying to connect to SSID ")) + ssid + String(F(" found with RSSI: ")) + + String(WiFi.RSSI(bestConnection)) + String(F("(")) + + String(constrain((100.0 + WiFi.RSSI(bestConnection)) * 2, 0, 100)) + + String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(bestConnection) + + String(F(" and channel: ")) + String(WiFi.channel(bestConnection))); + ret = WiFi.begin(ssid.c_str(), pass.c_str(), WiFi.channel(bestConnection), WiFi.BSSID(bestConnection), true); + WiFi.persistent(false); + _connecting = false; + } + + if(!ret) + { + int loop = 0; + + while(!isConnected() && loop < 200) + { + loop++; + delay(100); + } + + if(!isConnected()) + { + esp_wifi_disconnect(); + esp_wifi_stop(); + esp_wifi_deinit(); + + Log->println(F("Failed to connect. Wait for ESP restart.")); + delay(1000); + restartEsp(RestartReason::WifiInitFailed); + } + } + else + { + if(!_preferences->getBool(preference_wifi_converted, false)) + { + _preferences->putBool(preference_wifi_converted, true); + } + + int loop = 0; + + while(!isConnected() && loop < 200) + { + loop++; + delay(100); + } + + if(!isConnected()) + { + if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + { + restartEsp(RestartReason::RestartOnDisconnectWatchdog); + return false; + } + Log->print("Connection failed, retrying"); + _connectOnScanDone = true; + _openAP = false; + scan(false, true); + return false; + } + } + + return ret; } void WifiDevice::reconfigure() { - strcpy(WiFiDevice_reconfdetect, "reconfigure_wifi"); + bool changed = false; + wifi_config_t wifi_cfg; + if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to get Wi-Fi configuration in RAM"); + } + + if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { + Log->println("Failed to set storage Wi-Fi"); + } + + if(sizeof(wifi_cfg.sta.ssid) > 0) + { + memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); + changed = true; + } + if(sizeof(wifi_cfg.sta.password) > 0) + { + memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); + changed = true; + } + if(changed) + { + if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + Log->println("Failed to clear NVS Wi-Fi configuration"); + } + } + + _preferences->putString(preference_wifi_ssid, ""); + _preferences->putString(preference_wifi_pass, ""); delay(200); restartEsp(RestartReason::ReconfigureWifi); } bool WifiDevice::isConnected() { - return WiFi.isConnected(); -} - -ReconnectStatus WifiDevice::reconnect(bool force) -{ - _wm.setFindBestRSSI(_preferences->getBool(preference_find_best_rssi)); - - if((!isConnected() || force) && !_isReconnecting) - { - _isReconnecting = true; - WiFi.disconnect(); - int loop = 0; - - while(isConnected() && loop <20) - { - delay(100); - loop++; - } - - _wm.resetScan(); - _wm.autoConnect(); - _isReconnecting = false; - } - - if(!isConnected() && _disconnectTs > (esp_timer_get_time() / 1000) - 120000) _wm.setEnableConfigPortal(_startAp || !_preferences->getBool(preference_network_wifi_fallback_disabled, false)); - return isConnected() ? ReconnectStatus::Success : ReconnectStatus::Failure; + return (WiFi.status() == WL_CONNECTED); } void WifiDevice::onConnected() { - _isReconnecting = false; - _wm.setEnableConfigPortal(_startAp || !_preferences->getBool(preference_network_wifi_fallback_disabled, false)); + Log->println(F("Wi-Fi connected")); + _connectedChannel = WiFi.channel(); + _connectedBSSID = WiFi.BSSID(); + _connected = true; } void WifiDevice::onDisconnected() { - _disconnectTs = (esp_timer_get_time() / 1000); - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); - _wm.setEnableConfigPortal(false); - reconnect(); + if(_connected) + { + _connected = false; + _disconnectTs = (esp_timer_get_time() / 1000); + Log->println(F("Wi-Fi disconnected")); + + //QUICK RECONNECT + _connecting = true; + String ssid = _preferences->getString(preference_wifi_ssid, ""); + String pass = _preferences->getString(preference_wifi_pass, ""); + WiFi.begin(ssid.c_str(), pass.c_str(), _connectedChannel, _connectedBSSID, true); + WiFi.persistent(false); + + int loop = 0; + + while(!isConnected() && loop < 50) + { + loop++; + delay(100); + } + + _connecting = false; + //END QUICK RECONECT + + if(!isConnected()) + { + if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); + + WiFi.persistent(false); + WiFi.disconnect(true); + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(500); + + wifi_mode_t wifiMode; + esp_wifi_get_mode(&wifiMode); + + while (wifiMode != WIFI_MODE_STA || WiFi.status() == WL_CONNECTED) + { + delay(500); + Log->println(F("Waiting for WiFi mode change or disconnection.")); + esp_wifi_get_mode(&wifiMode); + } + + _connectOnScanDone = true; + _openAP = false; + scan(false, true); + } + } } int8_t WifiDevice::signalStrength() @@ -155,7 +428,7 @@ String WifiDevice::BSSIDstr() return WiFi.BSSIDstr(); } -void WifiDevice::clearRtcInitVar(WiFiManager *) +bool WifiDevice::isApOpen() { - memset(WiFiDevice_reconfdetect, 0, sizeof WiFiDevice_reconfdetect); + return _openAP; } \ No newline at end of file diff --git a/src/networkDevices/WifiDevice.h b/src/networkDevices/WifiDevice.h index 5a9787f..55da63f 100644 --- a/src/networkDevices/WifiDevice.h +++ b/src/networkDevices/WifiDevice.h @@ -4,7 +4,6 @@ #include #include #include "NetworkDevice.h" -#include "WiFiManager.h" #include "IPConfiguration.h" class WifiDevice : public NetworkDevice @@ -16,25 +15,32 @@ public: virtual void initialize(); virtual void reconfigure(); - virtual ReconnectStatus reconnect(bool force = false); + virtual void scan(bool passive = false, bool async = true); virtual bool isConnected(); + virtual bool isApOpen(); int8_t signalStrength() override; String localIP() override; String BSSIDstr() override; - private: - static void clearRtcInitVar(WiFiManager*); - + void openAP(); void onDisconnected(); void onConnected(); + bool connect(); - WiFiManager _wm; Preferences* _preferences = nullptr; - bool _startAp = false; - bool _isReconnecting = false; + int _foundNetworks = 0; + int _disconnectCount = 0; + bool _connectOnScanDone = false; + bool _connecting = false; + bool _openAP = false; + bool _startAP = true; + bool _convertOldWiFi = false; + bool _connected = false; + uint8_t _connectedChannel = 0; + uint8_t* _connectedBSSID; int64_t _disconnectTs = 0; }; diff --git a/updater/platformio.ini b/updater/platformio.ini index 1918816..41ca4ca 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -24,7 +24,8 @@ board_build.embed_txtfiles = build_type = release custom_build = release board_build.partitions = partitions.csv -build_unflags = +build_unflags = + -DESP32 -Werror=all -Wall build_flags = @@ -54,7 +55,7 @@ lib_ignore = WiFiProv lib_deps = PsychicHttp=symlink://../lib/PsychicHttp - WiFiManager=symlink://../lib/WiFiManager + ArduinoJson=symlink://../lib/ArduinoJson monitor_speed = 115200 monitor_filters = From 76e9767840e043b3d06bbde3643162b7d20865d7 Mon Sep 17 00:00:00 2001 From: iranl Date: Tue, 15 Oct 2024 17:02:15 +0200 Subject: [PATCH 13/30] Fix opener electronic strike actuation in HA --- src/NukiNetwork.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index 1aff483..e400742 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -880,7 +880,11 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); - if((int)aclPrefs[2]) json["pl_open"] = openAction; + +` if((strcmp(deviceType, "SmartLock") != 0 && (int)aclPrefs[2]) || (strcmp(deviceType, "SmartLock") == 0 && (int)aclPrefs[11])) + { + json["pl_open"] = openAction; + } json["stat_t"] = String("~") + mqtt_topic_lock_ha_state; json["stat_jam"] = "jammed"; From 03d6baf3893d28e7167f45a2f0553f0d2d5da30b Mon Sep 17 00:00:00 2001 From: iranl Date: Tue, 15 Oct 2024 17:17:18 +0200 Subject: [PATCH 14/30] Update src/NukiNetwork.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Girón --- src/NukiNetwork.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index e400742..f38c4f7 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -881,7 +881,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); -` if((strcmp(deviceType, "SmartLock") != 0 && (int)aclPrefs[2]) || (strcmp(deviceType, "SmartLock") == 0 && (int)aclPrefs[11])) + if((strcmp(deviceType, "SmartLock") != 0 && (int)aclPrefs[2]) || (strcmp(deviceType, "SmartLock") == 0 && (int)aclPrefs[11])) { json["pl_open"] = openAction; } From f61a51fcc44429325dec87caf65e6f5018e13449 Mon Sep 17 00:00:00 2001 From: iranl Date: Tue, 15 Oct 2024 17:22:01 +0200 Subject: [PATCH 15/30] Update NukiNetwork.cpp --- src/NukiNetwork.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index f38c4f7..96e6cbf 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -881,7 +881,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); - if((strcmp(deviceType, "SmartLock") != 0 && (int)aclPrefs[2]) || (strcmp(deviceType, "SmartLock") == 0 && (int)aclPrefs[11])) + if((strcmp(deviceType, "SmartLock") == 0 && (int)aclPrefs[2]) || (strcmp(deviceType, "SmartLock") != 0 && (int)aclPrefs[11])) { json["pl_open"] = openAction; } From 9b50227ea1746548847226a76f39425718f91064 Mon Sep 17 00:00:00 2001 From: iranl Date: Wed, 16 Oct 2024 16:39:17 +0200 Subject: [PATCH 16/30] Update WifiDevice.cpp --- src/networkDevices/WifiDevice.cpp | 57 +++++++++++++------------------ 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/src/networkDevices/WifiDevice.cpp b/src/networkDevices/WifiDevice.cpp index 3012fa1..01e6e35 100644 --- a/src/networkDevices/WifiDevice.cpp +++ b/src/networkDevices/WifiDevice.cpp @@ -69,6 +69,7 @@ void WifiDevice::initialize() } else if(_convertOldWiFi) { + Log->println("Trying to convert old WiFi settings"); _convertOldWiFi = false; _preferences->putBool(preference_wifi_converted, true); @@ -91,10 +92,9 @@ void WifiDevice::initialize() { if(tempSSID.length() > 0 && tempSSID == WiFi.SSID(i) && tempPass.length() > 0) { - ssid = tempSSID; - pass = tempPass; - _preferences->putString(preference_wifi_ssid, ssid); - _preferences->putString(preference_wifi_pass, pass); + _preferences->putString(preference_wifi_ssid, tempSSID); + _preferences->putString(preference_wifi_pass, tempPass); + Log->println("Succesfully converted old WiFi settings"); found = true; break; } @@ -113,6 +113,7 @@ void WifiDevice::initialize() _connectOnScanDone = true; _openAP = false; scan(false, true); + return; } else { @@ -120,6 +121,7 @@ void WifiDevice::initialize() _connectOnScanDone = false; _openAP = true; scan(false, true); + return; } } } @@ -128,40 +130,29 @@ void WifiDevice::initialize() ssid.trim(); pass.trim(); - if(ssid.length() > 0 && ssid != "~" && pass.length() > 0) + if(ssid.length() > 0 && pass.length() > 0) { Log->println(String("Attempting to connect to saved SSID ") + String(ssid)); _connectOnScanDone = true; _openAP = false; scan(false, true); + return; + } + else if(!_preferences->getBool(preference_wifi_converted, false)) + { + _connectOnScanDone = false; + _openAP = false; + _convertOldWiFi = true; + scan(false, true); + return; } else { - if(!_preferences->getBool(preference_wifi_converted, false)) - { - _connectOnScanDone = false; - _openAP = false; - _convertOldWiFi = true; - scan(false, true); - } - - ssid.trim(); - pass.trim(); - - if(ssid.length() > 0 && ssid != "~" && pass.length() > 0) - { - Log->println(String("Attempting to connect to saved SSID ") + String(ssid)); - _connectOnScanDone = true; - _openAP = false; - scan(false, true); - } - else - { - Log->println("No SSID or Wifi password saved, opening AP"); - _connectOnScanDone = false; - _openAP = true; - scan(false, true); - } + Log->println("No SSID or Wifi password saved, opening AP"); + _connectOnScanDone = false; + _openAP = true; + scan(false, true); + return; } } @@ -374,9 +365,9 @@ void WifiDevice::onDisconnected() String pass = _preferences->getString(preference_wifi_pass, ""); WiFi.begin(ssid.c_str(), pass.c_str(), _connectedChannel, _connectedBSSID, true); WiFi.persistent(false); - + int loop = 0; - + while(!isConnected() && loop < 50) { loop++; @@ -395,7 +386,7 @@ void WifiDevice::onDisconnected() WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(500); - + wifi_mode_t wifiMode; esp_wifi_get_mode(&wifiMode); From 93b1d47bdac908d6d750864bd36990ea3514af8f Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 18 Oct 2024 20:36:32 +0200 Subject: [PATCH 17/30] Improve Wifi connection and portal --- src/Config.h | 2 +- src/NukiNetwork.cpp | 17 +-- src/NukiNetwork.h | 2 +- src/WebCfgServer.cpp | 136 +++++++++++++-------- src/WebCfgServer.h | 2 +- src/networkDevices/WifiDevice.cpp | 195 ++++++++++++++---------------- src/networkDevices/WifiDevice.h | 1 + 7 files changed, 193 insertions(+), 162 deletions(-) diff --git a/src/Config.h b/src/Config.h index e302821..fd8bfc4 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.02" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "unknowndate" +#define NUKI_HUB_DATE "2024-10-18" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index 529bcde..b8498bf 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -170,6 +170,11 @@ NetworkDevice *NukiNetwork::device() return _device; } +bool NukiNetwork::isConnected() +{ + return _device->isConnected(); +} + #ifdef NUKI_HUB_UPDATER void NukiNetwork::initialize() { @@ -427,7 +432,7 @@ bool NukiNetwork::update() }); } - if(_logIp && device()->isConnected() && !_device->localIP().equals("0.0.0.0")) + if(_logIp && _device->isConnected() && !_device->localIP().equals("0.0.0.0")) { _logIp = false; Log->print(F("IP: ")); @@ -1083,11 +1088,12 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha baseTopic, _lockPath + mqtt_topic_uptime, deviceType, - "", + "duration", "", "diagnostic", "", - { { (char*)"en", (char*)"true" }}); + { { (char*)"en", (char*)"true" }, + { (char*)"unit_of_meas", (char*)"min"}}); if(_preferences->getBool(preference_mqtt_log_enabled, false)) { @@ -3906,9 +3912,4 @@ String NukiNetwork::localIP() { return _device->localIP(); } - -bool NukiNetwork::isConnected() -{ - return _device->isConnected(); -} #endif \ No newline at end of file diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index 602d74b..79872d7 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -26,6 +26,7 @@ public: void reconfigureDevice(); void scan(bool passive = false, bool async = true); bool isApOpen(); + bool isConnected(); void clearWifiFallback(); const String networkDeviceName() const; @@ -44,7 +45,6 @@ public: void disableAutoRestarts(); // disable on OTA start void disableMqtt(); String localIP(); - bool isConnected(); void subscribe(const char* prefix, const char* path); void initTopic(const char* prefix, const char* path, const char* value); diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index eb72821..ed4d76a 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -302,6 +302,28 @@ void WebCfgServer::initialize() } } +void WebCfgServer::printCheckBox(PsychicStreamResponse *response, const char *token, const char *description, const bool value, const char *htmlClass) +{ + response->print(""); + response->print(description); + response->print(""); + + response->print("print(token); + response->print("\" value=\"0\""); + response->print("/>"); + + response->print("print(token); + + response->print("\" class=\""); + response->print(htmlClass); + + response->print("\" value=\"1\""); + response->print(value ? " checked=\"checked\"" : ""); + response->print("/>"); +} + #ifndef CONFIG_IDF_TARGET_ESP32H2 esp_err_t WebCfgServer::buildSSIDListHtml(PsychicRequest *request) { @@ -377,6 +399,14 @@ esp_err_t WebCfgServer::buildWifiConnectHtml(PsychicRequest *request) printInputField(&response, "WIFISSID", "SSID", "", 32, "id=\"inputssid\"", false, true); printInputField(&response, "WIFIPASS", "Secret key", "", 63, "id=\"inputpass\"", false, true); response.print(""); + response.print("

    IP Address assignment

    "); + response.print(""); + printCheckBox(&response, "DHCPENA", "Enable DHCP", _preferences->getBool(preference_ip_dhcp_enabled), ""); + printInputField(&response, "IPADDR", "Static IP address", _preferences->getString(preference_ip_address).c_str(), 15, ""); + printInputField(&response, "IPSUB", "Subnet", _preferences->getString(preference_ip_subnet).c_str(), 15, ""); + printInputField(&response, "IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15, ""); + printInputField(&response, "DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15, ""); + response.print("
    "); response.print("
    "); response.print(""); response.print("

    "); @@ -412,6 +442,41 @@ bool WebCfgServer::processWiFi(PsychicRequest *request, String& message) { pass = value; } + else if(key == "DHCPENA") + { + if(_preferences->getBool(preference_ip_dhcp_enabled, true) != (value == "1")) + { + _preferences->putBool(preference_ip_dhcp_enabled, (value == "1")); + } + } + else if(key == "IPADDR") + { + if(_preferences->getString(preference_ip_address, "") != value) + { + _preferences->putString(preference_ip_address, value); + } + } + else if(key == "IPSUB") + { + if(_preferences->getString(preference_ip_subnet, "") != value) + { + _preferences->putString(preference_ip_subnet, value); + } + } + else if(key == "IPGTW") + { + if(_preferences->getString(preference_ip_gateway, "") != value) + { + _preferences->putString(preference_ip_gateway, value); + } + } + else if(key == "DNSSRV") + { + if(_preferences->getString(preference_ip_dns_server, "") != value) + { + _preferences->putString(preference_ip_dns_server, value); + } + } } ssid.trim(); @@ -419,55 +484,50 @@ bool WebCfgServer::processWiFi(PsychicRequest *request, String& message) if (ssid.length() > 0 && pass.length() > 0) { + if (_preferences->getBool(preference_ip_dhcp_enabled, true) && _preferences->getString(preference_ip_address, "").length() <= 0) + { + const IPConfiguration* _ipConfiguration = new IPConfiguration(_preferences); + + if(!_ipConfiguration->dhcpEnabled()) + { + WiFi.config(_ipConfiguration->ipAddress(), _ipConfiguration->dnsServer(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet()); + } + } + WiFi.begin(ssid, pass); int loop = 0; - while(WiFi.status() != WL_CONNECTED && loop < 150) + while(!_network->isConnected() && loop < 150) { delay(100); loop++; } - if (WiFi.status() != WL_CONNECTED) + if (!_network->isConnected()) { message = "Failed to connect to the given SSID with the given secret key, credentials not saved
    "; + return res; } else { - message = "Connection successful. Rebooting Nuki Hub.
    "; - if(WiFi.isConnected()) + if(_network->isConnected()) { - esp_wifi_disconnect(); + message = "Connection successful. Rebooting Nuki Hub.
    "; + _preferences->putString(preference_wifi_ssid, ssid); + _preferences->putString(preference_wifi_pass, pass); + res = true; } - - wifi_config_t wifi_cfg; - if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { - Log->println("Failed to get Wi-Fi configuration in RAM"); + else + { + message = "Failed to connect to the given SSID, no IP received, credentials not saved
    "; return res; } - - if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { - Log->println("Failed to set storage Wi-Fi"); - return res; - } - - memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); - memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); - - if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { - Log->println("Failed to set Wi-Fi configuration"); - return res; - } - - _preferences->putString(preference_wifi_ssid, ssid); - _preferences->putString(preference_wifi_pass, pass); - - res = true; } } else { message = "No SSID or secret key entered, credentials not saved
    "; + return res; } return res; @@ -4213,28 +4273,6 @@ esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) return res; } -void WebCfgServer::printCheckBox(PsychicStreamResponse *response, const char *token, const char *description, const bool value, const char *htmlClass) -{ - response->print(""); - response->print(description); - response->print(""); - - response->print("print(token); - response->print("\" value=\"0\""); - response->print("/>"); - - response->print("print(token); - - response->print("\" class=\""); - response->print(htmlClass); - - response->print("\" value=\"1\""); - response->print(value ? " checked=\"checked\"" : ""); - response->print("/>"); -} - void WebCfgServer::printTextarea(PsychicStreamResponse *response, const char *token, const char *description, diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 0a6e75d..e9be8b9 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -67,7 +67,6 @@ private: esp_err_t processUnpair(PsychicRequest *request, bool opener); esp_err_t processUpdate(PsychicRequest *request); esp_err_t processFactoryReset(PsychicRequest *request); - void printCheckBox(PsychicStreamResponse *response, const char* token, const char* description, const bool value, const char* htmlClass); void printTextarea(PsychicStreamResponse *response, const char *token, const char *description, const char *value, const size_t& maxLength, const bool& enabled = true, const bool& showLengthRestriction = false); void printDropDown(PsychicStreamResponse *response, const char *token, const char *description, const String preselectedValue, std::vector> options, const String className); void buildNavigationButton(PsychicStreamResponse *response, const char* caption, const char* targetPath, const char* labelText = ""); @@ -107,6 +106,7 @@ private: void buildHtmlHeader(PsychicStreamResponse *response, String additionalHeader = ""); void waitAndProcess(const bool blocking, const uint32_t duration); esp_err_t handleOtaUpload(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final); + void printCheckBox(PsychicStreamResponse *response, const char* token, const char* description, const bool value, const char* htmlClass); void printProgress(size_t prg, size_t sz); #ifndef CONFIG_IDF_TARGET_ESP32H2 esp_err_t buildWifiConnectHtml(PsychicRequest *request); diff --git a/src/networkDevices/WifiDevice.cpp b/src/networkDevices/WifiDevice.cpp index 01e6e35..5c7e893 100644 --- a/src/networkDevices/WifiDevice.cpp +++ b/src/networkDevices/WifiDevice.cpp @@ -22,20 +22,24 @@ void WifiDevice::initialize() String pass = _preferences->getString(preference_wifi_pass, ""); WiFi.setHostname(_hostname.c_str()); - if(!_ipConfiguration->dhcpEnabled()) - { - WiFi.config(_ipConfiguration->ipAddress(), _ipConfiguration->dnsServer(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet()); - } - WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) { - if(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) + if(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED || event == ARDUINO_EVENT_WIFI_STA_STOP) { if(!_openAP && !_connecting && _connected) { onDisconnected(); + _hasIP = false; } } + else if(event == ARDUINO_EVENT_WIFI_STA_GOT_IP) + { + _hasIP = true; + } + else if(event == ARDUINO_EVENT_WIFI_STA_LOST_IP) + { + _hasIP = false; + } else if(event == ARDUINO_EVENT_WIFI_STA_CONNECTED) { onConnected(); @@ -117,10 +121,7 @@ void WifiDevice::initialize() } else { - Log->println("No SSID or Wifi password saved, opening AP"); - _connectOnScanDone = false; - _openAP = true; - scan(false, true); + restartEsp(RestartReason::ReconfigureWifi); return; } } @@ -158,23 +159,28 @@ void WifiDevice::initialize() void WifiDevice::scan(bool passive, bool async) { - WiFi.scanDelete(); + if(!_connecting) + { + WiFi.scanDelete(); + WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); + WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); - if(async) - { - Log->println(F("Wi-Fi async scan started")); - } - else - { - Log->println(F("Wi-Fi sync scan started")); - } - if(passive) - { - WiFi.scanNetworks(async,false,true,75U); - } - else - { - WiFi.scanNetworks(async); + if(async) + { + Log->println(F("Wi-Fi async scan started")); + } + else + { + Log->println(F("Wi-Fi sync scan started")); + } + if(passive) + { + WiFi.scanNetworks(async,false,true,75U); + } + else + { + WiFi.scanNetworks(async); + } } } @@ -182,12 +188,9 @@ void WifiDevice::openAP() { if(_startAP) { - WiFi.persistent(false); WiFi.mode(WIFI_AP_STA); - WiFi.persistent(false); WiFi.softAPsetHostname(_hostname.c_str()); WiFi.softAP("NukiHub", "NukiHubESP32"); - WiFi.persistent(false); _startAP = false; } } @@ -197,7 +200,6 @@ bool WifiDevice::connect() bool ret = false; String ssid = _preferences->getString(preference_wifi_ssid, ""); String pass = _preferences->getString(preference_wifi_pass, ""); - WiFi.persistent(false); WiFi.mode(WIFI_STA); WiFi.setHostname(_hostname.c_str()); delay(500); @@ -239,99 +241,75 @@ bool WifiDevice::connect() else { _connecting = true; + esp_wifi_scan_stop(); Log->println(String(F("Trying to connect to SSID ")) + ssid + String(F(" found with RSSI: ")) + String(WiFi.RSSI(bestConnection)) + String(F("(")) + String(constrain((100.0 + WiFi.RSSI(bestConnection)) * 2, 0, 100)) + String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(bestConnection) + String(F(" and channel: ")) + String(WiFi.channel(bestConnection))); - ret = WiFi.begin(ssid.c_str(), pass.c_str(), WiFi.channel(bestConnection), WiFi.BSSID(bestConnection), true); - WiFi.persistent(false); - _connecting = false; - } - - if(!ret) - { - int loop = 0; - - while(!isConnected() && loop < 200) + + + if(!_ipConfiguration->dhcpEnabled()) { - loop++; - delay(100); + WiFi.config(_ipConfiguration->ipAddress(), _ipConfiguration->dnsServer(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet()); } - - if(!isConnected()) + + WiFi.begin(ssid, pass); + auto status = WiFi.waitForConnectResult(10000); + + switch (status) { - esp_wifi_disconnect(); - esp_wifi_stop(); - esp_wifi_deinit(); - - Log->println(F("Failed to connect. Wait for ESP restart.")); - delay(1000); - restartEsp(RestartReason::WifiInitFailed); + case WL_CONNECTED: + Log->println("WiFi connected"); + break; + case WL_NO_SSID_AVAIL: + Log->println("WiFi SSID not available"); + break; + case WL_CONNECT_FAILED: + Log->println("WiFi connection failed"); + break; + case WL_IDLE_STATUS: + Log->println("WiFi changing status"); + break; + case WL_DISCONNECTED: + Log->println("WiFi disconnected"); + break; + default: + Log->println("WiFi timeout"); + break; } - } - else - { - if(!_preferences->getBool(preference_wifi_converted, false)) - { - _preferences->putBool(preference_wifi_converted, true); - } - - int loop = 0; - - while(!isConnected() && loop < 200) - { - loop++; - delay(100); - } - - if(!isConnected()) + + if (status != WL_CONNECTED) { if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) { restartEsp(RestartReason::RestartOnDisconnectWatchdog); + _connecting = false; return false; } - Log->print("Connection failed, retrying"); + Log->println("Retrying"); _connectOnScanDone = true; _openAP = false; scan(false, true); + _connecting = false; return false; } + else + { + if(!_preferences->getBool(preference_wifi_converted, false)) + { + _preferences->putBool(preference_wifi_converted, true); + } + _connecting = false; + return true; + } } - return ret; + return false; } void WifiDevice::reconfigure() { - bool changed = false; - wifi_config_t wifi_cfg; - if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { - Log->println("Failed to get Wi-Fi configuration in RAM"); - } - - if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { - Log->println("Failed to set storage Wi-Fi"); - } - - if(sizeof(wifi_cfg.sta.ssid) > 0) - { - memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); - changed = true; - } - if(sizeof(wifi_cfg.sta.password) > 0) - { - memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); - changed = true; - } - if(changed) - { - if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { - Log->println("Failed to clear NVS Wi-Fi configuration"); - } - } - _preferences->putString(preference_wifi_ssid, ""); _preferences->putString(preference_wifi_pass, ""); delay(200); @@ -340,7 +318,16 @@ void WifiDevice::reconfigure() bool WifiDevice::isConnected() { - return (WiFi.status() == WL_CONNECTED); + if (WiFi.status() != WL_CONNECTED) + { + return false; + } + if (!_hasIP) + { + return false; + } + + return true; } void WifiDevice::onConnected() @@ -363,12 +350,17 @@ void WifiDevice::onDisconnected() _connecting = true; String ssid = _preferences->getString(preference_wifi_ssid, ""); String pass = _preferences->getString(preference_wifi_pass, ""); - WiFi.begin(ssid.c_str(), pass.c_str(), _connectedChannel, _connectedBSSID, true); - WiFi.persistent(false); + + if(!_ipConfiguration->dhcpEnabled()) + { + WiFi.config(_ipConfiguration->ipAddress(), _ipConfiguration->dnsServer(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet()); + } + + WiFi.begin(ssid, pass); int loop = 0; - while(!isConnected() && loop < 50) + while(!isConnected() && loop < 200) { loop++; delay(100); @@ -381,7 +373,6 @@ void WifiDevice::onDisconnected() { if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); - WiFi.persistent(false); WiFi.disconnect(true); WiFi.mode(WIFI_STA); WiFi.disconnect(); diff --git a/src/networkDevices/WifiDevice.h b/src/networkDevices/WifiDevice.h index 55da63f..50e9d25 100644 --- a/src/networkDevices/WifiDevice.h +++ b/src/networkDevices/WifiDevice.h @@ -40,6 +40,7 @@ private: bool _startAP = true; bool _convertOldWiFi = false; bool _connected = false; + bool _hasIP = false; uint8_t _connectedChannel = 0; uint8_t* _connectedBSSID; int64_t _disconnectTs = 0; From e6dd7646e54a37e365ab46cda5efc97b85d9814e Mon Sep 17 00:00:00 2001 From: bcutter Date: Sat, 19 Oct 2024 21:41:55 +0200 Subject: [PATCH 18/30] Add requirement for Nuki Lock pairing With "Bluetooth Pairing" disabled for a Nuki Lock, pairing Nuki Hub via button is not possible. Readme reflects this now. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d72b5a7..e29015a 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ In that case leave all fields starting with "MQTT SSL" blank. Otherwise see the ## Pairing with a Nuki Lock or Opener -Enable pairing mode on the Nuki Lock or Opener (press the button on the Nuki device for a few seconds) and power on the ESP32.
    +Enable pairing mode on the Nuki Lock or Opener (press the button on the Nuki device for a few seconds - make sure Bluetooth pairing is allowed for the Nuki Lock button before) and power on the ESP32.
    Pairing should be automatic.

    When pairing is successful, the web interface should show "Paired: Yes".
    From 915af5bc83765b3e427e7fbde282bac458ede789 Mon Sep 17 00:00:00 2001 From: iranl Date: Sat, 19 Oct 2024 22:19:21 +0200 Subject: [PATCH 19/30] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e29015a..bcf8281 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,9 @@ In that case leave all fields starting with "MQTT SSL" blank. Otherwise see the ## Pairing with a Nuki Lock or Opener -Enable pairing mode on the Nuki Lock or Opener (press the button on the Nuki device for a few seconds - make sure Bluetooth pairing is allowed for the Nuki Lock button before) and power on the ESP32.
    -Pairing should be automatic.
    +Make sure "Bluetooth pairing" is enabled for the Nuki device by enabling this setting in the official Nuki App in "Settings" > "Features & Configuration" > "Button and LED". +After enabling the setting press the button on the Nuki device for a few seconds.
    +Pairing should be automatic when the ESP32 is powered on.

    When pairing is successful, the web interface should show "Paired: Yes".
    MQTT nodes like lock state and battery level should now reflect the reported values from the lock.
    From f46b22f1ae04f5ac20bb96d251de39dde30d548c Mon Sep 17 00:00:00 2001 From: iranl Date: Sat, 19 Oct 2024 23:27:51 +0200 Subject: [PATCH 20/30] Fix release workflow --- resources/ota_manifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/ota_manifest.py b/resources/ota_manifest.py index 3e02fff..73eb0db 100644 --- a/resources/ota_manifest.py +++ b/resources/ota_manifest.py @@ -26,7 +26,7 @@ with open('ota/manifest.json', 'r+') as json_file: data[args.ota_type]['version'] = "No beta available" data[args.ota_type]['fullversion'] = "No beta available" data[args.ota_type]['build'] = "" - del(data[args.ota_type]['number']) + data[args.ota_type]['number'] = "0" else: if ("number" not in data[args.ota_type]): data[args.ota_type]['number'] = 1 From c60182737fc80c8aba1c0642dbd71e18b20aa8f5 Mon Sep 17 00:00:00 2001 From: iranl Date: Sat, 19 Oct 2024 23:51:02 +0200 Subject: [PATCH 21/30] Update README.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index bcf8281..181bbb1 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,21 @@ In a browser navigate to the IP address assigned to the ESP32. - Gpio [2-33]: See the "[GPIO lock control](#gpio-lock-control-optional)" section of this README. +### Import/Export Configuration + +The "Import/Export Configuration" menu option allows the importing and exporting of the NukiHub settings in JSON format.
    +
    +Create a (partial) backup of the current NukiHub settings by selecting any of the following:
    +- Basic export: Will backup all settings that are not considered confidential (as such passwords and pincodes are not included in this export). +- Export with redacted settings: Will backup basic settings and redacted settings such as passwords and pincodes. + +Both of the above options will not backup pairing data, so you will have to manually pair Nuki devices when importing this export on a factory reset or new device. + +- Export with redacted settings and pairing data: Will backup all settings and pairing data. Can be used to completely restore a factory reset or new device based on the settings of this device. (Re)pairing Nuki devices will not be needed when importing this export. +
    +To import settings copy and paste the contents of the JSON file that is created by any of the above export options and select "Import". +After importing the device will reboot. + ## Exposed MQTT Topics ### Lock From 9c75e30fefd99b34c05f596babe39b4172474d1e Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 20 Oct 2024 00:13:22 +0200 Subject: [PATCH 22/30] Prevent reboot loop on manual web request --- src/Config.h | 2 +- src/WebCfgServer.cpp | 50 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Config.h b/src/Config.h index fd8bfc4..75661ba 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.02" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-10-18" +#define NUKI_HUB_DATE "2024-10-19" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index ed4d76a..7aed68c 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -104,7 +104,23 @@ void WebCfgServer::initialize() }); _psychicServer->on("/reboot", HTTP_GET, [&](PsychicRequest *request){ if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - esp_err_t res = buildConfirmHtml(request, "Rebooting", 2, true); + + String value = ""; + if(request->hasParam("CONFIRMTOKEN")) + { + const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); + if(p->value() != "") value = p->value(); + } + else + { + return buildConfirmHtml(request, "No confirm code set.", 3, true); + } + + if(value != _confirmCode) + { + return request->redirect("/"); + } + esp_err_t res = buildConfirmHtml(request, "Rebooting...", 2, true); waitAndProcess(true, 1000); restartEsp(RestartReason::RequestedViaWebServer); return res; @@ -251,7 +267,22 @@ void WebCfgServer::initialize() }); _psychicServer->on("/reboottoota", HTTP_GET, [&](PsychicRequest *request){ if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - esp_err_t res = buildConfirmHtml(request, "Rebooting to other partition", 2, true); + String value = ""; + if(request->hasParam("CONFIRMTOKEN")) + { + const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); + if(p->value() != "") value = p->value(); + } + else + { + return buildConfirmHtml(request, "No confirm code set.", 3, true); + } + + if(value != _confirmCode) + { + return request->redirect("/"); + } + esp_err_t res = buildConfirmHtml(request, "Rebooting to other partition...", 2, true); waitAndProcess(true, 1000); esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)); restartEsp(RestartReason::OTAReboot); @@ -409,7 +440,9 @@ esp_err_t WebCfgServer::buildWifiConnectHtml(PsychicRequest *request) response.print(""); response.print("
    "); response.print(""); - response.print("

    "); + response.print("

    "); response.print(""); return response.endSend(); } @@ -668,7 +701,9 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) response.print("

    Manually update Nuki Hub

    "); response.print("

    Reboot to Nuki Hub Updater

    "); response.print("Click on the button to reboot to the Nuki Hub updater, where you can select the latest Nuki Hub binary to update"); - response.print("



    "); + response.print("



    "); response.print("

    Update Nuki Hub Updater

    "); response.print("Select the latest Nuki Hub updater binary to update the Nuki Hub updater"); response.print("
    Choose the nuki_hub_updater.bin file to upload:
    "); @@ -678,7 +713,9 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) response.print("
    "); response.print("

    Reboot to Nuki Hub

    "); response.print("Click on the button to reboot to Nuki Hub"); - response.print("


    "); + response.print("



    "); response.print("

    Update Nuki Hub

    "); response.print("Select the latest Nuki Hub binary to update Nuki Hub"); response.print("
    Choose the nuki_hub.bin file to upload:
    "); @@ -2991,7 +3028,8 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request) buildNavigationMenuEntry(&response, "Configure Wi-Fi", "/wifi"); } #endif - buildNavigationMenuEntry(&response, "Reboot Nuki Hub", "/reboot"); + String rebooturl = "/reboot?CONFIRMTOKEN=" + _confirmCode; + buildNavigationMenuEntry(&response, "Reboot Nuki Hub", rebooturl.c_str()); response.print(""); return response.endSend(); } From 7c8dbabb9fbb1224ccc3ab2ea5f08e10219baa63 Mon Sep 17 00:00:00 2001 From: technyon Date: Sun, 20 Oct 2024 06:15:35 +0200 Subject: [PATCH 23/30] fix clion cmake --- clion/CMakeLists.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/clion/CMakeLists.txt b/clion/CMakeLists.txt index d3c9ecb..1a2e7bf 100644 --- a/clion/CMakeLists.txt +++ b/clion/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16.0) -include($ENV{IDF_PATH}/tools/cmake/project.cmake) +# include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(nukihub) add_compile_definitions(CONFIG_IDF_TARGET_ESP32) @@ -32,10 +32,6 @@ set(SRCFILES ../src/Gpio.cpp ../src/Logger.cpp ../src/RestartReason.h - # include/RTOS.h - ../lib/WiFiManager/WiFiManager.cpp - ../lib/WiFiManager/wm_consts_en.h - ../lib/WiFiManager/wm_strings_en.h ../lib/nuki_ble/src/NukiBle.cpp ../lib/nuki_ble/src/NukiBle.hpp ../lib/nuki_ble/src/NukiLock.cpp @@ -63,6 +59,8 @@ file(GLOB_RECURSE SRCFILESREC lib/NimBLE-Arduino/src/*.h lib/ArduinoJson/src/*.h lib/ArduinoJson/src/*.hpp + lib/PsychicHttp/src/*.cpp + lib/PsychicHttp/src/*.h ) add_executable(dummy From a149f2e6b63bac0194f71d5209f371cc06157aab Mon Sep 17 00:00:00 2001 From: technyon Date: Sun, 20 Oct 2024 09:51:45 +0200 Subject: [PATCH 24/30] refactor GPIO detection code --- src/Config.h | 2 +- src/Gpio.cpp | 61 +++++++++++++++++++++++++-------------------- src/Gpio.h | 6 ++--- src/NukiWrapper.cpp | 2 +- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/Config.h b/src/Config.h index 75661ba..ef6633d 100644 --- a/src/Config.h +++ b/src/Config.h @@ -4,7 +4,7 @@ #define NUKI_HUB_VERSION "9.02" #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2024-10-19" +#define NUKI_HUB_DATE "2024-10-20" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/Gpio.cpp b/src/Gpio.cpp index eca41c2..80189ac 100644 --- a/src/Gpio.cpp +++ b/src/Gpio.cpp @@ -10,7 +10,6 @@ #include "networkDevices/W5500Definitions.h" Gpio* Gpio::_inst = nullptr; -const uint Gpio::_debounceTime = GPIO_DEBOUNCE_TIME; Gpio::Gpio(Preferences* preferences) : _preferences(preferences) @@ -29,42 +28,50 @@ Gpio::Gpio(Preferences* preferences) bool Gpio::isTriggered(const PinEntry& entry) { +// Log->println(" ------------ "); + const int threshold = 3; - int state = digitalRead(entry.pin); + uint8_t state = digitalRead(entry.pin); + uint8_t lastState = (_triggerState[entry.pin] & 0x80) >> 7; - if(entry.role == PinRole::GeneralInputPullDown) + uint8_t pinState = _triggerState[entry.pin] & 0x7f; + pinState = pinState << 1 | state; + _triggerState[entry.pin] = (pinState & 0x7f) | lastState << 7; +// Log->print("Trigger state: "); +// Log->println(_triggerState[entry.pin], 2); + + pinState = pinState & 0x07; +// Log->print("Val: "); +// Log->println(pinState); + + + if(pinState != 0x00 && pinState != 0x07) { - state = 1 - state; + return false; } +// Log->print("Last State: "); +// Log->println(lastState); +// Log->print("State: "); +// Log->println(state); - if(state == LOW) + if(state != lastState) { - if (_triggerCount[entry.pin] >= 0) - { - _triggerCount[entry.pin]++; - } - - if (_triggerCount[entry.pin] >= threshold) - { - _triggerCount[entry.pin] = -1; - return true; - } +// Log->print("State changed: "); +// Log->println(state); + _triggerState[entry.pin] = (pinState & 0x7f) | state << 7; } else { - if (_triggerCount[entry.pin] < 0) - { - _triggerCount[entry.pin]--; - - if(_triggerCount[entry.pin] <= -threshold) - { - _triggerCount[entry.pin] = 0; - } - } + return false; } - return false; + if(entry.role == PinRole::GeneralInputPullDown || entry.role == PinRole::GeneralInputPullUp) + { + return true; + } + + return state == LOW; } void Gpio::onTimer() @@ -113,10 +120,10 @@ void Gpio::isrOnTimer() void Gpio::init() { - _inst->_triggerCount.reserve(_inst->availablePins().size()); + _inst->_triggerState.reserve(_inst->availablePins().size()); for(int i=0; i<_inst->availablePins().size(); i++) { - _inst->_triggerCount.push_back(0); + _inst->_triggerState.push_back(0); } bool hasInputPin = false; diff --git a/src/Gpio.h b/src/Gpio.h index 2607df9..be54d16 100644 --- a/src/Gpio.h +++ b/src/Gpio.h @@ -84,6 +84,7 @@ private: void IRAM_ATTR onTimer(); bool IRAM_ATTR isTriggered(const PinEntry& pinEntry); GpioAction IRAM_ATTR getGpioAction(const PinRole& role) const; + static void IRAM_ATTR isrOnTimer(); #if defined(CONFIG_IDF_TARGET_ESP32C3) //Based on https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/peripherals/gpio.html and https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf @@ -127,15 +128,12 @@ private: }; std::vector _pinConfiguration; - static const uint _debounceTime; - - static void IRAM_ATTR isrOnTimer(); std::vector> _callbacks; static Gpio* _inst; - std::vector _triggerCount; + std::vector _triggerState; hw_timer_t* timer = nullptr; Preferences* _preferences = nullptr; diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index 8ed2900..4167aef 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -268,7 +268,7 @@ void NukiWrapper::update() if(_nukiOfficial->getOffCommandExecutedTs() > 0 && ts >= _nukiOfficial->getOffCommandExecutedTs()) { - nukiInst->_nextLockAction = _offCommand; + _nextLockAction = _offCommand; _nukiOfficial->clearOffCommandExecutedTs(); } if(_nextLockAction != (NukiLock::LockAction)0xff) From eb4db32b8919df951e50236452876be1e7d77399 Mon Sep 17 00:00:00 2001 From: technyon Date: Sun, 20 Oct 2024 14:03:18 +0200 Subject: [PATCH 25/30] add astylerc and apply code formatting --- .astylerc | 2 + src/Gpio.cpp | 465 ++++--- src/NukiDeviceId.cpp | 4 +- src/NukiNetwork.cpp | 1360 ++++++++++-------- src/NukiNetworkLock.cpp | 560 ++++---- src/NukiNetworkOpener.cpp | 743 +++++----- src/NukiOfficial.cpp | 25 +- src/NukiOpenerWrapper.cpp | 1775 +++++++++++++++++------ src/NukiPublisher.cpp | 4 +- src/NukiWrapper.cpp | 1776 ++++++++++++++++++------ src/WebCfgServer.cpp | 1012 ++++++++++---- src/main.cpp | 184 ++- src/networkDevices/EthernetDevice.cpp | 172 +-- src/networkDevices/IPConfiguration.cpp | 14 +- src/networkDevices/WifiDevice.cpp | 107 +- src/util/NetworkDeviceInstantiator.cpp | 286 ++-- src/util/NetworkUtil.cpp | 180 +-- 17 files changed, 5755 insertions(+), 2914 deletions(-) create mode 100644 .astylerc diff --git a/.astylerc b/.astylerc new file mode 100644 index 0000000..d31a299 --- /dev/null +++ b/.astylerc @@ -0,0 +1,2 @@ +--style=allman +--add-braces diff --git a/src/Gpio.cpp b/src/Gpio.cpp index 80189ac..8bf9f9e 100644 --- a/src/Gpio.cpp +++ b/src/Gpio.cpp @@ -12,7 +12,7 @@ Gpio* Gpio::_inst = nullptr; Gpio::Gpio(Preferences* preferences) -: _preferences(preferences) + : _preferences(preferences) { _inst = this; loadPinConfiguration(); @@ -80,35 +80,35 @@ void Gpio::onTimer() { switch(entry.role) { - case PinRole::InputLock: - case PinRole::InputUnlock: - case PinRole::InputUnlatch: - case PinRole::InputLockNgo: - case PinRole::InputLockNgoUnlatch: - case PinRole::InputElectricStrikeActuation: - case PinRole::InputActivateRTO: - case PinRole::InputActivateCM: - case PinRole::InputDeactivateRtoCm: - case PinRole::InputDeactivateRTO: - case PinRole::InputDeactivateCM: - case PinRole::GeneralInputPullDown: - case PinRole::GeneralInputPullUp: - if(isTriggered(entry)) - { - _inst->notify(getGpioAction(entry.role), entry.pin); - } - break; - case PinRole::OutputHighLocked: - case PinRole::OutputHighUnlocked: - case PinRole::OutputHighMotorBlocked: - case PinRole::OutputHighRtoActive: - case PinRole::OutputHighCmActive: - case PinRole::OutputHighRtoOrCmActive: - case PinRole::GeneralOutput: - case PinRole::Ethernet: - // ignore. This case should not occur since pins are configured as output - default: - break; + case PinRole::InputLock: + case PinRole::InputUnlock: + case PinRole::InputUnlatch: + case PinRole::InputLockNgo: + case PinRole::InputLockNgoUnlatch: + case PinRole::InputElectricStrikeActuation: + case PinRole::InputActivateRTO: + case PinRole::InputActivateCM: + case PinRole::InputDeactivateRtoCm: + case PinRole::InputDeactivateRTO: + case PinRole::InputDeactivateCM: + case PinRole::GeneralInputPullDown: + case PinRole::GeneralInputPullUp: + if(isTriggered(entry)) + { + _inst->notify(getGpioAction(entry.role), entry.pin); + } + break; + case PinRole::OutputHighLocked: + case PinRole::OutputHighUnlocked: + case PinRole::OutputHighMotorBlocked: + case PinRole::OutputHighRtoActive: + case PinRole::OutputHighCmActive: + case PinRole::OutputHighRtoOrCmActive: + case PinRole::GeneralOutput: + case PinRole::Ethernet: + // ignore. This case should not occur since pins are configured as output + default: + break; } } } @@ -139,37 +139,37 @@ void Gpio::init() switch(entry.role) { - case PinRole::InputLock: - case PinRole::InputUnlock: - case PinRole::InputUnlatch: - case PinRole::InputLockNgo: - case PinRole::InputLockNgoUnlatch: - case PinRole::InputElectricStrikeActuation: - case PinRole::InputActivateRTO: - case PinRole::InputActivateCM: - case PinRole::InputDeactivateRtoCm: - case PinRole::InputDeactivateRTO: - case PinRole::InputDeactivateCM: - case PinRole::GeneralInputPullUp: - pinMode(entry.pin, INPUT_PULLUP); - hasInputPin = true; - break; - case PinRole::GeneralInputPullDown: - pinMode(entry.pin, INPUT_PULLDOWN); - hasInputPin = true; - break; - case PinRole::OutputHighLocked: - case PinRole::OutputHighUnlocked: - case PinRole::OutputHighMotorBlocked: - case PinRole::OutputHighRtoActive: - case PinRole::OutputHighCmActive: - case PinRole::OutputHighRtoOrCmActive: - case PinRole::GeneralOutput: - pinMode(entry.pin, OUTPUT); - break; - case PinRole::Ethernet: - default: - break; + case PinRole::InputLock: + case PinRole::InputUnlock: + case PinRole::InputUnlatch: + case PinRole::InputLockNgo: + case PinRole::InputLockNgoUnlatch: + case PinRole::InputElectricStrikeActuation: + case PinRole::InputActivateRTO: + case PinRole::InputActivateCM: + case PinRole::InputDeactivateRtoCm: + case PinRole::InputDeactivateRTO: + case PinRole::InputDeactivateCM: + case PinRole::GeneralInputPullUp: + pinMode(entry.pin, INPUT_PULLUP); + hasInputPin = true; + break; + case PinRole::GeneralInputPullDown: + pinMode(entry.pin, INPUT_PULLDOWN); + hasInputPin = true; + break; + case PinRole::OutputHighLocked: + case PinRole::OutputHighUnlocked: + case PinRole::OutputHighMotorBlocked: + case PinRole::OutputHighRtoActive: + case PinRole::OutputHighCmActive: + case PinRole::OutputHighRtoOrCmActive: + case PinRole::GeneralOutput: + pinMode(entry.pin, OUTPUT); + break; + case PinRole::Ethernet: + default: + break; } } @@ -221,7 +221,10 @@ void Gpio::loadPinConfiguration() if(std::find(disabledPins.begin(), disabledPins.end(), entry.pin) == disabledPins.end()) { - if(entry.role == PinRole::Ethernet) entry.role = PinRole::Disabled; + if(entry.role == PinRole::Ethernet) + { + entry.role = PinRole::Disabled; + } entry.role = (PinRole) serialized[(i * 2 + 1)]; Log->println("Not found in Ethernet disabled pins"); Log->print(F("Role: ")); @@ -234,7 +237,10 @@ void Gpio::loadPinConfiguration() Log->print(F("Role: ")); Log->println(getRoleDescription(entry.role)); } - if(entry.role != PinRole::Disabled) _pinConfiguration.push_back(entry); + if(entry.role != PinRole::Disabled) + { + _pinConfiguration.push_back(entry); + } } } @@ -244,90 +250,94 @@ const std::vector Gpio::getDisabledPins() const switch(_preferences->getInt(preference_network_hardware, 0)) { - case 2: - disabledPins.push_back(ETH_PHY_CS_GENERIC_W5500); - disabledPins.push_back(ETH_PHY_IRQ_GENERIC_W5500); - disabledPins.push_back(ETH_PHY_RST_GENERIC_W5500); - disabledPins.push_back(ETH_PHY_SPI_SCK_GENERIC_W5500); - disabledPins.push_back(ETH_PHY_SPI_MISO_GENERIC_W5500); - disabledPins.push_back(ETH_PHY_SPI_MOSI_GENERIC_W5500); - break; - case 3: - disabledPins.push_back(ETH_PHY_CS_M5_W5500); - disabledPins.push_back(ETH_PHY_IRQ_M5_W5500); - disabledPins.push_back(ETH_PHY_RST_M5_W5500); - disabledPins.push_back(ETH_PHY_SPI_SCK_M5_W5500); - disabledPins.push_back(ETH_PHY_SPI_MISO_M5_W5500); - disabledPins.push_back(ETH_PHY_SPI_MOSI_M5_W5500); - break; - case 10: - disabledPins.push_back(ETH_PHY_CS_M5_W5500_S3); - disabledPins.push_back(ETH_PHY_IRQ_M5_W5500); - disabledPins.push_back(ETH_PHY_RST_M5_W5500); - disabledPins.push_back(ETH_PHY_SPI_SCK_M5_W5500_S3); - disabledPins.push_back(ETH_PHY_SPI_MISO_M5_W5500_S3); - disabledPins.push_back(ETH_PHY_SPI_MOSI_M5_W5500_S3); - break; - case 9: - disabledPins.push_back(ETH_PHY_CS_ETH01EVO); - disabledPins.push_back(ETH_PHY_IRQ_ETH01EVO); - disabledPins.push_back(ETH_PHY_RST_ETH01EVO); - disabledPins.push_back(ETH_PHY_SPI_SCK_ETH01EVO); - disabledPins.push_back(ETH_PHY_SPI_MISO_ETH01EVO); - disabledPins.push_back(ETH_PHY_SPI_MOSI_ETH01EVO); - break; - case 6: - disabledPins.push_back(ETH_PHY_CS_M5_W5500); - disabledPins.push_back(ETH_PHY_IRQ_M5_W5500); - disabledPins.push_back(ETH_PHY_RST_M5_W5500); - disabledPins.push_back(ETH_PHY_SPI_SCK_M5_W5500); - disabledPins.push_back(ETH_PHY_SPI_MISO_M5_W5500); - disabledPins.push_back(ETH_PHY_SPI_MOSI_M5_W5500); - break; - case 11: - disabledPins.push_back(_preferences->getInt(preference_network_custom_cs, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_irq, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_rst, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_sck, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_miso, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_mosi, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_pwr, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_mdc, -1)); - disabledPins.push_back(_preferences->getInt(preference_network_custom_mdio, -1)); - break; - #if defined(CONFIG_IDF_TARGET_ESP32) - case 4: - disabledPins.push_back(12); - disabledPins.push_back(ETH_RESET_PIN_LAN8720); - disabledPins.push_back(ETH_PHY_MDC_LAN8720); - disabledPins.push_back(ETH_PHY_MDIO_LAN8720); - break; - case 5: - disabledPins.push_back(16); - disabledPins.push_back(ETH_RESET_PIN_LAN8720); - disabledPins.push_back(ETH_PHY_MDC_LAN8720); - disabledPins.push_back(ETH_PHY_MDIO_LAN8720); - break; - case 8: - disabledPins.push_back(5); - disabledPins.push_back(ETH_RESET_PIN_LAN8720); - disabledPins.push_back(ETH_PHY_MDC_LAN8720); - disabledPins.push_back(ETH_PHY_MDIO_LAN8720); - break; - case 7: - disabledPins.push_back(-1); - disabledPins.push_back(ETH_RESET_PIN_LAN8720); - disabledPins.push_back(ETH_PHY_MDC_LAN8720); - disabledPins.push_back(ETH_PHY_MDIO_LAN8720); - break; - #endif - default: - break; + case 2: + disabledPins.push_back(ETH_PHY_CS_GENERIC_W5500); + disabledPins.push_back(ETH_PHY_IRQ_GENERIC_W5500); + disabledPins.push_back(ETH_PHY_RST_GENERIC_W5500); + disabledPins.push_back(ETH_PHY_SPI_SCK_GENERIC_W5500); + disabledPins.push_back(ETH_PHY_SPI_MISO_GENERIC_W5500); + disabledPins.push_back(ETH_PHY_SPI_MOSI_GENERIC_W5500); + break; + case 3: + disabledPins.push_back(ETH_PHY_CS_M5_W5500); + disabledPins.push_back(ETH_PHY_IRQ_M5_W5500); + disabledPins.push_back(ETH_PHY_RST_M5_W5500); + disabledPins.push_back(ETH_PHY_SPI_SCK_M5_W5500); + disabledPins.push_back(ETH_PHY_SPI_MISO_M5_W5500); + disabledPins.push_back(ETH_PHY_SPI_MOSI_M5_W5500); + break; + case 10: + disabledPins.push_back(ETH_PHY_CS_M5_W5500_S3); + disabledPins.push_back(ETH_PHY_IRQ_M5_W5500); + disabledPins.push_back(ETH_PHY_RST_M5_W5500); + disabledPins.push_back(ETH_PHY_SPI_SCK_M5_W5500_S3); + disabledPins.push_back(ETH_PHY_SPI_MISO_M5_W5500_S3); + disabledPins.push_back(ETH_PHY_SPI_MOSI_M5_W5500_S3); + break; + case 9: + disabledPins.push_back(ETH_PHY_CS_ETH01EVO); + disabledPins.push_back(ETH_PHY_IRQ_ETH01EVO); + disabledPins.push_back(ETH_PHY_RST_ETH01EVO); + disabledPins.push_back(ETH_PHY_SPI_SCK_ETH01EVO); + disabledPins.push_back(ETH_PHY_SPI_MISO_ETH01EVO); + disabledPins.push_back(ETH_PHY_SPI_MOSI_ETH01EVO); + break; + case 6: + disabledPins.push_back(ETH_PHY_CS_M5_W5500); + disabledPins.push_back(ETH_PHY_IRQ_M5_W5500); + disabledPins.push_back(ETH_PHY_RST_M5_W5500); + disabledPins.push_back(ETH_PHY_SPI_SCK_M5_W5500); + disabledPins.push_back(ETH_PHY_SPI_MISO_M5_W5500); + disabledPins.push_back(ETH_PHY_SPI_MOSI_M5_W5500); + break; + case 11: + disabledPins.push_back(_preferences->getInt(preference_network_custom_cs, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_irq, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_rst, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_sck, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_miso, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_mosi, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_pwr, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_mdc, -1)); + disabledPins.push_back(_preferences->getInt(preference_network_custom_mdio, -1)); + break; +#if defined(CONFIG_IDF_TARGET_ESP32) + case 4: + disabledPins.push_back(12); + disabledPins.push_back(ETH_RESET_PIN_LAN8720); + disabledPins.push_back(ETH_PHY_MDC_LAN8720); + disabledPins.push_back(ETH_PHY_MDIO_LAN8720); + break; + case 5: + disabledPins.push_back(16); + disabledPins.push_back(ETH_RESET_PIN_LAN8720); + disabledPins.push_back(ETH_PHY_MDC_LAN8720); + disabledPins.push_back(ETH_PHY_MDIO_LAN8720); + break; + case 8: + disabledPins.push_back(5); + disabledPins.push_back(ETH_RESET_PIN_LAN8720); + disabledPins.push_back(ETH_PHY_MDC_LAN8720); + disabledPins.push_back(ETH_PHY_MDIO_LAN8720); + break; + case 7: + disabledPins.push_back(-1); + disabledPins.push_back(ETH_RESET_PIN_LAN8720); + disabledPins.push_back(ETH_PHY_MDC_LAN8720); + disabledPins.push_back(ETH_PHY_MDIO_LAN8720); + break; +#endif + default: + break; } Log->print(F("GPIO Ethernet disabled pins:")); for_each_n(disabledPins.begin(), disabledPins.size(), - [](int x) { Log->print(" "); Log->print(x); }); + [](int x) + { + Log->print(" "); + Log->print(x); + }); Log->println(); return disabledPins; } @@ -393,52 +403,52 @@ String Gpio::getRoleDescription(const PinRole& role) const { switch(role) { - case PinRole::Disabled: - return "Disabled"; - case PinRole::InputLock: - return "Input: Lock"; - case PinRole::InputUnlock: - return "Input: Unlock"; - case PinRole::InputUnlatch: - return "Input: Unlatch"; - case PinRole::InputLockNgo: - return "Input: Lock n Go"; - case PinRole::InputLockNgoUnlatch: - return "Input: Lock n Go and unlatch"; - case PinRole::InputElectricStrikeActuation: - return "Input: Electric strike actuation"; - case PinRole::InputActivateRTO: - return "Input: Activate RTO"; - case PinRole::InputActivateCM: - return "Input: Activate CM"; - case PinRole::InputDeactivateRtoCm: - return "Input: Deactivate RTO/CM"; - case PinRole::InputDeactivateRTO: - return "Input: Deactivate RTO"; - case PinRole::InputDeactivateCM: - return "Input: Deactivate CM"; - case PinRole::OutputHighLocked: - return "Output: High when locked"; - case PinRole::OutputHighUnlocked: - return "Output: High when unlocked"; - case PinRole::OutputHighMotorBlocked: - return "Output: High when motor blocked"; - case PinRole::OutputHighRtoActive: - return "Output: High when RTO active"; - case PinRole::OutputHighCmActive: - return "Output: High when CM active"; - case PinRole::OutputHighRtoOrCmActive: - return "Output: High when RTO or CM active"; - case PinRole::GeneralOutput: - return "General output"; - case PinRole::GeneralInputPullDown: - return "General input (Pull-down)"; - case PinRole::GeneralInputPullUp: - return "General input (Pull-up)"; - case PinRole::Ethernet: - return "Ethernet"; - default: - return "Unknown"; + case PinRole::Disabled: + return "Disabled"; + case PinRole::InputLock: + return "Input: Lock"; + case PinRole::InputUnlock: + return "Input: Unlock"; + case PinRole::InputUnlatch: + return "Input: Unlatch"; + case PinRole::InputLockNgo: + return "Input: Lock n Go"; + case PinRole::InputLockNgoUnlatch: + return "Input: Lock n Go and unlatch"; + case PinRole::InputElectricStrikeActuation: + return "Input: Electric strike actuation"; + case PinRole::InputActivateRTO: + return "Input: Activate RTO"; + case PinRole::InputActivateCM: + return "Input: Activate CM"; + case PinRole::InputDeactivateRtoCm: + return "Input: Deactivate RTO/CM"; + case PinRole::InputDeactivateRTO: + return "Input: Deactivate RTO"; + case PinRole::InputDeactivateCM: + return "Input: Deactivate CM"; + case PinRole::OutputHighLocked: + return "Output: High when locked"; + case PinRole::OutputHighUnlocked: + return "Output: High when unlocked"; + case PinRole::OutputHighMotorBlocked: + return "Output: High when motor blocked"; + case PinRole::OutputHighRtoActive: + return "Output: High when RTO active"; + case PinRole::OutputHighCmActive: + return "Output: High when CM active"; + case PinRole::OutputHighRtoOrCmActive: + return "Output: High when RTO or CM active"; + case PinRole::GeneralOutput: + return "General output"; + case PinRole::GeneralInputPullDown: + return "General input (Pull-down)"; + case PinRole::GeneralInputPullUp: + return "General input (Pull-up)"; + case PinRole::Ethernet: + return "Ethernet"; + default: + return "Unknown"; } } @@ -447,46 +457,47 @@ GpioAction Gpio::getGpioAction(const PinRole &role) const { switch(role) { - case PinRole::Disabled: - return GpioAction::None; - case PinRole::InputLock: - return GpioAction::Lock; - case PinRole::InputUnlock: - return GpioAction::Unlock; - case PinRole::InputUnlatch: - return GpioAction::Unlatch; - case PinRole::InputLockNgo: - return GpioAction::LockNgo; - case PinRole::InputLockNgoUnlatch: - return GpioAction::LockNgoUnlatch; - case PinRole::InputElectricStrikeActuation: - return GpioAction::ElectricStrikeActuation; - case PinRole::InputActivateRTO: - return GpioAction::ActivateRTO; - case PinRole::InputActivateCM: - return GpioAction::ActivateCM; - case PinRole::InputDeactivateRtoCm: - return GpioAction::DeactivateRtoCm; - case PinRole::InputDeactivateRTO: - return GpioAction::DeactivateRTO; - case PinRole::InputDeactivateCM: - return GpioAction::DeactivateCM; + case PinRole::Disabled: + return GpioAction::None; + case PinRole::InputLock: + return GpioAction::Lock; + case PinRole::InputUnlock: + return GpioAction::Unlock; + case PinRole::InputUnlatch: + return GpioAction::Unlatch; + case PinRole::InputLockNgo: + return GpioAction::LockNgo; + case PinRole::InputLockNgoUnlatch: + return GpioAction::LockNgoUnlatch; + case PinRole::InputElectricStrikeActuation: + return GpioAction::ElectricStrikeActuation; + case PinRole::InputActivateRTO: + return GpioAction::ActivateRTO; + case PinRole::InputActivateCM: + return GpioAction::ActivateCM; + case PinRole::InputDeactivateRtoCm: + return GpioAction::DeactivateRtoCm; + case PinRole::InputDeactivateRTO: + return GpioAction::DeactivateRTO; + case PinRole::InputDeactivateCM: + return GpioAction::DeactivateCM; - case PinRole::GeneralInputPullDown: - case PinRole::GeneralInputPullUp: - return GpioAction::GeneralInput; + case PinRole::GeneralInputPullDown: + case PinRole::GeneralInputPullUp: + return GpioAction::GeneralInput; - case PinRole::GeneralOutput: - case PinRole::Ethernet: - case PinRole::OutputHighLocked: - case PinRole::OutputHighUnlocked: - case PinRole::OutputHighMotorBlocked: - case PinRole::OutputHighRtoActive: - case PinRole::OutputHighCmActive: - case PinRole::OutputHighRtoOrCmActive: - default: - return GpioAction::None; - }} + case PinRole::GeneralOutput: + case PinRole::Ethernet: + case PinRole::OutputHighLocked: + case PinRole::OutputHighUnlocked: + case PinRole::OutputHighMotorBlocked: + case PinRole::OutputHighRtoActive: + case PinRole::OutputHighCmActive: + case PinRole::OutputHighRtoOrCmActive: + default: + return GpioAction::None; + } +} void Gpio::getConfigurationText(String& text, const std::vector& pinConfiguration, const String& linebreak) const diff --git a/src/NukiDeviceId.cpp b/src/NukiDeviceId.cpp index 5c9bd42..6eb01bb 100644 --- a/src/NukiDeviceId.cpp +++ b/src/NukiDeviceId.cpp @@ -5,8 +5,8 @@ #include "PreferencesKeys.h" NukiDeviceId::NukiDeviceId(Preferences* preferences, const std::string& preferencesId) -: _preferences(preferences), - _preferencesId(preferencesId) + : _preferences(preferences), + _preferencesId(preferencesId) { _deviceId = _preferences->getUInt(_preferencesId.c_str(), 0); diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index b8498bf..9eec5bb 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -24,13 +24,13 @@ extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_ #ifndef NUKI_HUB_UPDATER NukiNetwork::NukiNetwork(Preferences *preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize) -: _preferences(preferences), - _gpio(gpio), - _buffer(buffer), - _bufferSize(bufferSize) + : _preferences(preferences), + _gpio(gpio), + _buffer(buffer), + _bufferSize(bufferSize) #else NukiNetwork::NukiNetwork(Preferences *preferences) -: _preferences(preferences) + : _preferences(preferences) #endif { // Remove obsolete W5500 hardware detection configuration @@ -43,7 +43,7 @@ NukiNetwork::NukiNetwork(Preferences *preferences) _webEnabled = _preferences->getBool(preference_webserver_enabled, true); _updateFromMQTT = _preferences->getBool(preference_update_from_mqtt, false); - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER memset(_maintenancePathPrefix, 0, sizeof(_maintenancePathPrefix)); size_t len = maintenancePathPrefix.length(); for(int i=0; i < len; i++) @@ -60,7 +60,7 @@ NukiNetwork::NukiNetwork(Preferences *preferences) { _mqttConnectionStateTopic[i] = connectionStateTopic.charAt(i); } - #endif +#endif setupDevice(); } @@ -76,25 +76,25 @@ void NukiNetwork::setupDevice() if(hardwareDetect == 0) { - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 hardwareDetect = 1; - #else +#else hardwareDetect = 11; - _preferences->putInt(preference_network_custom_addr, 1); - _preferences->putInt(preference_network_custom_cs, 8); - _preferences->putInt(preference_network_custom_irq, 9); - _preferences->putInt(preference_network_custom_rst, 10); - _preferences->putInt(preference_network_custom_sck, 11); - _preferences->putInt(preference_network_custom_miso, 12); - _preferences->putInt(preference_network_custom_mosi, 13); - _preferences->putBool(preference_ntw_reconfigure, true); - #endif + _preferences->putInt(preference_network_custom_addr, 1); + _preferences->putInt(preference_network_custom_cs, 8); + _preferences->putInt(preference_network_custom_irq, 9); + _preferences->putInt(preference_network_custom_rst, 10); + _preferences->putInt(preference_network_custom_sck, 11); + _preferences->putInt(preference_network_custom_miso, 12); + _preferences->putInt(preference_network_custom_mosi, 13); + _preferences->putBool(preference_ntw_reconfigure, true); +#endif _preferences->putInt(preference_network_hardware, hardwareDetect); } if(strcmp(WiFi_fallbackDetect, "wifi_fallback") == 0) { - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 if(!_firstBootAfterDeviceChange) { Log->println(F("Failed to connect to network. Wi-Fi fallback is disabled, rebooting.")); @@ -105,14 +105,20 @@ void NukiNetwork::setupDevice() Log->println(F("Switching to Wi-Fi device as fallback.")); _networkDeviceType = NetworkDeviceType::WiFi; - #else +#else int custEth = _preferences->getInt(preference_network_custom_phy, 0); - if(custEth<3) custEth++; - else custEth = 0; + if(custEth<3) + { + custEth++; + } + else + { + custEth = 0; + } _preferences->putInt(preference_network_custom_phy, custEth); _preferences->putBool(preference_ntw_reconfigure, true); - #endif +#endif } else { @@ -308,7 +314,8 @@ void NukiNetwork::initialize() } else { - Log->print(F("MQTT: Connecting with user: ")); Log->println(_mqttUser); + Log->print(F("MQTT: Connecting with user: ")); + Log->println(_mqttUser); _mqtt_cfg.credentials.username = _mqttUser; _mqtt_cfg.credentials.authentication.password = _mqttPass; } @@ -364,8 +371,14 @@ bool NukiNetwork::update() { _mqttConnectCounter = 0; - if(!_webEnabled) forceEnableWebServer = true; - if(_restartOnDisconnect && (esp_timer_get_time() / 1000) > 60000) restartEsp(RestartReason::RestartOnDisconnectWatchdog); + if(!_webEnabled) + { + forceEnableWebServer = true; + } + if(_restartOnDisconnect && (esp_timer_get_time() / 1000) > 60000) + { + restartEsp(RestartReason::RestartOnDisconnectWatchdog); + } } if(_device->isConnected() && !_mqttClientInitiated && strcmp(_mqttBrokerAddr, "") != 0) @@ -377,9 +390,18 @@ bool NukiNetwork::update() { MqttLoggerMode mode; - if(_preferences->getBool(preference_mqtt_log_enabled, false) && _preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::MqttAndSerialAndWeb; - else if (_preferences->getBool(preference_webserial_enabled, false)) mode = MqttLoggerMode::SerialAndWeb; - else mode = MqttLoggerMode::MqttAndSerial; + if(_preferences->getBool(preference_mqtt_log_enabled, false) && _preferences->getBool(preference_webserial_enabled, false)) + { + mode = MqttLoggerMode::MqttAndSerialAndWeb; + } + else if (_preferences->getBool(preference_webserial_enabled, false)) + { + mode = MqttLoggerMode::SerialAndWeb; + } + else + { + mode = MqttLoggerMode::MqttAndSerial; + } char* _path = new char[200]; memset(_path, 0, sizeof(_path)); @@ -401,29 +423,29 @@ bool NukiNetwork::update() { switch (pinEntry.role) { - case PinRole::GeneralInputPullDown: - case PinRole::GeneralInputPullUp: - if(rebGpio) - { - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); - publishString(_lockPath.c_str(), gpioPath, "input", false); - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - publishString(_lockPath.c_str(), gpioPath, std::to_string(digitalRead(pinEntry.pin)).c_str(), false); - } - break; - case PinRole::GeneralOutput: - if(rebGpio) - { - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); - publishString(_lockPath.c_str(), gpioPath, "output", false); - buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - publishString(_lockPath.c_str(), gpioPath, "0", false); - } + case PinRole::GeneralInputPullDown: + case PinRole::GeneralInputPullUp: + if(rebGpio) + { + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); + publishString(_lockPath.c_str(), gpioPath, "input", false); buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); - subscribe(_lockPath.c_str(), gpioPath); - break; - default: - break; + publishString(_lockPath.c_str(), gpioPath, std::to_string(digitalRead(pinEntry.pin)).c_str(), false); + } + break; + case PinRole::GeneralOutput: + if(rebGpio) + { + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_role}); + publishString(_lockPath.c_str(), gpioPath, "output", false); + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + publishString(_lockPath.c_str(), gpioPath, "0", false); + } + buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pinEntry.pin)).c_str(), mqtt_topic_gpio_state}); + subscribe(_lockPath.c_str(), gpioPath); + break; + default: + break; } } _gpio->addCallback([this](const GpioAction& action, const int& pin) @@ -455,7 +477,10 @@ bool NukiNetwork::update() delay(200); restartEsp(RestartReason::ReconfigureWebServer); } - else if(!_webEnabled) forceEnableWebServer = false; + else if(!_webEnabled) + { + forceEnableWebServer = false; + } delay(2000); } @@ -463,7 +488,10 @@ bool NukiNetwork::update() { if(_networkTimeout > 0 && (ts - _lastConnectedTs > _networkTimeout * 1000) && ts > 60000) { - if(!_webEnabled) forceEnableWebServer = true; + if(!_webEnabled) + { + forceEnableWebServer = true; + } Log->println("Network timeout has been reached, restarting ..."); delay(200); restartEsp(RestartReason::NetworkTimeoutWatchdog); @@ -514,21 +542,26 @@ bool NukiNetwork::update() JsonDocument doc; NetworkClientSecure *client = new NetworkClientSecure; - if (client) { + if (client) + { client->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); { HTTPClient https; https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); https.useHTTP10(true); - if (https.begin(*client, GITHUB_OTA_MANIFEST_URL)) { + if (https.begin(*client, GITHUB_OTA_MANIFEST_URL)) + { int httpResponseCode = https.GET(); if (httpResponseCode == HTTP_CODE_OK || httpResponseCode == HTTP_CODE_MOVED_PERMANENTLY) { DeserializationError jsonError = deserializeJson(doc, https.getStream()); - if (!jsonError) { otaManifestSuccess = true; } + if (!jsonError) + { + otaManifestSuccess = true; + } } } https.end(); @@ -540,14 +573,29 @@ bool NukiNetwork::update() { String currentVersion = NUKI_HUB_VERSION; - if(atof(doc["release"]["version"]) >= atof(currentVersion.c_str())) _latestVersion = doc["release"]["fullversion"]; - else if(currentVersion.indexOf("beta") > 0) _latestVersion = doc["beta"]["fullversion"]; - else if(currentVersion.indexOf("master") > 0) _latestVersion = doc["master"]["fullversion"]; - else _latestVersion = doc["release"]["fullversion"]; + if(atof(doc["release"]["version"]) >= atof(currentVersion.c_str())) + { + _latestVersion = doc["release"]["fullversion"]; + } + else if(currentVersion.indexOf("beta") > 0) + { + _latestVersion = doc["beta"]["fullversion"]; + } + else if(currentVersion.indexOf("master") > 0) + { + _latestVersion = doc["master"]["fullversion"]; + } + else + { + _latestVersion = doc["release"]["fullversion"]; + } publishString(_maintenancePathPrefix, mqtt_topic_info_nuki_hub_latest, _latestVersion, true); - if(strcmp(_latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) _preferences->putString(preference_latest_version, _latestVersion); + if(strcmp(_latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) + { + _preferences->putString(preference_latest_version, _latestVersion); + } } } } @@ -588,7 +636,8 @@ void NukiNetwork::mqtt_event_handler(void *handler_args, esp_event_base_t base, esp_mqtt_event_handle_t event = (esp_mqtt_event_t*)event_data; esp_mqtt_client_handle_t client = event->client; int msg_id; - switch ((esp_mqtt_event_id_t)event_id) { + switch ((esp_mqtt_event_id_t)event_id) + { case MQTT_EVENT_CONNECTED: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_CONNECTED"); Log->println("MQTT Connected"); @@ -619,7 +668,8 @@ void NukiNetwork::mqtt_event_handler(void *handler_args, esp_event_base_t base, break; case MQTT_EVENT_ERROR: ESP_LOGI(MQTT_TAG, "MQTT_EVENT_ERROR"); - if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) + { ESP_LOGI(MQTT_TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); } break; @@ -968,9 +1018,11 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - {{(char*)"pl_on", (char*)"1"}, - {(char*)"pl_off", (char*)"0"}, - {(char*)"val_tpl", (char*)"{{value_json.critical}}" }}); + { + {(char*)"pl_on", (char*)"1"}, + {(char*)"pl_off", (char*)"0"}, + {(char*)"val_tpl", (char*)"{{value_json.critical}}" } + }); // Battery voltage publishHassTopic("sensor", @@ -986,8 +1038,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "measurement", "diagnostic", "", - { {(char*)"unit_of_meas", (char*)"V"}, - {(char*)"val_tpl", (char*)"{{value_json.batteryVoltage}}" }}); + { + {(char*)"unit_of_meas", (char*)"V"}, + {(char*)"val_tpl", (char*)"{{value_json.batteryVoltage}}" } + }); // Trigger publishHassTopic("sensor", @@ -1003,7 +1057,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" } }); + { { (char*)"en", (char*)"true" } }); // MQTT Connected publishHassTopic("binary_sensor", @@ -1019,9 +1073,11 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - {{(char*)"pl_on", (char*)"online"}, - {(char*)"pl_off", (char*)"offline"}, - {(char*)"ic", (char*)"mdi:lan-connect"}}); + { + {(char*)"pl_on", (char*)"online"}, + {(char*)"pl_off", (char*)"offline"}, + {(char*)"ic", (char*)"mdi:lan-connect"} + }); // Reset publishHassTopic("switch", @@ -1037,13 +1093,15 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", String("~") + mqtt_topic_reset, - { { (char*)"ic", (char*)"mdi:restart" }, - { (char*)"pl_on", (char*)"1" }, - { (char*)"pl_off", (char*)"0" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"ic", (char*)"mdi:restart" }, + { (char*)"pl_on", (char*)"1" }, + { (char*)"pl_off", (char*)"0" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); - // Network device + // Network device publishHassTopic("sensor", "network_device", uidString, @@ -1057,7 +1115,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }}); + { { (char*)"en", (char*)"true" }}); // Nuki Hub Webserver enabled publishHassTopic("switch", @@ -1073,12 +1131,14 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", _lockPath + mqtt_topic_webserver_action, - { { (char*)"pl_on", (char*)"1" }, - { (char*)"pl_off", (char*)"0" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"pl_on", (char*)"1" }, + { (char*)"pl_off", (char*)"0" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); - // Uptime + // Uptime publishHassTopic("sensor", "uptime", uidString, @@ -1092,12 +1152,14 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - { (char*)"unit_of_meas", (char*)"min"}}); + { + { (char*)"en", (char*)"true" }, + { (char*)"unit_of_meas", (char*)"min"} + }); if(_preferences->getBool(preference_mqtt_log_enabled, false)) { - // MQTT Log + // MQTT Log publishHassTopic("sensor", "mqtt_log", uidString, @@ -1111,7 +1173,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }}); + { { (char*)"en", (char*)"true" }}); } else { @@ -1134,9 +1196,11 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { {(char*)"pl_on", (char*)"1"}, - {(char*)"pl_off", (char*)"0"}, - { (char*)"en", (char*)"true" }}); + { + {(char*)"pl_on", (char*)"1"}, + {(char*)"pl_off", (char*)"0"}, + { (char*)"en", (char*)"true" } + }); } else { @@ -1157,8 +1221,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - {(char*)"ic", (char*)"mdi:counter"}}); + { + { (char*)"en", (char*)"true" }, + {(char*)"ic", (char*)"mdi:counter"} + }); // Hardware version publishHassTopic("sensor", @@ -1174,8 +1240,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - {(char*)"ic", (char*)"mdi:counter"}}); + { + { (char*)"en", (char*)"true" }, + {(char*)"ic", (char*)"mdi:counter"} + }); // Nuki Hub version publishHassTopic("sensor", @@ -1191,8 +1259,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - {(char*)"ic", (char*)"mdi:counter"}}); + { + { (char*)"en", (char*)"true" }, + {(char*)"ic", (char*)"mdi:counter"} + }); // Nuki Hub build publishHassTopic("sensor", @@ -1208,8 +1278,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - {(char*)"ic", (char*)"mdi:counter"}}); + { + { (char*)"en", (char*)"true" }, + {(char*)"ic", (char*)"mdi:counter"} + }); // Nuki Hub restart reason publishHassTopic("sensor", @@ -1225,7 +1297,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }}); + { { (char*)"en", (char*)"true" }}); // Nuki Hub restart reason ESP publishHassTopic("sensor", @@ -1241,7 +1313,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }}); + { { (char*)"en", (char*)"true" }}); if(_checkUpdates) { @@ -1259,8 +1331,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - {(char*)"ic", (char*)"mdi:counter"}}); + { + { (char*)"en", (char*)"true" }, + {(char*)"ic", (char*)"mdi:counter"} + }); // NUKI Hub update char latest_version_topic[250]; @@ -1282,10 +1356,12 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - { (char*)"ent_pic", (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/master/icon/favicon-32x32.png" }, - { (char*)"rel_u", (char*)GITHUB_LATEST_RELEASE_URL }, - { (char*)"l_ver_t", (char*)latest_version_topic }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ent_pic", (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/master/icon/favicon-32x32.png" }, + { (char*)"rel_u", (char*)GITHUB_LATEST_RELEASE_URL }, + { (char*)"l_ver_t", (char*)latest_version_topic } + }); } else { @@ -1302,11 +1378,13 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", _lockPath + mqtt_topic_update, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_inst", (char*)"1" }, - { (char*)"ent_pic", (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/master/icon/favicon-32x32.png" }, - { (char*)"rel_u", (char*)GITHUB_LATEST_RELEASE_URL }, - { (char*)"l_ver_t", (char*)latest_version_topic }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_inst", (char*)"1" }, + { (char*)"ent_pic", (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/master/icon/favicon-32x32.png" }, + { (char*)"rel_u", (char*)GITHUB_LATEST_RELEASE_URL }, + { (char*)"l_ver_t", (char*)latest_version_topic } + }); } } else @@ -1329,8 +1407,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", "", - { { (char*)"en", (char*)"true" }, - {(char*)"ic", (char*)"mdi:ip"}}); + { + { (char*)"en", (char*)"true" }, + {(char*)"ic", (char*)"mdi:ip"} + }); // Query Lock State publishHassTopic("button", @@ -1346,8 +1426,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", String("~") + mqtt_topic_query_lockstate, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"1" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"1" } + }); // Query Config publishHassTopic("button", @@ -1363,8 +1445,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", String("~") + mqtt_topic_query_config, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"1" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"1" } + }); // Query Lock State Command result publishHassTopic("button", @@ -1380,8 +1464,10 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "", "diagnostic", String("~") + mqtt_topic_query_lockstate_command_result, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"1" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"1" } + }); publishHassTopic("sensor", "bluetooth_signal_strength", @@ -1396,7 +1482,7 @@ void NukiNetwork::publishHASSConfig(char* deviceType, const char* baseTopic, cha "measurement", "diagnostic", "", - { {(char*)"unit_of_meas", (char*)"dBm"} }); + { {(char*)"unit_of_meas", (char*)"dBm"} }); } void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, const char *baseTopic, char *name, char *uidString) @@ -1433,8 +1519,10 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "", String("~") + mqtt_topic_lock_action, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"unlatch" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"unlatch" } + }); } else { @@ -1457,8 +1545,10 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "", String("~") + mqtt_topic_lock_action, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"lockNgo" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"lockNgo" } + }); } else { @@ -1481,8 +1571,10 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "", String("~") + mqtt_topic_lock_action, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"lockNgoUnlatch" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"lockNgoUnlatch" } + }); } else { @@ -1503,8 +1595,10 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "diagnostic", String("~") + mqtt_topic_query_battery, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"1" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"1" } + }); if((int)basicLockConfigAclPrefs[6] == 1) { @@ -1522,13 +1616,15 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:led-variant-on" }, - { (char*)"pl_on", (char*)"{ \"ledEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"ledEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.ledEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:led-variant-on" }, + { (char*)"pl_on", (char*)"{ \"ledEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"ledEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.ledEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -1551,13 +1647,15 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:radiobox-marked" }, - { (char*)"pl_on", (char*)"{ \"buttonEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"buttonEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.buttonEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:radiobox-marked" }, + { (char*)"pl_on", (char*)"{ \"buttonEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"buttonEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.buttonEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -1580,12 +1678,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"autoLockEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"autoLockEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.autoLockEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"autoLockEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"autoLockEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.autoLockEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -1608,12 +1708,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"autoUnLockDisabled\": \"0\"}" }, - { (char*)"pl_off", (char*)"{ \"autoUnLockDisabled\": \"1\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.autoUnLockDisabled}}" }, - { (char*)"stat_on", (char*)"0" }, - { (char*)"stat_off", (char*)"1" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"autoUnLockDisabled\": \"0\"}" }, + { (char*)"pl_off", (char*)"{ \"autoUnLockDisabled\": \"1\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.autoUnLockDisabled}}" }, + { (char*)"stat_on", (char*)"0" }, + { (char*)"stat_off", (char*)"1" } + }); } else { @@ -1636,12 +1738,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"singleLock\": \"0\"}" }, - { (char*)"pl_off", (char*)"{ \"singleLock\": \"1\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.singleLock}}" }, - { (char*)"stat_on", (char*)"0" }, - { (char*)"stat_off", (char*)"1" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"singleLock\": \"0\"}" }, + { (char*)"pl_off", (char*)"{ \"singleLock\": \"1\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.singleLock}}" }, + { (char*)"stat_on", (char*)"0" }, + { (char*)"stat_off", (char*)"1" } + }); } else { @@ -1661,8 +1765,10 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "measurement", "diagnostic", "", - { {(char*)"unit_of_meas", (char*)"%"}, - {(char*)"val_tpl", (char*)"{{value_json.level}}" }}); + { + {(char*)"unit_of_meas", (char*)"%"}, + {(char*)"val_tpl", (char*)"{{value_json.level}}" } + }); if((int)basicLockConfigAclPrefs[7] == 1) { @@ -1679,12 +1785,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:brightness-6" }, - { (char*)"cmd_tpl", (char*)"{ \"ledBrightness\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.ledBrightness}}" }, - { (char*)"min", (char*)"0" }, - { (char*)"max", (char*)"5" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:brightness-6" }, + { (char*)"cmd_tpl", (char*)"{ \"ledBrightness\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.ledBrightness}}" }, + { (char*)"min", (char*)"0" }, + { (char*)"max", (char*)"5" } + }); } else { @@ -1707,12 +1815,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"autoUnlatch\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"autoUnlatch\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.autoUnlatch}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"autoUnlatch\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"autoUnlatch\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.autoUnlatch}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -1735,12 +1845,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"pairingEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"pairingEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.pairingEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"pairingEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"pairingEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.pairingEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -1762,12 +1874,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:timer-cog-outline" }, - { (char*)"cmd_tpl", (char*)"{ \"timeZoneOffset\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.timeZoneOffset}}" }, - { (char*)"min", (char*)"0" }, - { (char*)"max", (char*)"60" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:timer-cog-outline" }, + { (char*)"cmd_tpl", (char*)"{ \"timeZoneOffset\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.timeZoneOffset}}" }, + { (char*)"min", (char*)"0" }, + { (char*)"max", (char*)"60" } + }); } else { @@ -1790,12 +1904,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"dstMode\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"dstMode\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.dstMode}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"dstMode\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"dstMode\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.dstMode}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -1949,11 +2065,13 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"unlockedPositionOffsetDegrees\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.unlockedPositionOffsetDegrees}}" }, - { (char*)"min", (char*)"-90" }, - { (char*)"max", (char*)"180" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"unlockedPositionOffsetDegrees\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.unlockedPositionOffsetDegrees}}" }, + { (char*)"min", (char*)"-90" }, + { (char*)"max", (char*)"180" } + }); } else { @@ -1975,11 +2093,13 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"lockedPositionOffsetDegrees\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.lockedPositionOffsetDegrees}}" }, - { (char*)"min", (char*)"-180" }, - { (char*)"max", (char*)"90" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"lockedPositionOffsetDegrees\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.lockedPositionOffsetDegrees}}" }, + { (char*)"min", (char*)"-180" }, + { (char*)"max", (char*)"90" } + }); } else { @@ -2001,11 +2121,13 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"singleLockedPositionOffsetDegrees\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.singleLockedPositionOffsetDegrees}}" }, - { (char*)"min", (char*)"-180" }, - { (char*)"max", (char*)"180" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"singleLockedPositionOffsetDegrees\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.singleLockedPositionOffsetDegrees}}" }, + { (char*)"min", (char*)"-180" }, + { (char*)"max", (char*)"180" } + }); } else { @@ -2027,11 +2149,13 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"unlockedToLockedTransitionOffsetDegrees\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.unlockedToLockedTransitionOffsetDegrees}}" }, - { (char*)"min", (char*)"-180" }, - { (char*)"max", (char*)"180" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"unlockedToLockedTransitionOffsetDegrees\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.unlockedToLockedTransitionOffsetDegrees}}" }, + { (char*)"min", (char*)"-180" }, + { (char*)"max", (char*)"180" } + }); } else { @@ -2053,11 +2177,13 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"lockNgoTimeout\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.lockNgoTimeout}}" }, - { (char*)"min", (char*)"5" }, - { (char*)"max", (char*)"60" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"lockNgoTimeout\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.lockNgoTimeout}}" }, + { (char*)"min", (char*)"5" }, + { (char*)"max", (char*)"60" } + }); } else { @@ -2120,12 +2246,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"detachedCylinder\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"detachedCylinder\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.detachedCylinder}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"detachedCylinder\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"detachedCylinder\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.detachedCylinder}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2164,12 +2292,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"automaticBatteryTypeDetection\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"automaticBatteryTypeDetection\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.automaticBatteryTypeDetection}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"automaticBatteryTypeDetection\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"automaticBatteryTypeDetection\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.automaticBatteryTypeDetection}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2191,11 +2321,13 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"unlatchDuration\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.unlatchDuration}}" }, - { (char*)"min", (char*)"1" }, - { (char*)"max", (char*)"30" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"unlatchDuration\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.unlatchDuration}}" }, + { (char*)"min", (char*)"1" }, + { (char*)"max", (char*)"30" } + }); } else { @@ -2217,11 +2349,13 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"autoLockTimeOut\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.autoLockTimeOut}}" }, - { (char*)"min", (char*)"30" }, - { (char*)"max", (char*)"1800" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"autoLockTimeOut\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.autoLockTimeOut}}" }, + { (char*)"min", (char*)"30" }, + { (char*)"max", (char*)"1800" } + }); } else { @@ -2244,12 +2378,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"nightModeEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"nightModeEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.nightModeEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"nightModeEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"nightModeEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.nightModeEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2272,12 +2408,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pattern", (char*)"([0-1][0-9]|2[0-3]):[0-5][0-9]" }, - { (char*)"cmd_tpl", (char*)"{ \"nightModeStartTime\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.nightModeStartTime}}" }, - { (char*)"min", (char*)"5" }, - { (char*)"max", (char*)"5" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pattern", (char*)"([0-1][0-9]|2[0-3]):[0-5][0-9]" }, + { (char*)"cmd_tpl", (char*)"{ \"nightModeStartTime\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.nightModeStartTime}}" }, + { (char*)"min", (char*)"5" }, + { (char*)"max", (char*)"5" } + }); } else { @@ -2300,12 +2438,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pattern", (char*)"([0-1][0-9]|2[0-3]):[0-5][0-9]" }, - { (char*)"cmd_tpl", (char*)"{ \"nightModeEndTime\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.nightModeEndTime}}" }, - { (char*)"min", (char*)"5" }, - { (char*)"max", (char*)"5" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pattern", (char*)"([0-1][0-9]|2[0-3]):[0-5][0-9]" }, + { (char*)"cmd_tpl", (char*)"{ \"nightModeEndTime\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.nightModeEndTime}}" }, + { (char*)"min", (char*)"5" }, + { (char*)"max", (char*)"5" } + }); } else { @@ -2328,12 +2468,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"nightModeAutoLockEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"nightModeAutoLockEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.nightModeAutoLockEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"nightModeAutoLockEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"nightModeAutoLockEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.nightModeAutoLockEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2356,12 +2498,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"nightModeAutoUnlockDisabled\": \"0\"}" }, - { (char*)"pl_off", (char*)"{ \"nightModeAutoUnlockDisabled\": \"1\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.nightModeAutoUnlockDisabled}}" }, - { (char*)"stat_on", (char*)"0" }, - { (char*)"stat_off", (char*)"1" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"nightModeAutoUnlockDisabled\": \"0\"}" }, + { (char*)"pl_off", (char*)"{ \"nightModeAutoUnlockDisabled\": \"1\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.nightModeAutoUnlockDisabled}}" }, + { (char*)"stat_on", (char*)"0" }, + { (char*)"stat_off", (char*)"1" } + }); } else { @@ -2384,12 +2528,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"nightModeImmediateLockOnStart\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"nightModeImmediateLockOnStart\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.nightModeImmediateLockOnStart}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"nightModeImmediateLockOnStart\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"nightModeImmediateLockOnStart\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.nightModeImmediateLockOnStart}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2412,12 +2558,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"immediateAutoLockEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"immediateAutoLockEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.immediateAutoLockEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"immediateAutoLockEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"immediateAutoLockEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.immediateAutoLockEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2440,12 +2588,14 @@ void NukiNetwork::publishHASSConfigAdditionalLockEntities(char *deviceType, cons "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"autoUpdateEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"autoUpdateEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.autoUpdateEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"autoUpdateEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"autoUpdateEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.autoUpdateEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2472,9 +2622,11 @@ void NukiNetwork::publishHASSConfigDoorSensor(char *deviceType, const char *base "", "", "", - {{(char*)"pl_on", (char*)"doorOpened"}, - {(char*)"pl_off", (char*)"doorClosed"}, - {(char*)"pl_not_avail", (char*)"unavailable"}}); + { + {(char*)"pl_on", (char*)"doorOpened"}, + {(char*)"pl_off", (char*)"doorClosed"}, + {(char*)"pl_not_avail", (char*)"unavailable"} + }); } void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, const char *baseTopic, char *name, char *uidString) @@ -2510,8 +2662,10 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "", String("~") + mqtt_topic_lock_action, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"electricStrikeActuation" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"electricStrikeActuation" } + }); } else { @@ -2531,8 +2685,10 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "", "", - {{(char*)"pl_on", (char*)"on"}, - {(char*)"pl_off", (char*)"off"}}); + { + {(char*)"pl_on", (char*)"on"}, + {(char*)"pl_off", (char*)"off"} + }); if((int)aclPrefs[12] == 1 && (int)aclPrefs[13] == 1) { @@ -2549,11 +2705,13 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "", String("~") + mqtt_topic_lock_action, - {{ (char*)"en", (char*)"true" }, - {(char*)"stat_on", (char*)"on"}, - {(char*)"stat_off", (char*)"off"}, - {(char*)"pl_on", (char*)"activateCM"}, - {(char*)"pl_off", (char*)"deactivateCM"}}); + { + { (char*)"en", (char*)"true" }, + {(char*)"stat_on", (char*)"on"}, + {(char*)"stat_off", (char*)"off"}, + {(char*)"pl_on", (char*)"activateCM"}, + {(char*)"pl_off", (char*)"deactivateCM"} + }); } else { @@ -2573,8 +2731,10 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "", "", - {{(char*)"pl_on", (char*)"ring"}, - {(char*)"pl_off", (char*)"standby"}}); + { + {(char*)"pl_on", (char*)"ring"}, + {(char*)"pl_off", (char*)"standby"} + }); JsonDocument json; json = createHassJson(uidString, "_ring_event", "Ring", name, baseTopic, String("~") + mqtt_topic_lock_ring, deviceType, "doorbell", "", "", "", {{(char*)"val_tpl", (char*)"{ \"event_type\": \"{{ value }}\" }"}}); @@ -2600,13 +2760,15 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:led-variant-on" }, - { (char*)"pl_on", (char*)"{ \"ledEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"ledEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.ledEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:led-variant-on" }, + { (char*)"pl_on", (char*)"{ \"ledEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"ledEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.ledEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2629,13 +2791,15 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:radiobox-marked" }, - { (char*)"pl_on", (char*)"{ \"buttonEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"buttonEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.buttonEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:radiobox-marked" }, + { (char*)"pl_on", (char*)"{ \"buttonEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"buttonEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.buttonEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2657,14 +2821,16 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:volume-source" }, - { (char*)"cmd_tpl", (char*)"{ \"soundLevel\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.soundLevel}}" }, - { (char*)"min", (char*)"0" }, - { (char*)"max", (char*)"255" }, - { (char*)"mode", (char*)"slider" }, - { (char*)"step", (char*)"25.5" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:volume-source" }, + { (char*)"cmd_tpl", (char*)"{ \"soundLevel\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.soundLevel}}" }, + { (char*)"min", (char*)"0" }, + { (char*)"max", (char*)"255" }, + { (char*)"mode", (char*)"slider" }, + { (char*)"step", (char*)"25.5" } + }); } else { @@ -2687,12 +2853,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"pairingEnabled\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"pairingEnabled\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.pairingEnabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"pairingEnabled\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"pairingEnabled\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.pairingEnabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2714,12 +2882,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"ic", (char*)"mdi:timer-cog-outline" }, - { (char*)"cmd_tpl", (char*)"{ \"timeZoneOffset\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.timeZoneOffset}}" }, - { (char*)"min", (char*)"0" }, - { (char*)"max", (char*)"60" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"ic", (char*)"mdi:timer-cog-outline" }, + { (char*)"cmd_tpl", (char*)"{ \"timeZoneOffset\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.timeZoneOffset}}" }, + { (char*)"min", (char*)"0" }, + { (char*)"max", (char*)"60" } + }); } else { @@ -2742,12 +2912,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"dstMode\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"dstMode\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.dstMode}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"dstMode\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"dstMode\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.dstMode}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2934,12 +3106,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"busModeSwitch\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"busModeSwitch\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.busModeSwitch}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"busModeSwitch\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"busModeSwitch\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.busModeSwitch}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -2961,10 +3135,12 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"shortCircuitDuration\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.shortCircuitDuration}}" }, - { (char*)"min", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"shortCircuitDuration\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.shortCircuitDuration}}" }, + { (char*)"min", (char*)"0" } + }); } else { @@ -2986,12 +3162,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"electricStrikeDelay\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.electricStrikeDelay}}" }, - { (char*)"min", (char*)"0" }, - { (char*)"max", (char*)"30000" }, - { (char*)"step", (char*)"3000" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"electricStrikeDelay\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.electricStrikeDelay}}" }, + { (char*)"min", (char*)"0" }, + { (char*)"max", (char*)"30000" }, + { (char*)"step", (char*)"3000" } + }); } else { @@ -3014,12 +3192,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"randomElectricStrikeDelay\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"randomElectricStrikeDelay\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.randomElectricStrikeDelay}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"randomElectricStrikeDelay\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"randomElectricStrikeDelay\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.randomElectricStrikeDelay}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -3041,12 +3221,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"electricStrikeDuration\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.electricStrikeDuration}}" }, - { (char*)"min", (char*)"1000" }, - { (char*)"max", (char*)"30000" }, - { (char*)"step", (char*)"3000" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"electricStrikeDuration\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.electricStrikeDuration}}" }, + { (char*)"min", (char*)"1000" }, + { (char*)"max", (char*)"30000" }, + { (char*)"step", (char*)"3000" } + }); } else { @@ -3069,12 +3251,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"disableRtoAfterRing\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"disableRtoAfterRing\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.disableRtoAfterRing}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"disableRtoAfterRing\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"disableRtoAfterRing\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.disableRtoAfterRing}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -3096,11 +3280,13 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"rtoTimeout\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.rtoTimeout}}" }, - { (char*)"min", (char*)"5" }, - { (char*)"max", (char*)"60" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"rtoTimeout\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.rtoTimeout}}" }, + { (char*)"min", (char*)"5" }, + { (char*)"max", (char*)"60" } + }); } else { @@ -3143,12 +3329,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"cmd_tpl", (char*)"{ \"doorbellSuppressionDuration\": \"{{ value }}\" }" }, - { (char*)"val_tpl", (char*)"{{value_json.doorbellSuppressionDuration}}" }, - { (char*)"min", (char*)"500" }, - { (char*)"max", (char*)"10000" }, - { (char*)"step", (char*)"1000" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"cmd_tpl", (char*)"{ \"doorbellSuppressionDuration\": \"{{ value }}\" }" }, + { (char*)"val_tpl", (char*)"{{value_json.doorbellSuppressionDuration}}" }, + { (char*)"min", (char*)"500" }, + { (char*)"max", (char*)"10000" }, + { (char*)"step", (char*)"1000" } + }); } else { @@ -3239,12 +3427,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"soundConfirmation\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"soundConfirmation\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.soundConfirmation}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"soundConfirmation\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"soundConfirmation\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.soundConfirmation}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -3325,12 +3515,14 @@ void NukiNetwork::publishHASSConfigAdditionalOpenerEntities(char *deviceType, co "", "config", String("~") + mqtt_topic_config_action, - { { (char*)"en", (char*)"true" }, - { (char*)"pl_on", (char*)"{ \"automaticBatteryTypeDetection\": \"1\"}" }, - { (char*)"pl_off", (char*)"{ \"automaticBatteryTypeDetection\": \"0\"}" }, - { (char*)"val_tpl", (char*)"{{value_json.automaticBatteryTypeDetection}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + { + { (char*)"en", (char*)"true" }, + { (char*)"pl_on", (char*)"{ \"automaticBatteryTypeDetection\": \"1\"}" }, + { (char*)"pl_off", (char*)"{ \"automaticBatteryTypeDetection\": \"0\"}" }, + { (char*)"val_tpl", (char*)"{{value_json.automaticBatteryTypeDetection}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } else { @@ -3353,8 +3545,10 @@ void NukiNetwork::publishHASSConfigAccessLog(char *deviceType, const char *baseT "", "diagnostic", "", - { { (char*)"ic", (char*)"mdi:format-list-bulleted" }, - { (char*)"val_tpl", (char*)"{{ (value_json|selectattr('type', 'eq', 'LockAction')|selectattr('action', 'in', ['Lock', 'Unlock', 'Unlatch'])|first|default).authorizationName|default }}" }}); + { + { (char*)"ic", (char*)"mdi:format-list-bulleted" }, + { (char*)"val_tpl", (char*)"{{ (value_json|selectattr('type', 'eq', 'LockAction')|selectattr('action', 'in', ['Lock', 'Unlock', 'Unlatch'])|first|default).authorizationName|default }}" } + }); String rollingSate = "~"; rollingSate.concat(mqtt_topic_lock_log_rolling); @@ -3373,30 +3567,34 @@ void NukiNetwork::publishHASSConfigAccessLog(char *deviceType, const char *baseT "", "diagnostic", "", - { { (char*)"ic", (char*)"mdi:format-list-bulleted" }, - { (char*)"json_attr_t", (char*)rollingStateChr }, - { (char*)"val_tpl", (char*)"{{value_json.index}}" }}); + { + { (char*)"ic", (char*)"mdi:format-list-bulleted" }, + { (char*)"json_attr_t", (char*)rollingStateChr }, + { (char*)"val_tpl", (char*)"{{value_json.index}}" } + }); } void NukiNetwork::publishHASSConfigKeypad(char *deviceType, const char *baseTopic, char *name, char *uidString) { // Keypad battery critical - publishHassTopic("binary_sensor", - "keypad_battery_low", - uidString, - "_keypad_battery_low", - "Keypad battery low", - name, - baseTopic, - String("~") + mqtt_topic_battery_basic_json, - deviceType, - "battery", - "", - "diagnostic", - "", - {{(char*)"pl_on", (char*)"1"}, - {(char*)"pl_off", (char*)"0"}, - {(char*)"val_tpl", (char*)"{{value_json.keypadCritical}}" }}); + publishHassTopic("binary_sensor", + "keypad_battery_low", + uidString, + "_keypad_battery_low", + "Keypad battery low", + name, + baseTopic, + String("~") + mqtt_topic_battery_basic_json, + deviceType, + "battery", + "", + "diagnostic", + "", + { + {(char*)"pl_on", (char*)"1"}, + {(char*)"pl_off", (char*)"0"}, + {(char*)"val_tpl", (char*)"{{value_json.keypadCritical}}" } + }); // Query Keypad publishHassTopic("button", @@ -3412,8 +3610,10 @@ void NukiNetwork::publishHASSConfigKeypad(char *deviceType, const char *baseTopi "", "diagnostic", String("~") + mqtt_topic_query_keypad, - { { (char*)"en", (char*)"false" }, - { (char*)"pl_prs", (char*)"1" }}); + { + { (char*)"en", (char*)"false" }, + { (char*)"pl_prs", (char*)"1" } + }); publishHassTopic("sensor", "keypad_status", @@ -3428,8 +3628,10 @@ void NukiNetwork::publishHASSConfigKeypad(char *deviceType, const char *baseTopi "", "diagnostic", "", - { { (char*)"ic", (char*)"mdi:drag-vertical" }, - { (char*)"val_tpl", (char*)"{{ (value_json|selectattr('type', 'eq', 'KeypadAction')|first|default).completionStatus|default }}" }}); + { + { (char*)"ic", (char*)"mdi:drag-vertical" }, + { (char*)"val_tpl", (char*)"{{ (value_json|selectattr('type', 'eq', 'KeypadAction')|first|default).completionStatus|default }}" } + }); } void NukiNetwork::publishHASSWifiRssiConfig(char *deviceType, const char *baseTopic, char *name, char *uidString) @@ -3452,24 +3654,24 @@ void NukiNetwork::publishHASSWifiRssiConfig(char *deviceType, const char *baseTo "measurement", "diagnostic", "", - { {(char*)"unit_of_meas", (char*)"dBm"} }); + { {(char*)"unit_of_meas", (char*)"dBm"} }); } void NukiNetwork::publishHassTopic(const String& mqttDeviceType, - const String& mqttDeviceName, - const String& uidString, - const String& uidStringPostfix, - const String& displayName, - const String& name, - const String& baseTopic, - const String& stateTopic, - const String& deviceType, - const String& deviceClass, - const String& stateClass, - const String& entityCat, - const String& commandTopic, - std::vector> additionalEntries -) + const String& mqttDeviceName, + const String& uidString, + const String& uidStringPostfix, + const String& displayName, + const String& name, + const String& baseTopic, + const String& stateTopic, + const String& deviceType, + const String& deviceClass, + const String& stateClass, + const String& entityCat, + const String& commandTopic, + std::vector> additionalEntries + ) { if(!_mqttClientInitiated) { @@ -3522,10 +3724,10 @@ void NukiNetwork::removeTopic(const String& mqttPath, const String& mqttTopic) path.concat(mqttTopic); esp_mqtt_client_publish(_mqttClient, path.c_str(), "", 0, MQTT_QOS_LEVEL, 1); - #ifdef DEBUG_NUKIHUB +#ifdef DEBUG_NUKIHUB Log->print(F("Removing MQTT topic: ")); Log->println(path.c_str()); - #endif +#endif } @@ -3631,18 +3833,18 @@ void NukiNetwork::removeHASSConfigTopic(char *deviceType, char *name, char *uidS } JsonDocument NukiNetwork::createHassJson(const String& uidString, - const String& uidStringPostfix, - const String& displayName, - const String& name, - const String& baseTopic, - const String& stateTopic, - const String& deviceType, - const String& deviceClass, - const String& stateClass, - const String& entityCat, - const String& commandTopic, - std::vector> additionalEntries -) + const String& uidStringPostfix, + const String& displayName, + const String& name, + const String& baseTopic, + const String& stateTopic, + const String& deviceType, + const String& deviceClass, + const String& stateClass, + const String& entityCat, + const String& commandTopic, + std::vector> additionalEntries + ) { JsonDocument json; json.clear(); @@ -3702,190 +3904,196 @@ JsonDocument NukiNetwork::createHassJson(const String& uidString, return json; } -void NukiNetwork::batteryTypeToString(const Nuki::BatteryType battype, char* str) { - switch (battype) { +void NukiNetwork::batteryTypeToString(const Nuki::BatteryType battype, char* str) +{ + switch (battype) + { case Nuki::BatteryType::Alkali: - strcpy(str, "Alkali"); - break; + strcpy(str, "Alkali"); + break; case Nuki::BatteryType::Accumulators: - strcpy(str, "Accumulators"); - break; + strcpy(str, "Accumulators"); + break; case Nuki::BatteryType::Lithium: - strcpy(str, "Lithium"); - break; + strcpy(str, "Lithium"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetwork::advertisingModeToString(const Nuki::AdvertisingMode advmode, char* str) { - switch (advmode) { +void NukiNetwork::advertisingModeToString(const Nuki::AdvertisingMode advmode, char* str) +{ + switch (advmode) + { case Nuki::AdvertisingMode::Automatic: - strcpy(str, "Automatic"); - break; + strcpy(str, "Automatic"); + break; case Nuki::AdvertisingMode::Normal: - strcpy(str, "Normal"); - break; + strcpy(str, "Normal"); + break; case Nuki::AdvertisingMode::Slow: - strcpy(str, "Slow"); - break; + strcpy(str, "Slow"); + break; case Nuki::AdvertisingMode::Slowest: - strcpy(str, "Slowest"); - break; + strcpy(str, "Slowest"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetwork::timeZoneIdToString(const Nuki::TimeZoneId timeZoneId, char* str) { - switch (timeZoneId) { +void NukiNetwork::timeZoneIdToString(const Nuki::TimeZoneId timeZoneId, char* str) +{ + switch (timeZoneId) + { case Nuki::TimeZoneId::Africa_Cairo: - strcpy(str, "Africa/Cairo"); - break; + strcpy(str, "Africa/Cairo"); + break; case Nuki::TimeZoneId::Africa_Lagos: - strcpy(str, "Africa/Lagos"); - break; + strcpy(str, "Africa/Lagos"); + break; case Nuki::TimeZoneId::Africa_Maputo: - strcpy(str, "Africa/Maputo"); - break; + strcpy(str, "Africa/Maputo"); + break; case Nuki::TimeZoneId::Africa_Nairobi: - strcpy(str, "Africa/Nairobi"); - break; + strcpy(str, "Africa/Nairobi"); + break; case Nuki::TimeZoneId::America_Anchorage: - strcpy(str, "America/Anchorage"); - break; + strcpy(str, "America/Anchorage"); + break; case Nuki::TimeZoneId::America_Argentina_Buenos_Aires: - strcpy(str, "America/Argentina/Buenos_Aires"); - break; + strcpy(str, "America/Argentina/Buenos_Aires"); + break; case Nuki::TimeZoneId::America_Chicago: - strcpy(str, "America/Chicago"); - break; + strcpy(str, "America/Chicago"); + break; case Nuki::TimeZoneId::America_Denver: - strcpy(str, "America/Denver"); - break; + strcpy(str, "America/Denver"); + break; case Nuki::TimeZoneId::America_Halifax: - strcpy(str, "America/Halifax"); - break; + strcpy(str, "America/Halifax"); + break; case Nuki::TimeZoneId::America_Los_Angeles: - strcpy(str, "America/Los_Angeles"); - break; + strcpy(str, "America/Los_Angeles"); + break; case Nuki::TimeZoneId::America_Manaus: - strcpy(str, "America/Manaus"); - break; + strcpy(str, "America/Manaus"); + break; case Nuki::TimeZoneId::America_Mexico_City: - strcpy(str, "America/Mexico_City"); - break; + strcpy(str, "America/Mexico_City"); + break; case Nuki::TimeZoneId::America_New_York: - strcpy(str, "America/New_York"); - break; + strcpy(str, "America/New_York"); + break; case Nuki::TimeZoneId::America_Phoenix: - strcpy(str, "America/Phoenix"); - break; + strcpy(str, "America/Phoenix"); + break; case Nuki::TimeZoneId::America_Regina: - strcpy(str, "America/Regina"); - break; + strcpy(str, "America/Regina"); + break; case Nuki::TimeZoneId::America_Santiago: - strcpy(str, "America/Santiago"); - break; + strcpy(str, "America/Santiago"); + break; case Nuki::TimeZoneId::America_Sao_Paulo: - strcpy(str, "America/Sao_Paulo"); - break; + strcpy(str, "America/Sao_Paulo"); + break; case Nuki::TimeZoneId::America_St_Johns: - strcpy(str, "America/St_Johns"); - break; + strcpy(str, "America/St_Johns"); + break; case Nuki::TimeZoneId::Asia_Bangkok: - strcpy(str, "Asia/Bangkok"); - break; + strcpy(str, "Asia/Bangkok"); + break; case Nuki::TimeZoneId::Asia_Dubai: - strcpy(str, "Asia/Dubai"); - break; + strcpy(str, "Asia/Dubai"); + break; case Nuki::TimeZoneId::Asia_Hong_Kong: - strcpy(str, "Asia/Hong_Kong"); - break; + strcpy(str, "Asia/Hong_Kong"); + break; case Nuki::TimeZoneId::Asia_Jerusalem: - strcpy(str, "Asia/Jerusalem"); - break; + strcpy(str, "Asia/Jerusalem"); + break; case Nuki::TimeZoneId::Asia_Karachi: - strcpy(str, "Asia/Karachi"); - break; + strcpy(str, "Asia/Karachi"); + break; case Nuki::TimeZoneId::Asia_Kathmandu: - strcpy(str, "Asia/Kathmandu"); - break; + strcpy(str, "Asia/Kathmandu"); + break; case Nuki::TimeZoneId::Asia_Kolkata: - strcpy(str, "Asia/Kolkata"); - break; + strcpy(str, "Asia/Kolkata"); + break; case Nuki::TimeZoneId::Asia_Riyadh: - strcpy(str, "Asia/Riyadh"); - break; + strcpy(str, "Asia/Riyadh"); + break; case Nuki::TimeZoneId::Asia_Seoul: - strcpy(str, "Asia/Seoul"); - break; + strcpy(str, "Asia/Seoul"); + break; case Nuki::TimeZoneId::Asia_Shanghai: - strcpy(str, "Asia/Shanghai"); - break; + strcpy(str, "Asia/Shanghai"); + break; case Nuki::TimeZoneId::Asia_Tehran: - strcpy(str, "Asia/Tehran"); - break; + strcpy(str, "Asia/Tehran"); + break; case Nuki::TimeZoneId::Asia_Tokyo: - strcpy(str, "Asia/Tokyo"); - break; + strcpy(str, "Asia/Tokyo"); + break; case Nuki::TimeZoneId::Asia_Yangon: - strcpy(str, "Asia/Yangon"); - break; + strcpy(str, "Asia/Yangon"); + break; case Nuki::TimeZoneId::Australia_Adelaide: - strcpy(str, "Australia/Adelaide"); - break; + strcpy(str, "Australia/Adelaide"); + break; case Nuki::TimeZoneId::Australia_Brisbane: - strcpy(str, "Australia/Brisbane"); - break; + strcpy(str, "Australia/Brisbane"); + break; case Nuki::TimeZoneId::Australia_Darwin: - strcpy(str, "Australia/Darwin"); - break; + strcpy(str, "Australia/Darwin"); + break; case Nuki::TimeZoneId::Australia_Hobart: - strcpy(str, "Australia/Hobart"); - break; + strcpy(str, "Australia/Hobart"); + break; case Nuki::TimeZoneId::Australia_Perth: - strcpy(str, "Australia/Perth"); - break; + strcpy(str, "Australia/Perth"); + break; case Nuki::TimeZoneId::Australia_Sydney: - strcpy(str, "Australia/Sydney"); - break; + strcpy(str, "Australia/Sydney"); + break; case Nuki::TimeZoneId::Europe_Berlin: - strcpy(str, "Europe/Berlin"); - break; + strcpy(str, "Europe/Berlin"); + break; case Nuki::TimeZoneId::Europe_Helsinki: - strcpy(str, "Europe/Helsinki"); - break; + strcpy(str, "Europe/Helsinki"); + break; case Nuki::TimeZoneId::Europe_Istanbul: - strcpy(str, "Europe/Istanbul"); - break; + strcpy(str, "Europe/Istanbul"); + break; case Nuki::TimeZoneId::Europe_London: - strcpy(str, "Europe/London"); - break; + strcpy(str, "Europe/London"); + break; case Nuki::TimeZoneId::Europe_Moscow: - strcpy(str, "Europe/Moscow"); - break; + strcpy(str, "Europe/Moscow"); + break; case Nuki::TimeZoneId::Pacific_Auckland: - strcpy(str, "Pacific/Auckland"); - break; + strcpy(str, "Pacific/Auckland"); + break; case Nuki::TimeZoneId::Pacific_Guam: - strcpy(str, "Pacific/Guam"); - break; + strcpy(str, "Pacific/Guam"); + break; case Nuki::TimeZoneId::Pacific_Honolulu: - strcpy(str, "Pacific/Honolulu"); - break; + strcpy(str, "Pacific/Honolulu"); + break; case Nuki::TimeZoneId::Pacific_Pago_Pago: - strcpy(str, "Pacific/Pago_Pago"); - break; + strcpy(str, "Pacific/Pago_Pago"); + break; case Nuki::TimeZoneId::None: - strcpy(str, "None"); - break; + strcpy(str, "None"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } uint16_t NukiNetwork::subscribe(const char *topic, uint8_t qos) diff --git a/src/NukiNetworkLock.cpp b/src/NukiNetworkLock.cpp index 9a65f18..4397650 100644 --- a/src/NukiNetworkLock.cpp +++ b/src/NukiNetworkLock.cpp @@ -15,11 +15,11 @@ extern const uint8_t x509_crt_imported_bundle_bin_start[] asm("_binary_x509_crt_ extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_bundle_end"); NukiNetworkLock::NukiNetworkLock(NukiNetwork* network, NukiOfficial* nukiOfficial, Preferences* preferences, char* buffer, size_t bufferSize) -: _network(network), - _nukiOfficial(nukiOfficial), - _preferences(preferences), - _buffer(buffer), - _bufferSize(bufferSize) + : _network(network), + _nukiOfficial(nukiOfficial), + _preferences(preferences), + _buffer(buffer), + _bufferSize(bufferSize) { _nukiPublisher = new NukiPublisher(network, _mqttPath); _nukiOfficial->setPublisher(_nukiPublisher); @@ -201,21 +201,26 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, JsonDocument doc; NetworkClientSecure *client = new NetworkClientSecure; - if (client) { + if (client) + { client->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); { HTTPClient https; https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); https.useHTTP10(true); - if (https.begin(*client, GITHUB_OTA_MANIFEST_URL)) { + if (https.begin(*client, GITHUB_OTA_MANIFEST_URL)) + { int httpResponseCode = https.GET(); if (httpResponseCode == HTTP_CODE_OK || httpResponseCode == HTTP_CODE_MOVED_PERMANENTLY) { DeserializationError jsonError = deserializeJson(doc, https.getStream()); - if (!jsonError) { otaManifestSuccess = true; } + if (!jsonError) + { + otaManifestSuccess = true; + } } } https.end(); @@ -296,18 +301,27 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, else if(comparePrefixedPath(topic, mqtt_topic_webserver_action)) { if(strcmp(data, "") == 0 || - strcmp(data, "--") == 0) return; + strcmp(data, "--") == 0) + { + return; + } if(strcmp(data, "1") == 0) { - if(_preferences->getBool(preference_webserver_enabled, true) || forceEnableWebServer) return; + if(_preferences->getBool(preference_webserver_enabled, true) || forceEnableWebServer) + { + return; + } Log->println(F("Webserver enabled, restarting.")); _preferences->putBool(preference_webserver_enabled, true); } else if (strcmp(data, "0") == 0) { - if(!_preferences->getBool(preference_webserver_enabled, true) && !forceEnableWebServer) return; + if(!_preferences->getBool(preference_webserver_enabled, true) && !forceEnableWebServer) + { + return; + } Log->println(F("Webserver disabled, restarting.")); _preferences->putBool(preference_webserver_enabled, false); } @@ -320,9 +334,15 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, else if(comparePrefixedPath(topic, mqtt_topic_lock_log_rolling_last)) { if(strcmp(data, "") == 0 || - strcmp(data, "--") == 0) return; + strcmp(data, "--") == 0) + { + return; + } - if(atoi(data) > 0 && atoi(data) > _lastRollingLog) _lastRollingLog = atoi(data); + if(atoi(data) > 0 && atoi(data) > _lastRollingLog) + { + _lastRollingLog = atoi(data); + } } if(_nukiOfficial->getOffEnabled()) @@ -342,11 +362,14 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, if(comparePrefixedPath(topic, mqtt_topic_lock_action)) { if(strcmp(data, "") == 0 || - strcmp(data, "--") == 0 || - strcmp(data, "ack") == 0 || - strcmp(data, "unknown_action") == 0 || - strcmp(data, "denied") == 0 || - strcmp(data, "error") == 0) return; + strcmp(data, "--") == 0 || + strcmp(data, "ack") == 0 || + strcmp(data, "unknown_action") == 0 || + strcmp(data, "denied") == 0 || + strcmp(data, "error") == 0) + { + return; + } Log->print(F("Lock action received: ")); Log->println(data); @@ -358,18 +381,18 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, switch(lockActionResult) { - case LockActionResult::Success: - publishString(mqtt_topic_lock_action, "ack", false); - break; - case LockActionResult::UnknownAction: - publishString(mqtt_topic_lock_action, "unknown_action", false); - break; - case LockActionResult::AccessDenied: - publishString(mqtt_topic_lock_action, "denied", false); - break; - case LockActionResult::Failed: - publishString(mqtt_topic_lock_action, "error", false); - break; + case LockActionResult::Success: + publishString(mqtt_topic_lock_action, "ack", false); + break; + case LockActionResult::UnknownAction: + publishString(mqtt_topic_lock_action, "unknown_action", false); + break; + case LockActionResult::AccessDenied: + publishString(mqtt_topic_lock_action, "denied", false); + break; + case LockActionResult::Failed: + publishString(mqtt_topic_lock_action, "error", false); + break; } } @@ -379,7 +402,10 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, { if(_keypadCommandReceivedReceivedCallback != nullptr) { - if(strcmp(data, "--") == 0) return; + if(strcmp(data, "--") == 0) + { + return; + } _keypadCommandReceivedReceivedCallback(data, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled); @@ -439,7 +465,10 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, if(comparePrefixedPath(topic, mqtt_topic_config_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_configUpdateReceivedCallback != NULL) { @@ -451,7 +480,10 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, if(comparePrefixedPath(topic, mqtt_topic_keypad_json_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_keypadJsonCommandReceivedReceivedCallback != NULL) { @@ -463,7 +495,10 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, if(comparePrefixedPath(topic, mqtt_topic_timecontrol_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_timeControlCommandReceivedReceivedCallback != NULL) { @@ -475,7 +510,10 @@ void NukiNetworkLock::onMqttDataReceived(char* topic, int topic_len, char* data, if(comparePrefixedPath(topic, mqtt_topic_auth_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_authCommandReceivedReceivedCallback != NULL) { @@ -633,39 +671,39 @@ void NukiNetworkLock::publishState(NukiLock::LockState lockState) { switch(lockState) { - case NukiLock::LockState::Locked: - publishString(mqtt_topic_lock_ha_state, "locked", true); - publishString(mqtt_topic_lock_binary_state, "locked", true); - break; - case NukiLock::LockState::Locking: - publishString(mqtt_topic_lock_ha_state, "locking", true); - publishString(mqtt_topic_lock_binary_state, "locked", true); - break; - case NukiLock::LockState::Unlocking: - publishString(mqtt_topic_lock_ha_state, "unlocking", true); - publishString(mqtt_topic_lock_binary_state, "unlocked", true); - break; - case NukiLock::LockState::Unlocked: - case NukiLock::LockState::UnlockedLnga: - publishString(mqtt_topic_lock_ha_state, "unlocked", true); - publishString(mqtt_topic_lock_binary_state, "unlocked", true); - break; - case NukiLock::LockState::Unlatched: - publishString(mqtt_topic_lock_ha_state, "open", true); - publishString(mqtt_topic_lock_binary_state, "unlocked", true); - break; - case NukiLock::LockState::Unlatching: - publishString(mqtt_topic_lock_ha_state, "opening", true); - publishString(mqtt_topic_lock_binary_state, "unlocked", true); - break; - case NukiLock::LockState::Uncalibrated: - case NukiLock::LockState::Calibration: - case NukiLock::LockState::BootRun: - case NukiLock::LockState::MotorBlocked: - publishString(mqtt_topic_lock_ha_state, "jammed", true); - break; - default: - break; + case NukiLock::LockState::Locked: + publishString(mqtt_topic_lock_ha_state, "locked", true); + publishString(mqtt_topic_lock_binary_state, "locked", true); + break; + case NukiLock::LockState::Locking: + publishString(mqtt_topic_lock_ha_state, "locking", true); + publishString(mqtt_topic_lock_binary_state, "locked", true); + break; + case NukiLock::LockState::Unlocking: + publishString(mqtt_topic_lock_ha_state, "unlocking", true); + publishString(mqtt_topic_lock_binary_state, "unlocked", true); + break; + case NukiLock::LockState::Unlocked: + case NukiLock::LockState::UnlockedLnga: + publishString(mqtt_topic_lock_ha_state, "unlocked", true); + publishString(mqtt_topic_lock_binary_state, "unlocked", true); + break; + case NukiLock::LockState::Unlatched: + publishString(mqtt_topic_lock_ha_state, "open", true); + publishString(mqtt_topic_lock_binary_state, "unlocked", true); + break; + case NukiLock::LockState::Unlatching: + publishString(mqtt_topic_lock_ha_state, "opening", true); + publishString(mqtt_topic_lock_binary_state, "unlocked", true); + break; + case NukiLock::LockState::Uncalibrated: + case NukiLock::LockState::Calibration: + case NukiLock::LockState::BootRun: + case NukiLock::LockState::MotorBlocked: + publishString(mqtt_topic_lock_ha_state, "jammed", true); + break; + default: + break; } } @@ -686,7 +724,10 @@ void NukiNetworkLock::publishAuthorizationInfo(const std::list authIndex) { @@ -712,7 +753,7 @@ void NukiNetworkLock::publishAuthorizationInfo(const std::list().length() == 0 && _authEntries.count(log.authId) > 0) { - entry["authorizationName"] = _authEntries[log.authId]; + entry["authorizationName"] = _authEntries[log.authId]; } entry["timeYear"] = log.timeStampYear; @@ -728,69 +769,75 @@ void NukiNetworkLock::publishAuthorizationInfo(const std::list _lastRollingLog) @@ -804,8 +851,14 @@ void NukiNetworkLock::publishAuthorizationInfo(const std::list 0) { @@ -1013,7 +1066,10 @@ void NukiNetworkLock::publishKeypad(const std::list& entr jsonEntry["codeId"] = entry.codeId; - if(publishCode) jsonEntry["code"] = entry.code; + if(publishCode) + { + jsonEntry["code"] = entry.code; + } jsonEntry["enabled"] = entry.enabled; jsonEntry["name"] = entry.name; char createdDT[20]; @@ -1034,7 +1090,8 @@ void NukiNetworkLock::publishKeypad(const std::list& entr uint8_t allowedWeekdaysInt = entry.allowedWeekdays; JsonArray weekdays = jsonEntry["allowedWeekdays"].to(); - while(allowedWeekdaysInt > 0) { + while(allowedWeekdaysInt > 0) + { if(allowedWeekdaysInt >= 64) { weekdays.add("mon"); @@ -1111,24 +1168,26 @@ void NukiNetworkLock::publishKeypad(const std::list& entr std::string displayName = std::string("Keypad - ") + std::string((char*)codeName) + " - " + std::to_string(entry.codeId); _network->publishHassTopic("switch", - mqttDeviceName.c_str(), - uidString, - uidStringPostfix.c_str(), - displayName.c_str(), - _nukiName, - baseTopic.c_str(), - String("~") + basePath.c_str(), - (char*)"SmartLock", - "", - "", - "diagnostic", - String("~") + mqtt_topic_keypad_json_action, - { { (char*)"json_attr_t", (char*)basePathPrefixChr }, - { (char*)"pl_on", (char*)enaCommand.c_str() }, - { (char*)"pl_off", (char*)disCommand.c_str() }, - { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"SmartLock", + "", + "", + "diagnostic", + String("~") + mqtt_topic_keypad_json_action, + { + { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } ++index; @@ -1202,7 +1261,10 @@ void NukiNetworkLock::publishKeypad(const std::list& entr void NukiNetworkLock::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } char codeName[sizeof(entry.name) + 1]; memset(codeName, 0, sizeof(codeName)); @@ -1245,7 +1307,8 @@ void NukiNetworkLock::publishTimeControl(const std::list(); - while(weekdaysInt > 0) { + while(weekdaysInt > 0) + { if(weekdaysInt >= 64) { weekdays.add("mon"); @@ -1319,24 +1382,26 @@ void NukiNetworkLock::publishTimeControl(const std::listpublishHassTopic("switch", - mqttDeviceName.c_str(), - uidString, - uidStringPostfix.c_str(), - displayName.c_str(), - _nukiName, - baseTopic.c_str(), - String("~") + basePath.c_str(), - (char*)"SmartLock", - "", - "", - "diagnostic", - String("~") + mqtt_topic_timecontrol_action, - { { (char*)"json_attr_t", (char*)basePathPrefixChr }, - { (char*)"pl_on", (char*)enaCommand.c_str() }, - { (char*)"pl_off", (char*)disCommand.c_str() }, - { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"SmartLock", + "", + "", + "diagnostic", + String("~") + mqtt_topic_timecontrol_action, + { + { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } ++index; @@ -1393,7 +1458,8 @@ void NukiNetworkLock::publishAuth(const std::list& uint8_t allowedWeekdaysInt = entry.allowedWeekdays; JsonArray weekdays = jsonEntry["allowedWeekdays"].to(); - while(allowedWeekdaysInt > 0) { + while(allowedWeekdaysInt > 0) + { if(allowedWeekdaysInt >= 64) { weekdays.add("mon"); @@ -1466,24 +1532,26 @@ void NukiNetworkLock::publishAuth(const std::list& std::string displayName = std::string("Authorization - ") + std::to_string(entry.authId); _network->publishHassTopic("switch", - mqttDeviceName.c_str(), - uidString, - uidStringPostfix.c_str(), - displayName.c_str(), - _nukiName, - baseTopic.c_str(), - String("~") + basePath.c_str(), - (char*)"SmartLock", - "", - "", - "diagnostic", - String("~") + mqtt_topic_auth_action, - { { (char*)"json_attr_t", (char*)basePathPrefixChr }, - { (char*)"pl_on", (char*)enaCommand.c_str() }, - { (char*)"pl_off", (char*)disCommand.c_str() }, - { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"SmartLock", + "", + "", + "diagnostic", + String("~") + mqtt_topic_auth_action, + { + { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } ++index; @@ -1510,7 +1578,10 @@ void NukiNetworkLock::publishConfigCommandResult(const char* result) void NukiNetworkLock::publishKeypadCommandResult(const char* result) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } publishString(mqtt_topic_keypad_command_result, result, true); } @@ -1551,7 +1622,10 @@ void NukiNetworkLock::setConfigUpdateReceivedCallback(void (*configUpdateReceive void NukiNetworkLock::setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled)) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } _keypadCommandReceivedReceivedCallback = keypadCommandReceivedReceivedCallback; } @@ -1604,7 +1678,7 @@ bool NukiNetworkLock::comparePrefixedPath(const char *fullPath, const char *subP } void NukiNetworkLock::publishHASSConfig(char *deviceType, const char *baseTopic, char *name, char *uidString, const char *softwareVersion, const char *hardwareVersion, const bool& hasDoorSensor, const bool& hasKeypad, const bool& publishAuthData, char *lockAction, - char *unlockAction, char *openAction) + char *unlockAction, char *openAction) { _network->publishHASSConfig(deviceType, baseTopic, name, uidString, softwareVersion, hardwareVersion, "~/maintenance/mqttConnectionState", hasKeypad, lockAction, unlockAction, openAction); _network->publishHASSConfigAdditionalLockEntities(deviceType, baseTopic, name, uidString); @@ -1618,9 +1692,9 @@ void NukiNetworkLock::publishHASSConfig(char *deviceType, const char *baseTopic, _network->removeHASSConfigTopic((char*)"binary_sensor", (char*)"door_sensor", uidString); } - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 _network->publishHASSWifiRssiConfig(deviceType, baseTopic, name, uidString); - #endif +#endif if(publishAuthData) { @@ -1719,76 +1793,82 @@ uint8_t NukiNetworkLock::queryCommands() return qc; } -void NukiNetworkLock::buttonPressActionToString(const NukiLock::ButtonPressAction btnPressAction, char* str) { - switch (btnPressAction) { +void NukiNetworkLock::buttonPressActionToString(const NukiLock::ButtonPressAction btnPressAction, char* str) +{ + switch (btnPressAction) + { case NukiLock::ButtonPressAction::NoAction: - strcpy(str, "No Action"); - break; + strcpy(str, "No Action"); + break; case NukiLock::ButtonPressAction::Intelligent: - strcpy(str, "Intelligent"); - break; + strcpy(str, "Intelligent"); + break; case NukiLock::ButtonPressAction::Unlock: - strcpy(str, "Unlock"); - break; + strcpy(str, "Unlock"); + break; case NukiLock::ButtonPressAction::Lock: - strcpy(str, "Lock"); - break; + strcpy(str, "Lock"); + break; case NukiLock::ButtonPressAction::Unlatch: - strcpy(str, "Unlatch"); - break; + strcpy(str, "Unlatch"); + break; case NukiLock::ButtonPressAction::LockNgo: - strcpy(str, "Lock n Go"); - break; + strcpy(str, "Lock n Go"); + break; case NukiLock::ButtonPressAction::ShowStatus: - strcpy(str, "Show Status"); - break; + strcpy(str, "Show Status"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetworkLock::homeKitStatusToString(const int hkstatus, char* str) { - switch (hkstatus) { +void NukiNetworkLock::homeKitStatusToString(const int hkstatus, char* str) +{ + switch (hkstatus) + { case 0: - strcpy(str, "Not Available"); - break; + strcpy(str, "Not Available"); + break; case 1: - strcpy(str, "Disabled"); - break; + strcpy(str, "Disabled"); + break; case 2: - strcpy(str, "Enabled"); - break; + strcpy(str, "Enabled"); + break; case 3: - strcpy(str, "Enabled & Paired"); - break; + strcpy(str, "Enabled & Paired"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetworkLock::fobActionToString(const int fobact, char* str) { - switch (fobact) { +void NukiNetworkLock::fobActionToString(const int fobact, char* str) +{ + switch (fobact) + { case 0: - strcpy(str, "No Action"); - break; + strcpy(str, "No Action"); + break; case 1: - strcpy(str, "Unlock"); - break; + strcpy(str, "Unlock"); + break; case 2: - strcpy(str, "Lock"); - break; + strcpy(str, "Lock"); + break; case 3: - strcpy(str, "Lock n Go"); - break; + strcpy(str, "Lock n Go"); + break; case 4: - strcpy(str, "Intelligent"); - break; + strcpy(str, "Intelligent"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } const uint32_t NukiNetworkLock::getAuthId() const diff --git a/src/NukiNetworkOpener.cpp b/src/NukiNetworkOpener.cpp index 537d281..1d34d46 100644 --- a/src/NukiNetworkOpener.cpp +++ b/src/NukiNetworkOpener.cpp @@ -7,10 +7,10 @@ #include NukiNetworkOpener::NukiNetworkOpener(NukiNetwork* network, Preferences* preferences, char* buffer, size_t bufferSize) - : _preferences(preferences), - _network(network), - _buffer(buffer), - _bufferSize(bufferSize) + : _preferences(preferences), + _network(network), + _buffer(buffer), + _bufferSize(bufferSize) { _nukiPublisher = new NukiPublisher(network, _mqttPath); @@ -124,9 +124,9 @@ void NukiNetworkOpener::initialize() } _network->addReconnectedCallback([&]() - { - _reconnected = true; - }); + { + _reconnected = true; + }); } void NukiNetworkOpener::update() @@ -149,19 +149,28 @@ void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* dat if(comparePrefixedPath(topic, mqtt_topic_lock_log_rolling_last)) { if(strcmp(data, "") == 0 || - strcmp(data, "--") == 0) return; + strcmp(data, "--") == 0) + { + return; + } - if(atoi(data) > 0 && atoi(data) > _lastRollingLog) _lastRollingLog = atoi(data); + if(atoi(data) > 0 && atoi(data) > _lastRollingLog) + { + _lastRollingLog = atoi(data); + } } if(comparePrefixedPath(topic, mqtt_topic_lock_action)) { if(strcmp(data, "") == 0 || - strcmp(data, "--") == 0 || - strcmp(data, "ack") == 0 || - strcmp(data, "unknown_action") == 0 || - strcmp(data, "denied") == 0 || - strcmp(data, "error") == 0) return; + strcmp(data, "--") == 0 || + strcmp(data, "ack") == 0 || + strcmp(data, "unknown_action") == 0 || + strcmp(data, "denied") == 0 || + strcmp(data, "error") == 0) + { + return; + } Log->print(F("Opener action received: ")); Log->println(data); @@ -173,18 +182,18 @@ void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* dat switch(lockActionResult) { - case LockActionResult::Success: - publishString(mqtt_topic_lock_action, "ack", false); - break; - case LockActionResult::UnknownAction: - publishString(mqtt_topic_lock_action, "unknown_action", false); - break; - case LockActionResult::AccessDenied: - publishString(mqtt_topic_lock_action, "denied", false); - break; - case LockActionResult::Failed: - publishString(mqtt_topic_lock_action, "error", false); - break; + case LockActionResult::Success: + publishString(mqtt_topic_lock_action, "ack", false); + break; + case LockActionResult::UnknownAction: + publishString(mqtt_topic_lock_action, "unknown_action", false); + break; + case LockActionResult::AccessDenied: + publishString(mqtt_topic_lock_action, "denied", false); + break; + case LockActionResult::Failed: + publishString(mqtt_topic_lock_action, "error", false); + break; } } @@ -194,7 +203,10 @@ void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* dat { if(_keypadCommandReceivedReceivedCallback != nullptr) { - if(strcmp(data, "--") == 0) return; + if(strcmp(data, "--") == 0) + { + return; + } _keypadCommandReceivedReceivedCallback(data, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled); @@ -254,7 +266,10 @@ void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* dat if(comparePrefixedPath(topic, mqtt_topic_config_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_configUpdateReceivedCallback != NULL) { @@ -266,7 +281,10 @@ void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* dat if(comparePrefixedPath(topic, mqtt_topic_keypad_json_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_keypadJsonCommandReceivedReceivedCallback != NULL) { @@ -278,7 +296,10 @@ void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* dat if(comparePrefixedPath(topic, mqtt_topic_timecontrol_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_timeControlCommandReceivedReceivedCallback != NULL) { @@ -290,7 +311,10 @@ void NukiNetworkOpener::onMqttDataReceived(char* topic, int topic_len, char* dat if(comparePrefixedPath(topic, mqtt_topic_auth_action)) { - if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) return; + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } if(_authCommandReceivedReceivedCallback != NULL) { @@ -329,7 +353,9 @@ void NukiNetworkOpener::publishKeyTurnerState(const NukiOpener::OpenerState& key { publishString(mqtt_topic_lock_continuous_mode, "on", true); json["continuous_mode"] = 1; - } else { + } + else + { publishString(mqtt_topic_lock_continuous_mode, "off", true); json["continuous_mode"] = 0; } @@ -429,28 +455,28 @@ void NukiNetworkOpener::publishState(NukiOpener::OpenerState lockState) { switch (lockState.lockState) { - case NukiOpener::LockState::Locked: - publishString(mqtt_topic_lock_ha_state, "locked", true); - publishString(mqtt_topic_lock_binary_state, "locked", true); - break; - case NukiOpener::LockState::RTOactive: - publishString(mqtt_topic_lock_ha_state, "unlocked", true); - publishString(mqtt_topic_lock_binary_state, "unlocked", true); - break; - case NukiOpener::LockState::Open: - publishString(mqtt_topic_lock_ha_state, "open", true); - publishString(mqtt_topic_lock_binary_state, "unlocked", true); - break; - case NukiOpener::LockState::Opening: - publishString(mqtt_topic_lock_ha_state, "opening", true); - publishString(mqtt_topic_lock_binary_state, "unlocked", true); - break; - case NukiOpener::LockState::Undefined: - case NukiOpener::LockState::Uncalibrated: - publishString(mqtt_topic_lock_ha_state, "jammed", true); - break; - default: - break; + case NukiOpener::LockState::Locked: + publishString(mqtt_topic_lock_ha_state, "locked", true); + publishString(mqtt_topic_lock_binary_state, "locked", true); + break; + case NukiOpener::LockState::RTOactive: + publishString(mqtt_topic_lock_ha_state, "unlocked", true); + publishString(mqtt_topic_lock_binary_state, "unlocked", true); + break; + case NukiOpener::LockState::Open: + publishString(mqtt_topic_lock_ha_state, "open", true); + publishString(mqtt_topic_lock_binary_state, "unlocked", true); + break; + case NukiOpener::LockState::Opening: + publishString(mqtt_topic_lock_ha_state, "opening", true); + publishString(mqtt_topic_lock_binary_state, "unlocked", true); + break; + case NukiOpener::LockState::Undefined: + case NukiOpener::LockState::Uncalibrated: + publishString(mqtt_topic_lock_ha_state, "jammed", true); + break; + default: + break; } } } @@ -472,7 +498,10 @@ void NukiNetworkOpener::publishAuthorizationInfo(const std::list authIndex) { @@ -481,7 +510,7 @@ void NukiNetworkOpener::publishAuthorizationInfo(const std::list 0) { memset(_authName, 0, sizeof(_authName)); @@ -495,12 +524,12 @@ void NukiNetworkOpener::publishAuthorizationInfo(const std::list().length() == 0 && _authEntries.count(log.authId) > 0) { - entry["authorizationName"] = _authEntries[log.authId]; + entry["authorizationName"] = _authEntries[log.authId]; } - + entry["timeYear"] = log.timeStampYear; entry["timeMonth"] = log.timeStampMonth; entry["timeDay"] = log.timeStampDay; @@ -514,104 +543,111 @@ void NukiNetworkOpener::publishAuthorizationInfo(const std::list _lastRollingLog) @@ -625,8 +661,14 @@ void NukiNetworkOpener::publishAuthorizationInfo(const std::list 0) { @@ -858,7 +900,10 @@ void NukiNetworkOpener::publishKeypad(const std::list& en jsonEntry["codeId"] = entry.codeId; - if(publishCode) jsonEntry["code"] = entry.code; + if(publishCode) + { + jsonEntry["code"] = entry.code; + } jsonEntry["enabled"] = entry.enabled; jsonEntry["name"] = entry.name; char createdDT[20]; @@ -879,7 +924,8 @@ void NukiNetworkOpener::publishKeypad(const std::list& en uint8_t allowedWeekdaysInt = entry.allowedWeekdays; JsonArray weekdays = jsonEntry["allowedWeekdays"].to(); - while(allowedWeekdaysInt > 0) { + while(allowedWeekdaysInt > 0) + { if(allowedWeekdaysInt >= 64) { weekdays.add("mon"); @@ -956,24 +1002,26 @@ void NukiNetworkOpener::publishKeypad(const std::list& en std::string displayName = std::string("Keypad - ") + std::string((char*)codeName) + " - " + std::to_string(entry.codeId); _network->publishHassTopic("switch", - mqttDeviceName.c_str(), - uidString, - uidStringPostfix.c_str(), - displayName.c_str(), - _nukiName, - baseTopic.c_str(), - String("~") + basePath.c_str(), - (char*)"SmartLock", - "", - "", - "diagnostic", - String("~") + mqtt_topic_keypad_json_action, - { { (char*)"json_attr_t", (char*)basePathPrefixChr }, - { (char*)"pl_on", (char*)enaCommand.c_str() }, - { (char*)"pl_off", (char*)disCommand.c_str() }, - { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"SmartLock", + "", + "", + "diagnostic", + String("~") + mqtt_topic_keypad_json_action, + { + { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } ++index; @@ -1062,7 +1110,8 @@ void NukiNetworkOpener::publishTimeControl(const std::list(); - while(weekdaysInt > 0) { + while(weekdaysInt > 0) + { if(weekdaysInt >= 64) { weekdays.add("mon"); @@ -1134,24 +1183,26 @@ void NukiNetworkOpener::publishTimeControl(const std::listpublishHassTopic("switch", - mqttDeviceName.c_str(), - uidString, - uidStringPostfix.c_str(), - displayName.c_str(), - _nukiName, - baseTopic.c_str(), - String("~") + basePath.c_str(), - (char*)"Opener", - "", - "", - "diagnostic", - String("~") + mqtt_topic_timecontrol_action, - { { (char*)"json_attr_t", (char*)basePathPrefixChr }, - { (char*)"pl_on", (char*)enaCommand.c_str() }, - { (char*)"pl_off", (char*)disCommand.c_str() }, - { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"Opener", + "", + "", + "diagnostic", + String("~") + mqtt_topic_timecontrol_action, + { + { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } ++index; @@ -1208,7 +1259,8 @@ void NukiNetworkOpener::publishAuth(const std::list(); - while(allowedWeekdaysInt > 0) { + while(allowedWeekdaysInt > 0) + { if(allowedWeekdaysInt >= 64) { weekdays.add("mon"); @@ -1281,24 +1333,26 @@ void NukiNetworkOpener::publishAuth(const std::listpublishHassTopic("switch", - mqttDeviceName.c_str(), - uidString, - uidStringPostfix.c_str(), - displayName.c_str(), - _nukiName, - baseTopic.c_str(), - String("~") + basePath.c_str(), - (char*)"Opener", - "", - "", - "diagnostic", - String("~") + mqtt_topic_auth_action, - { { (char*)"json_attr_t", (char*)basePathPrefixChr }, - { (char*)"pl_on", (char*)enaCommand.c_str() }, - { (char*)"pl_off", (char*)disCommand.c_str() }, - { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, - { (char*)"stat_on", (char*)"1" }, - { (char*)"stat_off", (char*)"0" }}); + mqttDeviceName.c_str(), + uidString, + uidStringPostfix.c_str(), + displayName.c_str(), + _nukiName, + baseTopic.c_str(), + String("~") + basePath.c_str(), + (char*)"Opener", + "", + "", + "diagnostic", + String("~") + mqtt_topic_auth_action, + { + { (char*)"json_attr_t", (char*)basePathPrefixChr }, + { (char*)"pl_on", (char*)enaCommand.c_str() }, + { (char*)"pl_off", (char*)disCommand.c_str() }, + { (char*)"val_tpl", (char*)"{{value_json.enabled}}" }, + { (char*)"stat_on", (char*)"1" }, + { (char*)"stat_off", (char*)"0" } + }); } ++index; @@ -1325,7 +1379,10 @@ void NukiNetworkOpener::publishConfigCommandResult(const char* result) void NukiNetworkOpener::publishKeypadCommandResult(const char* result) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } publishString(mqtt_topic_keypad_command_result, result, true); } @@ -1361,7 +1418,10 @@ void NukiNetworkOpener::setConfigUpdateReceivedCallback(void (*configUpdateRecei void NukiNetworkOpener::setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled)) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } _keypadCommandReceivedReceivedCallback = keypadCommandReceivedReceivedCallback; } @@ -1423,7 +1483,10 @@ void NukiNetworkOpener::publishString(const char* topic, const char* value, bool void NukiNetworkOpener::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } char codeName[sizeof(entry.name) + 1]; memset(codeName, 0, sizeof(codeName)); @@ -1505,185 +1568,197 @@ uint8_t NukiNetworkOpener::queryCommands() return qc; } -void NukiNetworkOpener::buttonPressActionToString(const NukiOpener::ButtonPressAction btnPressAction, char* str) { - switch (btnPressAction) { +void NukiNetworkOpener::buttonPressActionToString(const NukiOpener::ButtonPressAction btnPressAction, char* str) +{ + switch (btnPressAction) + { case NukiOpener::ButtonPressAction::NoAction: - strcpy(str, "No Action"); - break; + strcpy(str, "No Action"); + break; case NukiOpener::ButtonPressAction::ToggleRTO: - strcpy(str, "Toggle RTO"); - break; + strcpy(str, "Toggle RTO"); + break; case NukiOpener::ButtonPressAction::ActivateRTO: - strcpy(str, "Activate RTO"); - break; + strcpy(str, "Activate RTO"); + break; case NukiOpener::ButtonPressAction::DeactivateRTO: - strcpy(str, "Deactivate RTO"); - break; + strcpy(str, "Deactivate RTO"); + break; case NukiOpener::ButtonPressAction::ToggleCM: - strcpy(str, "Toggle CM"); - break; + strcpy(str, "Toggle CM"); + break; case NukiOpener::ButtonPressAction::ActivateCM: - strcpy(str, "Activate CM"); - break; + strcpy(str, "Activate CM"); + break; case NukiOpener::ButtonPressAction::DectivateCM: - strcpy(str, "Deactivate CM"); - break; + strcpy(str, "Deactivate CM"); + break; case NukiOpener::ButtonPressAction::Open: - strcpy(str, "Open"); - break; + strcpy(str, "Open"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetworkOpener::fobActionToString(const int fobact, char* str) { - switch (fobact) { +void NukiNetworkOpener::fobActionToString(const int fobact, char* str) +{ + switch (fobact) + { case 0: - strcpy(str, "No Action"); - break; + strcpy(str, "No Action"); + break; case 1: - strcpy(str, "Toggle RTO"); - break; + strcpy(str, "Toggle RTO"); + break; case 2: - strcpy(str, "Activate RTO"); - break; + strcpy(str, "Activate RTO"); + break; case 3: - strcpy(str, "Deactivate RTO"); - break; + strcpy(str, "Deactivate RTO"); + break; case 7: - strcpy(str, "Open"); - break; + strcpy(str, "Open"); + break; case 8: - strcpy(str, "Ring"); - break; + strcpy(str, "Ring"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetworkOpener::capabilitiesToString(const int capabilities, char* str) { - switch (capabilities) { +void NukiNetworkOpener::capabilitiesToString(const int capabilities, char* str) +{ + switch (capabilities) + { case 0: - strcpy(str, "Door opener"); - break; + strcpy(str, "Door opener"); + break; case 1: - strcpy(str, "Both"); - break; + strcpy(str, "Both"); + break; case 2: - strcpy(str, "RTO"); - break; + strcpy(str, "RTO"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetworkOpener::operatingModeToString(const int opmode, char* str) { - switch (opmode) { +void NukiNetworkOpener::operatingModeToString(const int opmode, char* str) +{ + switch (opmode) + { case 0: - strcpy(str, "Generic door opener"); - break; + strcpy(str, "Generic door opener"); + break; case 1: - strcpy(str, "Analogue intercom"); - break; + strcpy(str, "Analogue intercom"); + break; case 2: - strcpy(str, "Digital intercom"); - break; + strcpy(str, "Digital intercom"); + break; case 3: - strcpy(str, "Siedle"); - break; + strcpy(str, "Siedle"); + break; case 4: - strcpy(str, "TCS"); - break; + strcpy(str, "TCS"); + break; case 5: - strcpy(str, "Bticino"); - break; + strcpy(str, "Bticino"); + break; case 6: - strcpy(str, "Siedle HTS"); - break; + strcpy(str, "Siedle HTS"); + break; case 7: - strcpy(str, "STR"); - break; + strcpy(str, "STR"); + break; case 8: - strcpy(str, "Ritto"); - break; + strcpy(str, "Ritto"); + break; case 9: - strcpy(str, "Fermax"); - break; + strcpy(str, "Fermax"); + break; case 10: - strcpy(str, "Comelit"); - break; + strcpy(str, "Comelit"); + break; case 11: - strcpy(str, "Urmet BiBus"); - break; + strcpy(str, "Urmet BiBus"); + break; case 12: - strcpy(str, "Urmet 2Voice"); - break; + strcpy(str, "Urmet 2Voice"); + break; case 13: - strcpy(str, "Golmar"); - break; + strcpy(str, "Golmar"); + break; case 14: - strcpy(str, "SKS"); - break; + strcpy(str, "SKS"); + break; case 15: - strcpy(str, "Spare"); - break; + strcpy(str, "Spare"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetworkOpener::doorbellSuppressionToString(const int dbsupr, char* str) { - switch (dbsupr) { +void NukiNetworkOpener::doorbellSuppressionToString(const int dbsupr, char* str) +{ + switch (dbsupr) + { case 0: - strcpy(str, "Off"); - break; + strcpy(str, "Off"); + break; case 1: - strcpy(str, "CM"); - break; + strcpy(str, "CM"); + break; case 2: - strcpy(str, "RTO"); - break; + strcpy(str, "RTO"); + break; case 3: - strcpy(str, "CM & RTO"); - break; + strcpy(str, "CM & RTO"); + break; case 4: - strcpy(str, "Ring"); - break; + strcpy(str, "Ring"); + break; case 5: - strcpy(str, "CM & Ring"); - break; + strcpy(str, "CM & Ring"); + break; case 6: - strcpy(str, "RTO & Ring"); - break; + strcpy(str, "RTO & Ring"); + break; case 7: - strcpy(str, "CM & RTO & Ring"); - break; + strcpy(str, "CM & RTO & Ring"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } -void NukiNetworkOpener::soundToString(const int sound, char* str) { - switch (sound) { +void NukiNetworkOpener::soundToString(const int sound, char* str) +{ + switch (sound) + { case 0: - strcpy(str, "No Sound"); - break; + strcpy(str, "No Sound"); + break; case 1: - strcpy(str, "Sound 1"); - break; + strcpy(str, "Sound 1"); + break; case 2: - strcpy(str, "Sound 2"); - break; + strcpy(str, "Sound 2"); + break; case 3: - strcpy(str, "Sound 3"); - break; + strcpy(str, "Sound 3"); + break; default: - strcpy(str, "undefined"); - break; - } + strcpy(str, "undefined"); + break; + } } diff --git a/src/NukiOfficial.cpp b/src/NukiOfficial.cpp index 9eaef7d..af10b63 100644 --- a/src/NukiOfficial.cpp +++ b/src/NukiOfficial.cpp @@ -136,7 +136,10 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value Log->print(F("Battery critical: ")); Log->println(offCritical); - if(!_disableNonJSON) _publisher->publishBool(mqtt_topic_battery_critical, offCritical, true); + if(!_disableNonJSON) + { + _publisher->publishBool(mqtt_topic_battery_critical, offCritical, true); + } publishBatteryJson = true; } else if(strcmp(topic, mqtt_topic_official_batteryCharging) == 0) @@ -146,7 +149,10 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value Log->print(F("Battery charging: ")); Log->println(offCharging); - if(!_disableNonJSON) _publisher->publishBool(mqtt_topic_battery_charging, offCharging, true); + if(!_disableNonJSON) + { + _publisher->publishBool(mqtt_topic_battery_charging, offCharging, true); + } publishBatteryJson = true; } else if(strcmp(topic, mqtt_topic_official_batteryChargeState) == 0) @@ -156,19 +162,28 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value Log->print(F("Battery level: ")); Log->println(offChargeState); - if(!_disableNonJSON) _publisher->publishInt(mqtt_topic_battery_level, offChargeState, true); + if(!_disableNonJSON) + { + _publisher->publishInt(mqtt_topic_battery_level, offChargeState, true); + } publishBatteryJson = true; } else if(strcmp(topic, mqtt_topic_official_keypadBatteryCritical) == 0) { offKeypadCritical = (strcmp(value, "true") == 0 ? 1 : 0); - if(!_disableNonJSON) _publisher->publishBool(mqtt_topic_battery_keypad_critical, offKeypadCritical, true); + if(!_disableNonJSON) + { + _publisher->publishBool(mqtt_topic_battery_keypad_critical, offKeypadCritical, true); + } publishBatteryJson = true; } else if(strcmp(topic, mqtt_topic_official_doorsensorBatteryCritical) == 0) { offDoorsensorCritical = (strcmp(value, "true") == 0 ? 1 : 0); - if(!_disableNonJSON) _publisher->publishBool(mqtt_topic_battery_doorsensor_critical, offDoorsensorCritical, true); + if(!_disableNonJSON) + { + _publisher->publishBool(mqtt_topic_battery_doorsensor_critical, offDoorsensorCritical, true); + } publishBatteryJson = true; } else if(strcmp(topic, mqtt_topic_official_commandResponse) == 0) diff --git a/src/NukiOpenerWrapper.cpp b/src/NukiOpenerWrapper.cpp index c78e0ed..69ee1ae 100644 --- a/src/NukiOpenerWrapper.cpp +++ b/src/NukiOpenerWrapper.cpp @@ -10,13 +10,13 @@ NukiOpenerWrapper* nukiOpenerInst; Preferences* nukiOpenerPreferences = nullptr; NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkOpener* network, Gpio* gpio, Preferences* preferences) -: _deviceName(deviceName), - _deviceId(deviceId), - _nukiOpener(deviceName, _deviceId->get()), - _bleScanner(scanner), - _network(network), - _gpio(gpio), - _preferences(preferences) + : _deviceName(deviceName), + _deviceId(deviceId), + _nukiOpener(deviceName, _deviceId->get()), + _bleScanner(scanner), + _network(network), + _gpio(gpio), + _preferences(preferences) { Log->print("Device id opener: "); Log->println(_deviceId->get()); @@ -64,14 +64,38 @@ void NukiOpenerWrapper::readSettings() int pwrLvl = _preferences->getInt(preference_ble_tx_power, 9); - if(pwrLvl >= 9) powerLevel = ESP_PWR_LVL_P9; - else if(pwrLvl >= 6) powerLevel = ESP_PWR_LVL_P6; - else if(pwrLvl >= 3) powerLevel = ESP_PWR_LVL_P6; - else if(pwrLvl >= 0) powerLevel = ESP_PWR_LVL_P3; - else if(pwrLvl >= -3) powerLevel = ESP_PWR_LVL_N3; - else if(pwrLvl >= -6) powerLevel = ESP_PWR_LVL_N6; - else if(pwrLvl >= -9) powerLevel = ESP_PWR_LVL_N9; - else if(pwrLvl >= -12) powerLevel = ESP_PWR_LVL_N12; + if(pwrLvl >= 9) + { + powerLevel = ESP_PWR_LVL_P9; + } + else if(pwrLvl >= 6) + { + powerLevel = ESP_PWR_LVL_P6; + } + else if(pwrLvl >= 3) + { + powerLevel = ESP_PWR_LVL_P6; + } + else if(pwrLvl >= 0) + { + powerLevel = ESP_PWR_LVL_P3; + } + else if(pwrLvl >= -3) + { + powerLevel = ESP_PWR_LVL_N3; + } + else if(pwrLvl >= -6) + { + powerLevel = ESP_PWR_LVL_N6; + } + else if(pwrLvl >= -9) + { + powerLevel = ESP_PWR_LVL_N9; + } + else if(pwrLvl >= -12) + { + powerLevel = ESP_PWR_LVL_N12; + } _nukiOpener.setPower(powerLevel); @@ -177,10 +201,10 @@ void NukiOpenerWrapper::update() uint8_t queryCommands = _network->queryCommands(); if(_restartBeaconTimeout > 0 && - ts > 60000 && - lastReceivedBeaconTs > 0 && - _disableBleWatchdogTs < ts && - (ts - lastReceivedBeaconTs > _restartBeaconTimeout * 1000)) + ts > 60000 && + lastReceivedBeaconTs > 0 && + _disableBleWatchdogTs < ts && + (ts - lastReceivedBeaconTs > _restartBeaconTimeout * 1000)) { Log->print("No BLE beacon received from the opener for "); Log->print((ts - lastReceivedBeaconTs) / 1000); @@ -293,7 +317,10 @@ void NukiOpenerWrapper::update() _nextLockAction = (NukiOpener::LockAction) 0xff; _network->publishRetry("--"); retryCount = 0; - if(_intervalLockstate > 10) _nextLockStateUpdateTs = ts + 10 * 1000; + if(_intervalLockstate > 10) + { + _nextLockStateUpdateTs = ts + 10 * 1000; + } } else { @@ -331,8 +358,14 @@ void NukiOpenerWrapper::activateCM() void NukiOpenerWrapper::deactivateRtoCm() { - if(_keyTurnerState.nukiState == NukiOpener::State::ContinuousMode) _nextLockAction = NukiOpener::LockAction::DeactivateCM; - else if(_keyTurnerState.lockState == NukiOpener::LockState::RTOactive) _nextLockAction = NukiOpener::LockAction::DeactivateRTO; + if(_keyTurnerState.nukiState == NukiOpener::State::ContinuousMode) + { + _nextLockAction = NukiOpener::LockAction::DeactivateCM; + } + else if(_keyTurnerState.lockState == NukiOpener::LockState::RTOactive) + { + _nextLockAction = NukiOpener::LockAction::DeactivateRTO; + } } void NukiOpenerWrapper::deactivateRTO() @@ -409,9 +442,9 @@ void NukiOpenerWrapper::updateKeyTurnerState() _retryLockstateCount = 0; if(_statusUpdated && - _keyTurnerState.lockState == NukiOpener::LockState::Locked && - _lastKeyTurnerState.lockState == NukiOpener::LockState::Locked && - _lastKeyTurnerState.nukiState == _keyTurnerState.nukiState) + _keyTurnerState.lockState == NukiOpener::LockState::Locked && + _lastKeyTurnerState.lockState == NukiOpener::LockState::Locked && + _lastKeyTurnerState.nukiState == _keyTurnerState.nukiState) { Log->println(F("Nuki opener: Ring detected (Locked)")); _network->publishRing(true); @@ -419,8 +452,8 @@ void NukiOpenerWrapper::updateKeyTurnerState() else { if(_keyTurnerState.lockState != _lastKeyTurnerState.lockState && - _keyTurnerState.lockState == NukiOpener::LockState::Open && - _keyTurnerState.trigger == NukiOpener::Trigger::Manual) + _keyTurnerState.lockState == NukiOpener::LockState::Open && + _keyTurnerState.trigger == NukiOpener::Trigger::Manual) { Log->println(F("Nuki opener: Ring detected (Open)")); _network->publishRing(false); @@ -460,10 +493,14 @@ void NukiOpenerWrapper::updateBatteryState() Log->print(F("Querying opener battery state: ")); result = _nukiOpener.requestBatteryReport(&_batteryReport); delay(250); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -500,14 +537,24 @@ void NukiOpenerWrapper::updateConfig() _hasKeypad = _nukiConfig.hasKeypad > 0 || _nukiConfig.hasKeypadV2 > 0; _firmwareVersion = std::to_string(_nukiConfig.firmwareVersion[0]) + "." + std::to_string(_nukiConfig.firmwareVersion[1]) + "." + std::to_string(_nukiConfig.firmwareVersion[2]); _hardwareVersion = std::to_string(_nukiConfig.hardwareRevision[0]) + "." + std::to_string(_nukiConfig.hardwareRevision[1]); - if(_preferences->getBool(preference_conf_info_enabled, true)) _network->publishConfig(_nukiConfig); + if(_preferences->getBool(preference_conf_info_enabled, true)) + { + _network->publishConfig(_nukiConfig); + } _retryConfigCount = 0; - if(_preferences->getBool(preference_timecontrol_info_enabled, false)) updateTimeControl(false); - if(_preferences->getBool(preference_auth_info_enabled)) updateAuth(false); + if(_preferences->getBool(preference_timecontrol_info_enabled, false)) + { + updateTimeControl(false); + } + if(_preferences->getBool(preference_auth_info_enabled)) + { + updateAuth(false); + } const int pinStatus = _preferences->getInt(preference_opener_pin_status, 4); - if(isPinSet()) { + if(isPinSet()) + { Nuki::CmdResult result = (Nuki::CmdResult)-1; int retryCount = 0; Log->println(F("Nuki opener PIN is set")); @@ -516,23 +563,29 @@ void NukiOpenerWrapper::updateConfig() { result = _nukiOpener.verifySecurityPin(); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } if(result != Nuki::CmdResult::Success) { Log->println(F("Nuki opener PIN is invalid")); - if(pinStatus != 2) { + if(pinStatus != 2) + { _preferences->putInt(preference_opener_pin_status, 2); } } else { Log->println(F("Nuki opener PIN is valid")); - if(pinStatus != 1) { + if(pinStatus != 1) + { _preferences->putInt(preference_opener_pin_status, 1); } } @@ -540,7 +593,8 @@ void NukiOpenerWrapper::updateConfig() else { Log->println(F("Nuki opener PIN is not set")); - if(pinStatus != 0) { + if(pinStatus != 0) + { _preferences->putInt(preference_opener_pin_status, 0); } } @@ -563,7 +617,10 @@ void NukiOpenerWrapper::updateConfig() if(_nukiAdvancedConfigValid) { - if(_preferences->getBool(preference_conf_info_enabled, true)) _network->publishAdvancedConfig(_nukiAdvancedConfig); + if(_preferences->getBool(preference_conf_info_enabled, true)) + { + _network->publishAdvancedConfig(_nukiAdvancedConfig); + } } else { @@ -604,10 +661,14 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) Log->print(F("Retrieve log entries: ")); result = _nukiOpener.retrieveLogEntries(0, _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 1, false); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } Log->println(result); @@ -625,11 +686,14 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) log.resize(_preferences->getInt(preference_authlog_max_entries, 3)); } - log.sort([](const NukiOpener::LogEntry& a, const NukiOpener::LogEntry& b) { return a.index < b.index; }); + log.sort([](const NukiOpener::LogEntry& a, const NukiOpener::LogEntry& b) + { + return a.index < b.index; + }); if(log.size() > 0) { - _network->publishAuthorizationInfo(log, true); + _network->publishAuthorizationInfo(log, true); } } } @@ -643,14 +707,17 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) log.resize(_preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG)); } - log.sort([](const NukiOpener::LogEntry& a, const NukiOpener::LogEntry& b) { return a.index < b.index; }); + log.sort([](const NukiOpener::LogEntry& a, const NukiOpener::LogEntry& b) + { + return a.index < b.index; + }); Log->print(F("Log size: ")); Log->println(log.size()); if(log.size() > 0) { - _network->publishAuthorizationInfo(log, false); + _network->publishAuthorizationInfo(log, false); } } @@ -659,7 +726,10 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) void NukiOpenerWrapper::updateKeypad(bool retrieved) { - if(!_preferences->getBool(preference_keypad_info_enabled)) return; + if(!_preferences->getBool(preference_keypad_info_enabled)) + { + return; + } if(!isPinValid()) { @@ -677,10 +747,14 @@ void NukiOpenerWrapper::updateKeypad(bool retrieved) Log->print(F("Querying opener keypad: ")); result = _nukiOpener.retrieveKeypadEntries(0, _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD)); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -697,7 +771,10 @@ void NukiOpenerWrapper::updateKeypad(bool retrieved) Log->print(F("Opener keypad codes: ")); Log->println(entries.size()); - entries.sort([](const NukiOpener::KeypadEntry& a, const NukiOpener::KeypadEntry& b) { return a.codeId < b.codeId; }); + entries.sort([](const NukiOpener::KeypadEntry& a, const NukiOpener::KeypadEntry& b) + { + return a.codeId < b.codeId; + }); if(entries.size() > _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD)) { @@ -726,7 +803,10 @@ void NukiOpenerWrapper::updateKeypad(bool retrieved) void NukiOpenerWrapper::updateTimeControl(bool retrieved) { - if(!_preferences->getBool(preference_timecontrol_info_enabled)) return; + if(!_preferences->getBool(preference_timecontrol_info_enabled)) + { + return; + } if(!isPinValid()) { @@ -744,10 +824,14 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) Log->print(F("Querying opener timecontrol: ")); result = _nukiOpener.retrieveTimeControlEntries(); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -764,7 +848,10 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) Log->print(F("Opener timecontrol entries: ")); Log->println(timeControlEntries.size()); - timeControlEntries.sort([](const NukiOpener::TimeControlEntry& a, const NukiOpener::TimeControlEntry& b) { return a.entryId < b.entryId; }); + timeControlEntries.sort([](const NukiOpener::TimeControlEntry& a, const NukiOpener::TimeControlEntry& b) + { + return a.entryId < b.entryId; + }); if(timeControlEntries.size() > _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL)) { @@ -793,7 +880,10 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) void NukiOpenerWrapper::updateAuth(bool retrieved) { - if(!_preferences->getBool(preference_auth_info_enabled)) return; + if(!_preferences->getBool(preference_auth_info_enabled)) + { + return; + } if(!retrieved) { @@ -805,10 +895,14 @@ void NukiOpenerWrapper::updateAuth(bool retrieved) Log->print(F("Querying opener authorization: ")); result = _nukiOpener.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); delay(250); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -825,7 +919,10 @@ void NukiOpenerWrapper::updateAuth(bool retrieved) Log->print(F("Opener authorization entries: ")); Log->println(authEntries.size()); - authEntries.sort([](const NukiOpener::AuthorizationEntry& a, const NukiOpener::AuthorizationEntry& b) { return a.authId < b.authId; }); + authEntries.sort([](const NukiOpener::AuthorizationEntry& a, const NukiOpener::AuthorizationEntry& b) + { + return a.authId < b.authId; + }); if(authEntries.size() > _preferences->getInt(preference_auth_max_entries, MAX_AUTH)) { @@ -859,14 +956,38 @@ void NukiOpenerWrapper::postponeBleWatchdog() NukiOpener::LockAction NukiOpenerWrapper::lockActionToEnum(const char *str) { - if(strcmp(str, "activateRTO") == 0 || strcmp(str, "ActivateRTO") == 0) return NukiOpener::LockAction::ActivateRTO; - else if(strcmp(str, "deactivateRTO") == 0 || strcmp(str, "DeactivateRTO") == 0) return NukiOpener::LockAction::DeactivateRTO; - else if(strcmp(str, "electricStrikeActuation") == 0 || strcmp(str, "ElectricStrikeActuation") == 0) return NukiOpener::LockAction::ElectricStrikeActuation; - else if(strcmp(str, "activateCM") == 0 || strcmp(str, "ActivateCM") == 0) return NukiOpener::LockAction::ActivateCM; - else if(strcmp(str, "deactivateCM") == 0 || strcmp(str, "DeactivateCM") == 0) return NukiOpener::LockAction::DeactivateCM; - else if(strcmp(str, "fobAction2") == 0 || strcmp(str, "FobAction2") == 0) return NukiOpener::LockAction::FobAction2; - else if(strcmp(str, "fobAction1") == 0 || strcmp(str, "FobAction1") == 0) return NukiOpener::LockAction::FobAction1; - else if(strcmp(str, "fobAction3") == 0 || strcmp(str, "FobAction3") == 0) return NukiOpener::LockAction::FobAction3; + if(strcmp(str, "activateRTO") == 0 || strcmp(str, "ActivateRTO") == 0) + { + return NukiOpener::LockAction::ActivateRTO; + } + else if(strcmp(str, "deactivateRTO") == 0 || strcmp(str, "DeactivateRTO") == 0) + { + return NukiOpener::LockAction::DeactivateRTO; + } + else if(strcmp(str, "electricStrikeActuation") == 0 || strcmp(str, "ElectricStrikeActuation") == 0) + { + return NukiOpener::LockAction::ElectricStrikeActuation; + } + else if(strcmp(str, "activateCM") == 0 || strcmp(str, "ActivateCM") == 0) + { + return NukiOpener::LockAction::ActivateCM; + } + else if(strcmp(str, "deactivateCM") == 0 || strcmp(str, "DeactivateCM") == 0) + { + return NukiOpener::LockAction::DeactivateCM; + } + else if(strcmp(str, "fobAction2") == 0 || strcmp(str, "FobAction2") == 0) + { + return NukiOpener::LockAction::FobAction2; + } + else if(strcmp(str, "fobAction1") == 0 || strcmp(str, "FobAction1") == 0) + { + return NukiOpener::LockAction::FobAction1; + } + else if(strcmp(str, "fobAction3") == 0 || strcmp(str, "FobAction3") == 0) + { + return NukiOpener::LockAction::FobAction3; + } return (NukiOpener::LockAction)0xff; } @@ -879,11 +1000,20 @@ LockActionResult NukiOpenerWrapper::onLockActionReceivedCallback(const char *val if(strlen(value) > 0) { action = nukiOpenerInst->lockActionToEnum(value); - if((int)action == 0xff) return LockActionResult::UnknownAction; + if((int)action == 0xff) + { + return LockActionResult::UnknownAction; + } + } + else + { + return LockActionResult::UnknownAction; } - else return LockActionResult::UnknownAction; } - else return LockActionResult::UnknownAction; + else + { + return LockActionResult::UnknownAction; + } nukiOpenerPreferences = new Preferences(); nukiOpenerPreferences->begin("nukihub", true); @@ -908,137 +1038,425 @@ void NukiOpenerWrapper::onConfigUpdateReceivedCallback(const char *value) Nuki::AdvertisingMode NukiOpenerWrapper::advertisingModeToEnum(const char *str) { - if(strcmp(str, "Automatic") == 0) return Nuki::AdvertisingMode::Automatic; - else if(strcmp(str, "Normal") == 0) return Nuki::AdvertisingMode::Normal; - else if(strcmp(str, "Slow") == 0) return Nuki::AdvertisingMode::Slow; - else if(strcmp(str, "Slowest") == 0) return Nuki::AdvertisingMode::Slowest; + if(strcmp(str, "Automatic") == 0) + { + return Nuki::AdvertisingMode::Automatic; + } + else if(strcmp(str, "Normal") == 0) + { + return Nuki::AdvertisingMode::Normal; + } + else if(strcmp(str, "Slow") == 0) + { + return Nuki::AdvertisingMode::Slow; + } + else if(strcmp(str, "Slowest") == 0) + { + return Nuki::AdvertisingMode::Slowest; + } return (Nuki::AdvertisingMode)0xff; } Nuki::TimeZoneId NukiOpenerWrapper::timeZoneToEnum(const char *str) { - if(strcmp(str, "Africa/Cairo") == 0) return Nuki::TimeZoneId::Africa_Cairo; - else if(strcmp(str, "Africa/Lagos") == 0) return Nuki::TimeZoneId::Africa_Lagos; - else if(strcmp(str, "Africa/Maputo") == 0) return Nuki::TimeZoneId::Africa_Maputo; - else if(strcmp(str, "Africa/Nairobi") == 0) return Nuki::TimeZoneId::Africa_Nairobi; - else if(strcmp(str, "America/Anchorage") == 0) return Nuki::TimeZoneId::America_Anchorage; - else if(strcmp(str, "America/Argentina/Buenos_Aires") == 0) return Nuki::TimeZoneId::America_Argentina_Buenos_Aires; - else if(strcmp(str, "America/Chicago") == 0) return Nuki::TimeZoneId::America_Chicago; - else if(strcmp(str, "America/Denver") == 0) return Nuki::TimeZoneId::America_Denver; - else if(strcmp(str, "America/Halifax") == 0) return Nuki::TimeZoneId::America_Halifax; - else if(strcmp(str, "America/Los_Angeles") == 0) return Nuki::TimeZoneId::America_Los_Angeles; - else if(strcmp(str, "America/Manaus") == 0) return Nuki::TimeZoneId::America_Manaus; - else if(strcmp(str, "America/Mexico_City") == 0) return Nuki::TimeZoneId::America_Mexico_City; - else if(strcmp(str, "America/New_York") == 0) return Nuki::TimeZoneId::America_New_York; - else if(strcmp(str, "America/Phoenix") == 0) return Nuki::TimeZoneId::America_Phoenix; - else if(strcmp(str, "America/Regina") == 0) return Nuki::TimeZoneId::America_Regina; - else if(strcmp(str, "America/Santiago") == 0) return Nuki::TimeZoneId::America_Santiago; - else if(strcmp(str, "America/Sao_Paulo") == 0) return Nuki::TimeZoneId::America_Sao_Paulo; - else if(strcmp(str, "America/St_Johns") == 0) return Nuki::TimeZoneId::America_St_Johns; - else if(strcmp(str, "Asia/Bangkok") == 0) return Nuki::TimeZoneId::Asia_Bangkok; - else if(strcmp(str, "Asia/Dubai") == 0) return Nuki::TimeZoneId::Asia_Dubai; - else if(strcmp(str, "Asia/Hong_Kong") == 0) return Nuki::TimeZoneId::Asia_Hong_Kong; - else if(strcmp(str, "Asia/Jerusalem") == 0) return Nuki::TimeZoneId::Asia_Jerusalem; - else if(strcmp(str, "Asia/Karachi") == 0) return Nuki::TimeZoneId::Asia_Karachi; - else if(strcmp(str, "Asia/Kathmandu") == 0) return Nuki::TimeZoneId::Asia_Kathmandu; - else if(strcmp(str, "Asia/Kolkata") == 0) return Nuki::TimeZoneId::Asia_Kolkata; - else if(strcmp(str, "Asia/Riyadh") == 0) return Nuki::TimeZoneId::Asia_Riyadh; - else if(strcmp(str, "Asia/Seoul") == 0) return Nuki::TimeZoneId::Asia_Seoul; - else if(strcmp(str, "Asia/Shanghai") == 0) return Nuki::TimeZoneId::Asia_Shanghai; - else if(strcmp(str, "Asia/Tehran") == 0) return Nuki::TimeZoneId::Asia_Tehran; - else if(strcmp(str, "Asia/Tokyo") == 0) return Nuki::TimeZoneId::Asia_Tokyo; - else if(strcmp(str, "Asia/Yangon") == 0) return Nuki::TimeZoneId::Asia_Yangon; - else if(strcmp(str, "Australia/Adelaide") == 0) return Nuki::TimeZoneId::Australia_Adelaide; - else if(strcmp(str, "Australia/Brisbane") == 0) return Nuki::TimeZoneId::Australia_Brisbane; - else if(strcmp(str, "Australia/Darwin") == 0) return Nuki::TimeZoneId::Australia_Darwin; - else if(strcmp(str, "Australia/Hobart") == 0) return Nuki::TimeZoneId::Australia_Hobart; - else if(strcmp(str, "Australia/Perth") == 0) return Nuki::TimeZoneId::Australia_Perth; - else if(strcmp(str, "Australia/Sydney") == 0) return Nuki::TimeZoneId::Australia_Sydney; - else if(strcmp(str, "Europe/Berlin") == 0) return Nuki::TimeZoneId::Europe_Berlin; - else if(strcmp(str, "Europe/Helsinki") == 0) return Nuki::TimeZoneId::Europe_Helsinki; - else if(strcmp(str, "Europe/Istanbul") == 0) return Nuki::TimeZoneId::Europe_Istanbul; - else if(strcmp(str, "Europe/London") == 0) return Nuki::TimeZoneId::Europe_London; - else if(strcmp(str, "Europe/Moscow") == 0) return Nuki::TimeZoneId::Europe_Moscow; - else if(strcmp(str, "Pacific/Auckland") == 0) return Nuki::TimeZoneId::Pacific_Auckland; - else if(strcmp(str, "Pacific/Guam") == 0) return Nuki::TimeZoneId::Pacific_Guam; - else if(strcmp(str, "Pacific/Honolulu") == 0) return Nuki::TimeZoneId::Pacific_Honolulu; - else if(strcmp(str, "Pacific/Pago_Pago") == 0) return Nuki::TimeZoneId::Pacific_Pago_Pago; - else if(strcmp(str, "None") == 0) return Nuki::TimeZoneId::None; + if(strcmp(str, "Africa/Cairo") == 0) + { + return Nuki::TimeZoneId::Africa_Cairo; + } + else if(strcmp(str, "Africa/Lagos") == 0) + { + return Nuki::TimeZoneId::Africa_Lagos; + } + else if(strcmp(str, "Africa/Maputo") == 0) + { + return Nuki::TimeZoneId::Africa_Maputo; + } + else if(strcmp(str, "Africa/Nairobi") == 0) + { + return Nuki::TimeZoneId::Africa_Nairobi; + } + else if(strcmp(str, "America/Anchorage") == 0) + { + return Nuki::TimeZoneId::America_Anchorage; + } + else if(strcmp(str, "America/Argentina/Buenos_Aires") == 0) + { + return Nuki::TimeZoneId::America_Argentina_Buenos_Aires; + } + else if(strcmp(str, "America/Chicago") == 0) + { + return Nuki::TimeZoneId::America_Chicago; + } + else if(strcmp(str, "America/Denver") == 0) + { + return Nuki::TimeZoneId::America_Denver; + } + else if(strcmp(str, "America/Halifax") == 0) + { + return Nuki::TimeZoneId::America_Halifax; + } + else if(strcmp(str, "America/Los_Angeles") == 0) + { + return Nuki::TimeZoneId::America_Los_Angeles; + } + else if(strcmp(str, "America/Manaus") == 0) + { + return Nuki::TimeZoneId::America_Manaus; + } + else if(strcmp(str, "America/Mexico_City") == 0) + { + return Nuki::TimeZoneId::America_Mexico_City; + } + else if(strcmp(str, "America/New_York") == 0) + { + return Nuki::TimeZoneId::America_New_York; + } + else if(strcmp(str, "America/Phoenix") == 0) + { + return Nuki::TimeZoneId::America_Phoenix; + } + else if(strcmp(str, "America/Regina") == 0) + { + return Nuki::TimeZoneId::America_Regina; + } + else if(strcmp(str, "America/Santiago") == 0) + { + return Nuki::TimeZoneId::America_Santiago; + } + else if(strcmp(str, "America/Sao_Paulo") == 0) + { + return Nuki::TimeZoneId::America_Sao_Paulo; + } + else if(strcmp(str, "America/St_Johns") == 0) + { + return Nuki::TimeZoneId::America_St_Johns; + } + else if(strcmp(str, "Asia/Bangkok") == 0) + { + return Nuki::TimeZoneId::Asia_Bangkok; + } + else if(strcmp(str, "Asia/Dubai") == 0) + { + return Nuki::TimeZoneId::Asia_Dubai; + } + else if(strcmp(str, "Asia/Hong_Kong") == 0) + { + return Nuki::TimeZoneId::Asia_Hong_Kong; + } + else if(strcmp(str, "Asia/Jerusalem") == 0) + { + return Nuki::TimeZoneId::Asia_Jerusalem; + } + else if(strcmp(str, "Asia/Karachi") == 0) + { + return Nuki::TimeZoneId::Asia_Karachi; + } + else if(strcmp(str, "Asia/Kathmandu") == 0) + { + return Nuki::TimeZoneId::Asia_Kathmandu; + } + else if(strcmp(str, "Asia/Kolkata") == 0) + { + return Nuki::TimeZoneId::Asia_Kolkata; + } + else if(strcmp(str, "Asia/Riyadh") == 0) + { + return Nuki::TimeZoneId::Asia_Riyadh; + } + else if(strcmp(str, "Asia/Seoul") == 0) + { + return Nuki::TimeZoneId::Asia_Seoul; + } + else if(strcmp(str, "Asia/Shanghai") == 0) + { + return Nuki::TimeZoneId::Asia_Shanghai; + } + else if(strcmp(str, "Asia/Tehran") == 0) + { + return Nuki::TimeZoneId::Asia_Tehran; + } + else if(strcmp(str, "Asia/Tokyo") == 0) + { + return Nuki::TimeZoneId::Asia_Tokyo; + } + else if(strcmp(str, "Asia/Yangon") == 0) + { + return Nuki::TimeZoneId::Asia_Yangon; + } + else if(strcmp(str, "Australia/Adelaide") == 0) + { + return Nuki::TimeZoneId::Australia_Adelaide; + } + else if(strcmp(str, "Australia/Brisbane") == 0) + { + return Nuki::TimeZoneId::Australia_Brisbane; + } + else if(strcmp(str, "Australia/Darwin") == 0) + { + return Nuki::TimeZoneId::Australia_Darwin; + } + else if(strcmp(str, "Australia/Hobart") == 0) + { + return Nuki::TimeZoneId::Australia_Hobart; + } + else if(strcmp(str, "Australia/Perth") == 0) + { + return Nuki::TimeZoneId::Australia_Perth; + } + else if(strcmp(str, "Australia/Sydney") == 0) + { + return Nuki::TimeZoneId::Australia_Sydney; + } + else if(strcmp(str, "Europe/Berlin") == 0) + { + return Nuki::TimeZoneId::Europe_Berlin; + } + else if(strcmp(str, "Europe/Helsinki") == 0) + { + return Nuki::TimeZoneId::Europe_Helsinki; + } + else if(strcmp(str, "Europe/Istanbul") == 0) + { + return Nuki::TimeZoneId::Europe_Istanbul; + } + else if(strcmp(str, "Europe/London") == 0) + { + return Nuki::TimeZoneId::Europe_London; + } + else if(strcmp(str, "Europe/Moscow") == 0) + { + return Nuki::TimeZoneId::Europe_Moscow; + } + else if(strcmp(str, "Pacific/Auckland") == 0) + { + return Nuki::TimeZoneId::Pacific_Auckland; + } + else if(strcmp(str, "Pacific/Guam") == 0) + { + return Nuki::TimeZoneId::Pacific_Guam; + } + else if(strcmp(str, "Pacific/Honolulu") == 0) + { + return Nuki::TimeZoneId::Pacific_Honolulu; + } + else if(strcmp(str, "Pacific/Pago_Pago") == 0) + { + return Nuki::TimeZoneId::Pacific_Pago_Pago; + } + else if(strcmp(str, "None") == 0) + { + return Nuki::TimeZoneId::None; + } return (Nuki::TimeZoneId)0xff; } uint8_t NukiOpenerWrapper::fobActionToInt(const char *str) { - if(strcmp(str, "No Action") == 0) return 0; - else if(strcmp(str, "Toggle RTO") == 0) return 1; - else if(strcmp(str, "Activate RTO") == 0) return 2; - else if(strcmp(str, "Deactivate RTO") == 0) return 3; - else if(strcmp(str, "Open") == 0) return 7; - else if(strcmp(str, "Ring") == 0) return 8; + if(strcmp(str, "No Action") == 0) + { + return 0; + } + else if(strcmp(str, "Toggle RTO") == 0) + { + return 1; + } + else if(strcmp(str, "Activate RTO") == 0) + { + return 2; + } + else if(strcmp(str, "Deactivate RTO") == 0) + { + return 3; + } + else if(strcmp(str, "Open") == 0) + { + return 7; + } + else if(strcmp(str, "Ring") == 0) + { + return 8; + } return 99; } uint8_t NukiOpenerWrapper::operatingModeToInt(const char *str) { - if(strcmp(str, "Generic door opener") == 0) return 0; - else if(strcmp(str, "Analogue intercom") == 0) return 1; - else if(strcmp(str, "Digital intercom") == 0) return 2; - else if(strcmp(str, "Siedle") == 0) return 3; - else if(strcmp(str, "TCS") == 0) return 4; - else if(strcmp(str, "Bticino") == 0) return 5; - else if(strcmp(str, "Siedle HTS") == 0) return 6; - else if(strcmp(str, "STR") == 0) return 7; - else if(strcmp(str, "Ritto") == 0) return 8; - else if(strcmp(str, "Fermax") == 0) return 9; - else if(strcmp(str, "Comelit") == 0) return 10; - else if(strcmp(str, "Urmet BiBus") == 0) return 11; - else if(strcmp(str, "Urmet 2Voice") == 0) return 12; - else if(strcmp(str, "Golmar") == 0) return 13; - else if(strcmp(str, "SKS") == 0) return 14; - else if(strcmp(str, "Spare") == 0) return 15; + if(strcmp(str, "Generic door opener") == 0) + { + return 0; + } + else if(strcmp(str, "Analogue intercom") == 0) + { + return 1; + } + else if(strcmp(str, "Digital intercom") == 0) + { + return 2; + } + else if(strcmp(str, "Siedle") == 0) + { + return 3; + } + else if(strcmp(str, "TCS") == 0) + { + return 4; + } + else if(strcmp(str, "Bticino") == 0) + { + return 5; + } + else if(strcmp(str, "Siedle HTS") == 0) + { + return 6; + } + else if(strcmp(str, "STR") == 0) + { + return 7; + } + else if(strcmp(str, "Ritto") == 0) + { + return 8; + } + else if(strcmp(str, "Fermax") == 0) + { + return 9; + } + else if(strcmp(str, "Comelit") == 0) + { + return 10; + } + else if(strcmp(str, "Urmet BiBus") == 0) + { + return 11; + } + else if(strcmp(str, "Urmet 2Voice") == 0) + { + return 12; + } + else if(strcmp(str, "Golmar") == 0) + { + return 13; + } + else if(strcmp(str, "SKS") == 0) + { + return 14; + } + else if(strcmp(str, "Spare") == 0) + { + return 15; + } return 99; } uint8_t NukiOpenerWrapper::doorbellSuppressionToInt(const char *str) { - if(strcmp(str, "Off") == 0) return 0; - else if(strcmp(str, "CM") == 0) return 1; - else if(strcmp(str, "RTO") == 0) return 2; - else if(strcmp(str, "CM & RTO") == 0) return 3; - else if(strcmp(str, "Ring") == 0) return 4; - else if(strcmp(str, "CM & Ring") == 0) return 5; - else if(strcmp(str, "RTO & Ring") == 0) return 6; - else if(strcmp(str, "CM & RTO & Ring") == 0) return 7; + if(strcmp(str, "Off") == 0) + { + return 0; + } + else if(strcmp(str, "CM") == 0) + { + return 1; + } + else if(strcmp(str, "RTO") == 0) + { + return 2; + } + else if(strcmp(str, "CM & RTO") == 0) + { + return 3; + } + else if(strcmp(str, "Ring") == 0) + { + return 4; + } + else if(strcmp(str, "CM & Ring") == 0) + { + return 5; + } + else if(strcmp(str, "RTO & Ring") == 0) + { + return 6; + } + else if(strcmp(str, "CM & RTO & Ring") == 0) + { + return 7; + } return 99; } uint8_t NukiOpenerWrapper::soundToInt(const char *str) { - if(strcmp(str, "No Sound") == 0) return 0; - else if(strcmp(str, "Sound 1") == 0) return 1; - else if(strcmp(str, "Sound 2") == 0) return 2; - else if(strcmp(str, "Sound 3") == 0) return 3; + if(strcmp(str, "No Sound") == 0) + { + return 0; + } + else if(strcmp(str, "Sound 1") == 0) + { + return 1; + } + else if(strcmp(str, "Sound 2") == 0) + { + return 2; + } + else if(strcmp(str, "Sound 3") == 0) + { + return 3; + } return 99; } NukiOpener::ButtonPressAction NukiOpenerWrapper::buttonPressActionToEnum(const char* str) { - if(strcmp(str, "No Action") == 0) return NukiOpener::ButtonPressAction::NoAction; - else if(strcmp(str, "Toggle RTO") == 0) return NukiOpener::ButtonPressAction::ToggleRTO; - else if(strcmp(str, "Activate RTO") == 0) return NukiOpener::ButtonPressAction::ActivateRTO; - else if(strcmp(str, "Deactivate RTO") == 0) return NukiOpener::ButtonPressAction::DeactivateRTO; - else if(strcmp(str, "Toggle CM") == 0) return NukiOpener::ButtonPressAction::ToggleCM; - else if(strcmp(str, "Activate CM") == 0) return NukiOpener::ButtonPressAction::ActivateCM; - else if(strcmp(str, "Deactivate CM") == 0) return NukiOpener::ButtonPressAction::DectivateCM; - else if(strcmp(str, "Open") == 0) return NukiOpener::ButtonPressAction::Open; + if(strcmp(str, "No Action") == 0) + { + return NukiOpener::ButtonPressAction::NoAction; + } + else if(strcmp(str, "Toggle RTO") == 0) + { + return NukiOpener::ButtonPressAction::ToggleRTO; + } + else if(strcmp(str, "Activate RTO") == 0) + { + return NukiOpener::ButtonPressAction::ActivateRTO; + } + else if(strcmp(str, "Deactivate RTO") == 0) + { + return NukiOpener::ButtonPressAction::DeactivateRTO; + } + else if(strcmp(str, "Toggle CM") == 0) + { + return NukiOpener::ButtonPressAction::ToggleCM; + } + else if(strcmp(str, "Activate CM") == 0) + { + return NukiOpener::ButtonPressAction::ActivateCM; + } + else if(strcmp(str, "Deactivate CM") == 0) + { + return NukiOpener::ButtonPressAction::DectivateCM; + } + else if(strcmp(str, "Open") == 0) + { + return NukiOpener::ButtonPressAction::Open; + } return (NukiOpener::ButtonPressAction)0xff; } Nuki::BatteryType NukiOpenerWrapper::batteryTypeToEnum(const char* str) { - if(strcmp(str, "Alkali") == 0) return Nuki::BatteryType::Alkali; - else if(strcmp(str, "Accumulators") == 0) return Nuki::BatteryType::Accumulators; - else if(strcmp(str, "Lithium") == 0) return Nuki::BatteryType::Lithium; + if(strcmp(str, "Alkali") == 0) + { + return Nuki::BatteryType::Alkali; + } + else if(strcmp(str, "Accumulators") == 0) + { + return Nuki::BatteryType::Accumulators; + } + else if(strcmp(str, "Lithium") == 0) + { + return Nuki::BatteryType::Lithium; + } return (Nuki::BatteryType)0xff; } @@ -1104,10 +1522,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) { if(strlen(jsonchar) <= 32) { - if(strcmp((const char*)_nukiConfig.name, jsonchar) == 0) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setName(std::string(jsonchar)); + if(strcmp((const char*)_nukiConfig.name, jsonchar) == 0) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setName(std::string(jsonchar)); + } + } + else + { + jsonResult[basicKeys[i]] = "valueTooLong"; } - else jsonResult[basicKeys[i]] = "valueTooLong"; } else if(strcmp(basicKeys[i], "latitude") == 0) { @@ -1115,10 +1542,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue > 0) { - if(_nukiConfig.latitude == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setLatitude(keyvalue); + if(_nukiConfig.latitude == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setLatitude(keyvalue); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "longitude") == 0) { @@ -1126,10 +1562,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue > 0) { - if(_nukiConfig.longitude == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setLongitude(keyvalue); + if(_nukiConfig.longitude == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setLongitude(keyvalue); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "pairingEnabled") == 0) { @@ -1137,10 +1582,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.pairingEnabled == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.enablePairing((keyvalue > 0)); + if(_nukiConfig.pairingEnabled == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.enablePairing((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "buttonEnabled") == 0) { @@ -1148,10 +1602,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.buttonEnabled == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.enableButton((keyvalue > 0)); + if(_nukiConfig.buttonEnabled == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.enableButton((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "ledFlashEnabled") == 0) { @@ -1159,10 +1622,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.ledFlashEnabled == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.enableLedFlash((keyvalue > 0)); + if(_nukiConfig.ledFlashEnabled == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.enableLedFlash((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "timeZoneOffset") == 0) { @@ -1170,10 +1642,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 0 && keyvalue <= 60) { - if(_nukiConfig.timeZoneOffset == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setTimeZoneOffset(keyvalue); + if(_nukiConfig.timeZoneOffset == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setTimeZoneOffset(keyvalue); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "dstMode") == 0) { @@ -1181,10 +1662,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.dstMode == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.enableDst((keyvalue > 0)); + if(_nukiConfig.dstMode == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.enableDst((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "fobAction1") == 0) { @@ -1192,10 +1682,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(fobAct1 != 99) { - if(_nukiConfig.fobAction1 == fobAct1) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setFobAction(1, fobAct1); + if(_nukiConfig.fobAction1 == fobAct1) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setFobAction(1, fobAct1); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "fobAction2") == 0) { @@ -1203,10 +1702,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(fobAct2 != 99) { - if(_nukiConfig.fobAction2 == fobAct2) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setFobAction(2, fobAct2); + if(_nukiConfig.fobAction2 == fobAct2) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setFobAction(2, fobAct2); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "fobAction3") == 0) { @@ -1214,10 +1722,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(fobAct3 != 99) { - if(_nukiConfig.fobAction3 == fobAct3) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setFobAction(3, fobAct3); + if(_nukiConfig.fobAction3 == fobAct3) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setFobAction(3, fobAct3); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "operatingMode") == 0) { @@ -1225,10 +1742,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(opmode != 99) { - if(_nukiConfig.operatingMode == opmode) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setOperatingMode(opmode); + if(_nukiConfig.operatingMode == opmode) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setOperatingMode(opmode); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "advertisingMode") == 0) { @@ -1236,10 +1762,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if((int)advmode != 0xff) { - if(_nukiConfig.advertisingMode == advmode) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setAdvertisingMode(advmode); + if(_nukiConfig.advertisingMode == advmode) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setAdvertisingMode(advmode); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "timeZone") == 0) { @@ -1247,27 +1782,47 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if((int)tzid != 0xff) { - if(_nukiConfig.timeZoneId == tzid) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiOpener.setTimeZoneId(tzid); + if(_nukiConfig.timeZoneId == tzid) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setTimeZoneId(tzid); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } - if(cmdResult != Nuki::CmdResult::Success) { + if(cmdResult != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } - if(cmdResult == Nuki::CmdResult::Success) basicUpdated = true; + if(cmdResult == Nuki::CmdResult::Success) + { + basicUpdated = true; + } - if(!jsonResult[basicKeys[i]]) { + if(!jsonResult[basicKeys[i]]) + { char resultStr[15] = {0}; NukiOpener::cmdResultToString(cmdResult, resultStr); jsonResult[basicKeys[i]] = resultStr; } } - else jsonResult[basicKeys[i]] = "accessDenied"; + else + { + jsonResult[basicKeys[i]] = "accessDenied"; + } } } @@ -1296,10 +1851,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 0) { - if(_nukiAdvancedConfig.intercomID == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setIntercomID(keyvalue); + if(_nukiAdvancedConfig.intercomID == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setIntercomID(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "busModeSwitch") == 0) { @@ -1307,10 +1871,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.busModeSwitch == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setBusModeSwitch((keyvalue > 0)); + if(_nukiAdvancedConfig.busModeSwitch == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setBusModeSwitch((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "shortCircuitDuration") == 0) { @@ -1318,10 +1891,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 0) { - if(_nukiAdvancedConfig.shortCircuitDuration == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setShortCircuitDuration(keyvalue); + if(_nukiAdvancedConfig.shortCircuitDuration == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setShortCircuitDuration(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "electricStrikeDelay") == 0) { @@ -1329,10 +1911,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 0 && keyvalue <= 30000) { - if(_nukiAdvancedConfig.electricStrikeDelay == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setElectricStrikeDelay(keyvalue); + if(_nukiAdvancedConfig.electricStrikeDelay == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setElectricStrikeDelay(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "randomElectricStrikeDelay") == 0) { @@ -1340,10 +1931,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.randomElectricStrikeDelay == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.enableRandomElectricStrikeDelay((keyvalue > 0)); + if(_nukiAdvancedConfig.randomElectricStrikeDelay == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.enableRandomElectricStrikeDelay((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "electricStrikeDuration") == 0) { @@ -1351,10 +1951,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 1000 && keyvalue <= 30000) { - if(_nukiAdvancedConfig.electricStrikeDuration == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setElectricStrikeDuration(keyvalue); + if(_nukiAdvancedConfig.electricStrikeDuration == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setElectricStrikeDuration(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "disableRtoAfterRing") == 0) { @@ -1362,10 +1971,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.disableRtoAfterRing == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.disableRtoAfterRing((keyvalue > 0)); + if(_nukiAdvancedConfig.disableRtoAfterRing == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.disableRtoAfterRing((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "rtoTimeout") == 0) { @@ -1373,10 +1991,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 5 && keyvalue <= 60) { - if(_nukiAdvancedConfig.rtoTimeout == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setRtoTimeout(keyvalue); + if(_nukiAdvancedConfig.rtoTimeout == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setRtoTimeout(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "doorbellSuppression") == 0) { @@ -1384,10 +2011,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(dbsupr != 99) { - if(_nukiAdvancedConfig.doorbellSuppression == dbsupr) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setDoorbellSuppression(dbsupr); + if(_nukiAdvancedConfig.doorbellSuppression == dbsupr) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setDoorbellSuppression(dbsupr); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "doorbellSuppressionDuration") == 0) { @@ -1395,10 +2031,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 500 && keyvalue <= 10000) { - if(_nukiAdvancedConfig.doorbellSuppressionDuration == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setDoorbellSuppressionDuration(keyvalue); + if(_nukiAdvancedConfig.doorbellSuppressionDuration == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setDoorbellSuppressionDuration(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "soundRing") == 0) { @@ -1406,10 +2051,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(sound != 99) { - if(_nukiAdvancedConfig.soundRing == sound) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setSoundRing(sound); + if(_nukiAdvancedConfig.soundRing == sound) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setSoundRing(sound); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "soundOpen") == 0) { @@ -1417,10 +2071,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(sound != 99) { - if(_nukiAdvancedConfig.soundOpen == sound) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setSoundOpen(sound); + if(_nukiAdvancedConfig.soundOpen == sound) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setSoundOpen(sound); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "soundRto") == 0) { @@ -1428,10 +2091,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(sound != 99) { - if(_nukiAdvancedConfig.soundRto == sound) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setSoundRto(sound); + if(_nukiAdvancedConfig.soundRto == sound) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setSoundRto(sound); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "soundCm") == 0) { @@ -1439,10 +2111,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(sound != 99) { - if(_nukiAdvancedConfig.soundCm == sound) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setSoundCm(sound); + if(_nukiAdvancedConfig.soundCm == sound) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setSoundCm(sound); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "soundConfirmation") == 0) { @@ -1450,10 +2131,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.soundConfirmation == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.enableSoundConfirmation((keyvalue > 0)); + if(_nukiAdvancedConfig.soundConfirmation == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.enableSoundConfirmation((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "soundLevel") == 0) { @@ -1461,10 +2151,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 0 && keyvalue <= 255) { - if(_nukiAdvancedConfig.soundLevel == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setSoundLevel(keyvalue); + if(_nukiAdvancedConfig.soundLevel == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setSoundLevel(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "singleButtonPressAction") == 0) { @@ -1472,10 +2171,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if((int)sbpa != 0xff) { - if(_nukiAdvancedConfig.singleButtonPressAction == sbpa) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setSingleButtonPressAction(sbpa); + if(_nukiAdvancedConfig.singleButtonPressAction == sbpa) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setSingleButtonPressAction(sbpa); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "doubleButtonPressAction") == 0) { @@ -1483,10 +2191,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if((int)dbpa != 0xff) { - if(_nukiAdvancedConfig.doubleButtonPressAction == dbpa) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setDoubleButtonPressAction(dbpa); + if(_nukiAdvancedConfig.doubleButtonPressAction == dbpa) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setDoubleButtonPressAction(dbpa); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "batteryType") == 0) { @@ -1494,10 +2211,19 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if((int)battype != 0xff) { - if(_nukiAdvancedConfig.batteryType == battype) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.setBatteryType(battype); + if(_nukiAdvancedConfig.batteryType == battype) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.setBatteryType(battype); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "automaticBatteryTypeDetection") == 0) { @@ -1505,33 +2231,58 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.automaticBatteryTypeDetection == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiOpener.enableAutoBatteryTypeDetection((keyvalue > 0)); + if(_nukiAdvancedConfig.automaticBatteryTypeDetection == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiOpener.enableAutoBatteryTypeDetection((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } if(cmdResult != Nuki::CmdResult::Success) { ++retryCount; } - else break; + else + { + break; + } } - if(cmdResult == Nuki::CmdResult::Success) advancedUpdated = true; + if(cmdResult == Nuki::CmdResult::Success) + { + advancedUpdated = true; + } - if(!jsonResult[advancedKeys[j]]) { + if(!jsonResult[advancedKeys[j]]) + { char resultStr[15] = {0}; NukiOpener::cmdResultToString(cmdResult, resultStr); jsonResult[advancedKeys[j]] = resultStr; } } - else jsonResult[advancedKeys[j]] = "accessDenied"; + else + { + jsonResult[advancedKeys[j]] = "accessDenied"; + } } } - if(basicUpdated || advancedUpdated) jsonResult["general"] = "success"; - else jsonResult["general"] = "noChange"; + if(basicUpdated || advancedUpdated) + { + jsonResult["general"] = "success"; + } + else + { + jsonResult["general"] = "noChange"; + } _nextConfigUpdateTs = (esp_timer_get_time() / 1000) + 300; @@ -1565,30 +2316,33 @@ void NukiOpenerWrapper::gpioActionCallback(const GpioAction &action, const int& { switch(action) { - case GpioAction::ElectricStrikeActuation: - nukiOpenerInst->electricStrikeActuation(); - break; - case GpioAction::ActivateRTO: - nukiOpenerInst->activateRTO(); - break; - case GpioAction::ActivateCM: - nukiOpenerInst->activateCM(); - break; - case GpioAction::DeactivateRtoCm: - nukiOpenerInst->deactivateRtoCm(); - break; - case GpioAction::DeactivateRTO: - nukiOpenerInst->deactivateRTO(); - break; - case GpioAction::DeactivateCM: - nukiOpenerInst->deactivateCM(); - break; + case GpioAction::ElectricStrikeActuation: + nukiOpenerInst->electricStrikeActuation(); + break; + case GpioAction::ActivateRTO: + nukiOpenerInst->activateRTO(); + break; + case GpioAction::ActivateCM: + nukiOpenerInst->activateCM(); + break; + case GpioAction::DeactivateRtoCm: + nukiOpenerInst->deactivateRtoCm(); + break; + case GpioAction::DeactivateRTO: + nukiOpenerInst->deactivateRTO(); + break; + case GpioAction::DeactivateCM: + nukiOpenerInst->deactivateCM(); + break; } } void NukiOpenerWrapper::onKeypadCommandReceived(const char *command, const uint &id, const String &name, const String &code, const int& enabled) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } if(!_preferences->getBool(preference_keypad_control_enabled, false)) { @@ -1703,10 +2457,14 @@ void NukiOpenerWrapper::onKeypadCommandReceived(const char *command, const uint return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } if((int)result != -1) @@ -1772,21 +2530,57 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json["code"].is()) code = json["code"].as(); - else code = 12; + if(json["code"].is()) + { + code = json["code"].as(); + } + else + { + code = 12; + } - if(json["enabled"].is()) enabled = json["enabled"].as(); - else enabled = 2; + if(json["enabled"].is()) + { + enabled = json["enabled"].as(); + } + else + { + enabled = 2; + } - if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); - else timeLimited = 2; + if(json["timeLimited"].is()) + { + timeLimited = json["timeLimited"].as(); + } + else + { + timeLimited = 2; + } - if(json["name"].is()) name = json["name"].as(); - if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); - if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); - if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); - if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); - if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["name"].is()) + { + name = json["name"].as(); + } + if(json["allowedFrom"].is()) + { + allowedFrom = json["allowedFrom"].as(); + } + if(json["allowedUntil"].is()) + { + allowedUntil = json["allowedUntil"].as(); + } + if(json["allowedWeekdays"].is()) + { + allowedWeekdays = json["allowedWeekdays"].as(); + } + if(json["allowedFromTime"].is()) + { + allowedFromTime = json["allowedFromTime"].as(); + } + if(json["allowedUntilTime"].is()) + { + allowedUntilTime = json["allowedUntilTime"].as(); + } if(action) { @@ -1802,7 +2596,8 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) while(retryCount < _nrOfRetries + 1) { - if(strcmp(action, "delete") == 0) { + if(strcmp(action, "delete") == 0) + { if(idExists) { result = _nukiOpener.deleteKeypadEntry(codeId); @@ -1939,13 +2734,34 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) } } - if(allowedWeekdays.indexOf("mon") >= 0) allowedWeekdaysInt += 64; - if(allowedWeekdays.indexOf("tue") >= 0) allowedWeekdaysInt += 32; - if(allowedWeekdays.indexOf("wed") >= 0) allowedWeekdaysInt += 16; - if(allowedWeekdays.indexOf("thu") >= 0) allowedWeekdaysInt += 8; - if(allowedWeekdays.indexOf("fri") >= 0) allowedWeekdaysInt += 4; - if(allowedWeekdays.indexOf("sat") >= 0) allowedWeekdaysInt += 2; - if(allowedWeekdays.indexOf("sun") >= 0) allowedWeekdaysInt += 1; + if(allowedWeekdays.indexOf("mon") >= 0) + { + allowedWeekdaysInt += 64; + } + if(allowedWeekdays.indexOf("tue") >= 0) + { + allowedWeekdaysInt += 32; + } + if(allowedWeekdays.indexOf("wed") >= 0) + { + allowedWeekdaysInt += 16; + } + if(allowedWeekdays.indexOf("thu") >= 0) + { + allowedWeekdaysInt += 8; + } + if(allowedWeekdays.indexOf("fri") >= 0) + { + allowedWeekdaysInt += 4; + } + if(allowedWeekdays.indexOf("sat") >= 0) + { + allowedWeekdaysInt += 2; + } + if(allowedWeekdays.indexOf("sun") >= 0) + { + allowedWeekdaysInt += 1; + } } if(strcmp(action, "add") == 0) @@ -2020,17 +2836,32 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) for(const auto& entry : entries) { - if (codeId != entry.codeId) continue; - else foundExisting = true; + if (codeId != entry.codeId) + { + continue; + } + else + { + foundExisting = true; + } if(name.length() < 1) { memset(oldName, 0, sizeof(oldName)); memcpy(oldName, entry.name, sizeof(entry.name)); } - if(code == 12) code = entry.code; - if(enabled == 2) enabled = entry.enabled; - if(timeLimited == 2) timeLimited = entry.timeLimited; + if(code == 12) + { + code = entry.code; + } + if(enabled == 2) + { + enabled = entry.enabled; + } + if(timeLimited == 2) + { + timeLimited = entry.timeLimited; + } if(allowedFrom.length() < 1) { allowedFrom = "old"; @@ -2051,7 +2882,10 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) allowedUntilAr[4] = entry.allowedUntilMin; allowedUntilAr[5] = entry.allowedUntilSec; } - if(allowedWeekdays.length() < 1) allowedWeekdaysInt = entry.allowedWeekdays; + if(allowedWeekdays.length() < 1) + { + allowedWeekdaysInt = entry.allowedWeekdays; + } if(allowedFromTime.length() < 1) { allowedFromTime = "old"; @@ -2149,10 +2983,14 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } updateKeypad(false); @@ -2209,12 +3047,27 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) String lockAction; NukiOpener::LockAction timeControlLockAction; - if(json["enabled"].is()) enabled = json["enabled"].as(); - else enabled = 2; + if(json["enabled"].is()) + { + enabled = json["enabled"].as(); + } + else + { + enabled = 2; + } - if(json["weekdays"].is()) weekdays = json["weekdays"].as(); - if(json["time"].is()) time = json["time"].as(); - if(json["lockAction"].is()) lockAction = json["lockAction"].as(); + if(json["weekdays"].is()) + { + weekdays = json["weekdays"].as(); + } + if(json["time"].is()) + { + time = json["time"].as(); + } + if(json["lockAction"].is()) + { + lockAction = json["lockAction"].as(); + } if(lockAction.length() > 0) { @@ -2241,7 +3094,8 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) while(retryCount < _nrOfRetries + 1) { - if(strcmp(action, "delete") == 0) { + if(strcmp(action, "delete") == 0) + { if(idExists) { result = _nukiOpener.removeTimeControlEntry(entryId); @@ -2281,13 +3135,34 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) } } - if(weekdays.indexOf("mon") >= 0) weekdaysInt += 64; - if(weekdays.indexOf("tue") >= 0) weekdaysInt += 32; - if(weekdays.indexOf("wed") >= 0) weekdaysInt += 16; - if(weekdays.indexOf("thu") >= 0) weekdaysInt += 8; - if(weekdays.indexOf("fri") >= 0) weekdaysInt += 4; - if(weekdays.indexOf("sat") >= 0) weekdaysInt += 2; - if(weekdays.indexOf("sun") >= 0) weekdaysInt += 1; + if(weekdays.indexOf("mon") >= 0) + { + weekdaysInt += 64; + } + if(weekdays.indexOf("tue") >= 0) + { + weekdaysInt += 32; + } + if(weekdays.indexOf("wed") >= 0) + { + weekdaysInt += 16; + } + if(weekdays.indexOf("thu") >= 0) + { + weekdaysInt += 8; + } + if(weekdays.indexOf("fri") >= 0) + { + weekdaysInt += 4; + } + if(weekdays.indexOf("sat") >= 0) + { + weekdaysInt += 2; + } + if(weekdays.indexOf("sun") >= 0) + { + weekdaysInt += 1; + } if(strcmp(action, "add") == 0) { @@ -2325,18 +3200,33 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) for(const auto& entry : timeControlEntries) { - if (entryId != entry.entryId) continue; - else foundExisting = true; + if (entryId != entry.entryId) + { + continue; + } + else + { + foundExisting = true; + } - if(enabled == 2) enabled = entry.enabled; - if(weekdays.length() < 1) weekdaysInt = entry.weekdays; + if(enabled == 2) + { + enabled = entry.enabled; + } + if(weekdays.length() < 1) + { + weekdaysInt = entry.weekdays; + } if(time.length() < 1) { time = "old"; timeAr[0] = entry.timeHour; timeAr[1] = entry.timeMin; } - if(lockAction.length() < 1) timeControlLockAction = entry.lockAction; + if(lockAction.length() < 1) + { + timeControlLockAction = entry.lockAction; + } } if(!foundExisting) @@ -2375,10 +3265,14 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } if((int)result != -1) @@ -2443,22 +3337,58 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json["remoteAllowed"].is()) remoteAllowed = json["remoteAllowed"].as(); - else remoteAllowed = 2; + if(json["remoteAllowed"].is()) + { + remoteAllowed = json["remoteAllowed"].as(); + } + else + { + remoteAllowed = 2; + } - if(json["enabled"].is()) enabled = json["enabled"].as(); - else enabled = 2; + if(json["enabled"].is()) + { + enabled = json["enabled"].as(); + } + else + { + enabled = 2; + } - if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); - else timeLimited = 2; + if(json["timeLimited"].is()) + { + timeLimited = json["timeLimited"].as(); + } + else + { + timeLimited = 2; + } - if(json["name"].is()) name = json["name"].as(); + if(json["name"].is()) + { + name = json["name"].as(); + } //if(json["sharedKey"].is()) sharedKey = json["sharedKey"].as(); - if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); - if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); - if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); - if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); - if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["allowedFrom"].is()) + { + allowedFrom = json["allowedFrom"].as(); + } + if(json["allowedUntil"].is()) + { + allowedUntil = json["allowedUntil"].as(); + } + if(json["allowedWeekdays"].is()) + { + allowedWeekdays = json["allowedWeekdays"].as(); + } + if(json["allowedFromTime"].is()) + { + allowedFromTime = json["allowedFromTime"].as(); + } + if(json["allowedUntilTime"].is()) + { + allowedUntilTime = json["allowedUntilTime"].as(); + } if(action) { @@ -2610,13 +3540,34 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) } } - if(allowedWeekdays.indexOf("mon") >= 0) allowedWeekdaysInt += 64; - if(allowedWeekdays.indexOf("tue") >= 0) allowedWeekdaysInt += 32; - if(allowedWeekdays.indexOf("wed") >= 0) allowedWeekdaysInt += 16; - if(allowedWeekdays.indexOf("thu") >= 0) allowedWeekdaysInt += 8; - if(allowedWeekdays.indexOf("fri") >= 0) allowedWeekdaysInt += 4; - if(allowedWeekdays.indexOf("sat") >= 0) allowedWeekdaysInt += 2; - if(allowedWeekdays.indexOf("sun") >= 0) allowedWeekdaysInt += 1; + if(allowedWeekdays.indexOf("mon") >= 0) + { + allowedWeekdaysInt += 64; + } + if(allowedWeekdays.indexOf("tue") >= 0) + { + allowedWeekdaysInt += 32; + } + if(allowedWeekdays.indexOf("wed") >= 0) + { + allowedWeekdaysInt += 16; + } + if(allowedWeekdays.indexOf("thu") >= 0) + { + allowedWeekdaysInt += 8; + } + if(allowedWeekdays.indexOf("fri") >= 0) + { + allowedWeekdaysInt += 4; + } + if(allowedWeekdays.indexOf("sat") >= 0) + { + allowedWeekdaysInt += 2; + } + if(allowedWeekdays.indexOf("sun") >= 0) + { + allowedWeekdaysInt += 1; + } } if(strcmp(action, "add") == 0) @@ -2705,17 +3656,32 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) for(const auto& entry : entries) { - if (authId != entry.authId) continue; - else foundExisting = true; + if (authId != entry.authId) + { + continue; + } + else + { + foundExisting = true; + } if(name.length() < 1) { memset(oldName, 0, sizeof(oldName)); memcpy(oldName, entry.name, sizeof(entry.name)); } - if(remoteAllowed == 2) remoteAllowed = entry.remoteAllowed; - if(enabled == 2) enabled = entry.enabled; - if(timeLimited == 2) timeLimited = entry.timeLimited; + if(remoteAllowed == 2) + { + remoteAllowed = entry.remoteAllowed; + } + if(enabled == 2) + { + enabled = entry.enabled; + } + if(timeLimited == 2) + { + timeLimited = entry.timeLimited; + } if(allowedFrom.length() < 1) { allowedFrom = "old"; @@ -2736,7 +3702,10 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) allowedUntilAr[4] = entry.allowedUntilMinute; allowedUntilAr[5] = entry.allowedUntilSecond; } - if(allowedWeekdays.length() < 1) allowedWeekdaysInt = entry.allowedWeekdays; + if(allowedWeekdays.length() < 1) + { + allowedWeekdaysInt = entry.allowedWeekdays; + } if(allowedFromTime.length() < 1) { allowedFromTime = "old"; @@ -2834,10 +3803,14 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } updateAuth(false); @@ -2902,10 +3875,14 @@ void NukiOpenerWrapper::readConfig() result = _nukiOpener.requestConfig(&_nukiConfig); _nukiConfigValid = result == Nuki::CmdResult::Success; - if(!_nukiConfigValid) { + if(!_nukiConfigValid) + { ++retryCount; } - else break; + else + { + break; + } } char resultStr[20]; @@ -2925,13 +3902,17 @@ void NukiOpenerWrapper::readAdvancedConfig() while(retryCount < _nrOfRetries + 1) { - result = _nukiOpener.requestAdvancedConfig(&_nukiAdvancedConfig); + result = _nukiOpener.requestAdvancedConfig(&_nukiAdvancedConfig); _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; - if(!_nukiAdvancedConfigValid) { + if(!_nukiAdvancedConfigValid) + { ++retryCount; } - else break; + else + { + break; + } } char resultStr[20]; @@ -2943,15 +3924,27 @@ void NukiOpenerWrapper::readAdvancedConfig() void NukiOpenerWrapper::setupHASS() { - if(!_nukiConfigValid) return; - if(_preferences->getUInt(preference_nuki_id_opener, 0) != _nukiConfig.nukiId) return; + if(!_nukiConfigValid) + { + return; + } + if(_preferences->getUInt(preference_nuki_id_opener, 0) != _nukiConfig.nukiId) + { + return; + } String baseTopic = _preferences->getString(preference_mqtt_opener_path); char uidString[20]; itoa(_nukiConfig.nukiId, uidString, 16); - if(_preferences->getBool(preference_opener_continuous_mode, false)) _network->publishHASSConfig((char*)"Opener", baseTopic.c_str(), (char*)_nukiConfig.name, uidString, _firmwareVersion.c_str(), _hardwareVersion.c_str(), _publishAuthData, _hasKeypad, (char*)"deactivateCM", (char*)"activateCM", (char*)"electricStrikeActuation"); - else _network->publishHASSConfig((char*)"Opener", baseTopic.c_str(), (char*)_nukiConfig.name, uidString, _firmwareVersion.c_str(), _hardwareVersion.c_str(), _publishAuthData, _hasKeypad, (char*)"deactivateRTO", (char*)"activateRTO", (char*)"electricStrikeActuation"); + if(_preferences->getBool(preference_opener_continuous_mode, false)) + { + _network->publishHASSConfig((char*)"Opener", baseTopic.c_str(), (char*)_nukiConfig.name, uidString, _firmwareVersion.c_str(), _hardwareVersion.c_str(), _publishAuthData, _hasKeypad, (char*)"deactivateCM", (char*)"activateCM", (char*)"electricStrikeActuation"); + } + else + { + _network->publishHASSConfig((char*)"Opener", baseTopic.c_str(), (char*)_nukiConfig.name, uidString, _firmwareVersion.c_str(), _hardwareVersion.c_str(), _publishAuthData, _hasKeypad, (char*)"deactivateRTO", (char*)"activateRTO", (char*)"electricStrikeActuation"); + } _hassSetupCompleted = true; @@ -3002,15 +3995,15 @@ void NukiOpenerWrapper::updateGpioOutputs() { switch(entry.role) { - case PinRole::OutputHighRtoActive: - _gpio->setPinOutput(entry.pin, rtoActive ? HIGH : LOW); - break; - case PinRole::OutputHighCmActive: - _gpio->setPinOutput(entry.pin, cmActive ? HIGH : LOW); - break; - case PinRole::OutputHighRtoOrCmActive: - _gpio->setPinOutput(entry.pin, rtoActive || cmActive ? HIGH : LOW); - break; + case PinRole::OutputHighRtoActive: + _gpio->setPinOutput(entry.pin, rtoActive ? HIGH : LOW); + break; + case PinRole::OutputHighCmActive: + _gpio->setPinOutput(entry.pin, cmActive ? HIGH : LOW); + break; + case PinRole::OutputHighRtoOrCmActive: + _gpio->setPinOutput(entry.pin, rtoActive || cmActive ? HIGH : LOW); + break; } } } \ No newline at end of file diff --git a/src/NukiPublisher.cpp b/src/NukiPublisher.cpp index 78376ec..2ee3289 100644 --- a/src/NukiPublisher.cpp +++ b/src/NukiPublisher.cpp @@ -2,8 +2,8 @@ NukiPublisher::NukiPublisher(NukiNetwork *network, const char* mqttPath) -: _network(network), - _mqttPath(mqttPath) + : _network(network), + _mqttPath(mqttPath) { } diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index 4167aef..b710dc8 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -12,14 +12,14 @@ NukiWrapper* nukiInst = nullptr; NukiWrapper::NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkLock* network, NukiOfficial* nukiOfficial, Gpio* gpio, Preferences* preferences) -: _deviceName(deviceName), - _deviceId(deviceId), - _bleScanner(scanner), - _nukiLock(deviceName, _deviceId->get()), - _network(network), - _nukiOfficial(nukiOfficial), - _gpio(gpio), - _preferences(preferences) + : _deviceName(deviceName), + _deviceId(deviceId), + _bleScanner(scanner), + _nukiLock(deviceName, _deviceId->get()), + _network(network), + _nukiOfficial(nukiOfficial), + _gpio(gpio), + _preferences(preferences) { Log->print("Device id lock: "); Log->println(_deviceId->get()); @@ -61,24 +61,27 @@ void NukiWrapper::initialize(const bool& firstStart) if(firstStart) { Log->println("First start, setting preference defaults"); - - #ifndef CONFIG_IDF_TARGET_ESP32H2 + +#ifndef CONFIG_IDF_TARGET_ESP32H2 wifi_config_t wifi_cfg; - if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) + { Log->println("Failed to get Wi-Fi configuration in RAM"); } - if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { + if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) + { Log->println("Failed to set storage Wi-Fi"); } memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); - if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) + { Log->println("Failed to clear NVS Wi-Fi configuration"); } - #endif +#endif _preferences->putBool(preference_check_updates, true); _preferences->putBool(preference_opener_continuous_mode, false); _preferences->putBool(preference_official_hybrid_enabled, false); @@ -130,14 +133,38 @@ void NukiWrapper::readSettings() esp_power_level_t powerLevel; int pwrLvl = _preferences->getInt(preference_ble_tx_power, 9); - if(pwrLvl >= 9) powerLevel = ESP_PWR_LVL_P9; - else if(pwrLvl >= 6) powerLevel = ESP_PWR_LVL_P6; - else if(pwrLvl >= 3) powerLevel = ESP_PWR_LVL_P6; - else if(pwrLvl >= 0) powerLevel = ESP_PWR_LVL_P3; - else if(pwrLvl >= -3) powerLevel = ESP_PWR_LVL_N3; - else if(pwrLvl >= -6) powerLevel = ESP_PWR_LVL_N6; - else if(pwrLvl >= -9) powerLevel = ESP_PWR_LVL_N9; - else if(pwrLvl >= -12) powerLevel = ESP_PWR_LVL_N12; + if(pwrLvl >= 9) + { + powerLevel = ESP_PWR_LVL_P9; + } + else if(pwrLvl >= 6) + { + powerLevel = ESP_PWR_LVL_P6; + } + else if(pwrLvl >= 3) + { + powerLevel = ESP_PWR_LVL_P6; + } + else if(pwrLvl >= 0) + { + powerLevel = ESP_PWR_LVL_P3; + } + else if(pwrLvl >= -3) + { + powerLevel = ESP_PWR_LVL_N3; + } + else if(pwrLvl >= -6) + { + powerLevel = ESP_PWR_LVL_N6; + } + else if(pwrLvl >= -9) + { + powerLevel = ESP_PWR_LVL_N9; + } + else if(pwrLvl >= -12) + { + powerLevel = ESP_PWR_LVL_N12; + } _nukiLock.setPower(powerLevel); @@ -252,10 +279,10 @@ void NukiWrapper::update() uint8_t queryCommands = _network->queryCommands(); if(_restartBeaconTimeout > 0 && - ts > 60000 && - lastReceivedBeaconTs > 0 && - _disableBleWatchdogTs < ts && - (ts - lastReceivedBeaconTs > _restartBeaconTimeout * 1000)) + ts > 60000 && + lastReceivedBeaconTs > 0 && + _disableBleWatchdogTs < ts && + (ts - lastReceivedBeaconTs > _restartBeaconTimeout * 1000)) { Log->print("No BLE beacon received from the lock for "); Log->print((ts - lastReceivedBeaconTs) / 1000); @@ -309,9 +336,16 @@ void NukiWrapper::update() _nextLockAction = (NukiLock::LockAction) 0xff; _network->publishRetry("--"); retryCount = 0; - if(!_nukiOfficial->getOffConnected()) _statusUpdated = true; Log->println(F("Lock: updating status after action")); + if(!_nukiOfficial->getOffConnected()) + { + _statusUpdated = true; + } + Log->println(F("Lock: updating status after action")); _statusUpdatedTs = ts; - if(_intervalLockstate > 10) _nextLockStateUpdateTs = ts + 10 * 1000; + if(_intervalLockstate > 10) + { + _nextLockStateUpdateTs = ts + 10 * 1000; + } } else { @@ -496,13 +530,16 @@ void NukiWrapper::updateKeyTurnerState() const NukiLock::LockState& lockState = _keyTurnerState.lockState; - if(lockState != _lastKeyTurnerState.lockState) _statusUpdatedTs = esp_timer_get_time() / 1000; + if(lockState != _lastKeyTurnerState.lockState) + { + _statusUpdatedTs = esp_timer_get_time() / 1000; + } if(lockState == NukiLock::LockState::Locked || - lockState == NukiLock::LockState::Unlocked || - lockState == NukiLock::LockState::Calibration || - lockState == NukiLock::LockState::BootRun || - lockState == NukiLock::LockState::MotorBlocked) + lockState == NukiLock::LockState::Unlocked || + lockState == NukiLock::LockState::Calibration || + lockState == NukiLock::LockState::BootRun || + lockState == NukiLock::LockState::MotorBlocked) { if(_publishAuthData && (lockState == NukiLock::LockState::Locked || lockState == NukiLock::LockState::Unlocked)) { @@ -543,10 +580,14 @@ void NukiWrapper::updateBatteryState() Log->print("): "); result = _nukiLock.requestBatteryReport(&_batteryReport); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -583,13 +624,23 @@ void NukiWrapper::updateConfig() _hasKeypad = _nukiConfig.hasKeypad > 0 || _nukiConfig.hasKeypadV2 > 0; _firmwareVersion = std::to_string(_nukiConfig.firmwareVersion[0]) + "." + std::to_string(_nukiConfig.firmwareVersion[1]) + "." + std::to_string(_nukiConfig.firmwareVersion[2]); _hardwareVersion = std::to_string(_nukiConfig.hardwareRevision[0]) + "." + std::to_string(_nukiConfig.hardwareRevision[1]); - if(_preferences->getBool(preference_conf_info_enabled, true)) _network->publishConfig(_nukiConfig); - if(_preferences->getBool(preference_timecontrol_info_enabled)) updateTimeControl(false); - if(_preferences->getBool(preference_auth_info_enabled)) updateAuth(false); + if(_preferences->getBool(preference_conf_info_enabled, true)) + { + _network->publishConfig(_nukiConfig); + } + if(_preferences->getBool(preference_timecontrol_info_enabled)) + { + updateTimeControl(false); + } + if(_preferences->getBool(preference_auth_info_enabled)) + { + updateAuth(false); + } const int pinStatus = _preferences->getInt(preference_lock_pin_status, 4); - if(isPinSet()) { + if(isPinSet()) + { Nuki::CmdResult result = (Nuki::CmdResult)-1; int retryCount = 0; Log->println(F("Nuki Lock PIN is set")); @@ -597,23 +648,29 @@ void NukiWrapper::updateConfig() while(retryCount < _nrOfRetries + 1) { result = _nukiLock.verifySecurityPin(); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } if(result != Nuki::CmdResult::Success) { Log->println(F("Nuki Lock PIN is invalid")); - if(pinStatus != 2) { + if(pinStatus != 2) + { _preferences->putInt(preference_lock_pin_status, 2); } } else { Log->println(F("Nuki Lock PIN is valid")); - if(pinStatus != 1) { + if(pinStatus != 1) + { _preferences->putInt(preference_lock_pin_status, 1); } } @@ -621,7 +678,8 @@ void NukiWrapper::updateConfig() else { Log->println(F("Nuki Lock PIN is not set")); - if(pinStatus != 0) { + if(pinStatus != 0) + { _preferences->putInt(preference_lock_pin_status, 0); } } @@ -644,7 +702,10 @@ void NukiWrapper::updateConfig() if(_nukiAdvancedConfigValid) { - if(_preferences->getBool(preference_conf_info_enabled, true)) _network->publishAdvancedConfig(_nukiAdvancedConfig); + if(_preferences->getBool(preference_conf_info_enabled, true)) + { + _network->publishAdvancedConfig(_nukiAdvancedConfig); + } } else { @@ -684,10 +745,14 @@ void NukiWrapper::updateAuthData(bool retrieved) { Log->print(F("Retrieve log entries: ")); result = _nukiLock.retrieveLogEntries(0, _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 1, false); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -704,11 +769,14 @@ void NukiWrapper::updateAuthData(bool retrieved) log.resize(_preferences->getInt(preference_authlog_max_entries, 3)); } - log.sort([](const NukiLock::LogEntry& a, const NukiLock::LogEntry& b) { return a.index < b.index; }); + log.sort([](const NukiLock::LogEntry& a, const NukiLock::LogEntry& b) + { + return a.index < b.index; + }); if(log.size() > 0) { - _network->publishAuthorizationInfo(log, true); + _network->publishAuthorizationInfo(log, true); } } } @@ -722,14 +790,17 @@ void NukiWrapper::updateAuthData(bool retrieved) log.resize(_preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG)); } - log.sort([](const NukiLock::LogEntry& a, const NukiLock::LogEntry& b) { return a.index < b.index; }); + log.sort([](const NukiLock::LogEntry& a, const NukiLock::LogEntry& b) + { + return a.index < b.index; + }); Log->print(F("Log size: ")); Log->println(log.size()); if(log.size() > 0) { - _network->publishAuthorizationInfo(log, false); + _network->publishAuthorizationInfo(log, false); } } @@ -738,7 +809,10 @@ void NukiWrapper::updateAuthData(bool retrieved) void NukiWrapper::updateKeypad(bool retrieved) { - if(!_preferences->getBool(preference_keypad_info_enabled)) return; + if(!_preferences->getBool(preference_keypad_info_enabled)) + { + return; + } if(!isPinValid()) { @@ -755,10 +829,14 @@ void NukiWrapper::updateKeypad(bool retrieved) { Log->print(F("Querying lock keypad: ")); result = _nukiLock.retrieveKeypadEntries(0, _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD)); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -775,7 +853,10 @@ void NukiWrapper::updateKeypad(bool retrieved) Log->print(F("Lock keypad codes: ")); Log->println(entries.size()); - entries.sort([](const NukiLock::KeypadEntry& a, const NukiLock::KeypadEntry& b) { return a.codeId < b.codeId; }); + entries.sort([](const NukiLock::KeypadEntry& a, const NukiLock::KeypadEntry& b) + { + return a.codeId < b.codeId; + }); if(entries.size() > _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD)) { @@ -804,7 +885,10 @@ void NukiWrapper::updateKeypad(bool retrieved) void NukiWrapper::updateTimeControl(bool retrieved) { - if(!_preferences->getBool(preference_timecontrol_info_enabled)) return; + if(!_preferences->getBool(preference_timecontrol_info_enabled)) + { + return; + } if(!isPinValid()) { @@ -821,10 +905,14 @@ void NukiWrapper::updateTimeControl(bool retrieved) { Log->print(F("Querying lock timecontrol: ")); result = _nukiLock.retrieveTimeControlEntries(); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -841,7 +929,10 @@ void NukiWrapper::updateTimeControl(bool retrieved) Log->print(F("Lock timecontrol entries: ")); Log->println(timeControlEntries.size()); - timeControlEntries.sort([](const NukiLock::TimeControlEntry& a, const NukiLock::TimeControlEntry& b) { return a.entryId < b.entryId; }); + timeControlEntries.sort([](const NukiLock::TimeControlEntry& a, const NukiLock::TimeControlEntry& b) + { + return a.entryId < b.entryId; + }); if(timeControlEntries.size() > _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL)) { @@ -870,7 +961,10 @@ void NukiWrapper::updateTimeControl(bool retrieved) void NukiWrapper::updateAuth(bool retrieved) { - if(!_preferences->getBool(preference_auth_info_enabled)) return; + if(!_preferences->getBool(preference_auth_info_enabled)) + { + return; + } if(!retrieved) { @@ -882,10 +976,14 @@ void NukiWrapper::updateAuth(bool retrieved) Log->print(F("Querying lock authorization: ")); result = _nukiLock.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); delay(250); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } printCommandResult(result); @@ -902,7 +1000,10 @@ void NukiWrapper::updateAuth(bool retrieved) Log->print(F("Lock authorization entries: ")); Log->println(authEntries.size()); - authEntries.sort([](const NukiLock::AuthorizationEntry& a, const NukiLock::AuthorizationEntry& b) { return a.authId < b.authId; }); + authEntries.sort([](const NukiLock::AuthorizationEntry& a, const NukiLock::AuthorizationEntry& b) + { + return a.authId < b.authId; + }); if(authEntries.size() > _preferences->getInt(preference_auth_max_entries, MAX_AUTH)) { @@ -936,15 +1037,42 @@ void NukiWrapper::postponeBleWatchdog() NukiLock::LockAction NukiWrapper::lockActionToEnum(const char *str) { - if(strcmp(str, "unlock") == 0 || strcmp(str, "Unlock") == 0) return NukiLock::LockAction::Unlock; - else if(strcmp(str, "lock") == 0 || strcmp(str, "Lock") == 0) return NukiLock::LockAction::Lock; - else if(strcmp(str, "unlatch") == 0 || strcmp(str, "Unlatch") == 0) return NukiLock::LockAction::Unlatch; - else if(strcmp(str, "lockNgo") == 0 || strcmp(str, "LockNgo") == 0) return NukiLock::LockAction::LockNgo; - else if(strcmp(str, "lockNgoUnlatch") == 0 || strcmp(str, "LockNgoUnlatch") == 0) return NukiLock::LockAction::LockNgoUnlatch; - else if(strcmp(str, "fullLock") == 0 || strcmp(str, "FullLock") == 0) return NukiLock::LockAction::FullLock; - else if(strcmp(str, "fobAction2") == 0 || strcmp(str, "FobAction2") == 0) return NukiLock::LockAction::FobAction2; - else if(strcmp(str, "fobAction1") == 0 || strcmp(str, "FobAction1") == 0) return NukiLock::LockAction::FobAction1; - else if(strcmp(str, "fobAction3") == 0 || strcmp(str, "FobAction3") == 0) return NukiLock::LockAction::FobAction3; + if(strcmp(str, "unlock") == 0 || strcmp(str, "Unlock") == 0) + { + return NukiLock::LockAction::Unlock; + } + else if(strcmp(str, "lock") == 0 || strcmp(str, "Lock") == 0) + { + return NukiLock::LockAction::Lock; + } + else if(strcmp(str, "unlatch") == 0 || strcmp(str, "Unlatch") == 0) + { + return NukiLock::LockAction::Unlatch; + } + else if(strcmp(str, "lockNgo") == 0 || strcmp(str, "LockNgo") == 0) + { + return NukiLock::LockAction::LockNgo; + } + else if(strcmp(str, "lockNgoUnlatch") == 0 || strcmp(str, "LockNgoUnlatch") == 0) + { + return NukiLock::LockAction::LockNgoUnlatch; + } + else if(strcmp(str, "fullLock") == 0 || strcmp(str, "FullLock") == 0) + { + return NukiLock::LockAction::FullLock; + } + else if(strcmp(str, "fobAction2") == 0 || strcmp(str, "FobAction2") == 0) + { + return NukiLock::LockAction::FobAction2; + } + else if(strcmp(str, "fobAction1") == 0 || strcmp(str, "FobAction1") == 0) + { + return NukiLock::LockAction::FobAction1; + } + else if(strcmp(str, "fobAction3") == 0 || strcmp(str, "FobAction3") == 0) + { + return NukiLock::LockAction::FobAction3; + } return (NukiLock::LockAction)0xff; } @@ -962,18 +1090,30 @@ LockActionResult NukiWrapper::onLockActionReceived(const char *value) if(strlen(value) > 0) { action = nukiInst->lockActionToEnum(value); - if((int)action == 0xff) return LockActionResult::UnknownAction; + if((int)action == 0xff) + { + return LockActionResult::UnknownAction; + } + } + else + { + return LockActionResult::UnknownAction; } - else return LockActionResult::UnknownAction; } - else return LockActionResult::UnknownAction; + else + { + return LockActionResult::UnknownAction; + } uint32_t aclPrefs[17]; _preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs)); if((action == NukiLock::LockAction::Lock && (int)aclPrefs[0] == 1) || (action == NukiLock::LockAction::Unlock && (int)aclPrefs[1] == 1) || (action == NukiLock::LockAction::Unlatch && (int)aclPrefs[2] == 1) || (action == NukiLock::LockAction::LockNgo && (int)aclPrefs[3] == 1) || (action == NukiLock::LockAction::LockNgoUnlatch && (int)aclPrefs[4] == 1) || (action == NukiLock::LockAction::FullLock && (int)aclPrefs[5] == 1) || (action == NukiLock::LockAction::FobAction1 && (int)aclPrefs[6] == 1) || (action == NukiLock::LockAction::FobAction2 && (int)aclPrefs[7] == 1) || (action == NukiLock::LockAction::FobAction3 && (int)aclPrefs[8] == 1)) { - if(!_nukiOfficial->getOffConnected()) nukiInst->_nextLockAction = action; + if(!_nukiOfficial->getOffConnected()) + { + nukiInst->_nextLockAction = action; + } else { if(_preferences->getBool(preference_official_hybrid_actions, false)) @@ -1010,92 +1150,290 @@ bool NukiWrapper::offConnected() Nuki::AdvertisingMode NukiWrapper::advertisingModeToEnum(const char *str) { - if(strcmp(str, "Automatic") == 0) return Nuki::AdvertisingMode::Automatic; - else if(strcmp(str, "Normal") == 0) return Nuki::AdvertisingMode::Normal; - else if(strcmp(str, "Slow") == 0) return Nuki::AdvertisingMode::Slow; - else if(strcmp(str, "Slowest") == 0) return Nuki::AdvertisingMode::Slowest; + if(strcmp(str, "Automatic") == 0) + { + return Nuki::AdvertisingMode::Automatic; + } + else if(strcmp(str, "Normal") == 0) + { + return Nuki::AdvertisingMode::Normal; + } + else if(strcmp(str, "Slow") == 0) + { + return Nuki::AdvertisingMode::Slow; + } + else if(strcmp(str, "Slowest") == 0) + { + return Nuki::AdvertisingMode::Slowest; + } return (Nuki::AdvertisingMode)0xff; } Nuki::TimeZoneId NukiWrapper::timeZoneToEnum(const char *str) { - if(strcmp(str, "Africa/Cairo") == 0) return Nuki::TimeZoneId::Africa_Cairo; - else if(strcmp(str, "Africa/Lagos") == 0) return Nuki::TimeZoneId::Africa_Lagos; - else if(strcmp(str, "Africa/Maputo") == 0) return Nuki::TimeZoneId::Africa_Maputo; - else if(strcmp(str, "Africa/Nairobi") == 0) return Nuki::TimeZoneId::Africa_Nairobi; - else if(strcmp(str, "America/Anchorage") == 0) return Nuki::TimeZoneId::America_Anchorage; - else if(strcmp(str, "America/Argentina/Buenos_Aires") == 0) return Nuki::TimeZoneId::America_Argentina_Buenos_Aires; - else if(strcmp(str, "America/Chicago") == 0) return Nuki::TimeZoneId::America_Chicago; - else if(strcmp(str, "America/Denver") == 0) return Nuki::TimeZoneId::America_Denver; - else if(strcmp(str, "America/Halifax") == 0) return Nuki::TimeZoneId::America_Halifax; - else if(strcmp(str, "America/Los_Angeles") == 0) return Nuki::TimeZoneId::America_Los_Angeles; - else if(strcmp(str, "America/Manaus") == 0) return Nuki::TimeZoneId::America_Manaus; - else if(strcmp(str, "America/Mexico_City") == 0) return Nuki::TimeZoneId::America_Mexico_City; - else if(strcmp(str, "America/New_York") == 0) return Nuki::TimeZoneId::America_New_York; - else if(strcmp(str, "America/Phoenix") == 0) return Nuki::TimeZoneId::America_Phoenix; - else if(strcmp(str, "America/Regina") == 0) return Nuki::TimeZoneId::America_Regina; - else if(strcmp(str, "America/Santiago") == 0) return Nuki::TimeZoneId::America_Santiago; - else if(strcmp(str, "America/Sao_Paulo") == 0) return Nuki::TimeZoneId::America_Sao_Paulo; - else if(strcmp(str, "America/St_Johns") == 0) return Nuki::TimeZoneId::America_St_Johns; - else if(strcmp(str, "Asia/Bangkok") == 0) return Nuki::TimeZoneId::Asia_Bangkok; - else if(strcmp(str, "Asia/Dubai") == 0) return Nuki::TimeZoneId::Asia_Dubai; - else if(strcmp(str, "Asia/Hong_Kong") == 0) return Nuki::TimeZoneId::Asia_Hong_Kong; - else if(strcmp(str, "Asia/Jerusalem") == 0) return Nuki::TimeZoneId::Asia_Jerusalem; - else if(strcmp(str, "Asia/Karachi") == 0) return Nuki::TimeZoneId::Asia_Karachi; - else if(strcmp(str, "Asia/Kathmandu") == 0) return Nuki::TimeZoneId::Asia_Kathmandu; - else if(strcmp(str, "Asia/Kolkata") == 0) return Nuki::TimeZoneId::Asia_Kolkata; - else if(strcmp(str, "Asia/Riyadh") == 0) return Nuki::TimeZoneId::Asia_Riyadh; - else if(strcmp(str, "Asia/Seoul") == 0) return Nuki::TimeZoneId::Asia_Seoul; - else if(strcmp(str, "Asia/Shanghai") == 0) return Nuki::TimeZoneId::Asia_Shanghai; - else if(strcmp(str, "Asia/Tehran") == 0) return Nuki::TimeZoneId::Asia_Tehran; - else if(strcmp(str, "Asia/Tokyo") == 0) return Nuki::TimeZoneId::Asia_Tokyo; - else if(strcmp(str, "Asia/Yangon") == 0) return Nuki::TimeZoneId::Asia_Yangon; - else if(strcmp(str, "Australia/Adelaide") == 0) return Nuki::TimeZoneId::Australia_Adelaide; - else if(strcmp(str, "Australia/Brisbane") == 0) return Nuki::TimeZoneId::Australia_Brisbane; - else if(strcmp(str, "Australia/Darwin") == 0) return Nuki::TimeZoneId::Australia_Darwin; - else if(strcmp(str, "Australia/Hobart") == 0) return Nuki::TimeZoneId::Australia_Hobart; - else if(strcmp(str, "Australia/Perth") == 0) return Nuki::TimeZoneId::Australia_Perth; - else if(strcmp(str, "Australia/Sydney") == 0) return Nuki::TimeZoneId::Australia_Sydney; - else if(strcmp(str, "Europe/Berlin") == 0) return Nuki::TimeZoneId::Europe_Berlin; - else if(strcmp(str, "Europe/Helsinki") == 0) return Nuki::TimeZoneId::Europe_Helsinki; - else if(strcmp(str, "Europe/Istanbul") == 0) return Nuki::TimeZoneId::Europe_Istanbul; - else if(strcmp(str, "Europe/London") == 0) return Nuki::TimeZoneId::Europe_London; - else if(strcmp(str, "Europe/Moscow") == 0) return Nuki::TimeZoneId::Europe_Moscow; - else if(strcmp(str, "Pacific/Auckland") == 0) return Nuki::TimeZoneId::Pacific_Auckland; - else if(strcmp(str, "Pacific/Guam") == 0) return Nuki::TimeZoneId::Pacific_Guam; - else if(strcmp(str, "Pacific/Honolulu") == 0) return Nuki::TimeZoneId::Pacific_Honolulu; - else if(strcmp(str, "Pacific/Pago_Pago") == 0) return Nuki::TimeZoneId::Pacific_Pago_Pago; - else if(strcmp(str, "None") == 0) return Nuki::TimeZoneId::None; + if(strcmp(str, "Africa/Cairo") == 0) + { + return Nuki::TimeZoneId::Africa_Cairo; + } + else if(strcmp(str, "Africa/Lagos") == 0) + { + return Nuki::TimeZoneId::Africa_Lagos; + } + else if(strcmp(str, "Africa/Maputo") == 0) + { + return Nuki::TimeZoneId::Africa_Maputo; + } + else if(strcmp(str, "Africa/Nairobi") == 0) + { + return Nuki::TimeZoneId::Africa_Nairobi; + } + else if(strcmp(str, "America/Anchorage") == 0) + { + return Nuki::TimeZoneId::America_Anchorage; + } + else if(strcmp(str, "America/Argentina/Buenos_Aires") == 0) + { + return Nuki::TimeZoneId::America_Argentina_Buenos_Aires; + } + else if(strcmp(str, "America/Chicago") == 0) + { + return Nuki::TimeZoneId::America_Chicago; + } + else if(strcmp(str, "America/Denver") == 0) + { + return Nuki::TimeZoneId::America_Denver; + } + else if(strcmp(str, "America/Halifax") == 0) + { + return Nuki::TimeZoneId::America_Halifax; + } + else if(strcmp(str, "America/Los_Angeles") == 0) + { + return Nuki::TimeZoneId::America_Los_Angeles; + } + else if(strcmp(str, "America/Manaus") == 0) + { + return Nuki::TimeZoneId::America_Manaus; + } + else if(strcmp(str, "America/Mexico_City") == 0) + { + return Nuki::TimeZoneId::America_Mexico_City; + } + else if(strcmp(str, "America/New_York") == 0) + { + return Nuki::TimeZoneId::America_New_York; + } + else if(strcmp(str, "America/Phoenix") == 0) + { + return Nuki::TimeZoneId::America_Phoenix; + } + else if(strcmp(str, "America/Regina") == 0) + { + return Nuki::TimeZoneId::America_Regina; + } + else if(strcmp(str, "America/Santiago") == 0) + { + return Nuki::TimeZoneId::America_Santiago; + } + else if(strcmp(str, "America/Sao_Paulo") == 0) + { + return Nuki::TimeZoneId::America_Sao_Paulo; + } + else if(strcmp(str, "America/St_Johns") == 0) + { + return Nuki::TimeZoneId::America_St_Johns; + } + else if(strcmp(str, "Asia/Bangkok") == 0) + { + return Nuki::TimeZoneId::Asia_Bangkok; + } + else if(strcmp(str, "Asia/Dubai") == 0) + { + return Nuki::TimeZoneId::Asia_Dubai; + } + else if(strcmp(str, "Asia/Hong_Kong") == 0) + { + return Nuki::TimeZoneId::Asia_Hong_Kong; + } + else if(strcmp(str, "Asia/Jerusalem") == 0) + { + return Nuki::TimeZoneId::Asia_Jerusalem; + } + else if(strcmp(str, "Asia/Karachi") == 0) + { + return Nuki::TimeZoneId::Asia_Karachi; + } + else if(strcmp(str, "Asia/Kathmandu") == 0) + { + return Nuki::TimeZoneId::Asia_Kathmandu; + } + else if(strcmp(str, "Asia/Kolkata") == 0) + { + return Nuki::TimeZoneId::Asia_Kolkata; + } + else if(strcmp(str, "Asia/Riyadh") == 0) + { + return Nuki::TimeZoneId::Asia_Riyadh; + } + else if(strcmp(str, "Asia/Seoul") == 0) + { + return Nuki::TimeZoneId::Asia_Seoul; + } + else if(strcmp(str, "Asia/Shanghai") == 0) + { + return Nuki::TimeZoneId::Asia_Shanghai; + } + else if(strcmp(str, "Asia/Tehran") == 0) + { + return Nuki::TimeZoneId::Asia_Tehran; + } + else if(strcmp(str, "Asia/Tokyo") == 0) + { + return Nuki::TimeZoneId::Asia_Tokyo; + } + else if(strcmp(str, "Asia/Yangon") == 0) + { + return Nuki::TimeZoneId::Asia_Yangon; + } + else if(strcmp(str, "Australia/Adelaide") == 0) + { + return Nuki::TimeZoneId::Australia_Adelaide; + } + else if(strcmp(str, "Australia/Brisbane") == 0) + { + return Nuki::TimeZoneId::Australia_Brisbane; + } + else if(strcmp(str, "Australia/Darwin") == 0) + { + return Nuki::TimeZoneId::Australia_Darwin; + } + else if(strcmp(str, "Australia/Hobart") == 0) + { + return Nuki::TimeZoneId::Australia_Hobart; + } + else if(strcmp(str, "Australia/Perth") == 0) + { + return Nuki::TimeZoneId::Australia_Perth; + } + else if(strcmp(str, "Australia/Sydney") == 0) + { + return Nuki::TimeZoneId::Australia_Sydney; + } + else if(strcmp(str, "Europe/Berlin") == 0) + { + return Nuki::TimeZoneId::Europe_Berlin; + } + else if(strcmp(str, "Europe/Helsinki") == 0) + { + return Nuki::TimeZoneId::Europe_Helsinki; + } + else if(strcmp(str, "Europe/Istanbul") == 0) + { + return Nuki::TimeZoneId::Europe_Istanbul; + } + else if(strcmp(str, "Europe/London") == 0) + { + return Nuki::TimeZoneId::Europe_London; + } + else if(strcmp(str, "Europe/Moscow") == 0) + { + return Nuki::TimeZoneId::Europe_Moscow; + } + else if(strcmp(str, "Pacific/Auckland") == 0) + { + return Nuki::TimeZoneId::Pacific_Auckland; + } + else if(strcmp(str, "Pacific/Guam") == 0) + { + return Nuki::TimeZoneId::Pacific_Guam; + } + else if(strcmp(str, "Pacific/Honolulu") == 0) + { + return Nuki::TimeZoneId::Pacific_Honolulu; + } + else if(strcmp(str, "Pacific/Pago_Pago") == 0) + { + return Nuki::TimeZoneId::Pacific_Pago_Pago; + } + else if(strcmp(str, "None") == 0) + { + return Nuki::TimeZoneId::None; + } return (Nuki::TimeZoneId)0xff; } uint8_t NukiWrapper::fobActionToInt(const char *str) { - if(strcmp(str, "No Action") == 0) return 0; - else if(strcmp(str, "Unlock") == 0) return 1; - else if(strcmp(str, "Lock") == 0) return 2; - else if(strcmp(str, "Lock n Go") == 0) return 3; - else if(strcmp(str, "Intelligent") == 0) return 4; + if(strcmp(str, "No Action") == 0) + { + return 0; + } + else if(strcmp(str, "Unlock") == 0) + { + return 1; + } + else if(strcmp(str, "Lock") == 0) + { + return 2; + } + else if(strcmp(str, "Lock n Go") == 0) + { + return 3; + } + else if(strcmp(str, "Intelligent") == 0) + { + return 4; + } return 99; } NukiLock::ButtonPressAction NukiWrapper::buttonPressActionToEnum(const char* str) { - if(strcmp(str, "No Action") == 0) return NukiLock::ButtonPressAction::NoAction; - else if(strcmp(str, "Intelligent") == 0) return NukiLock::ButtonPressAction::Intelligent; - else if(strcmp(str, "Unlock") == 0) return NukiLock::ButtonPressAction::Unlock; - else if(strcmp(str, "Lock") == 0) return NukiLock::ButtonPressAction::Lock; - else if(strcmp(str, "Unlatch") == 0) return NukiLock::ButtonPressAction::Unlatch; - else if(strcmp(str, "Lock n Go") == 0) return NukiLock::ButtonPressAction::LockNgo; - else if(strcmp(str, "Show Status") == 0) return NukiLock::ButtonPressAction::ShowStatus; + if(strcmp(str, "No Action") == 0) + { + return NukiLock::ButtonPressAction::NoAction; + } + else if(strcmp(str, "Intelligent") == 0) + { + return NukiLock::ButtonPressAction::Intelligent; + } + else if(strcmp(str, "Unlock") == 0) + { + return NukiLock::ButtonPressAction::Unlock; + } + else if(strcmp(str, "Lock") == 0) + { + return NukiLock::ButtonPressAction::Lock; + } + else if(strcmp(str, "Unlatch") == 0) + { + return NukiLock::ButtonPressAction::Unlatch; + } + else if(strcmp(str, "Lock n Go") == 0) + { + return NukiLock::ButtonPressAction::LockNgo; + } + else if(strcmp(str, "Show Status") == 0) + { + return NukiLock::ButtonPressAction::ShowStatus; + } return (NukiLock::ButtonPressAction)0xff; } Nuki::BatteryType NukiWrapper::batteryTypeToEnum(const char* str) { - if(strcmp(str, "Alkali") == 0) return Nuki::BatteryType::Alkali; - else if(strcmp(str, "Accumulators") == 0) return Nuki::BatteryType::Accumulators; - else if(strcmp(str, "Lithium") == 0) return Nuki::BatteryType::Lithium; + if(strcmp(str, "Alkali") == 0) + { + return Nuki::BatteryType::Alkali; + } + else if(strcmp(str, "Accumulators") == 0) + { + return Nuki::BatteryType::Accumulators; + } + else if(strcmp(str, "Lithium") == 0) + { + return Nuki::BatteryType::Lithium; + } return (Nuki::BatteryType)0xff; } @@ -1165,10 +1503,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) { if(strlen(jsonchar) <= 32) { - if(strcmp((const char*)_nukiConfig.name, jsonchar) == 0) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setName(std::string(jsonchar)); + if(strcmp((const char*)_nukiConfig.name, jsonchar) == 0) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setName(std::string(jsonchar)); + } + } + else + { + jsonResult[basicKeys[i]] = "valueTooLong"; } - else jsonResult[basicKeys[i]] = "valueTooLong"; } else if(strcmp(basicKeys[i], "latitude") == 0) { @@ -1176,10 +1523,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue > 0) { - if(_nukiConfig.latitude == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setLatitude(keyvalue); + if(_nukiConfig.latitude == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setLatitude(keyvalue); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "longitude") == 0) { @@ -1187,10 +1543,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue > 0) { - if(_nukiConfig.longitude == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setLongitude(keyvalue); + if(_nukiConfig.longitude == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setLongitude(keyvalue); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "autoUnlatch") == 0) { @@ -1198,10 +1563,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.autoUnlatch == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.enableAutoUnlatch((keyvalue > 0)); + if(_nukiConfig.autoUnlatch == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableAutoUnlatch((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "pairingEnabled") == 0) { @@ -1209,10 +1583,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.pairingEnabled == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.enablePairing((keyvalue > 0)); + if(_nukiConfig.pairingEnabled == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enablePairing((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "buttonEnabled") == 0) { @@ -1220,10 +1603,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.buttonEnabled == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.enableButton((keyvalue > 0)); + if(_nukiConfig.buttonEnabled == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableButton((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "ledEnabled") == 0) { @@ -1231,10 +1623,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.ledEnabled == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.enableLedFlash((keyvalue > 0)); + if(_nukiConfig.ledEnabled == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableLedFlash((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "ledBrightness") == 0) { @@ -1242,10 +1643,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 0 && keyvalue <= 5) { - if(_nukiConfig.ledBrightness == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setLedBrightness(keyvalue); + if(_nukiConfig.ledBrightness == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setLedBrightness(keyvalue); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "timeZoneOffset") == 0) { @@ -1253,10 +1663,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 0 && keyvalue <= 60) { - if(_nukiConfig.timeZoneOffset == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setTimeZoneOffset(keyvalue); + if(_nukiConfig.timeZoneOffset == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setTimeZoneOffset(keyvalue); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "dstMode") == 0) { @@ -1264,10 +1683,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.dstMode == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.enableDst((keyvalue > 0)); + if(_nukiConfig.dstMode == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableDst((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "fobAction1") == 0) { @@ -1275,10 +1703,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(fobAct1 != 99) { - if(_nukiConfig.fobAction1 == fobAct1) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setFobAction(1, fobAct1); + if(_nukiConfig.fobAction1 == fobAct1) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setFobAction(1, fobAct1); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "fobAction2") == 0) { @@ -1286,10 +1723,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(fobAct2 != 99) { - if(_nukiConfig.fobAction2 == fobAct2) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setFobAction(2, fobAct2); + if(_nukiConfig.fobAction2 == fobAct2) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setFobAction(2, fobAct2); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "fobAction3") == 0) { @@ -1297,10 +1743,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(fobAct3 != 99) { - if(_nukiConfig.fobAction3 == fobAct3) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setFobAction(3, fobAct3); + if(_nukiConfig.fobAction3 == fobAct3) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setFobAction(3, fobAct3); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "singleLock") == 0) { @@ -1308,10 +1763,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiConfig.singleLock == keyvalue) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.enableSingleLock((keyvalue > 0)); + if(_nukiConfig.singleLock == keyvalue) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableSingleLock((keyvalue > 0)); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "advertisingMode") == 0) { @@ -1319,10 +1783,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if((int)advmode != 0xff) { - if(_nukiConfig.advertisingMode == advmode) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setAdvertisingMode(advmode); + if(_nukiConfig.advertisingMode == advmode) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setAdvertisingMode(advmode); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } else if(strcmp(basicKeys[i], "timeZone") == 0) { @@ -1330,27 +1803,47 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if((int)tzid != 0xff) { - if(_nukiConfig.timeZoneId == tzid) jsonResult[basicKeys[i]] = "unchanged"; - else cmdResult = _nukiLock.setTimeZoneId(tzid); + if(_nukiConfig.timeZoneId == tzid) + { + jsonResult[basicKeys[i]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setTimeZoneId(tzid); + } + } + else + { + jsonResult[basicKeys[i]] = "invalidValue"; } - else jsonResult[basicKeys[i]] = "invalidValue"; } - if(cmdResult != Nuki::CmdResult::Success) { + if(cmdResult != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } - if(cmdResult == Nuki::CmdResult::Success) basicUpdated = true; + if(cmdResult == Nuki::CmdResult::Success) + { + basicUpdated = true; + } - if(!jsonResult[basicKeys[i]]) { + if(!jsonResult[basicKeys[i]]) + { char resultStr[15] = {0}; NukiLock::cmdResultToString(cmdResult, resultStr); jsonResult[basicKeys[i]] = resultStr; } } - else jsonResult[basicKeys[i]] = "accessDenied"; + else + { + jsonResult[basicKeys[i]] = "accessDenied"; + } } } @@ -1379,10 +1872,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= -90 && keyvalue <= 180) { - if(_nukiAdvancedConfig.unlockedPositionOffsetDegrees == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setUnlockedPositionOffsetDegrees(keyvalue); + if(_nukiAdvancedConfig.unlockedPositionOffsetDegrees == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setUnlockedPositionOffsetDegrees(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "lockedPositionOffsetDegrees") == 0) { @@ -1390,10 +1892,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= -180 && keyvalue <= 90) { - if(_nukiAdvancedConfig.lockedPositionOffsetDegrees == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setLockedPositionOffsetDegrees(keyvalue); + if(_nukiAdvancedConfig.lockedPositionOffsetDegrees == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setLockedPositionOffsetDegrees(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "singleLockedPositionOffsetDegrees") == 0) { @@ -1401,10 +1912,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= -180 && keyvalue <= 180) { - if(_nukiAdvancedConfig.singleLockedPositionOffsetDegrees == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setSingleLockedPositionOffsetDegrees(keyvalue); + if(_nukiAdvancedConfig.singleLockedPositionOffsetDegrees == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setSingleLockedPositionOffsetDegrees(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "unlockedToLockedTransitionOffsetDegrees") == 0) { @@ -1412,10 +1932,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= -180 && keyvalue <= 180) { - if(_nukiAdvancedConfig.unlockedToLockedTransitionOffsetDegrees == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setUnlockedToLockedTransitionOffsetDegrees(keyvalue); + if(_nukiAdvancedConfig.unlockedToLockedTransitionOffsetDegrees == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setUnlockedToLockedTransitionOffsetDegrees(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "lockNgoTimeout") == 0) { @@ -1423,10 +1952,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 5 && keyvalue <= 60) { - if(_nukiAdvancedConfig.lockNgoTimeout == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setLockNgoTimeout(keyvalue); + if(_nukiAdvancedConfig.lockNgoTimeout == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setLockNgoTimeout(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "singleButtonPressAction") == 0) { @@ -1434,10 +1972,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if((int)sbpa != 0xff) { - if(_nukiAdvancedConfig.singleButtonPressAction == sbpa) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setSingleButtonPressAction(sbpa); + if(_nukiAdvancedConfig.singleButtonPressAction == sbpa) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setSingleButtonPressAction(sbpa); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "doubleButtonPressAction") == 0) { @@ -1445,10 +1992,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if((int)dbpa != 0xff) { - if(_nukiAdvancedConfig.doubleButtonPressAction == dbpa) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setDoubleButtonPressAction(dbpa); + if(_nukiAdvancedConfig.doubleButtonPressAction == dbpa) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setDoubleButtonPressAction(dbpa); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "detachedCylinder") == 0) { @@ -1456,10 +2012,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.detachedCylinder == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableDetachedCylinder((keyvalue > 0)); + if(_nukiAdvancedConfig.detachedCylinder == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableDetachedCylinder((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "batteryType") == 0) { @@ -1467,10 +2032,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if((int)battype != 0xff) { - if(_nukiAdvancedConfig.batteryType == battype) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setBatteryType(battype); + if(_nukiAdvancedConfig.batteryType == battype) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setBatteryType(battype); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "automaticBatteryTypeDetection") == 0) { @@ -1478,10 +2052,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.automaticBatteryTypeDetection == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableAutoBatteryTypeDetection((keyvalue > 0)); + if(_nukiAdvancedConfig.automaticBatteryTypeDetection == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableAutoBatteryTypeDetection((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "unlatchDuration") == 0) { @@ -1489,10 +2072,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 1 && keyvalue <= 30) { - if(_nukiAdvancedConfig.unlatchDuration == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setUnlatchDuration(keyvalue); + if(_nukiAdvancedConfig.unlatchDuration == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setUnlatchDuration(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "autoLockTimeOut") == 0) { @@ -1500,10 +2092,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue >= 30 && keyvalue <= 1800) { - if(_nukiAdvancedConfig.autoLockTimeOut == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setAutoLockTimeOut(keyvalue); + if(_nukiAdvancedConfig.autoLockTimeOut == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setAutoLockTimeOut(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "autoUnLockDisabled") == 0) { @@ -1511,10 +2112,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.autoUnLockDisabled == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.disableAutoUnlock((keyvalue > 0)); + if(_nukiAdvancedConfig.autoUnLockDisabled == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.disableAutoUnlock((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "nightModeEnabled") == 0) { @@ -1522,10 +2132,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.nightModeEnabled == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableNightMode((keyvalue > 0)); + if(_nukiAdvancedConfig.nightModeEnabled == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableNightMode((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "nightModeStartTime") == 0) { @@ -1535,10 +2154,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) keyvalue[1] = (uint8_t)keystr.substring(3, 5).toInt(); if(keyvalue[0] >= 0 && keyvalue[0] <= 23 && keyvalue[1] >= 0 && keyvalue[1] <= 59) { - if(_nukiAdvancedConfig.nightModeStartTime == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setNightModeStartTime(keyvalue); + if(_nukiAdvancedConfig.nightModeStartTime == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setNightModeStartTime(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "nightModeEndTime") == 0) { @@ -1548,10 +2176,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) keyvalue[1] = (uint8_t)keystr.substring(3, 5).toInt(); if(keyvalue[0] >= 0 && keyvalue[0] <= 23 && keyvalue[1] >= 0 && keyvalue[1] <= 59) { - if(_nukiAdvancedConfig.nightModeEndTime == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.setNightModeEndTime(keyvalue); + if(_nukiAdvancedConfig.nightModeEndTime == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.setNightModeEndTime(keyvalue); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "nightModeAutoLockEnabled") == 0) { @@ -1559,10 +2196,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.nightModeAutoLockEnabled == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableNightModeAutoLock((keyvalue > 0)); + if(_nukiAdvancedConfig.nightModeAutoLockEnabled == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableNightModeAutoLock((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "nightModeAutoUnlockDisabled") == 0) { @@ -1570,10 +2216,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.nightModeAutoUnlockDisabled == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.disableNightModeAutoUnlock((keyvalue > 0)); + if(_nukiAdvancedConfig.nightModeAutoUnlockDisabled == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.disableNightModeAutoUnlock((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "nightModeImmediateLockOnStart") == 0) { @@ -1581,10 +2236,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.nightModeImmediateLockOnStart == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableNightModeImmediateLockOnStart((keyvalue > 0)); + if(_nukiAdvancedConfig.nightModeImmediateLockOnStart == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableNightModeImmediateLockOnStart((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "autoLockEnabled") == 0) { @@ -1592,10 +2256,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.autoLockEnabled == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableAutoLock((keyvalue > 0)); + if(_nukiAdvancedConfig.autoLockEnabled == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableAutoLock((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "immediateAutoLockEnabled") == 0) { @@ -1603,10 +2276,19 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.immediateAutoLockEnabled == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableImmediateAutoLock((keyvalue > 0)); + if(_nukiAdvancedConfig.immediateAutoLockEnabled == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableImmediateAutoLock((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } else if(strcmp(advancedKeys[j], "autoUpdateEnabled") == 0) { @@ -1614,32 +2296,58 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(keyvalue == 0 || keyvalue == 1) { - if(_nukiAdvancedConfig.autoUpdateEnabled == keyvalue) jsonResult[advancedKeys[j]] = "unchanged"; - else cmdResult = _nukiLock.enableAutoUpdate((keyvalue > 0)); + if(_nukiAdvancedConfig.autoUpdateEnabled == keyvalue) + { + jsonResult[advancedKeys[j]] = "unchanged"; + } + else + { + cmdResult = _nukiLock.enableAutoUpdate((keyvalue > 0)); + } + } + else + { + jsonResult[advancedKeys[j]] = "invalidValue"; } - else jsonResult[advancedKeys[j]] = "invalidValue"; } - if(cmdResult != Nuki::CmdResult::Success) { + if(cmdResult != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } - if(cmdResult == Nuki::CmdResult::Success) advancedUpdated = true; + if(cmdResult == Nuki::CmdResult::Success) + { + advancedUpdated = true; + } - if(!jsonResult[advancedKeys[j]]) { + if(!jsonResult[advancedKeys[j]]) + { char resultStr[15] = {0}; NukiLock::cmdResultToString(cmdResult, resultStr); jsonResult[advancedKeys[j]] = resultStr; } } - else jsonResult[advancedKeys[j]] = "accessDenied"; + else + { + jsonResult[advancedKeys[j]] = "accessDenied"; + } } } - if(basicUpdated || advancedUpdated) jsonResult["general"] = "success"; - else jsonResult["general"] = "noChange"; + if(basicUpdated || advancedUpdated) + { + jsonResult["general"] = "success"; + } + else + { + jsonResult["general"] = "noChange"; + } _nextConfigUpdateTs = (esp_timer_get_time() / 1000) + 300; @@ -1679,57 +2387,75 @@ void NukiWrapper::onGpioActionReceived(const GpioAction &action, const int &pin) { switch(action) { - case GpioAction::Lock: - if(!_nukiOfficial->getOffConnected()) nukiInst->lock(); - else - { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); - _offCommand = NukiLock::LockAction::Lock; - _network->publishOffAction(2); - } - break; - case GpioAction::Unlock: - if(!_nukiOfficial->getOffConnected()) nukiInst->unlock(); - else - { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); - _offCommand = NukiLock::LockAction::Unlock; - _network->publishOffAction(1); - } - break; - case GpioAction::Unlatch: - if(!_nukiOfficial->getOffConnected()) nukiInst->unlatch(); - else - { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); - _offCommand = NukiLock::LockAction::Unlatch; - _network->publishOffAction(3); - } - break; - case GpioAction::LockNgo: - if(!_nukiOfficial->getOffConnected()) nukiInst->lockngo(); - else - { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); - _offCommand = NukiLock::LockAction::LockNgo; - _network->publishOffAction(4); - } - break; - case GpioAction::LockNgoUnlatch: - if(!_nukiOfficial->getOffConnected()) nukiInst->lockngounlatch(); - else - { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); - _offCommand = NukiLock::LockAction::LockNgoUnlatch; - _network->publishOffAction(5); - } - break; + case GpioAction::Lock: + if(!_nukiOfficial->getOffConnected()) + { + nukiInst->lock(); + } + else + { + _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _offCommand = NukiLock::LockAction::Lock; + _network->publishOffAction(2); + } + break; + case GpioAction::Unlock: + if(!_nukiOfficial->getOffConnected()) + { + nukiInst->unlock(); + } + else + { + _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _offCommand = NukiLock::LockAction::Unlock; + _network->publishOffAction(1); + } + break; + case GpioAction::Unlatch: + if(!_nukiOfficial->getOffConnected()) + { + nukiInst->unlatch(); + } + else + { + _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _offCommand = NukiLock::LockAction::Unlatch; + _network->publishOffAction(3); + } + break; + case GpioAction::LockNgo: + if(!_nukiOfficial->getOffConnected()) + { + nukiInst->lockngo(); + } + else + { + _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _offCommand = NukiLock::LockAction::LockNgo; + _network->publishOffAction(4); + } + break; + case GpioAction::LockNgoUnlatch: + if(!_nukiOfficial->getOffConnected()) + { + nukiInst->lockngounlatch(); + } + else + { + _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _offCommand = NukiLock::LockAction::LockNgoUnlatch; + _network->publishOffAction(5); + } + break; } } void NukiWrapper::onKeypadCommandReceived(const char *command, const uint &id, const String &name, const String &code, const int& enabled) { - if(_disableNonJSON) return; + if(_disableNonJSON) + { + return; + } if(!_preferences->getBool(preference_keypad_control_enabled)) { @@ -1844,10 +2570,14 @@ void NukiWrapper::onKeypadCommandReceived(const char *command, const uint &id, c return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } if((int)result != -1) @@ -1913,21 +2643,57 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json["code"].is()) code = json["code"].as(); - else code = 12; + if(json["code"].is()) + { + code = json["code"].as(); + } + else + { + code = 12; + } - if(json["enabled"].is()) enabled = json["enabled"].as(); - else enabled = 2; + if(json["enabled"].is()) + { + enabled = json["enabled"].as(); + } + else + { + enabled = 2; + } - if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); - else timeLimited = 2; + if(json["timeLimited"].is()) + { + timeLimited = json["timeLimited"].as(); + } + else + { + timeLimited = 2; + } - if(json["name"].is()) name = json["name"].as(); - if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); - if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); - if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); - if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); - if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["name"].is()) + { + name = json["name"].as(); + } + if(json["allowedFrom"].is()) + { + allowedFrom = json["allowedFrom"].as(); + } + if(json["allowedUntil"].is()) + { + allowedUntil = json["allowedUntil"].as(); + } + if(json["allowedWeekdays"].is()) + { + allowedWeekdays = json["allowedWeekdays"].as(); + } + if(json["allowedFromTime"].is()) + { + allowedFromTime = json["allowedFromTime"].as(); + } + if(json["allowedUntilTime"].is()) + { + allowedUntilTime = json["allowedUntilTime"].as(); + } if(action) { @@ -1943,7 +2709,8 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) while(retryCount < _nrOfRetries + 1) { - if(strcmp(action, "delete") == 0) { + if(strcmp(action, "delete") == 0) + { if(idExists) { result = _nukiLock.deleteKeypadEntry(codeId); @@ -2080,13 +2847,34 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) } } - if(allowedWeekdays.indexOf("mon") >= 0) allowedWeekdaysInt += 64; - if(allowedWeekdays.indexOf("tue") >= 0) allowedWeekdaysInt += 32; - if(allowedWeekdays.indexOf("wed") >= 0) allowedWeekdaysInt += 16; - if(allowedWeekdays.indexOf("thu") >= 0) allowedWeekdaysInt += 8; - if(allowedWeekdays.indexOf("fri") >= 0) allowedWeekdaysInt += 4; - if(allowedWeekdays.indexOf("sat") >= 0) allowedWeekdaysInt += 2; - if(allowedWeekdays.indexOf("sun") >= 0) allowedWeekdaysInt += 1; + if(allowedWeekdays.indexOf("mon") >= 0) + { + allowedWeekdaysInt += 64; + } + if(allowedWeekdays.indexOf("tue") >= 0) + { + allowedWeekdaysInt += 32; + } + if(allowedWeekdays.indexOf("wed") >= 0) + { + allowedWeekdaysInt += 16; + } + if(allowedWeekdays.indexOf("thu") >= 0) + { + allowedWeekdaysInt += 8; + } + if(allowedWeekdays.indexOf("fri") >= 0) + { + allowedWeekdaysInt += 4; + } + if(allowedWeekdays.indexOf("sat") >= 0) + { + allowedWeekdaysInt += 2; + } + if(allowedWeekdays.indexOf("sun") >= 0) + { + allowedWeekdaysInt += 1; + } } if(strcmp(action, "add") == 0) @@ -2161,17 +2949,32 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) for(const auto& entry : entries) { - if (codeId != entry.codeId) continue; - else foundExisting = true; + if (codeId != entry.codeId) + { + continue; + } + else + { + foundExisting = true; + } if(name.length() < 1) { memset(oldName, 0, sizeof(oldName)); memcpy(oldName, entry.name, sizeof(entry.name)); } - if(code == 12) code = entry.code; - if(enabled == 2) enabled = entry.enabled; - if(timeLimited == 2) timeLimited = entry.timeLimited; + if(code == 12) + { + code = entry.code; + } + if(enabled == 2) + { + enabled = entry.enabled; + } + if(timeLimited == 2) + { + timeLimited = entry.timeLimited; + } if(allowedFrom.length() < 1) { allowedFrom = "old"; @@ -2192,7 +2995,10 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) allowedUntilAr[4] = entry.allowedUntilMin; allowedUntilAr[5] = entry.allowedUntilSec; } - if(allowedWeekdays.length() < 1) allowedWeekdaysInt = entry.allowedWeekdays; + if(allowedWeekdays.length() < 1) + { + allowedWeekdaysInt = entry.allowedWeekdays; + } if(allowedFromTime.length() < 1) { allowedFromTime = "old"; @@ -2290,10 +3096,14 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } updateKeypad(false); @@ -2350,12 +3160,27 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) String lockAction; NukiLock::LockAction timeControlLockAction; - if(json["enabled"].is()) enabled = json["enabled"].as(); - else enabled = 2; + if(json["enabled"].is()) + { + enabled = json["enabled"].as(); + } + else + { + enabled = 2; + } - if(json["weekdays"].is()) weekdays = json["weekdays"].as(); - if(json["time"].is()) time = json["time"].as(); - if(json["lockAction"].is()) lockAction = json["lockAction"].as(); + if(json["weekdays"].is()) + { + weekdays = json["weekdays"].as(); + } + if(json["time"].is()) + { + time = json["time"].as(); + } + if(json["lockAction"].is()) + { + lockAction = json["lockAction"].as(); + } if(lockAction.length() > 0) { @@ -2382,7 +3207,8 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) while(retryCount < _nrOfRetries + 1) { - if(strcmp(action, "delete") == 0) { + if(strcmp(action, "delete") == 0) + { if(idExists) { result = _nukiLock.removeTimeControlEntry(entryId); @@ -2422,13 +3248,34 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) } } - if(weekdays.indexOf("mon") >= 0) weekdaysInt += 64; - if(weekdays.indexOf("tue") >= 0) weekdaysInt += 32; - if(weekdays.indexOf("wed") >= 0) weekdaysInt += 16; - if(weekdays.indexOf("thu") >= 0) weekdaysInt += 8; - if(weekdays.indexOf("fri") >= 0) weekdaysInt += 4; - if(weekdays.indexOf("sat") >= 0) weekdaysInt += 2; - if(weekdays.indexOf("sun") >= 0) weekdaysInt += 1; + if(weekdays.indexOf("mon") >= 0) + { + weekdaysInt += 64; + } + if(weekdays.indexOf("tue") >= 0) + { + weekdaysInt += 32; + } + if(weekdays.indexOf("wed") >= 0) + { + weekdaysInt += 16; + } + if(weekdays.indexOf("thu") >= 0) + { + weekdaysInt += 8; + } + if(weekdays.indexOf("fri") >= 0) + { + weekdaysInt += 4; + } + if(weekdays.indexOf("sat") >= 0) + { + weekdaysInt += 2; + } + if(weekdays.indexOf("sun") >= 0) + { + weekdaysInt += 1; + } if(strcmp(action, "add") == 0) { @@ -2467,18 +3314,33 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) for(const auto& entry : timeControlEntries) { - if (entryId != entry.entryId) continue; - else foundExisting = true; + if (entryId != entry.entryId) + { + continue; + } + else + { + foundExisting = true; + } - if(enabled == 2) enabled = entry.enabled; - if(weekdays.length() < 1) weekdaysInt = entry.weekdays; + if(enabled == 2) + { + enabled = entry.enabled; + } + if(weekdays.length() < 1) + { + weekdaysInt = entry.weekdays; + } if(time.length() < 1) { time = "old"; timeAr[0] = entry.timeHour; timeAr[1] = entry.timeMin; } - if(lockAction.length() < 1) timeControlLockAction = entry.lockAction; + if(lockAction.length() < 1) + { + timeControlLockAction = entry.lockAction; + } } if(!foundExisting) @@ -2518,10 +3380,14 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } if((int)result != -1) @@ -2586,22 +3452,58 @@ void NukiWrapper::onAuthCommandReceived(const char *value) String allowedFromTime; String allowedUntilTime; - if(json["remoteAllowed"].is()) remoteAllowed = json["remoteAllowed"].as(); - else remoteAllowed = 2; + if(json["remoteAllowed"].is()) + { + remoteAllowed = json["remoteAllowed"].as(); + } + else + { + remoteAllowed = 2; + } - if(json["enabled"].is()) enabled = json["enabled"].as(); - else enabled = 2; + if(json["enabled"].is()) + { + enabled = json["enabled"].as(); + } + else + { + enabled = 2; + } - if(json["timeLimited"].is()) timeLimited = json["timeLimited"].as(); - else timeLimited = 2; + if(json["timeLimited"].is()) + { + timeLimited = json["timeLimited"].as(); + } + else + { + timeLimited = 2; + } - if(json["name"].is()) name = json["name"].as(); + if(json["name"].is()) + { + name = json["name"].as(); + } //if(json["sharedKey"].is()) sharedKey = json["sharedKey"].as(); - if(json["allowedFrom"].is()) allowedFrom = json["allowedFrom"].as(); - if(json["allowedUntil"].is()) allowedUntil = json["allowedUntil"].as(); - if(json["allowedWeekdays"].is()) allowedWeekdays = json["allowedWeekdays"].as(); - if(json["allowedFromTime"].is()) allowedFromTime = json["allowedFromTime"].as(); - if(json["allowedUntilTime"].is()) allowedUntilTime = json["allowedUntilTime"].as(); + if(json["allowedFrom"].is()) + { + allowedFrom = json["allowedFrom"].as(); + } + if(json["allowedUntil"].is()) + { + allowedUntil = json["allowedUntil"].as(); + } + if(json["allowedWeekdays"].is()) + { + allowedWeekdays = json["allowedWeekdays"].as(); + } + if(json["allowedFromTime"].is()) + { + allowedFromTime = json["allowedFromTime"].as(); + } + if(json["allowedUntilTime"].is()) + { + allowedUntilTime = json["allowedUntilTime"].as(); + } if(action) { @@ -2617,7 +3519,8 @@ void NukiWrapper::onAuthCommandReceived(const char *value) while(retryCount < _nrOfRetries) { - if(strcmp(action, "delete") == 0) { + if(strcmp(action, "delete") == 0) + { if(idExists) { result = _nukiLock.deleteAuthorizationEntry(authId); @@ -2753,13 +3656,34 @@ void NukiWrapper::onAuthCommandReceived(const char *value) } } - if(allowedWeekdays.indexOf("mon") >= 0) allowedWeekdaysInt += 64; - if(allowedWeekdays.indexOf("tue") >= 0) allowedWeekdaysInt += 32; - if(allowedWeekdays.indexOf("wed") >= 0) allowedWeekdaysInt += 16; - if(allowedWeekdays.indexOf("thu") >= 0) allowedWeekdaysInt += 8; - if(allowedWeekdays.indexOf("fri") >= 0) allowedWeekdaysInt += 4; - if(allowedWeekdays.indexOf("sat") >= 0) allowedWeekdaysInt += 2; - if(allowedWeekdays.indexOf("sun") >= 0) allowedWeekdaysInt += 1; + if(allowedWeekdays.indexOf("mon") >= 0) + { + allowedWeekdaysInt += 64; + } + if(allowedWeekdays.indexOf("tue") >= 0) + { + allowedWeekdaysInt += 32; + } + if(allowedWeekdays.indexOf("wed") >= 0) + { + allowedWeekdaysInt += 16; + } + if(allowedWeekdays.indexOf("thu") >= 0) + { + allowedWeekdaysInt += 8; + } + if(allowedWeekdays.indexOf("fri") >= 0) + { + allowedWeekdaysInt += 4; + } + if(allowedWeekdays.indexOf("sat") >= 0) + { + allowedWeekdaysInt += 2; + } + if(allowedWeekdays.indexOf("sun") >= 0) + { + allowedWeekdaysInt += 1; + } } if(strcmp(action, "add") == 0) @@ -2849,17 +3773,32 @@ void NukiWrapper::onAuthCommandReceived(const char *value) for(const auto& entry : entries) { - if (authId != entry.authId) continue; - else foundExisting = true; + if (authId != entry.authId) + { + continue; + } + else + { + foundExisting = true; + } if(name.length() < 1) { memset(oldName, 0, sizeof(oldName)); memcpy(oldName, entry.name, sizeof(entry.name)); } - if(remoteAllowed == 2) remoteAllowed = entry.remoteAllowed; - if(enabled == 2) enabled = entry.enabled; - if(timeLimited == 2) timeLimited = entry.timeLimited; + if(remoteAllowed == 2) + { + remoteAllowed = entry.remoteAllowed; + } + if(enabled == 2) + { + enabled = entry.enabled; + } + if(timeLimited == 2) + { + timeLimited = entry.timeLimited; + } if(allowedFrom.length() < 1) { allowedFrom = "old"; @@ -2880,7 +3819,10 @@ void NukiWrapper::onAuthCommandReceived(const char *value) allowedUntilAr[4] = entry.allowedUntilMinute; allowedUntilAr[5] = entry.allowedUntilSecond; } - if(allowedWeekdays.length() < 1) allowedWeekdaysInt = entry.allowedWeekdays; + if(allowedWeekdays.length() < 1) + { + allowedWeekdaysInt = entry.allowedWeekdays; + } if(allowedFromTime.length() < 1) { allowedFromTime = "old"; @@ -2979,10 +3921,14 @@ void NukiWrapper::onAuthCommandReceived(const char *value) return; } - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; } - else break; + else + { + break; + } } updateAuth(false); @@ -3054,12 +4000,16 @@ void NukiWrapper::readConfig() Log->print(F("Lock config result: ")); Log->println(resultStr); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; Log->println("Failed to retrieve lock config, retrying in 1s"); delay(1000); } - else break; + else + { + break; + } } } @@ -3070,7 +4020,7 @@ void NukiWrapper::readAdvancedConfig() while(retryCount < _nrOfRetries + 1) { - result = _nukiLock.requestAdvancedConfig(&_nukiAdvancedConfig); + result = _nukiLock.requestAdvancedConfig(&_nukiAdvancedConfig); _nukiAdvancedConfigValid = result == Nuki::CmdResult::Success; char resultStr[20]; @@ -3078,19 +4028,29 @@ void NukiWrapper::readAdvancedConfig() Log->print(F("Lock advanced config result: ")); Log->println(resultStr); - if(result != Nuki::CmdResult::Success) { + if(result != Nuki::CmdResult::Success) + { ++retryCount; Log->println("Failed to retrieve lock advanced config, retrying in 1s"); delay(1000); } - else break; + else + { + break; + } } } void NukiWrapper::setupHASS() { - if(!_nukiConfigValid) return; - if(_preferences->getUInt(preference_nuki_id_lock, 0) != _nukiConfig.nukiId) return; + if(!_nukiConfigValid) + { + return; + } + if(_preferences->getUInt(preference_nuki_id_lock, 0) != _nukiConfig.nukiId) + { + return; + } String baseTopic = _preferences->getString(preference_mqtt_lock_path); char uidString[20]; @@ -3155,15 +4115,15 @@ void NukiWrapper::updateGpioOutputs() { switch(entry.role) { - case PinRole::OutputHighLocked: - _gpio->setPinOutput(entry.pin, lockState == LockState::Locked || lockState == LockState::Locking ? HIGH : LOW); - break; - case PinRole::OutputHighUnlocked: - _gpio->setPinOutput(entry.pin, lockState == LockState::Locked || lockState == LockState::Locking ? LOW : HIGH); - break; - case PinRole::OutputHighMotorBlocked: - _gpio->setPinOutput(entry.pin, lockState == LockState::MotorBlocked ? HIGH : LOW); - break; + case PinRole::OutputHighLocked: + _gpio->setPinOutput(entry.pin, lockState == LockState::Locked || lockState == LockState::Locking ? HIGH : LOW); + break; + case PinRole::OutputHighUnlocked: + _gpio->setPinOutput(entry.pin, lockState == LockState::Locked || lockState == LockState::Locking ? LOW : HIGH); + break; + case PinRole::OutputHighMotorBlocked: + _gpio->setPinOutput(entry.pin, lockState == LockState::MotorBlocked ? HIGH : LOW); + break; } } } diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 7aed68c..d4cf0d0 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -22,21 +22,21 @@ extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_ #include "ArduinoJson.h" WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) -: _nuki(nuki), - _nukiOpener(nukiOpener), - _network(network), - _gpio(gpio), - _preferences(preferences), - _allowRestartToPortal(allowRestartToPortal), - _partitionType(partitionType), - _psychicServer(psychicServer) + : _nuki(nuki), + _nukiOpener(nukiOpener), + _network(network), + _gpio(gpio), + _preferences(preferences), + _allowRestartToPortal(allowRestartToPortal), + _partitionType(partitionType), + _psychicServer(psychicServer) #else WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) -: _network(network), - _preferences(preferences), - _allowRestartToPortal(allowRestartToPortal), - _partitionType(partitionType), - _psychicServer(psychicServer) + : _network(network), + _preferences(preferences), + _allowRestartToPortal(allowRestartToPortal), + _partitionType(partitionType), + _psychicServer(psychicServer) #endif { _hostname = _preferences->getString(preference_hostname, ""); @@ -58,7 +58,7 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool _confirmCode = generateConfirmCode(); - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER _pinsConfigured = true; if(_nuki != nullptr && !_nuki->isPinSet()) @@ -71,45 +71,64 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool } _brokerConfigured = _preferences->getString(preference_mqtt_broker).length() > 0 && _preferences->getInt(preference_mqtt_broker_port) > 0; - #endif +#endif } void WebCfgServer::initialize() { - _psychicServer->on("/", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } if(!_network->isApOpen()) { - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER return buildHtml(request); - #else +#else return buildOtaHtml(request); - #endif +#endif } - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 else { return buildWifiConnectHtml(request); } - #endif +#endif }); - _psychicServer->on("/style.css", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/style.css", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return sendCss(request); }); - _psychicServer->on("/favicon.ico", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/favicon.ico", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return sendFavicon(request); }); - _psychicServer->on("/reboot", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - + _psychicServer->on("/reboot", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } + String value = ""; if(request->hasParam("CONFIRMTOKEN")) { const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); - if(p->value() != "") value = p->value(); + if(p->value() != "") + { + value = p->value(); + } } else { @@ -128,13 +147,21 @@ void WebCfgServer::initialize() if(_network->isApOpen()) { - #ifndef CONFIG_IDF_TARGET_ESP32H2 - _psychicServer->on("/ssidlist", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); +#ifndef CONFIG_IDF_TARGET_ESP32H2 + _psychicServer->on("/ssidlist", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildSSIDListHtml(request); }); - _psychicServer->on("/savewifi", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/savewifi", HTTP_POST, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } String message = ""; bool connected = processWiFi(request, message); esp_err_t res = buildConfirmHtml(request, message, 10, true); @@ -147,64 +174,116 @@ void WebCfgServer::initialize() } return res; }); - #endif +#endif } else { - #ifndef NUKI_HUB_UPDATER - _psychicServer->on("/import", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); +#ifndef NUKI_HUB_UPDATER + _psychicServer->on("/import", HTTP_POST, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } String message = ""; bool restart = processImport(request, message); return buildConfirmHtml(request, message, 3, true); }); - _psychicServer->on("/export", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/export", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return sendSettings(request); }); - _psychicServer->on("/impexpcfg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/impexpcfg", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildImportExportHtml(request); }); - _psychicServer->on("/status", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/status", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildStatusHtml(request); }); - _psychicServer->on("/acclvl", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/acclvl", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildAccLvlHtml(request); }); - _psychicServer->on("/custntw", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/custntw", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildCustomNetworkConfigHtml(request); }); - _psychicServer->on("/advanced", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/advanced", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildAdvancedConfigHtml(request); }); - _psychicServer->on("/cred", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/cred", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildCredHtml(request); }); - _psychicServer->on("/mqttconfig", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/mqttconfig", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildMqttConfigHtml(request); }); - _psychicServer->on("/nukicfg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/nukicfg", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildNukiConfigHtml(request); }); - _psychicServer->on("/gpiocfg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/gpiocfg", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildGpioConfigHtml(request); }); - #ifndef CONFIG_IDF_TARGET_ESP32H2 - _psychicServer->on("/wifi", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); +#ifndef CONFIG_IDF_TARGET_ESP32H2 + _psychicServer->on("/wifi", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildConfigureWifiHtml(request); }); - _psychicServer->on("/wifimanager", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/wifimanager", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } if(_allowRestartToPortal) { esp_err_t res = buildConfirmHtml(request, "Restarting. Connect to ESP access point (\"NukiHub\" with password \"NukiHubESP32\") to reconfigure Wi-Fi.", 0); @@ -214,41 +293,73 @@ void WebCfgServer::initialize() } return(ESP_OK); }); - #endif - _psychicServer->on("/unpairlock", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); +#endif + _psychicServer->on("/unpairlock", HTTP_POST, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return processUnpair(request, false); }); - _psychicServer->on("/unpairopener", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/unpairopener", HTTP_POST, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return processUnpair(request, true); }); - _psychicServer->on("/factoryreset", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/factoryreset", HTTP_POST, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return processFactoryReset(request); }); - _psychicServer->on("/infopg", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/infopg", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildInfoHtml(request); }); - _psychicServer->on("/debugon", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/debugon", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } _preferences->putBool(preference_publish_debug_info, true); return buildConfirmHtml(request, "Debug On", 3, true); }); - _psychicServer->on("/debugoff", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/debugoff", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } _preferences->putBool(preference_publish_debug_info, false); return buildConfirmHtml(request, "Debug Off", 3, true); }); - _psychicServer->on("/savecfg", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/savecfg", HTTP_POST, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } String message = ""; bool restart = processArgs(request, message); return buildConfirmHtml(request, message, 3, true); }); - _psychicServer->on("/savegpiocfg", HTTP_POST, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/savegpiocfg", HTTP_POST, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } processGpioArgs(request); esp_err_t res = buildConfirmHtml(request, "Saving GPIO configuration. Restarting.", 3, true); Log->println(F("Restarting")); @@ -256,22 +367,37 @@ void WebCfgServer::initialize() restartEsp(RestartReason::GpioConfigurationUpdated); return res; }); - #endif - _psychicServer->on("/ota", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); +#endif + _psychicServer->on("/ota", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildOtaHtml(request); }); - _psychicServer->on("/otadebug", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/otadebug", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } return buildOtaHtml(request, true); }); - _psychicServer->on("/reboottoota", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + _psychicServer->on("/reboottoota", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } String value = ""; if(request->hasParam("CONFIRMTOKEN")) { const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); - if(p->value() != "") value = p->value(); + if(p->value() != "") + { + value = p->value(); + } } else { @@ -288,25 +414,36 @@ void WebCfgServer::initialize() restartEsp(RestartReason::OTAReboot); return res; }); - _psychicServer->on("/autoupdate", HTTP_GET, [&](PsychicRequest *request){ - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - #ifndef NUKI_HUB_UPDATER + _psychicServer->on("/autoupdate", HTTP_GET, [&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } +#ifndef NUKI_HUB_UPDATER return processUpdate(request); - #else +#else return request->redirect("/"); - #endif +#endif }); PsychicUploadHandler *updateHandler = new PsychicUploadHandler(); updateHandler->onUpload([&](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final) - { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); - return handleOtaUpload(request, filename, index, data, len, final); - } - ); + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } + return handleOtaUpload(request, filename, index, data, len, final); + } + ); - updateHandler->onRequest([&](PsychicRequest *request) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + updateHandler->onRequest([&](PsychicRequest *request) + { + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) if(!request->authenticate(_credUser, _credPassword)) + { + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + } String result; if (!Update.hasError()) @@ -318,7 +455,8 @@ void WebCfgServer::initialize() restartEsp(RestartReason::OTACompleted); return res; } - else { + else + { result = " Update.errorString() " + String(Update.errorString()); Log->print("ERROR : error "); Log->println(result.c_str()); @@ -464,7 +602,10 @@ bool WebCfgServer::processWiFi(PsychicRequest *request, String& message) if(index < params -1) { const PsychicWebParameter* next = request->getParam(index+1); - if(key == next->name()) continue; + if(key == next->name()) + { + continue; + } } if(key == "WIFISSID") @@ -532,8 +673,8 @@ bool WebCfgServer::processWiFi(PsychicRequest *request, String& message) int loop = 0; while(!_network->isConnected() && loop < 150) { - delay(100); - loop++; + delay(100); + loop++; } if (!_network->isConnected()) @@ -578,10 +719,16 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) if(request->hasParam("errored")) { const PsychicWebParameter* p = request->getParam("errored"); - if(p->value() != "") errored = true; + if(p->value() != "") + { + errored = true; + } } - if(errored) response.print("
    Over-the-air update errored. Please check the logs for more info

    "); + if(errored) + { + response.print("
    Over-the-air update errored. Please check the logs for more info

    "); + } if(_partitionType == 0) { @@ -592,12 +739,13 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) return response.endSend(); } - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER bool manifestSuccess = false; JsonDocument doc; NetworkClientSecure *clientOTAUpdate = new NetworkClientSecure; - if (clientOTAUpdate) { + if (clientOTAUpdate) + { clientOTAUpdate->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start); { HTTPClient httpsOTAClient; @@ -605,13 +753,17 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) httpsOTAClient.setTimeout(2500); httpsOTAClient.useHTTP10(true); - if (httpsOTAClient.begin(*clientOTAUpdate, GITHUB_OTA_MANIFEST_URL)) { + if (httpsOTAClient.begin(*clientOTAUpdate, GITHUB_OTA_MANIFEST_URL)) + { int httpResponseCodeOTA = httpsOTAClient.GET(); if (httpResponseCodeOTA == HTTP_CODE_OK || httpResponseCodeOTA == HTTP_CODE_MOVED_PERMANENTLY) { DeserializationError jsonError = deserializeJson(doc, httpsOTAClient.getStream()); - if (!jsonError) { manifestSuccess = true; } + if (!jsonError) + { + manifestSuccess = true; + } } httpsOTAClient.end(); } @@ -626,14 +778,20 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) String release_type; - if(debug) release_type = "debug"; - else release_type = "release"; + if(debug) + { + release_type = "debug"; + } + else + { + release_type = "release"; + } - #ifndef DEBUG_NUKIHUB +#ifndef DEBUG_NUKIHUB String build_type = "release"; - #else +#else String build_type = "debug"; - #endif +#endif response.print("
    "); response.print("

    "); response.print("

    "); @@ -686,14 +844,29 @@ esp_err_t WebCfgServer::buildOtaHtml(PsychicRequest *request, bool debug) String currentVersion = NUKI_HUB_VERSION; const char* latestVersion; - if(atof(doc["release"]["version"]) >= atof(currentVersion.c_str())) latestVersion = doc["release"]["fullversion"]; - else if(currentVersion.indexOf("beta") > 0) latestVersion = doc["beta"]["fullversion"]; - else if(currentVersion.indexOf("master") > 0) latestVersion = doc["master"]["fullversion"]; - else latestVersion = doc["release"]["fullversion"]; + if(atof(doc["release"]["version"]) >= atof(currentVersion.c_str())) + { + latestVersion = doc["release"]["fullversion"]; + } + else if(currentVersion.indexOf("beta") > 0) + { + latestVersion = doc["beta"]["fullversion"]; + } + else if(currentVersion.indexOf("master") > 0) + { + latestVersion = doc["master"]["fullversion"]; + } + else + { + latestVersion = doc["release"]["fullversion"]; + } - if(strcmp(latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) _preferences->putString(preference_latest_version, latestVersion); + if(strcmp(latestVersion, _preferences->getString(preference_latest_version).c_str()) != 0) + { + _preferences->putString(preference_latest_version, latestVersion); + } } - #endif +#endif response.print("
    "); if(_partitionType == 1) @@ -777,7 +950,10 @@ void WebCfgServer::buildHtmlHeader(PsychicStreamResponse *response, String addit { response->print(""); response->print(""); - if(strcmp(additionalHeader.c_str(), "") != 0) response->print(additionalHeader); + if(strcmp(additionalHeader.c_str(), "") != 0) + { + response->print(additionalHeader); + } response->print(""); response->print("Nuki Hub"); } @@ -798,13 +974,17 @@ void WebCfgServer::waitAndProcess(const bool blocking, const uint32_t duration) } } -void WebCfgServer::printProgress(size_t prg, size_t sz) { - Log->printf("Progress: %d%%\n", (prg*100)/_otaContentLen); +void WebCfgServer::printProgress(size_t prg, size_t sz) +{ + Log->printf("Progress: %d%%\n", (prg*100)/_otaContentLen); } esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final) { - if(!request->url().endsWith("/uploadota")) return(ESP_FAIL); + if(!request->url().endsWith("/uploadota")) + { + return(ESP_FAIL); + } if(filename == "") { @@ -812,8 +992,10 @@ esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& f return(ESP_FAIL); } - if (!Update.hasError()) { - if (!index){ + if (!Update.hasError()) + { + if (!index) + { Update.clearError(); Log->println("Starting manual OTA update"); @@ -831,14 +1013,15 @@ esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& f } _otaStartTs = esp_timer_get_time() / 1000; - esp_task_wdt_config_t twdt_config = { + esp_task_wdt_config_t twdt_config = + { .timeout_ms = 30000, .idle_core_mask = 0, .trigger_panic = false, }; esp_task_wdt_reconfigure(&twdt_config); - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER _network->disableAutoRestarts(); _network->disableMqtt(); if(_nuki != nullptr) @@ -849,11 +1032,12 @@ esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& f { _nukiOpener->disableWatchdog(); } - #endif +#endif Log->print("handleFileUpload Name: "); Log->println(filename); - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) + { if (!Update.hasError()) { Update.abort(); @@ -864,8 +1048,10 @@ esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& f } } - if ((len) && (!Update.hasError())) { - if (Update.write(data, len) != len) { + if ((len) && (!Update.hasError())) + { + if (Update.write(data, len) != len) + { if (!Update.hasError()) { Update.abort(); @@ -876,13 +1062,16 @@ esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& f } } - if ((final) && (!Update.hasError())) { - if (Update.end(true)) { + if ((final) && (!Update.hasError())) + { + if (Update.end(true)) + { Log->print("Update Success: "); Log->print(index+len); Log->println(" written"); } - else { + else + { if (!Update.hasError()) { Update.abort(); @@ -988,8 +1177,8 @@ void WebCfgServer::printInputField(PsychicStreamResponse *response, } if(strcmp(value, "") != 0) { - response->print(" value=\""); - response->print(value); + response->print(" value=\""); + response->print(value); } response->print("\" name=\""); response->print(token); @@ -1020,12 +1209,18 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) if(request->hasParam("redacted")) { const PsychicWebParameter* p = request->getParam("redacted"); - if(p->value() == "1") redacted = true; + if(p->value() == "1") + { + redacted = true; + } } if(request->hasParam("pairing")) { const PsychicWebParameter* p = request->getParam("pairing"); - if(p->value() == "1") pairing = true; + if(p->value() == "1") + { + pairing = true; + } } JsonDocument json; @@ -1040,47 +1235,68 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) for(const auto& key : keysPrefs) { - if(strcmp(key, preference_show_secrets) == 0) continue; - if(strcmp(key, preference_latest_version) == 0) continue; - if(strcmp(key, preference_device_id_lock) == 0) continue; - if(strcmp(key, preference_device_id_opener) == 0) continue; - if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end()) continue; - if(!_preferences->isKey(key)) json[key] = ""; - else if(std::find(boolPrefs.begin(), boolPrefs.end(), key) != boolPrefs.end()) json[key] = _preferences->getBool(key) ? "1" : "0"; + if(strcmp(key, preference_show_secrets) == 0) + { + continue; + } + if(strcmp(key, preference_latest_version) == 0) + { + continue; + } + if(strcmp(key, preference_device_id_lock) == 0) + { + continue; + } + if(strcmp(key, preference_device_id_opener) == 0) + { + continue; + } + if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end()) + { + continue; + } + if(!_preferences->isKey(key)) + { + json[key] = ""; + } + else if(std::find(boolPrefs.begin(), boolPrefs.end(), key) != boolPrefs.end()) + { + json[key] = _preferences->getBool(key) ? "1" : "0"; + } else { switch(_preferences->getType(key)) { - case PT_I8: - json[key] = String(_preferences->getChar(key)); - break; - case PT_I16: - json[key] = String(_preferences->getShort(key)); - break; - case PT_I32: - json[key] = String(_preferences->getInt(key)); - break; - case PT_I64: - json[key] = String(_preferences->getLong64(key)); - break; - case PT_U8: - json[key] = String(_preferences->getUChar(key)); - break; - case PT_U16: - json[key] = String(_preferences->getUShort(key)); - break; - case PT_U32: - json[key] = String(_preferences->getUInt(key)); - break; - case PT_U64: - json[key] = String(_preferences->getULong64(key)); - break; - case PT_STR: - json[key] = _preferences->getString(key); - break; - default: - json[key] = _preferences->getString(key); - break; + case PT_I8: + json[key] = String(_preferences->getChar(key)); + break; + case PT_I16: + json[key] = String(_preferences->getShort(key)); + break; + case PT_I32: + json[key] = String(_preferences->getInt(key)); + break; + case PT_I64: + json[key] = String(_preferences->getLong64(key)); + break; + case PT_U8: + json[key] = String(_preferences->getUChar(key)); + break; + case PT_U16: + json[key] = String(_preferences->getUShort(key)); + break; + case PT_U32: + json[key] = String(_preferences->getUInt(key)); + break; + case PT_U64: + json[key] = String(_preferences->getULong64(key)); + break; + case PT_STR: + json[key] = _preferences->getString(key); + break; + default: + json[key] = _preferences->getString(key); + break; } } } @@ -1102,21 +1318,24 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) nukiBlePref.end(); char text[255]; text[0] = '\0'; - for(int i = 0 ; i < 6 ; i++) { + for(int i = 0 ; i < 6 ; i++) + { size_t offset = strlen(text); sprintf(&(text[offset]), "%02x", currentBleAddress[i]); } json["bleAddressLock"] = text; memset(text, 0, sizeof(text)); text[0] = '\0'; - for(int i = 0 ; i < 32 ; i++) { + for(int i = 0 ; i < 32 ; i++) + { size_t offset = strlen(text); sprintf(&(text[offset]), "%02x", secretKeyK[i]); } json["secretKeyKLock"] = text; memset(text, 0, sizeof(text)); text[0] = '\0'; - for(int i = 0 ; i < 4 ; i++) { + for(int i = 0 ; i < 4 ; i++) + { size_t offset = strlen(text); sprintf(&(text[offset]), "%02x", authorizationId[i]); } @@ -1139,21 +1358,24 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) nukiBlePref.end(); char text[255]; text[0] = '\0'; - for(int i = 0 ; i < 6 ; i++) { + for(int i = 0 ; i < 6 ; i++) + { size_t offset = strlen(text); sprintf(&(text[offset]), "%02x", currentBleAddressOpn[i]); } json["bleAddressOpener"] = text; memset(text, 0, sizeof(text)); text[0] = '\0'; - for(int i = 0 ; i < 32 ; i++) { + for(int i = 0 ; i < 32 ; i++) + { size_t offset = strlen(text); sprintf(&(text[offset]), "%02x", secretKeyKOpn[i]); } json["secretKeyKOpener"] = text; memset(text, 0, sizeof(text)); text[0] = '\0'; - for(int i = 0 ; i < 4 ; i++) { + for(int i = 0 ; i < 4 ; i++) + { size_t offset = strlen(text); sprintf(&(text[offset]), "%02x", authorizationIdOpn[i]); } @@ -1166,14 +1388,21 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request) for(const auto& key : bytePrefs) { size_t storedLength = _preferences->getBytesLength(key); - if(storedLength == 0) continue; + if(storedLength == 0) + { + continue; + } uint8_t serialized[storedLength]; memset(serialized, 0, sizeof(serialized)); size_t size = _preferences->getBytes(key, serialized, sizeof(serialized)); - if(size == 0) continue; + if(size == 0) + { + continue; + } char text[255]; text[0] = '\0'; - for(int i = 0 ; i < size ; i++) { + for(int i = 0 ; i < size ; i++) + { size_t offset = strlen(text); sprintf(&(text[offset]), "%02x", serialized[i]); } @@ -1223,7 +1452,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) if(index < params -1) { const PsychicWebParameter* next = request->getParam(index+1); - if(key == next->name()) continue; + if(key == next->name()) + { + continue; + } } if(key == "MQTTSERVER") @@ -1333,7 +1565,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) if(value.toInt() > 1) { networkReconfigure = true; - if(value.toInt() != 11) _preferences->putInt(preference_network_custom_phy, 0); + if(value.toInt() != 11) + { + _preferences->putInt(preference_network_custom_phy, 0); + } } _preferences->putInt(preference_network_hardware, value.toInt()); Log->print(F("Setting changed: ")); @@ -1497,8 +1732,14 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) { if(_preferences->getString(preference_mqtt_hass_discovery, "") != value) { - if (_nuki != nullptr) _nuki->disableHASS(); - if (_nukiOpener != nullptr) _nukiOpener->disableHASS(); + if (_nuki != nullptr) + { + _nuki->disableHASS(); + } + if (_nukiOpener != nullptr) + { + _nukiOpener->disableHASS(); + } _preferences->putString(preference_mqtt_hass_discovery, value); Log->print(F("Setting changed: ")); Log->println(key); @@ -1600,7 +1841,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) if(_preferences->getBool(preference_official_hybrid_enabled, false) != (value == "1")) { _preferences->putBool(preference_official_hybrid_enabled, (value == "1")); - if((value == "1")) _preferences->putBool(preference_register_as_app, true); + if((value == "1")) + { + _preferences->putBool(preference_register_as_app, true); + } Log->print(F("Setting changed: ")); Log->println(key); configChanged = true; @@ -1611,7 +1855,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) if(_preferences->getBool(preference_official_hybrid_actions, false) != (value == "1")) { _preferences->putBool(preference_official_hybrid_actions, (value == "1")); - if(value == "1") _preferences->putBool(preference_register_as_app, true); + if(value == "1") + { + _preferences->putBool(preference_register_as_app, true); + } Log->print(F("Setting changed: ")); Log->println(key); //configChanged = true; @@ -2510,27 +2757,45 @@ bool WebCfgServer::processArgs(PsychicRequest *request, String& message) } else if(key == "LCKBLEADDR") { - if(value.length() == 12) for(int i=0; i().length() > 0) _preferences->putBool(key, (doc[key].as() == "1" ? true : false)); - else _preferences->remove(key); + if (doc[key].as().length() > 0) + { + _preferences->putBool(key, (doc[key].as() == "1" ? true : false)); + } + else + { + _preferences->remove(key); + } continue; } if(std::find(intPrefs.begin(), intPrefs.end(), key) != intPrefs.end()) { - if (doc[key].as().length() > 0) _preferences->putInt(key, doc[key].as()); - else _preferences->remove(key); + if (doc[key].as().length() > 0) + { + _preferences->putInt(key, doc[key].as()); + } + else + { + _preferences->remove(key); + } continue; } - if (doc[key].as().length() > 0) _preferences->putString(key, doc[key].as()); - else _preferences->remove(key); + if (doc[key].as().length() > 0) + { + _preferences->putString(key, doc[key].as()); + } + else + { + _preferences->remove(key); + } } for(const auto& key : bytePrefs) @@ -2774,7 +3072,10 @@ bool WebCfgServer::processImport(PsychicRequest *request, String& message) { String value = doc[key].as(); unsigned char tmpchar[32]; - for(int i=0; iputBytes(key, (byte*)(&tmpchar), (value.length() / 2)); memset(tmpchar, 0, sizeof(tmpchar)); } @@ -2788,7 +3089,10 @@ bool WebCfgServer::processImport(PsychicRequest *request, String& message) if (doc["bleAddressLock"].as().length() == 12) { String value = doc["bleAddressLock"].as(); - for(int i=0; i().length() == 64) { String value = doc["secretKeyKLock"].as(); - for(int i=0; i().length() == 8) { String value = doc["authorizationIdLock"].as(); - for(int i=0; i().length() > 0) _nuki->setPin(doc["securityPinCodeLock"].as()); - else _nuki->setPin(0xffff); + if(doc["securityPinCodeLock"].as().length() > 0) + { + _nuki->setPin(doc["securityPinCodeLock"].as()); + } + else + { + _nuki->setPin(0xffff); + } } nukiBlePref.begin("NukiHubopener", false); if(!doc["bleAddressOpener"].isNull()) @@ -2822,7 +3138,10 @@ bool WebCfgServer::processImport(PsychicRequest *request, String& message) if (doc["bleAddressOpener"].as().length() == 12) { String value = doc["bleAddressOpener"].as(); - for(int i=0; i().length() == 64) { String value = doc["secretKeyKOpener"].as(); - for(int i=0; i().length() == 8) { String value = doc["authorizationIdOpener"].as(); - for(int i=0; i().length() > 0) _nukiOpener->setPin(doc["securityPinCodeOpener"].as()); - else _nukiOpener->setPin(0xffff); + if(doc["securityPinCodeOpener"].as().length() > 0) + { + _nukiOpener->setPin(doc["securityPinCodeOpener"].as()); + } + else + { + _nukiOpener->setPin(0xffff); + } } configChanged = true; @@ -2917,12 +3248,12 @@ esp_err_t WebCfgServer::buildCustomNetworkConfigHtml(PsychicRequest *request) response.print(""); printDropDown(&response, "NWCUSTPHY", "PHY", String(_preferences->getInt(preference_network_custom_phy)), getNetworkCustomPHYOptions(), ""); printInputField(&response, "NWCUSTADDR", "ADDR", _preferences->getInt(preference_network_custom_addr, 1), 6, ""); - #if defined(CONFIG_IDF_TARGET_ESP32) +#if defined(CONFIG_IDF_TARGET_ESP32) printDropDown(&response, "NWCUSTCLK", "CLK", String(_preferences->getInt(preference_network_custom_clk, 0)), getNetworkCustomCLKOptions(), "internalopt"); printInputField(&response, "NWCUSTPWR", "PWR", _preferences->getInt(preference_network_custom_pwr, 12), 6, "class=\"internalopt\""); printInputField(&response, "NWCUSTMDIO", "MDIO", _preferences->getInt(preference_network_custom_mdio), 6, "class=\"internalopt\""); printInputField(&response, "NWCUSTMDC", "MDC", _preferences->getInt(preference_network_custom_mdc), 6, "class=\"internalopt\""); - #endif +#endif printInputField(&response, "NWCUSTIRQ", "IRQ", _preferences->getInt(preference_network_custom_irq, -1), 6, "class=\"externalopt\""); printInputField(&response, "NWCUSTRST", "RST", _preferences->getInt(preference_network_custom_rst, -1), 6, "class=\"externalopt\""); printInputField(&response, "NWCUSTCS", "CS", _preferences->getInt(preference_network_custom_cs, -1), 6, "class=\"externalopt\""); @@ -2950,9 +3281,9 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request) { response.print("
    WEBSERIAL IS ENABLED, ONLY ENABLE WHEN DEBUGGING AND DISABLE ASAP
    "); } - #ifdef DEBUG_NUKIHUB +#ifdef DEBUG_NUKIHUB response.print("
    RUNNING DEBUG BUILD, SWITCH TO RELEASE BUILD ASAP
    "); - #endif +#endif response.print("

    Info


    "); response.print(""); printParameter(&response, "Hostname", _hostname.c_str(), "", "hostname"); @@ -3022,12 +3353,12 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request) { buildNavigationMenuEntry(&response, "Open Webserial", "/webserial"); } - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 if(_allowRestartToPortal) { buildNavigationMenuEntry(&response, "Configure Wi-Fi", "/wifi"); } - #endif +#endif String rebooturl = "/reboot?CONFIRMTOKEN=" + _confirmCode; buildNavigationMenuEntry(&response, "Reboot Nuki Hub", rebooturl.c_str()); response.print(""); @@ -3094,9 +3425,9 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request) } response.print("

    Factory reset Nuki Hub

    "); response.print("

    This will reset all settings to default and unpair Nuki Lock and/or Opener."); - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 response.print("Optionally will also reset WiFi settings and reopen WiFi manager portal."); - #endif +#endif response.print("

    "); response.print(""); response.print("
    "); @@ -3104,9 +3435,9 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request) message.concat(_confirmCode); message.concat(" to confirm factory reset"); printInputField(&response, "CONFIRMTOKEN", message.c_str(), "", 10, ""); - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 printCheckBox(&response, "WIFI", "Also reset WiFi settings", false, ""); - #endif +#endif response.print("
    "); response.print("
    "); response.print(""); @@ -3132,14 +3463,17 @@ esp_err_t WebCfgServer::buildMqttConfigHtml(PsychicRequest *request) response.print(""); printInputField(&response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30, ""); printInputField(&response, "HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261, ""); - if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox(&response, "OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), ""); + if(_preferences->getBool(preference_opener_enabled, false)) + { + printCheckBox(&response, "OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), ""); + } printTextarea(&response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, true, true); printTextarea(&response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, true, true); printTextarea(&response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, true, true); printDropDown(&response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions(), ""); - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 printInputField(&response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6, ""); - #endif +#endif printInputField(&response, "NETTIMEOUT", "MQTT Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5, ""); printCheckBox(&response, "RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect), ""); printCheckBox(&response, "MQTTLOG", "Enable MQTT logging", _preferences->getBool(preference_mqtt_log_enabled), ""); @@ -3230,7 +3564,10 @@ esp_err_t WebCfgServer::buildStatusHtml(PsychicRequest *request) json["mqttState"] = "Yes"; mqttDone = true; } - else json["mqttState"] = "No"; + else + { + json["mqttState"] = "No"; + } if(_nuki != nullptr) { @@ -3244,11 +3581,20 @@ esp_err_t WebCfgServer::buildStatusHtml(PsychicRequest *request) if(_nuki->isPaired()) { json["lockPin"] = pinStateToString(_preferences->getInt(preference_lock_pin_status, 4)); - if(strcmp(lockStateArr, "undefined") != 0) lockDone = true; + if(strcmp(lockStateArr, "undefined") != 0) + { + lockDone = true; + } + } + else + { + json["lockPin"] = "Not Paired"; } - else json["lockPin"] = "Not Paired"; } - else lockDone = true; + else + { + lockDone = true; + } if(_nukiOpener != nullptr) { char openerStateArr[20]; @@ -3257,42 +3603,64 @@ esp_err_t WebCfgServer::buildStatusHtml(PsychicRequest *request) String openerPaired = (_nukiOpener->isPaired() ? ("Yes (BLE Address " + _nukiOpener->getBleAddress().toString() + ")").c_str() : "No"); json["openerPaired"] = openerPaired; - if(_nukiOpener->keyTurnerState().nukiState == NukiOpener::State::ContinuousMode) json["openerState"] = "Open (Continuous Mode)"; - else json["openerState"] = openerState; + if(_nukiOpener->keyTurnerState().nukiState == NukiOpener::State::ContinuousMode) + { + json["openerState"] = "Open (Continuous Mode)"; + } + else + { + json["openerState"] = openerState; + } if(_nukiOpener->isPaired()) { json["openerPin"] = pinStateToString(_preferences->getInt(preference_opener_pin_status, 4)); - if(strcmp(openerStateArr, "undefined") != 0) openerDone = true; + if(strcmp(openerStateArr, "undefined") != 0) + { + openerDone = true; + } + } + else + { + json["openerPin"] = "Not Paired"; } - else json["openerPin"] = "Not Paired"; } - else openerDone = true; + else + { + openerDone = true; + } if(_preferences->getBool(preference_check_updates)) { json["latestFirmware"] = _preferences->getString(preference_latest_version); latestDone = true; } - else latestDone = true; + else + { + latestDone = true; + } - if(mqttDone && lockDone && openerDone && latestDone) json["stop"] = 1; + if(mqttDone && lockDone && openerDone && latestDone) + { + json["stop"] = 1; + } serializeJson(json, jsonStr); return request->reply(200, "application/json", jsonStr.c_str()); } -String WebCfgServer::pinStateToString(uint8_t value) { +String WebCfgServer::pinStateToString(uint8_t value) +{ switch(value) { - case 0: - return String("PIN not set"); - case 1: - return String("PIN valid"); - case 2: - return String("PIN set but invalid"); - default: - return String("Unknown"); + case 0: + return String("PIN not set"); + case 1: + return String("PIN valid"); + case 2: + return String("PIN set but invalid"); + default: + return String("Unknown"); } } @@ -3485,9 +3853,15 @@ esp_err_t WebCfgServer::buildNukiConfigHtml(PsychicRequest *request) response.print("

    Basic Nuki Configuration

    "); response.print("
    "); printCheckBox(&response, "LOCKENA", "Nuki Lock enabled", _preferences->getBool(preference_lock_enabled), ""); - if(_preferences->getBool(preference_lock_enabled)) printInputField(&response, "MQTTPATH", "MQTT Nuki Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180, ""); + if(_preferences->getBool(preference_lock_enabled)) + { + printInputField(&response, "MQTTPATH", "MQTT Nuki Lock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180, ""); + } printCheckBox(&response, "OPENA", "Nuki Opener enabled", _preferences->getBool(preference_opener_enabled), ""); - if(_preferences->getBool(preference_opener_enabled)) printInputField(&response, "MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180, ""); + if(_preferences->getBool(preference_opener_enabled)) + { + printInputField(&response, "MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180, ""); + } response.print("

    "); response.print("

    Advanced Nuki Configuration

    "); response.print(""); @@ -3501,8 +3875,14 @@ esp_err_t WebCfgServer::buildNukiConfigHtml(PsychicRequest *request) } printInputField(&response, "NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10, ""); printInputField(&response, "TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10, ""); - if(_preferences->getBool(preference_lock_enabled, true)) printCheckBox(&response, "REGAPP", "Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), ""); - if(_preferences->getBool(preference_opener_enabled, false)) printCheckBox(&response, "REGAPPOPN", "Opener: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_opener_as_app), ""); + if(_preferences->getBool(preference_lock_enabled, true)) + { + printCheckBox(&response, "REGAPP", "Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), ""); + } + if(_preferences->getBool(preference_opener_enabled, false)) + { + printCheckBox(&response, "REGAPPOPN", "Opener: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_opener_as_app), ""); + } printInputField(&response, "RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10, ""); printInputField(&response, "TXPWR", "BLE transmit power in dB (minimum -12, maximum 9)", _preferences->getInt(preference_ble_tx_power, 9), 10, ""); @@ -3591,11 +3971,11 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print(NUKI_HUB_VERSION); response.print("\nBuild: "); response.print(NUKI_HUB_BUILD); - #ifndef DEBUG_NUKIHUB +#ifndef DEBUG_NUKIHUB response.print("\nBuild type: Release"); - #else +#else response.print("\nBuild type: Debug"); - #endif +#endif response.print("\nBuild date: "); response.print(NUKI_HUB_DATE); response.print("\nUpdater version: "); @@ -3616,7 +3996,7 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print(ESP.getFreeHeap()); response.print("\nTotal internal heap: "); response.print(ESP.getHeapSize()); - #ifdef CONFIG_SOC_SPIRAM_SUPPORTED +#ifdef CONFIG_SOC_SPIRAM_SUPPORTED if(esp_psram_get_size() > 0) { response.print("\nPSRAM Available: Yes"); @@ -3631,9 +4011,9 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) { response.print("\nPSRAM Available: No"); } - #else +#else response.print("\nPSRAM Available: No"); - #endif +#endif response.print("\nNetwork task stack high watermark: "); response.print(uxTaskGetStackHighWaterMark(networkTaskHandle)); response.print("\nNuki task stack high watermark: "); @@ -3675,14 +4055,14 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) if(_network->networkDeviceName() == "Built-in Wi-Fi") { - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 response.print("\nSSID: "); response.print(WiFi.SSID()); response.print("\nBSSID of AP: "); response.print(_network->networkBSSID()); response.print("\nESP32 MAC address: "); response.print(WiFi.macAddress()); - #endif +#endif } else { @@ -3692,7 +4072,10 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print("\n\n------------ NETWORK SETTINGS ------------"); response.print("\nNuki Hub hostname: "); response.print(_preferences->getString(preference_hostname, "")); - if(_preferences->getBool(preference_ip_dhcp_enabled, true)) response.print("\nDHCP enabled: Yes"); + if(_preferences->getBool(preference_ip_dhcp_enabled, true)) + { + response.print("\nDHCP enabled: Yes"); + } else { response.print("\nDHCP enabled: No"); @@ -3706,20 +4089,32 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print(_preferences->getString(preference_ip_dns_server, "")); } - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 if(_network->networkDeviceName() == "Built-in Wi-Fi") { response.print("\nRSSI Publish interval (s): "); - if(_preferences->getInt(preference_rssi_publish_interval, 60) < 0) response.print("Disabled"); - else response.print(_preferences->getInt(preference_rssi_publish_interval, 60)); + if(_preferences->getInt(preference_rssi_publish_interval, 60) < 0) + { + response.print("Disabled"); + } + else + { + response.print(_preferences->getInt(preference_rssi_publish_interval, 60)); + } } - #endif +#endif response.print("\nRestart ESP32 on network disconnect enabled: "); response.print(_preferences->getBool(preference_restart_on_disconnect, false) ? "Yes" : "No"); response.print("\nMQTT Timeout until restart (s): "); - if(_preferences->getInt(preference_network_timeout, 60) < 0) response.print("Disabled"); - else response.print(_preferences->getInt(preference_network_timeout, 60)); + if(_preferences->getInt(preference_network_timeout, 60) < 0) + { + response.print("Disabled"); + } + else + { + response.print(_preferences->getInt(preference_network_timeout, 60)); + } response.print("\n\n------------ MQTT ------------"); response.print("\nMQTT connected: "); response.print(_network->mqttConnectionState() > 0 ? "Yes" : "No"); @@ -3803,9 +4198,15 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print("\nNuki Hub configuration URL for HA: "); response.print(_preferences->getString(preference_mqtt_hass_cu_url, "").length() > 0 ? _preferences->getString(preference_mqtt_hass_cu_url, "") : "http://" + _network->localIP()); } - else response.print("No"); + else + { + response.print("No"); + } response.print("\n\n------------ NUKI LOCK ------------"); - if(_nuki == nullptr || !_preferences->getBool(preference_lock_enabled, true)) response.print("\nLock enabled: No"); + if(_nuki == nullptr || !_preferences->getBool(preference_lock_enabled, true)) + { + response.print("\nLock enabled: No"); + } else { response.print("\nLock enabled: Yes"); @@ -3835,7 +4236,10 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print("\nRegister as: "); response.print(_preferences->getBool(preference_register_as_app, false) ? "App" : "Bridge"); response.print("\n\n------------ HYBRID MODE ------------"); - if(!_preferences->getBool(preference_official_hybrid_enabled, false)) response.print("\nHybrid mode enabled: No"); + if(!_preferences->getBool(preference_official_hybrid_enabled, false)) + { + response.print("\nHybrid mode enabled: No"); + } else { response.print("\nHybrid mode enabled: Yes"); @@ -3992,7 +4396,10 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) } response.print("\n\n------------ NUKI OPENER ------------"); - if(_nukiOpener == nullptr || !_preferences->getBool(preference_opener_enabled, false)) response.print("\nOpener enabled: No"); + if(_nukiOpener == nullptr || !_preferences->getBool(preference_opener_enabled, false)) + { + response.print("\nOpener enabled: No"); + } else { response.print("\nOpener enabled: Yes"); @@ -4159,7 +4566,10 @@ esp_err_t WebCfgServer::processUnpair(PsychicRequest *request, bool opener) if(request->hasParam("CONFIRMTOKEN")) { const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); - if(p->value() != "") value = p->value(); + if(p->value() != "") + { + value = p->value(); + } } if(value != _confirmCode) @@ -4191,7 +4601,10 @@ esp_err_t WebCfgServer::processUpdate(PsychicRequest *request) if(request->hasParam("token")) { const PsychicWebParameter* p = request->getParam("token"); - if(p->value() != "") value = p->value(); + if(p->value() != "") + { + value = p->value(); + } } if(value != _confirmCode) @@ -4256,7 +4669,10 @@ esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) if(request->hasParam("CONFIRMTOKEN")) { const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); - if(p->value() != "") value = p->value(); + if(p->value() != "") + { + value = p->value(); + } } bool resetWifi = false; @@ -4270,7 +4686,10 @@ esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) if(request->hasParam("WIFI")) { const PsychicWebParameter* p = request->getParam("WIFI"); - if(p->value() != "") value = p->value(); + if(p->value() != "") + { + value = p->value(); + } } if(value2 == "1") @@ -4299,12 +4718,12 @@ esp_err_t WebCfgServer::processFactoryReset(PsychicRequest *request) _preferences->clear(); - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 if(resetWifi) { _network->reconfigureDevice(); } - #endif +#endif waitAndProcess(false, 3000); restartEsp(RestartReason::NukiHubReset); @@ -4365,8 +4784,14 @@ void WebCfgServer::printDropDown(PsychicStreamResponse *response, const char *to for(const auto& option : options) { - if(option.first == preselectedValue) response->print(""); - if(strcmp(id, "") == 0) response->print("
    "); response->print(description); response->print(""); + if(strcmp(id, "") == 0) + { + response->print(""); + } else { response->print("print(id); response->print("\">"); } - if(strcmp(link, "") == 0) response->print(value); + if(strcmp(link, "") == 0) + { + response->print(value); + } else { response->print("> WebCfgServer::getNetworkCustomPHYOp options.push_back(std::make_pair("1", "W5500")); options.push_back(std::make_pair("2", "DN9051")); options.push_back(std::make_pair("3", "KSZ8851SNL")); - #if defined(CONFIG_IDF_TARGET_ESP32) +#if defined(CONFIG_IDF_TARGET_ESP32) options.push_back(std::make_pair("4", "LAN8720")); options.push_back(std::make_pair("5", "RTL8201")); options.push_back(std::make_pair("6", "TLK110")); options.push_back(std::make_pair("7", "DP83848")); options.push_back(std::make_pair("8", "KSZ8041")); options.push_back(std::make_pair("9", "KSZ8081")); - #endif +#endif return options; } diff --git a/src/main.cpp b/src/main.cpp index 32d6b4e..173acd6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,8 +77,8 @@ TaskHandle_t networkTaskHandle = nullptr; #ifndef NUKI_HUB_UPDATER ssize_t write_fn(void* cookie, const char* buf, ssize_t size) { - Log->write((uint8_t *)buf, (size_t)size); - return size; + Log->write((uint8_t *)buf, (size_t)size); + return size; } void ets_putc_handler(char c) @@ -87,21 +87,25 @@ void ets_putc_handler(char c) static size_t buf_pos = 0; buf[buf_pos] = c; buf_pos++; - if (c == '\n' || buf_pos == sizeof(buf)) { + if (c == '\n' || buf_pos == sizeof(buf)) + { write_fn(NULL, buf, buf_pos); buf_pos = 0; } } -int _log_vprintf(const char *fmt, va_list args) { +int _log_vprintf(const char *fmt, va_list args) +{ int ret = vsnprintf(log_print_buffer, sizeof(log_print_buffer), fmt, args); - if (ret >= 0){ + if (ret >= 0) + { Log->write((uint8_t *)log_print_buffer, (size_t)ret); } return 0; //return vprintf(fmt, args); } -void setReroute(){ +void setReroute() +{ esp_log_set_vprintf(_log_vprintf); if(preferences->getBool(preference_mqtt_log_enabled)) { @@ -152,7 +156,10 @@ void networkTask(void *pvParameters) setReroute(); } #endif - if(connected && openerEnabled) networkOpener->update(); + if(connected && openerEnabled) + { + networkOpener->update(); + } #endif if((esp_timer_get_time() / 1000) - networkLoopTs > 120000) @@ -229,10 +236,10 @@ void bootloopDetection() } if(esp_reset_reason() == esp_reset_reason_t::ESP_RST_PANIC || - esp_reset_reason() == esp_reset_reason_t::ESP_RST_INT_WDT || - esp_reset_reason() == esp_reset_reason_t::ESP_RST_TASK_WDT || - true || - esp_reset_reason() == esp_reset_reason_t::ESP_RST_WDT) + esp_reset_reason() == esp_reset_reason_t::ESP_RST_INT_WDT || + esp_reset_reason() == esp_reset_reason_t::ESP_RST_TASK_WDT || + true || + esp_reset_reason() == esp_reset_reason_t::ESP_RST_WDT) { bootloopCounter++; Log->print(F("Bootloop counter incremented: ")); @@ -263,38 +270,48 @@ uint8_t checkPartition() Log->print(F("Partition subtype: ")); Log->println(running_partition->subtype); - if(running_partition->size == 1966080) return 0; //OLD PARTITION TABLE - else if(running_partition->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0) return 1; //NEW PARTITION TABLE, RUNNING MAIN APP - else return 2; //NEW PARTITION TABLE, RUNNING UPDATER APP + if(running_partition->size == 1966080) + { + return 0; //OLD PARTITION TABLE + } + else if(running_partition->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0) + { + return 1; //NEW PARTITION TABLE, RUNNING MAIN APP + } + else + { + return 2; //NEW PARTITION TABLE, RUNNING UPDATER APP + } } esp_err_t _http_event_handler(esp_http_client_event_t *evt) { - switch (evt->event_id) { - case HTTP_EVENT_ERROR: - Log->println("HTTP_EVENT_ERROR"); - break; - case HTTP_EVENT_ON_CONNECTED: - Log->println("HTTP_EVENT_ON_CONNECTED"); - break; - case HTTP_EVENT_HEADER_SENT: - Log->println("HTTP_EVENT_HEADER_SENT"); - break; - case HTTP_EVENT_ON_HEADER: - Log->println("HTTP_EVENT_ON_HEADER"); - break; - case HTTP_EVENT_ON_DATA: - Log->println("HTTP_EVENT_ON_DATA"); - break; - case HTTP_EVENT_ON_FINISH: - Log->println("HTTP_EVENT_ON_FINISH"); - break; - case HTTP_EVENT_DISCONNECTED: - Log->println("HTTP_EVENT_DISCONNECTED"); - break; - case HTTP_EVENT_REDIRECT: - Log->println("HTTP_EVENT_REDIRECT"); - break; + switch (evt->event_id) + { + case HTTP_EVENT_ERROR: + Log->println("HTTP_EVENT_ERROR"); + break; + case HTTP_EVENT_ON_CONNECTED: + Log->println("HTTP_EVENT_ON_CONNECTED"); + break; + case HTTP_EVENT_HEADER_SENT: + Log->println("HTTP_EVENT_HEADER_SENT"); + break; + case HTTP_EVENT_ON_HEADER: + Log->println("HTTP_EVENT_ON_HEADER"); + break; + case HTTP_EVENT_ON_DATA: + Log->println("HTTP_EVENT_ON_DATA"); + break; + case HTTP_EVENT_ON_FINISH: + Log->println("HTTP_EVENT_ON_FINISH"); + break; + case HTTP_EVENT_DISCONNECTED: + Log->println("HTTP_EVENT_DISCONNECTED"); + break; + case HTTP_EVENT_REDIRECT: + Log->println("HTTP_EVENT_REDIRECT"); + break; } return ESP_OK; } @@ -315,14 +332,16 @@ void otaTask(void *pvParameter) preferences->putString(preference_ota_main_url, ""); } Log->println("Starting OTA task"); - esp_http_client_config_t config = { + esp_http_client_config_t config = + { .url = updateUrl.c_str(), .event_handler = _http_event_handler, .crt_bundle_attach = esp_crt_bundle_attach, .keep_alive_enable = true, }; - esp_https_ota_config_t ota_config = { + esp_https_ota_config_t ota_config = + { .http_config = &config, }; Log->print(F("Attempting to download update from ")); @@ -334,19 +353,23 @@ void otaTask(void *pvParameter) while (retryCount <= retryMax) { esp_err_t ret = esp_https_ota(&ota_config); - if (ret == ESP_OK) { + if (ret == ESP_OK) + { Log->println("OTA Succeeded, Rebooting..."); esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)); restartEsp(RestartReason::OTACompleted); break; - } else { + } + else + { Log->println("Firmware upgrade failed, retrying in 5 seconds"); retryCount++; esp_task_wdt_reset(); delay(5000); continue; } - while (1) { + while (1) + { vTaskDelay(1000 / portTICK_PERIOD_MS); } } @@ -359,7 +382,8 @@ void otaTask(void *pvParameter) void setupTasks(bool ota) { // configMAX_PRIORITIES is 25 - esp_task_wdt_config_t twdt_config = { + esp_task_wdt_config_t twdt_config = + { .timeout_ms = 300000, .idle_core_mask = 0, .trigger_panic = true, @@ -375,13 +399,13 @@ void setupTasks(bool ota) { xTaskCreatePinnedToCore(networkTask, "ntw", preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), NULL, 3, &networkTaskHandle, 1); esp_task_wdt_add(networkTaskHandle); - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER if(!network->isApOpen()) { xTaskCreatePinnedToCore(nukiTask, "nuki", preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), NULL, 2, &nukiTaskHandle, 0); esp_task_wdt_add(nukiTaskHandle); } - #endif +#endif } } @@ -392,13 +416,13 @@ void setup() Serial.begin(115200); Log = &Serial; - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER stdout = funopen(NULL, NULL, &write_fn, NULL, NULL); static char linebuf[1024]; setvbuf(stdout, linebuf, _IOLBF, sizeof(linebuf)); esp_rom_install_channel_putc(1, &ets_putc_handler); //ets_install_putc1(&ets_putc_handler); - #endif +#endif preferences = new Preferences(); preferences->begin("nukihub", false); @@ -408,24 +432,36 @@ void setup() initializeRestartReason(); - if((partitionType==1 && preferences->getString(preference_ota_updater_url, "").length() > 0) || (partitionType==2 && preferences->getString(preference_ota_main_url, "").length() > 0)) doOta = true; + if((partitionType==1 && preferences->getString(preference_ota_updater_url, "").length() > 0) || (partitionType==2 && preferences->getString(preference_ota_main_url, "").length() > 0)) + { + doOta = true; + } - #ifndef NUKI_HUB_UPDATER +#ifndef NUKI_HUB_UPDATER if(preferences->getBool(preference_enable_bootloop_reset, false)) { bootloopDetection(); } - #endif +#endif - #ifdef NUKI_HUB_UPDATER +#ifdef NUKI_HUB_UPDATER Log->print(F("Nuki Hub OTA version ")); Log->println(NUKI_HUB_VERSION); Log->print(F("Nuki Hub OTA build ")); Log->println(); - if(preferences->getString(preference_updater_version, "") != NUKI_HUB_VERSION) preferences->putString(preference_updater_version, NUKI_HUB_VERSION); - if(preferences->getString(preference_updater_build, "") != NUKI_HUB_BUILD) preferences->putString(preference_updater_build, NUKI_HUB_BUILD); - if(preferences->getString(preference_updater_date, "") != NUKI_HUB_DATE) preferences->putString(preference_updater_date, NUKI_HUB_DATE); + if(preferences->getString(preference_updater_version, "") != NUKI_HUB_VERSION) + { + preferences->putString(preference_updater_version, NUKI_HUB_VERSION); + } + if(preferences->getString(preference_updater_build, "") != NUKI_HUB_BUILD) + { + preferences->putString(preference_updater_build, NUKI_HUB_BUILD); + } + if(preferences->getString(preference_updater_date, "") != NUKI_HUB_DATE) + { + preferences->putString(preference_updater_date, NUKI_HUB_DATE); + } network = new NukiNetwork(preferences); network->initialize(); @@ -436,9 +472,12 @@ void setup() webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); psychicServer->listen(80); - psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); + psychicServer->onNotFound([](PsychicRequest* request) + { + return request->redirect("/"); + }); } - #else +#else Log->print(F("Nuki Hub version ")); Log->println(NUKI_HUB_VERSION); Log->print(F("Nuki Hub build ")); @@ -466,7 +505,7 @@ void setup() network = new NukiNetwork(preferences, gpio, mqttLockPath, CharBuffer::get(), buffer_size); network->initialize(); - + lockEnabled = preferences->getBool(preference_lock_enabled); openerEnabled = preferences->getBool(preference_opener_enabled); @@ -477,7 +516,7 @@ void setup() lockEnabled = false; openerEnabled = false; } - + bleScanner = new BleScanner::Scanner(); // Scan interval and window according to Nuki recommendations: // https://developer.nuki.io/t/bluetooth-specification-questions/1109/27 @@ -522,10 +561,13 @@ void setup() { webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); webCfgServer->initialize(); - psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/"); }); + psychicServer->onNotFound([](PsychicRequest* request) + { + return request->redirect("/"); + }); } /* - #ifdef DEBUG_NUKIHUB +#ifdef DEBUG_NUKIHUB else psychicServer->onNotFound([](PsychicRequest* request) { return request->redirect("/webserial"); }); if(preferences->getBool(preference_webserial_enabled, false)) @@ -534,21 +576,27 @@ void setup() WebSerial.begin(asyncServer); WebSerial.setBuffer(1024); } - #endif +#endif */ } } - #endif +#endif - if(doOta) setupTasks(true); - else setupTasks(false); + if(doOta) + { + setupTasks(true); + } + else + { + setupTasks(false); + } - #ifdef DEBUG_NUKIHUB +#ifdef DEBUG_NUKIHUB Log->print("Task Name\tStatus\tPrio\tHWM\tTask\tAffinity\n"); char stats_buffer[1024]; vTaskList(stats_buffer); Log->println(stats_buffer); - #endif +#endif } void loop() diff --git a/src/networkDevices/EthernetDevice.cpp b/src/networkDevices/EthernetDevice.cpp index 9a31c7f..1a0c78b 100644 --- a/src/networkDevices/EthernetDevice.cpp +++ b/src/networkDevices/EthernetDevice.cpp @@ -7,44 +7,44 @@ RTC_NOINIT_ATTR bool criticalEthFailure; extern char WiFi_fallbackDetect[14]; EthernetDevice::EthernetDevice(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration, const std::string& deviceName, uint8_t phy_addr, int power, int mdc, int mdio, eth_phy_type_t ethtype, eth_clock_mode_t clock_mode) -: NetworkDevice(hostname, ipConfiguration), - _deviceName(deviceName), - _phy_addr(phy_addr), - _power(power), - _mdc(mdc), - _mdio(mdio), - _type(ethtype), - _clock_mode(clock_mode), - _useSpi(false), - _preferences(preferences) + : NetworkDevice(hostname, ipConfiguration), + _deviceName(deviceName), + _phy_addr(phy_addr), + _power(power), + _mdc(mdc), + _mdio(mdio), + _type(ethtype), + _clock_mode(clock_mode), + _useSpi(false), + _preferences(preferences) { init(); } EthernetDevice::EthernetDevice(const String &hostname, - Preferences *preferences, - const IPConfiguration *ipConfiguration, - const std::string &deviceName, - uint8_t phy_addr, - int cs, - int irq, - int rst, - int spi_sck, - int spi_miso, - int spi_mosi, - eth_phy_type_t ethtype) - : NetworkDevice(hostname, ipConfiguration), - _deviceName(deviceName), - _phy_addr(phy_addr), - _cs(cs), - _irq(irq), - _rst(rst), - _spi_sck(spi_sck), - _spi_miso(spi_miso), - _spi_mosi(spi_mosi), - _type(ethtype), - _useSpi(true), - _preferences(preferences) + Preferences *preferences, + const IPConfiguration *ipConfiguration, + const std::string &deviceName, + uint8_t phy_addr, + int cs, + int irq, + int rst, + int spi_sck, + int spi_miso, + int spi_mosi, + eth_phy_type_t ethtype) + : NetworkDevice(hostname, ipConfiguration), + _deviceName(deviceName), + _phy_addr(phy_addr), + _cs(cs), + _irq(irq), + _rst(rst), + _spi_sck(spi_sck), + _spi_miso(spi_miso), + _spi_mosi(spi_mosi), + _type(ethtype), + _useSpi(true), + _preferences(preferences) { init(); } @@ -78,7 +78,7 @@ void EthernetDevice::initialize() _hardwareInitialized = ETH.begin(_type, _phy_addr, _cs, _irq, _rst, SPI); criticalEthFailure = false; } - #ifdef CONFIG_IDF_TARGET_ESP32 +#ifdef CONFIG_IDF_TARGET_ESP32 else { Log->println(F("Use RMII")); @@ -90,7 +90,7 @@ void EthernetDevice::initialize() _checkIpTs = (esp_timer_get_time() / 1000) + 2000; } } - #endif +#endif if(_hardwareInitialized) { @@ -138,55 +138,56 @@ void EthernetDevice::update() void EthernetDevice::onNetworkEvent(arduino_event_id_t event, arduino_event_info_t info) { - switch (event) { - case ARDUINO_EVENT_ETH_START: - Log->println("ETH Started"); - ETH.setHostname(_hostname.c_str()); - break; - case ARDUINO_EVENT_ETH_CONNECTED: - Log->println("ETH Connected"); - if(!localIP().equals("0.0.0.0")) - { - _connected = true; - } - break; - case ARDUINO_EVENT_ETH_GOT_IP: - Log->printf("ETH Got IP: '%s'\n", esp_netif_get_desc(info.got_ip.esp_netif)); - Log->println(ETH); - - // For RMII devices, this check is handled in the update() method. - if(_useSpi && !_ipConfiguration->dhcpEnabled() && _ipConfiguration->ipAddress() != ETH.localIP()) - { - Log->printf("Static IP not used, retrying to set static IP"); - ETH.config(_ipConfiguration->ipAddress(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet(), _ipConfiguration->dnsServer()); - ETH.begin(_type, _phy_addr, _cs, _irq, _rst, SPI); - } - + switch (event) + { + case ARDUINO_EVENT_ETH_START: + Log->println("ETH Started"); + ETH.setHostname(_hostname.c_str()); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + Log->println("ETH Connected"); + if(!localIP().equals("0.0.0.0")) + { _connected = true; - if(_preferences->getBool(preference_ntw_reconfigure, false)) - { - _preferences->putBool(preference_ntw_reconfigure, false); - } - break; - case ARDUINO_EVENT_ETH_LOST_IP: - Log->println("ETH Lost IP"); - _connected = false; - onDisconnected(); - break; - case ARDUINO_EVENT_ETH_DISCONNECTED: - Log->println("ETH Disconnected"); - _connected = false; - onDisconnected(); - break; - case ARDUINO_EVENT_ETH_STOP: - Log->println("ETH Stopped"); - _connected = false; - onDisconnected(); - break; - default: - Log->print("ETH Event: "); - Log->println(event); - break; + } + break; + case ARDUINO_EVENT_ETH_GOT_IP: + Log->printf("ETH Got IP: '%s'\n", esp_netif_get_desc(info.got_ip.esp_netif)); + Log->println(ETH); + + // For RMII devices, this check is handled in the update() method. + if(_useSpi && !_ipConfiguration->dhcpEnabled() && _ipConfiguration->ipAddress() != ETH.localIP()) + { + Log->printf("Static IP not used, retrying to set static IP"); + ETH.config(_ipConfiguration->ipAddress(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet(), _ipConfiguration->dnsServer()); + ETH.begin(_type, _phy_addr, _cs, _irq, _rst, SPI); + } + + _connected = true; + if(_preferences->getBool(preference_ntw_reconfigure, false)) + { + _preferences->putBool(preference_ntw_reconfigure, false); + } + break; + case ARDUINO_EVENT_ETH_LOST_IP: + Log->println("ETH Lost IP"); + _connected = false; + onDisconnected(); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Log->println("ETH Disconnected"); + _connected = false; + onDisconnected(); + break; + case ARDUINO_EVENT_ETH_STOP: + Log->println("ETH Stopped"); + _connected = false; + onDisconnected(); + break; + default: + Log->print("ETH Event: "); + Log->println(event); + break; } } @@ -212,7 +213,10 @@ bool EthernetDevice::isApOpen() void EthernetDevice::onDisconnected() { - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); + if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + { + restartEsp(RestartReason::RestartOnDisconnectWatchdog); + } } int8_t EthernetDevice::signalStrength() diff --git a/src/networkDevices/IPConfiguration.cpp b/src/networkDevices/IPConfiguration.cpp index 4f374ad..fadcfb6 100644 --- a/src/networkDevices/IPConfiguration.cpp +++ b/src/networkDevices/IPConfiguration.cpp @@ -3,7 +3,7 @@ #include "../Logger.h" IPConfiguration::IPConfiguration(Preferences *preferences) -: _preferences(preferences) + : _preferences(preferences) { if(!dhcpEnabled() && _preferences->getString(preference_ip_address, "").length() <= 0) { @@ -23,10 +23,14 @@ IPConfiguration::IPConfiguration(Preferences *preferences) } else { - Log->print(F("IP address: ")); Log->print(ipAddress()); - Log->print(F(", Subnet: ")); Log->print(subnet()); - Log->print(F(", Gateway: ")); Log->print(defaultGateway()); - Log->print(F(", DNS: ")); Log->println(dnsServer()); + Log->print(F("IP address: ")); + Log->print(ipAddress()); + Log->print(F(", Subnet: ")); + Log->print(subnet()); + Log->print(F(", Gateway: ")); + Log->print(defaultGateway()); + Log->print(F(", DNS: ")); + Log->println(dnsServer()); } } diff --git a/src/networkDevices/WifiDevice.cpp b/src/networkDevices/WifiDevice.cpp index 5c7e893..46513af 100644 --- a/src/networkDevices/WifiDevice.cpp +++ b/src/networkDevices/WifiDevice.cpp @@ -6,8 +6,8 @@ #include "../RestartReason.h" WifiDevice::WifiDevice(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration) -: NetworkDevice(hostname, ipConfiguration), - _preferences(preferences) + : NetworkDevice(hostname, ipConfiguration), + _preferences(preferences) { } @@ -52,10 +52,10 @@ void WifiDevice::initialize() for (int i = 0; i < _foundNetworks; i++) { Log->println(String(F("SSID ")) + WiFi.SSID(i) + String(F(" found with RSSI: ")) + - String(WiFi.RSSI(i)) + String(F("(")) + - String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + - String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + - String(F(" and channel: ")) + String(WiFi.channel(i))); + String(WiFi.RSSI(i)) + String(F("(")) + + String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + + String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + + String(F(" and channel: ")) + String(WiFi.channel(i))); } if (_connectOnScanDone && _foundNetworks > 0) @@ -78,11 +78,13 @@ void WifiDevice::initialize() _preferences->putBool(preference_wifi_converted, true); wifi_config_t wifi_cfg; - if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + if(esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) + { Log->println("Failed to get Wi-Fi configuration in RAM"); } - if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { + if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) + { Log->println("Failed to set storage Wi-Fi"); } @@ -107,7 +109,8 @@ void WifiDevice::initialize() memset(wifi_cfg.sta.ssid, 0, sizeof(wifi_cfg.sta.ssid)); memset(wifi_cfg.sta.password, 0, sizeof(wifi_cfg.sta.password)); - if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + if (esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) + { Log->println("Failed to clear NVS Wi-Fi configuration"); } @@ -159,7 +162,7 @@ void WifiDevice::initialize() void WifiDevice::scan(bool passive, bool async) { - if(!_connecting) + if(!_connecting) { WiFi.scanDelete(); WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); @@ -210,10 +213,10 @@ bool WifiDevice::connect() if (ssid == WiFi.SSID(i)) { Log->println(String(F("Saved SSID ")) + ssid + String(F(" found with RSSI: ")) + - String(WiFi.RSSI(i)) + String(F("(")) + - String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + - String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + - String(F(" and channel: ")) + String(WiFi.channel(i))); + String(WiFi.RSSI(i)) + String(F("(")) + + String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) + + String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) + + String(F(" and channel: ")) + String(WiFi.channel(i))); if (bestConnection == -1) { bestConnection = i; @@ -232,7 +235,10 @@ bool WifiDevice::connect() { Log->print("No network found with SSID: "); Log->println(ssid); - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); + if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + { + restartEsp(RestartReason::RestartOnDisconnectWatchdog); + } _connectOnScanDone = true; _openAP = false; scan(false, true); @@ -243,42 +249,42 @@ bool WifiDevice::connect() _connecting = true; esp_wifi_scan_stop(); Log->println(String(F("Trying to connect to SSID ")) + ssid + String(F(" found with RSSI: ")) + - String(WiFi.RSSI(bestConnection)) + String(F("(")) + - String(constrain((100.0 + WiFi.RSSI(bestConnection)) * 2, 0, 100)) + - String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(bestConnection) + - String(F(" and channel: ")) + String(WiFi.channel(bestConnection))); - - + String(WiFi.RSSI(bestConnection)) + String(F("(")) + + String(constrain((100.0 + WiFi.RSSI(bestConnection)) * 2, 0, 100)) + + String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(bestConnection) + + String(F(" and channel: ")) + String(WiFi.channel(bestConnection))); + + if(!_ipConfiguration->dhcpEnabled()) { WiFi.config(_ipConfiguration->ipAddress(), _ipConfiguration->dnsServer(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet()); } - + WiFi.begin(ssid, pass); auto status = WiFi.waitForConnectResult(10000); - + switch (status) { - case WL_CONNECTED: + case WL_CONNECTED: Log->println("WiFi connected"); break; - case WL_NO_SSID_AVAIL: + case WL_NO_SSID_AVAIL: Log->println("WiFi SSID not available"); break; - case WL_CONNECT_FAILED: + case WL_CONNECT_FAILED: Log->println("WiFi connection failed"); break; - case WL_IDLE_STATUS: + case WL_IDLE_STATUS: Log->println("WiFi changing status"); break; - case WL_DISCONNECTED: + case WL_DISCONNECTED: Log->println("WiFi disconnected"); break; - default: + default: Log->println("WiFi timeout"); break; } - + if (status != WL_CONNECTED) { if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) @@ -326,7 +332,7 @@ bool WifiDevice::isConnected() { return false; } - + return true; } @@ -350,12 +356,12 @@ void WifiDevice::onDisconnected() _connecting = true; String ssid = _preferences->getString(preference_wifi_ssid, ""); String pass = _preferences->getString(preference_wifi_pass, ""); - + if(!_ipConfiguration->dhcpEnabled()) { WiFi.config(_ipConfiguration->ipAddress(), _ipConfiguration->dnsServer(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet()); } - + WiFi.begin(ssid, pass); int loop = 0; @@ -371,26 +377,29 @@ void WifiDevice::onDisconnected() if(!isConnected()) { - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) restartEsp(RestartReason::RestartOnDisconnectWatchdog); + if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + { + restartEsp(RestartReason::RestartOnDisconnectWatchdog); + } - WiFi.disconnect(true); - WiFi.mode(WIFI_STA); - WiFi.disconnect(); - delay(500); + WiFi.disconnect(true); + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(500); - wifi_mode_t wifiMode; - esp_wifi_get_mode(&wifiMode); + wifi_mode_t wifiMode; + esp_wifi_get_mode(&wifiMode); - while (wifiMode != WIFI_MODE_STA || WiFi.status() == WL_CONNECTED) - { - delay(500); - Log->println(F("Waiting for WiFi mode change or disconnection.")); - esp_wifi_get_mode(&wifiMode); - } + while (wifiMode != WIFI_MODE_STA || WiFi.status() == WL_CONNECTED) + { + delay(500); + Log->println(F("Waiting for WiFi mode change or disconnection.")); + esp_wifi_get_mode(&wifiMode); + } - _connectOnScanDone = true; - _openAP = false; - scan(false, true); + _connectOnScanDone = true; + _openAP = false; + scan(false, true); } } } diff --git a/src/util/NetworkDeviceInstantiator.cpp b/src/util/NetworkDeviceInstantiator.cpp index 0b46666..af4fda5 100644 --- a/src/util/NetworkDeviceInstantiator.cpp +++ b/src/util/NetworkDeviceInstantiator.cpp @@ -13,153 +13,153 @@ NetworkDevice *NetworkDeviceInstantiator::Create(NetworkDeviceType networkDevice switch (networkDeviceType) { - case NetworkDeviceType::W5500: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "Generic W5500", - ETH_PHY_ADDR_W5500, - ETH_PHY_CS_GENERIC_W5500, - ETH_PHY_IRQ_GENERIC_W5500, - ETH_PHY_RST_GENERIC_W5500, - ETH_PHY_SPI_SCK_GENERIC_W5500, - ETH_PHY_SPI_MISO_GENERIC_W5500, - ETH_PHY_SPI_MOSI_GENERIC_W5500, - ETH_PHY_W5500); - break; - case NetworkDeviceType::W5500M5: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "M5Stack Atom POE", - ETH_PHY_ADDR_W5500, - ETH_PHY_CS_M5_W5500, - ETH_PHY_IRQ_M5_W5500, - ETH_PHY_RST_M5_W5500, - ETH_PHY_SPI_SCK_M5_W5500, - ETH_PHY_SPI_MISO_M5_W5500, - ETH_PHY_SPI_MOSI_M5_W5500, - ETH_PHY_W5500); - break; - case NetworkDeviceType::W5500M5S3: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "M5Stack Atom POE S3", - ETH_PHY_ADDR_W5500, - ETH_PHY_CS_M5_W5500_S3, - ETH_PHY_IRQ_M5_W5500, - ETH_PHY_RST_M5_W5500, - ETH_PHY_SPI_SCK_M5_W5500_S3, - ETH_PHY_SPI_MISO_M5_W5500_S3, - ETH_PHY_SPI_MOSI_M5_W5500_S3, - ETH_PHY_W5500); - break; - case NetworkDeviceType::ETH01_Evo: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "ETH01-Evo", - ETH_PHY_ADDR_ETH01EVO, - ETH_PHY_CS_ETH01EVO, - ETH_PHY_IRQ_ETH01EVO, - ETH_PHY_RST_ETH01EVO, - ETH_PHY_SPI_SCK_ETH01EVO, - ETH_PHY_SPI_MISO_ETH01EVO, - ETH_PHY_SPI_MOSI_ETH01EVO, - ETH_PHY_TYPE_DM9051); - break; - case NetworkDeviceType::M5STACK_PoESP32_Unit: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "M5STACK PoESP32 Unit", - ETH_PHY_ADDR_W5500, - ETH_PHY_CS_M5_W5500, - ETH_PHY_IRQ_M5_W5500, - ETH_PHY_RST_M5_W5500, - ETH_PHY_SPI_SCK_M5_W5500, - ETH_PHY_SPI_MISO_M5_W5500, - ETH_PHY_SPI_MOSI_M5_W5500, - ETH_PHY_W5500); - break; - case NetworkDeviceType::CUSTOM: + case NetworkDeviceType::W5500: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "Generic W5500", + ETH_PHY_ADDR_W5500, + ETH_PHY_CS_GENERIC_W5500, + ETH_PHY_IRQ_GENERIC_W5500, + ETH_PHY_RST_GENERIC_W5500, + ETH_PHY_SPI_SCK_GENERIC_W5500, + ETH_PHY_SPI_MISO_GENERIC_W5500, + ETH_PHY_SPI_MOSI_GENERIC_W5500, + ETH_PHY_W5500); + break; + case NetworkDeviceType::W5500M5: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "M5Stack Atom POE", + ETH_PHY_ADDR_W5500, + ETH_PHY_CS_M5_W5500, + ETH_PHY_IRQ_M5_W5500, + ETH_PHY_RST_M5_W5500, + ETH_PHY_SPI_SCK_M5_W5500, + ETH_PHY_SPI_MISO_M5_W5500, + ETH_PHY_SPI_MOSI_M5_W5500, + ETH_PHY_W5500); + break; + case NetworkDeviceType::W5500M5S3: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "M5Stack Atom POE S3", + ETH_PHY_ADDR_W5500, + ETH_PHY_CS_M5_W5500_S3, + ETH_PHY_IRQ_M5_W5500, + ETH_PHY_RST_M5_W5500, + ETH_PHY_SPI_SCK_M5_W5500_S3, + ETH_PHY_SPI_MISO_M5_W5500_S3, + ETH_PHY_SPI_MOSI_M5_W5500_S3, + ETH_PHY_W5500); + break; + case NetworkDeviceType::ETH01_Evo: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "ETH01-Evo", + ETH_PHY_ADDR_ETH01EVO, + ETH_PHY_CS_ETH01EVO, + ETH_PHY_IRQ_ETH01EVO, + ETH_PHY_RST_ETH01EVO, + ETH_PHY_SPI_SCK_ETH01EVO, + ETH_PHY_SPI_MISO_ETH01EVO, + ETH_PHY_SPI_MOSI_ETH01EVO, + ETH_PHY_TYPE_DM9051); + break; + case NetworkDeviceType::M5STACK_PoESP32_Unit: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "M5STACK PoESP32 Unit", + ETH_PHY_ADDR_W5500, + ETH_PHY_CS_M5_W5500, + ETH_PHY_IRQ_M5_W5500, + ETH_PHY_RST_M5_W5500, + ETH_PHY_SPI_SCK_M5_W5500, + ETH_PHY_SPI_MISO_M5_W5500, + ETH_PHY_SPI_MOSI_M5_W5500, + ETH_PHY_W5500); + break; + case NetworkDeviceType::CUSTOM: + { + int custPHY = preferences->getInt(preference_network_custom_phy, 0); + + if(custPHY >= 1 && custPHY <= 3) { - int custPHY = preferences->getInt(preference_network_custom_phy, 0); + std::string custName; + eth_phy_type_t custEthtype; - if(custPHY >= 1 && custPHY <= 3) + switch(custPHY) { - std::string custName; - eth_phy_type_t custEthtype; - - switch(custPHY) - { - case 1: - custName = "Custom (W5500)"; - custEthtype = ETH_PHY_W5500; - break; - case 2: - custName = "Custom (DN9051)"; - custEthtype = ETH_PHY_DM9051; - break; - case 3: - custName = "Custom (KSZ8851SNL)"; - custEthtype = ETH_PHY_KSZ8851; - break; - default: - custName = "Custom (W5500)"; - custEthtype = ETH_PHY_W5500; - break; - } - - device = new EthernetDevice(hostname, preferences, ipConfiguration, custName, - preferences->getInt(preference_network_custom_addr, -1), - preferences->getInt(preference_network_custom_cs, -1), - preferences->getInt(preference_network_custom_irq, -1), - preferences->getInt(preference_network_custom_rst, -1), - preferences->getInt(preference_network_custom_sck, -1), - preferences->getInt(preference_network_custom_miso, -1), - preferences->getInt(preference_network_custom_mosi, -1), - custEthtype); - } -#if defined(CONFIG_IDF_TARGET_ESP32) - else if(custPHY >= 4 && custPHY <= 9) - { - int custCLKpref = preferences->getInt(preference_network_custom_clk, 0); - - std::string custName = NetworkUtil::GetCustomEthernetDeviceName(custPHY); - eth_phy_type_t custEthtype = NetworkUtil::GetCustomEthernetType(custPHY); - eth_clock_mode_t custCLK = NetworkUtil::GetCustomClock(custCLKpref); - - device = new EthernetDevice(hostname, preferences, ipConfiguration, custName, preferences->getInt(preference_network_custom_addr, -1), preferences->getInt(preference_network_custom_pwr, -1), preferences->getInt(preference_network_custom_mdc, -1), preferences->getInt(preference_network_custom_mdio, -1), custEthtype, custCLK); - } -#endif -#ifndef CONFIG_IDF_TARGET_ESP32H2 - else - { - device = new WifiDevice(hostname, preferences, ipConfiguration); - } -#endif - } - break; -#if defined(CONFIG_IDF_TARGET_ESP32) - case NetworkDeviceType::Olimex_LAN8720: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "Olimex (LAN8720)", ETH_PHY_ADDR_LAN8720, 12, ETH_PHY_MDC_LAN8720, ETH_PHY_MDIO_LAN8720, ETH_PHY_TYPE_LAN8720, ETH_CLOCK_GPIO17_OUT); - break; - case NetworkDeviceType::WT32_LAN8720: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "WT32-ETH01", 1, 16); - break; - case NetworkDeviceType::GL_S10: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "GL-S10", 1, 5, ETH_PHY_MDC_LAN8720, ETH_PHY_MDIO_LAN8720, ETH_PHY_IP101, ETH_CLOCK_GPIO0_IN); - break; - case NetworkDeviceType::LilyGO_T_ETH_POE: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "LilyGO T-ETH-POE", 0, -1, ETH_PHY_MDC_LAN8720, ETH_PHY_MDIO_LAN8720, ETH_PHY_TYPE_LAN8720, ETH_CLOCK_GPIO17_OUT); - break; -#endif -#ifndef CONFIG_IDF_TARGET_ESP32H2 - case NetworkDeviceType::WiFi: - device = new WifiDevice(hostname, preferences, ipConfiguration); - break; - default: - device = new WifiDevice(hostname, preferences, ipConfiguration); - break; -#else + case 1: + custName = "Custom (W5500)"; + custEthtype = ETH_PHY_W5500; + break; + case 2: + custName = "Custom (DN9051)"; + custEthtype = ETH_PHY_DM9051; + break; + case 3: + custName = "Custom (KSZ8851SNL)"; + custEthtype = ETH_PHY_KSZ8851; + break; default: - device = new EthernetDevice(hostname, preferences, ipConfiguration, "Custom (W5500)", - preferences->getInt(preference_network_custom_addr, -1), - preferences->getInt(preference_network_custom_cs, -1), - preferences->getInt(preference_network_custom_irq, -1), - preferences->getInt(preference_network_custom_rst, -1), - preferences->getInt(preference_network_custom_sck, -1), - preferences->getInt(preference_network_custom_miso, -1), - preferences->getInt(preference_network_custom_mosi, -1), - ETH_PHY_W5500); - break; + custName = "Custom (W5500)"; + custEthtype = ETH_PHY_W5500; + break; + } + + device = new EthernetDevice(hostname, preferences, ipConfiguration, custName, + preferences->getInt(preference_network_custom_addr, -1), + preferences->getInt(preference_network_custom_cs, -1), + preferences->getInt(preference_network_custom_irq, -1), + preferences->getInt(preference_network_custom_rst, -1), + preferences->getInt(preference_network_custom_sck, -1), + preferences->getInt(preference_network_custom_miso, -1), + preferences->getInt(preference_network_custom_mosi, -1), + custEthtype); + } +#if defined(CONFIG_IDF_TARGET_ESP32) + else if(custPHY >= 4 && custPHY <= 9) + { + int custCLKpref = preferences->getInt(preference_network_custom_clk, 0); + + std::string custName = NetworkUtil::GetCustomEthernetDeviceName(custPHY); + eth_phy_type_t custEthtype = NetworkUtil::GetCustomEthernetType(custPHY); + eth_clock_mode_t custCLK = NetworkUtil::GetCustomClock(custCLKpref); + + device = new EthernetDevice(hostname, preferences, ipConfiguration, custName, preferences->getInt(preference_network_custom_addr, -1), preferences->getInt(preference_network_custom_pwr, -1), preferences->getInt(preference_network_custom_mdc, -1), preferences->getInt(preference_network_custom_mdio, -1), custEthtype, custCLK); + } +#endif +#ifndef CONFIG_IDF_TARGET_ESP32H2 + else + { + device = new WifiDevice(hostname, preferences, ipConfiguration); + } +#endif + } + break; +#if defined(CONFIG_IDF_TARGET_ESP32) + case NetworkDeviceType::Olimex_LAN8720: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "Olimex (LAN8720)", ETH_PHY_ADDR_LAN8720, 12, ETH_PHY_MDC_LAN8720, ETH_PHY_MDIO_LAN8720, ETH_PHY_TYPE_LAN8720, ETH_CLOCK_GPIO17_OUT); + break; + case NetworkDeviceType::WT32_LAN8720: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "WT32-ETH01", 1, 16); + break; + case NetworkDeviceType::GL_S10: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "GL-S10", 1, 5, ETH_PHY_MDC_LAN8720, ETH_PHY_MDIO_LAN8720, ETH_PHY_IP101, ETH_CLOCK_GPIO0_IN); + break; + case NetworkDeviceType::LilyGO_T_ETH_POE: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "LilyGO T-ETH-POE", 0, -1, ETH_PHY_MDC_LAN8720, ETH_PHY_MDIO_LAN8720, ETH_PHY_TYPE_LAN8720, ETH_CLOCK_GPIO17_OUT); + break; +#endif +#ifndef CONFIG_IDF_TARGET_ESP32H2 + case NetworkDeviceType::WiFi: + device = new WifiDevice(hostname, preferences, ipConfiguration); + break; + default: + device = new WifiDevice(hostname, preferences, ipConfiguration); + break; +#else + default: + device = new EthernetDevice(hostname, preferences, ipConfiguration, "Custom (W5500)", + preferences->getInt(preference_network_custom_addr, -1), + preferences->getInt(preference_network_custom_cs, -1), + preferences->getInt(preference_network_custom_irq, -1), + preferences->getInt(preference_network_custom_rst, -1), + preferences->getInt(preference_network_custom_sck, -1), + preferences->getInt(preference_network_custom_miso, -1), + preferences->getInt(preference_network_custom_mosi, -1), + ETH_PHY_W5500); + break; #endif } diff --git a/src/util/NetworkUtil.cpp b/src/util/NetworkUtil.cpp index 603b4e8..1dc057d 100644 --- a/src/util/NetworkUtil.cpp +++ b/src/util/NetworkUtil.cpp @@ -6,50 +6,50 @@ NetworkDeviceType NetworkUtil::GetDeviceTypeFromPreference(int hardwareDetect, i { switch (hardwareDetect) { - case 1: + case 1: + return NetworkDeviceType::WiFi; + break; + case 2: + return NetworkDeviceType::W5500; + break; + case 3: + return NetworkDeviceType::W5500M5; + break; + case 4: + return NetworkDeviceType::Olimex_LAN8720; + break; + case 5: + return NetworkDeviceType::WT32_LAN8720; + break; + case 6: + return NetworkDeviceType::M5STACK_PoESP32_Unit; + break; + case 7: + return NetworkDeviceType::LilyGO_T_ETH_POE; + break; + case 8: + return NetworkDeviceType::GL_S10; + break; + case 9: + return NetworkDeviceType::ETH01_Evo; + break; + case 10: + return NetworkDeviceType::W5500M5S3; + break; + case 11: + if(customPhy> 0) + { + return NetworkDeviceType::CUSTOM; + } + else + { return NetworkDeviceType::WiFi; - break; - case 2: - return NetworkDeviceType::W5500; - break; - case 3: - return NetworkDeviceType::W5500M5; - break; - case 4: - return NetworkDeviceType::Olimex_LAN8720; - break; - case 5: - return NetworkDeviceType::WT32_LAN8720; - break; - case 6: - return NetworkDeviceType::M5STACK_PoESP32_Unit; - break; - case 7: - return NetworkDeviceType::LilyGO_T_ETH_POE; - break; - case 8: - return NetworkDeviceType::GL_S10; - break; - case 9: - return NetworkDeviceType::ETH01_Evo; - break; - case 10: - return NetworkDeviceType::W5500M5S3; - break; - case 11: - if(customPhy> 0) - { - return NetworkDeviceType::CUSTOM; - } - else - { - return NetworkDeviceType::WiFi; - } - break; - default: - Log->println(F("Unknown hardware selected, falling back to Wi-Fi.")); - return NetworkDeviceType::WiFi; - break; + } + break; + default: + Log->println(F("Unknown hardware selected, falling back to Wi-Fi.")); + return NetworkDeviceType::WiFi; + break; } } @@ -57,20 +57,20 @@ std::string NetworkUtil::GetCustomEthernetDeviceName(int custPHY) { switch(custPHY) { - case 4: - return "Custom (LAN8720)"; - case 5: - return"Custom (RTL8201)"; - case 6: - return "Custom (TLK110)"; - case 7: - return "Custom (DP83848)"; - case 8: - return "Custom (KSZ8041)"; - case 9: - return "Custom (KSZ8081)"; - default: - return"Custom (LAN8720)"; + case 4: + return "Custom (LAN8720)"; + case 5: + return"Custom (RTL8201)"; + case 6: + return "Custom (TLK110)"; + case 7: + return "Custom (DP83848)"; + case 8: + return "Custom (KSZ8041)"; + case 9: + return "Custom (KSZ8081)"; + default: + return"Custom (LAN8720)"; } } @@ -79,27 +79,27 @@ eth_phy_type_t NetworkUtil::GetCustomEthernetType(int custPHY) { switch(custPHY) { - case 4: - return ETH_PHY_TYPE_LAN8720; - break; - case 5: - return ETH_PHY_RTL8201; - break; - case 6: - return ETH_PHY_TLK110; - break; - case 7: - return ETH_PHY_DP83848; - break; - case 8: - return ETH_PHY_KSZ8041; - break; - case 9: - return ETH_PHY_KSZ8081; - break; - default: - return ETH_PHY_TYPE_LAN8720; - break; + case 4: + return ETH_PHY_TYPE_LAN8720; + break; + case 5: + return ETH_PHY_RTL8201; + break; + case 6: + return ETH_PHY_TLK110; + break; + case 7: + return ETH_PHY_DP83848; + break; + case 8: + return ETH_PHY_KSZ8041; + break; + case 9: + return ETH_PHY_KSZ8081; + break; + default: + return ETH_PHY_TYPE_LAN8720; + break; } } @@ -107,18 +107,18 @@ eth_clock_mode_t NetworkUtil::GetCustomClock(int custCLKpref) { switch(custCLKpref) { - case 0: - return ETH_CLOCK_GPIO0_IN; - break; - case 2: - return ETH_CLOCK_GPIO16_OUT; - break; - case 3: - return ETH_CLOCK_GPIO17_OUT; - break; - default: - return ETH_CLOCK_GPIO17_OUT; - break; + case 0: + return ETH_CLOCK_GPIO0_IN; + break; + case 2: + return ETH_CLOCK_GPIO16_OUT; + break; + case 3: + return ETH_CLOCK_GPIO17_OUT; + break; + default: + return ETH_CLOCK_GPIO17_OUT; + break; } } #endif \ No newline at end of file From d44485fb3f97c70f33c277616c2add4d4ba6b1d0 Mon Sep 17 00:00:00 2001 From: technyon Date: Sun, 20 Oct 2024 14:04:44 +0200 Subject: [PATCH 26/30] exclude lib in astylerc --- .astylerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.astylerc b/.astylerc index d31a299..9bb5e7b 100644 --- a/.astylerc +++ b/.astylerc @@ -1,2 +1,3 @@ --style=allman --add-braces +--exclude=lib From 1d079a9aad1e36afa61fad574ff986ea0759dbf0 Mon Sep 17 00:00:00 2001 From: technyon Date: Sun, 20 Oct 2024 14:07:16 +0200 Subject: [PATCH 27/30] prevent astyle from writing backup files --- .astylerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.astylerc b/.astylerc index 9bb5e7b..d154129 100644 --- a/.astylerc +++ b/.astylerc @@ -1,3 +1,5 @@ --style=allman --add-braces --exclude=lib +--suffix=none + From 9ca377870f63fd5991ba788a6ac3a32157531ed0 Mon Sep 17 00:00:00 2001 From: technyon Date: Sun, 20 Oct 2024 14:25:04 +0200 Subject: [PATCH 28/30] add espMillis() to replace esp_timer_get_time() / 1000 --- clion/CMakeLists.txt | 1 + src/EspMillis.h | 7 +++++ src/NukiNetwork.cpp | 8 +++--- src/NukiNetwork.h | 1 + src/NukiNetworkLock.h | 1 + src/NukiNetworkOpener.cpp | 4 +-- src/NukiNetworkOpener.h | 1 + src/NukiOpenerWrapper.cpp | 18 ++++++------- src/NukiWrapper.cpp | 38 +++++++++++++-------------- src/NukiWrapper.h | 1 + src/WebCfgServer.cpp | 4 +-- src/main.cpp | 11 ++++---- src/networkDevices/EthernetDevice.cpp | 6 ++--- src/networkDevices/NetworkDevice.h | 1 + src/networkDevices/WifiDevice.cpp | 8 +++--- 15 files changed, 62 insertions(+), 48 deletions(-) create mode 100644 src/EspMillis.h diff --git a/clion/CMakeLists.txt b/clion/CMakeLists.txt index 1a2e7bf..508f2a0 100644 --- a/clion/CMakeLists.txt +++ b/clion/CMakeLists.txt @@ -51,6 +51,7 @@ set(SRCFILES ../src/util/NetworkDeviceInstantiator.cpp ../src/NukiOfficial.cpp ../src/NukiPublisher.cpp + ../src/EspMillis.h ) file(GLOB_RECURSE SRCFILESREC diff --git a/src/EspMillis.h b/src/EspMillis.h new file mode 100644 index 0000000..95c7c0e --- /dev/null +++ b/src/EspMillis.h @@ -0,0 +1,7 @@ +#pragma once +#include + +inline int64_t espMillis() +{ + return esp_timer_get_time() / 1000; +} \ No newline at end of file diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index 9eec5bb..dd73ca4 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -359,7 +359,7 @@ void NukiNetwork::readSettings() bool NukiNetwork::update() { - int64_t ts = (esp_timer_get_time() / 1000); + int64_t ts = espMillis(); _device->update(); if(!_mqttEnabled || _device->isApOpen()) @@ -375,7 +375,7 @@ bool NukiNetwork::update() { forceEnableWebServer = true; } - if(_restartOnDisconnect && (esp_timer_get_time() / 1000) > 60000) + if(_restartOnDisconnect && espMillis() > 60000) { restartEsp(RestartReason::RestartOnDisconnectWatchdog); } @@ -604,7 +604,7 @@ bool NukiNetwork::update() { uint8_t pin = gpioTs.first; int64_t ts = gpioTs.second; - if(ts != 0 && (((esp_timer_get_time() / 1000) - ts) >= GPIO_DEBOUNCE_TIME)) + if(ts != 0 && ((espMillis() - ts) >= GPIO_DEBOUNCE_TIME)) { _gpioTs[pin] = 0; @@ -814,7 +814,7 @@ void NukiNetwork::parseGpioTopics(char* topic, int topic_len, char* data, int da void NukiNetwork::gpioActionCallback(const GpioAction &action, const int &pin) { - _gpioTs[pin] = (esp_timer_get_time() / 1000); + _gpioTs[pin] = espMillis(); } void NukiNetwork::disableAutoRestarts() diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index 79872d7..6908686 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -7,6 +7,7 @@ #include "networkDevices/IPConfiguration.h" #include "enums/NetworkDeviceType.h" #include "util/NetworkUtil.h" +#include "EspMillis.h" #ifndef NUKI_HUB_UPDATER #include "MqttReceiver.h" diff --git a/src/NukiNetworkLock.h b/src/NukiNetworkLock.h index 505d8b7..fd045a1 100644 --- a/src/NukiNetworkLock.h +++ b/src/NukiNetworkLock.h @@ -14,6 +14,7 @@ #include "LockActionResult.h" #include "NukiOfficial.h" #include "NukiPublisher.h" +#include "EspMillis.h" class NukiNetworkLock : public MqttReceiver { diff --git a/src/NukiNetworkOpener.cpp b/src/NukiNetworkOpener.cpp index 1d34d46..71069c1 100644 --- a/src/NukiNetworkOpener.cpp +++ b/src/NukiNetworkOpener.cpp @@ -131,7 +131,7 @@ void NukiNetworkOpener::initialize() void NukiNetworkOpener::update() { - if(_resetRingStateTs != 0 && (esp_timer_get_time() / 1000) >= _resetRingStateTs) + if(_resetRingStateTs != 0 && espMillis() >= _resetRingStateTs) { _resetRingStateTs = 0; publishString(mqtt_topic_lock_binary_ring, "standby", true); @@ -441,7 +441,7 @@ void NukiNetworkOpener::publishRing(const bool locked) } publishString(mqtt_topic_lock_binary_ring, "ring", true); - _resetRingStateTs = (esp_timer_get_time() / 1000) + 2000; + _resetRingStateTs = espMillis() + 2000; } void NukiNetworkOpener::publishState(NukiOpener::OpenerState lockState) diff --git a/src/NukiNetworkOpener.h b/src/NukiNetworkOpener.h index 5b9e394..08aafd3 100644 --- a/src/NukiNetworkOpener.h +++ b/src/NukiNetworkOpener.h @@ -6,6 +6,7 @@ #include "NukiConstants.h" #include "NukiOpenerConstants.h" #include "NukiNetworkLock.h" +#include "EspMillis.h" class NukiNetworkOpener : public MqttReceiver { diff --git a/src/NukiOpenerWrapper.cpp b/src/NukiOpenerWrapper.cpp index 69ee1ae..56b7733 100644 --- a/src/NukiOpenerWrapper.cpp +++ b/src/NukiOpenerWrapper.cpp @@ -197,7 +197,7 @@ void NukiOpenerWrapper::update() } int64_t lastReceivedBeaconTs = _nukiOpener.getLastReceivedBeaconTs(); - int64_t ts = (esp_timer_get_time() / 1000); + int64_t ts = espMillis(); uint8_t queryCommands = _network->queryCommands(); if(_restartBeaconTimeout > 0 && @@ -435,7 +435,7 @@ void NukiOpenerWrapper::updateKeyTurnerState() postponeBleWatchdog(); if(_retryLockstateCount < _nrOfRetries + 1) { - _nextLockStateUpdateTs = (esp_timer_get_time() / 1000) + _retryDelay; + _nextLockStateUpdateTs = espMillis() + _retryDelay; } return; } @@ -638,7 +638,7 @@ void NukiOpenerWrapper::updateConfig() { ++_retryConfigCount; Log->println(F("Invalid/Unexpected opener config and/or advanced config recieved, retrying in 10 seconds")); - int64_t ts = (esp_timer_get_time() / 1000); + int64_t ts = espMillis(); _nextConfigUpdateTs = ts + 10000; } } @@ -675,7 +675,7 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) printCommandResult(result); if(result == Nuki::CmdResult::Success) { - _waitAuthLogUpdateTs = (esp_timer_get_time() / 1000) + 5000; + _waitAuthLogUpdateTs = espMillis() + 5000; delay(100); std::list log; @@ -760,7 +760,7 @@ void NukiOpenerWrapper::updateKeypad(bool retrieved) printCommandResult(result); if(result == Nuki::CmdResult::Success) { - _waitKeypadUpdateTs = (esp_timer_get_time() / 1000) + 5000; + _waitKeypadUpdateTs = espMillis() + 5000; } } else @@ -837,7 +837,7 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) printCommandResult(result); if(result == Nuki::CmdResult::Success) { - _waitTimeControlUpdateTs = (esp_timer_get_time() / 1000) + 5000; + _waitTimeControlUpdateTs = espMillis() + 5000; } } else @@ -951,7 +951,7 @@ void NukiOpenerWrapper::updateAuth(bool retrieved) void NukiOpenerWrapper::postponeBleWatchdog() { - _disableBleWatchdogTs = (esp_timer_get_time() / 1000) + 15000; + _disableBleWatchdogTs = espMillis() + 15000; } NukiOpener::LockAction NukiOpenerWrapper::lockActionToEnum(const char *str) @@ -2284,7 +2284,7 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) jsonResult["general"] = "noChange"; } - _nextConfigUpdateTs = (esp_timer_get_time() / 1000) + 300; + _nextConfigUpdateTs = espMillis() + 300; serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); _network->publishConfigCommandResult(_resbuf); @@ -3283,7 +3283,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) _network->publishTimeControlCommandResult(resultStr); } - _nextConfigUpdateTs = (esp_timer_get_time() / 1000) + 300; + _nextConfigUpdateTs = espMillis() + 300; } else { diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index b710dc8..8c7e2ef 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -275,7 +275,7 @@ void NukiWrapper::update() } int64_t lastReceivedBeaconTs = _nukiLock.getLastReceivedBeaconTs(); - int64_t ts = (esp_timer_get_time() / 1000); + int64_t ts = espMillis(); uint8_t queryCommands = _network->queryCommands(); if(_restartBeaconTimeout > 0 && @@ -521,7 +521,7 @@ void NukiWrapper::updateKeyTurnerState() Log->print(F("Query lock state retrying in ")); Log->print(_retryDelay); Log->println("ms"); - _nextLockStateUpdateTs = (esp_timer_get_time() / 1000) + _retryDelay; + _nextLockStateUpdateTs = espMillis() + _retryDelay; } return; } @@ -532,7 +532,7 @@ void NukiWrapper::updateKeyTurnerState() if(lockState != _lastKeyTurnerState.lockState) { - _statusUpdatedTs = esp_timer_get_time() / 1000; + _statusUpdatedTs = espMillis(); } if(lockState == NukiLock::LockState::Locked || @@ -550,7 +550,7 @@ void NukiWrapper::updateKeyTurnerState() updateGpioOutputs(); } - else if(!_nukiOfficial->getOffConnected() && (esp_timer_get_time() / 1000) < _statusUpdatedTs + 10000) + else if(!_nukiOfficial->getOffConnected() && espMillis() < _statusUpdatedTs + 10000) { _statusUpdated = true; Log->println(F("Lock: Keep updating status on intermediate lock state")); @@ -723,7 +723,7 @@ void NukiWrapper::updateConfig() { ++_retryConfigCount; Log->println(F("Invalid/Unexpected lock config and/or advanced config recieved, retrying in 10 seconds")); - int64_t ts = (esp_timer_get_time() / 1000); + int64_t ts = espMillis(); _nextConfigUpdateTs = ts + 10000; } } @@ -758,7 +758,7 @@ void NukiWrapper::updateAuthData(bool retrieved) printCommandResult(result); if(result == Nuki::CmdResult::Success) { - _waitAuthLogUpdateTs = (esp_timer_get_time() / 1000) + 5000; + _waitAuthLogUpdateTs = espMillis() + 5000; delay(100); std::list log; @@ -842,7 +842,7 @@ void NukiWrapper::updateKeypad(bool retrieved) printCommandResult(result); if(result == Nuki::CmdResult::Success) { - _waitKeypadUpdateTs = (esp_timer_get_time() / 1000) + 5000; + _waitKeypadUpdateTs = espMillis() + 5000; } } else @@ -918,7 +918,7 @@ void NukiWrapper::updateTimeControl(bool retrieved) printCommandResult(result); if(result == Nuki::CmdResult::Success) { - _waitTimeControlUpdateTs = (esp_timer_get_time() / 1000) + 5000; + _waitTimeControlUpdateTs = espMillis() + 5000; } } else @@ -1032,7 +1032,7 @@ void NukiWrapper::updateAuth(bool retrieved) void NukiWrapper::postponeBleWatchdog() { - _disableBleWatchdogTs = (esp_timer_get_time() / 1000) + 15000; + _disableBleWatchdogTs = espMillis() + 15000; } NukiLock::LockAction NukiWrapper::lockActionToEnum(const char *str) @@ -1118,7 +1118,7 @@ LockActionResult NukiWrapper::onLockActionReceived(const char *value) { if(_preferences->getBool(preference_official_hybrid_actions, false)) { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _nukiOfficial->setOffCommandExecutedTs(espMillis() + 2000); _offCommand = action; _network->publishOffAction((int)action); } @@ -2349,7 +2349,7 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) jsonResult["general"] = "noChange"; } - _nextConfigUpdateTs = (esp_timer_get_time() / 1000) + 300; + _nextConfigUpdateTs = espMillis() + 300; serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); _network->publishConfigCommandResult(_resbuf); @@ -2394,7 +2394,7 @@ void NukiWrapper::onGpioActionReceived(const GpioAction &action, const int &pin) } else { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _nukiOfficial->setOffCommandExecutedTs(espMillis() + 2000); _offCommand = NukiLock::LockAction::Lock; _network->publishOffAction(2); } @@ -2406,7 +2406,7 @@ void NukiWrapper::onGpioActionReceived(const GpioAction &action, const int &pin) } else { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _nukiOfficial->setOffCommandExecutedTs(espMillis() + 2000); _offCommand = NukiLock::LockAction::Unlock; _network->publishOffAction(1); } @@ -2418,7 +2418,7 @@ void NukiWrapper::onGpioActionReceived(const GpioAction &action, const int &pin) } else { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _nukiOfficial->setOffCommandExecutedTs(espMillis() + 2000); _offCommand = NukiLock::LockAction::Unlatch; _network->publishOffAction(3); } @@ -2430,7 +2430,7 @@ void NukiWrapper::onGpioActionReceived(const GpioAction &action, const int &pin) } else { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _nukiOfficial->setOffCommandExecutedTs(espMillis() + 2000); _offCommand = NukiLock::LockAction::LockNgo; _network->publishOffAction(4); } @@ -2442,7 +2442,7 @@ void NukiWrapper::onGpioActionReceived(const GpioAction &action, const int &pin) } else { - _nukiOfficial->setOffCommandExecutedTs((esp_timer_get_time() / 1000) + 2000); + _nukiOfficial->setOffCommandExecutedTs(espMillis() + 2000); _offCommand = NukiLock::LockAction::LockNgoUnlatch; _network->publishOffAction(5); } @@ -3398,7 +3398,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) _network->publishTimeControlCommandResult(resultStr); } - _nextConfigUpdateTs = (esp_timer_get_time() / 1000) + 300; + _nextConfigUpdateTs = espMillis() + 300; } else { @@ -3967,7 +3967,7 @@ void NukiWrapper::notify(Nuki::EventType eventType) { if(!_nukiOfficial->getOffConnected()) { - if(_nukiOfficial->getOffEnabled() && _intervalHybridLockstate > 0 && (esp_timer_get_time() / 1000) > (_intervalHybridLockstate * 1000)) + if(_nukiOfficial->getOffEnabled() && _intervalHybridLockstate > 0 && espMillis() > (_intervalHybridLockstate * 1000)) { Log->println("OffKeyTurnerStatusUpdated"); _statusUpdated = true; @@ -3978,7 +3978,7 @@ void NukiWrapper::notify(Nuki::EventType eventType) { Log->println("KeyTurnerStatusUpdated"); _statusUpdated = true; - _statusUpdatedTs = esp_timer_get_time() / 1000; + _statusUpdatedTs = espMillis(); _network->publishStatusUpdated(_statusUpdated); } } diff --git a/src/NukiWrapper.h b/src/NukiWrapper.h index ee695f9..1a29641 100644 --- a/src/NukiWrapper.h +++ b/src/NukiWrapper.h @@ -9,6 +9,7 @@ #include "LockActionResult.h" #include "NukiDeviceId.h" #include "NukiOfficial.h" +#include "EspMillis.h" class NukiWrapper : public Nuki::SmartlockEventHandler { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index d4cf0d0..0c97b2d 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -1012,7 +1012,7 @@ esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& f return(ESP_FAIL); } - _otaStartTs = esp_timer_get_time() / 1000; + _otaStartTs = espMillis(); esp_task_wdt_config_t twdt_config = { .timeout_ms = 30000, @@ -3985,7 +3985,7 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request) response.print("\nUpdater build date: "); response.print(_preferences->getString(preference_updater_date, "")); response.print("\nUptime (min): "); - response.print(esp_timer_get_time() / 1000 / 1000 / 60); + response.print(espMillis() / 1000 / 60); response.print("\nConfig version: "); response.print(_preferences->getInt(preference_config_version)); response.print("\nLast restart reason FW: "); diff --git a/src/main.cpp b/src/main.cpp index 173acd6..0d2cbf1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include "esp_https_ota.h" #include "esp_task_wdt.h" #include "Config.h" +#include "EspMillis.h" #ifndef NUKI_HUB_UPDATER #include "NukiWrapper.h" @@ -131,7 +132,7 @@ void networkTask(void *pvParameters) } while(true) { - int64_t ts = (esp_timer_get_time() / 1000); + int64_t ts = espMillis(); if(ts > 120000 && ts < 125000) { if(bootloopCounter > 0) @@ -162,10 +163,10 @@ void networkTask(void *pvParameters) } #endif - if((esp_timer_get_time() / 1000) - networkLoopTs > 120000) + if(espMillis() - networkLoopTs > 120000) { Log->println("networkTask is running"); - networkLoopTs = esp_timer_get_time() / 1000; + networkLoopTs = espMillis(); } esp_task_wdt_reset(); @@ -212,10 +213,10 @@ void nukiTask(void *pvParameters) nukiOpener->update(); } - if((esp_timer_get_time() / 1000) - nukiLoopTs > 120000) + if(espMillis() - nukiLoopTs > 120000) { Log->println("nukiTask is running"); - nukiLoopTs = esp_timer_get_time() / 1000; + nukiLoopTs = espMillis(); } esp_task_wdt_reset(); diff --git a/src/networkDevices/EthernetDevice.cpp b/src/networkDevices/EthernetDevice.cpp index 1a0c78b..4ec1070 100644 --- a/src/networkDevices/EthernetDevice.cpp +++ b/src/networkDevices/EthernetDevice.cpp @@ -87,7 +87,7 @@ void EthernetDevice::initialize() criticalEthFailure = false; if(!_ipConfiguration->dhcpEnabled()) { - _checkIpTs = (esp_timer_get_time() / 1000) + 2000; + _checkIpTs = espMillis() + 2000; } } #endif @@ -126,7 +126,7 @@ void EthernetDevice::update() { Log->println(F("ETH Set static IP")); ETH.config(_ipConfiguration->ipAddress(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet(), _ipConfiguration->dnsServer()); - _checkIpTs = (esp_timer_get_time() / 1000) + 2000; + _checkIpTs = espMillis() + 2000; } else { @@ -213,7 +213,7 @@ bool EthernetDevice::isApOpen() void EthernetDevice::onDisconnected() { - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + if(_preferences->getBool(preference_restart_on_disconnect, false) && (espMillis() > 60000)) { restartEsp(RestartReason::RestartOnDisconnectWatchdog); } diff --git a/src/networkDevices/NetworkDevice.h b/src/networkDevices/NetworkDevice.h index 7c32fc4..abbade2 100644 --- a/src/networkDevices/NetworkDevice.h +++ b/src/networkDevices/NetworkDevice.h @@ -1,5 +1,6 @@ #pragma once #include "IPConfiguration.h" +#include "../EspMillis.h" class NetworkDevice { diff --git a/src/networkDevices/WifiDevice.cpp b/src/networkDevices/WifiDevice.cpp index 46513af..8625db3 100644 --- a/src/networkDevices/WifiDevice.cpp +++ b/src/networkDevices/WifiDevice.cpp @@ -235,7 +235,7 @@ bool WifiDevice::connect() { Log->print("No network found with SSID: "); Log->println(ssid); - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + if(_preferences->getBool(preference_restart_on_disconnect, false) && (espMillis() > 60000)) { restartEsp(RestartReason::RestartOnDisconnectWatchdog); } @@ -287,7 +287,7 @@ bool WifiDevice::connect() if (status != WL_CONNECTED) { - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + if(_preferences->getBool(preference_restart_on_disconnect, false) && (espMillis() > 60000)) { restartEsp(RestartReason::RestartOnDisconnectWatchdog); _connecting = false; @@ -349,7 +349,7 @@ void WifiDevice::onDisconnected() if(_connected) { _connected = false; - _disconnectTs = (esp_timer_get_time() / 1000); + _disconnectTs = espMillis(); Log->println(F("Wi-Fi disconnected")); //QUICK RECONNECT @@ -377,7 +377,7 @@ void WifiDevice::onDisconnected() if(!isConnected()) { - if(_preferences->getBool(preference_restart_on_disconnect, false) && ((esp_timer_get_time() / 1000) > 60000)) + if(_preferences->getBool(preference_restart_on_disconnect, false) && (espMillis() > 60000)) { restartEsp(RestartReason::RestartOnDisconnectWatchdog); } From 8997ac49d2fe062dd97e6c161b456eb79f37ef3e Mon Sep 17 00:00:00 2001 From: technyon Date: Sun, 20 Oct 2024 14:38:28 +0200 Subject: [PATCH 29/30] fix building updater --- src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 0d2cbf1..b71e6f9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,7 +7,6 @@ #include "esp_https_ota.h" #include "esp_task_wdt.h" #include "Config.h" -#include "EspMillis.h" #ifndef NUKI_HUB_UPDATER #include "NukiWrapper.h" @@ -20,6 +19,8 @@ #include "Logger.h" #include "PreferencesKeys.h" #include "RestartReason.h" +#include "EspMillis.h" + /* #ifdef DEBUG_NUKIHUB #include @@ -52,6 +53,7 @@ int64_t restartTs = ((2^64) - (5 * 1000 * 60000)) / 1000; #include "../../src/PreferencesKeys.h" #include "../../src/RestartReason.h" #include "../../src/NukiNetwork.h" +#include "../../src/EspMillis.h" int64_t restartTs = 10 * 1000 * 60000; From acfed7a992da302357d1a51ce310c00e25cc3b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Ole=20Sch=C3=BCmann?= Date: Sun, 20 Oct 2024 23:11:53 +0700 Subject: [PATCH 30/30] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 181bbb1..3c25bde 100644 --- a/README.md +++ b/README.md @@ -675,9 +675,9 @@ Examples: ## GPIO lock control (optional) -The lock can be controlled via GPIO.
    +The lock can be controlled via GPIO. To trigger actions, a connection to ground has to be present for at lease 300ms (or to +3.3V for "General input (pull-down)").

    -To enable GPIO control, go the the "GPIO Configuration" page where each GPIO can be configured for a specific role: +To enable GPIO control, go the the "GPIO Configuration" page where each GPIO can e configured for a specific role: - Disabled: The GPIO is disabled - Input: Lock: When connect to Ground, a lock command is sent to the lock - Input: Unlock: When connect to Ground, an unlock command is sent to the lock
    +

    Basic Request Examples

    +
    + +

    Static Serving

    +

    + + +

    +

    Text File

    + +

    Simple POST Form

    +
    + + +
    + + +
    + +
    + +

    Basic File Upload

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

    Multipart POST Form

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

    Websocket Demo

    + + + +
    + +
    + + + +

    EventSource Demo

    + +
    + +
    + + +
    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 + + PSYCHICHTTP + + + + + + + + + +

    +

    PsychicHttp OTA Update

    +
    + +

    This page allows to test OTA update with PsychicHttp, and file naming convention below : +

      +
    • "*code.bin" for code (firmware) update, eg "v1_code.bin"
    • +
    • "*littlefs.bin" for data (littlefs) update, eg "v1_littlefs.bin"
    • +
    +
    + +
    +
    + +
    +
    + +
    +

    Update must be done for each of the files provided (code, littlefs). Once updates are made, the ESP32 can be restarted. + +

    +
    + + + + \ No newline at end of file diff --git a/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate1.png b/lib/PsychicHttp/examples/arduino/arduino_ota/images/otaupdate1.png new file mode 100644 index 0000000000000000000000000000000000000000..c47e4e94d33d763192220a239951a1161d17bae8 GIT binary patch literal 60478 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
    %>f3PU-PW+IlK+a!K0Qp)u z7}TyBqGjCRN8D|v?H6vp9CbD`DgR&=)Hs#9uf1^krGiH3Ep_48FR|Pw2>aafT#xgu z=gVQEAx>iDrpu`DzWFe6CC{dKP~GfUX0TB{z)691{GIWOv3G>og%}%_Y>7{hH5d*P zTW+j!%_7lIw=h9(7)7st*iA`U6XKZ&DFeu7@&ti;#{4i-dOSl2_U?J2cYD@J{@&s$+iNBf2Ht<=y{ zQTeTj66O!n9u+E@_aw~|OYsUz0||jYlX?s^Ogp1@3f6}Y-DOY%N z4LE%U4}WZJv~$4Ex%4F*IVbCC#7K|5>i~@64x9>;8V)l4G$Z`{V8X*wfE1MV+E=Y9 z!8crjKdbKgZUMMLZ*4Yq%wM>{Py}53Re0Y8+iIABzs(Xgy=~gPO98xcIT5|t#L+L% z1NObC>a%Eb6C|3xx%o{D>OZ%iirF^A<3@FIsmIOd77`ZR=E<>TAK4IFgs!z}VHT9}_1@Yc&&Qio0dF6o1pi(^ zaL#J0qV>$zUke1)pIc$XZXljp3bN?q8ba`G$a{D z@ZBu92+4Z+rO)-%ZMt+aPM;2*PlVr}iWIxlW?5UEkkPQKjU2K3*=*hmn#FP*pU-Sr z-t>fHncYh@g{W=4R{jmk%o^b9=(bEhoDAT3iomk1h`0&-h&5&g35-F43I{6D#Bnam zp8J&2<|n;Yx4oua`a_cW>(DmszrlcAMjX+3tle!!TQ5 zt&fE!O+JPGyF-5CoP4r>qfIJjYX;3&f6AQz(;7=S@?hc#&4kOr0%%9dqn;G;0%06qhzW$_q|jvpp*N( zAcDGl!m-mpLPfpJ%QZM%{E3mK=fjIJDWlfTf@8IOeMpp^K}HEvkAx+JM8Ph_NPM$J zv8n-<*5MYt?-9HjVtiNhHBPs0oxk2Pz3#hB-xb<)18>twSGQWNg&T-b2`CXDR)?_uTiHTkFXgMnCd| zZ7zNt`54gq+2bTp@