From 7cf540126ce36108fadc1505868b5151e0e2402b Mon Sep 17 00:00:00 2001 From: Linar Yusupov Date: Fri, 23 Feb 2024 19:38:23 +0300 Subject: [PATCH] Academy: build of WebUI for 'Uno R4 WiFi' target --- .github/workflows/main.yml | 1 + .../source/SoftRF/src/driver/WiFi.cpp | 2 - .../source/SoftRF/src/platform/ESP32.cpp | 2 - .../source/SoftRF/src/platform/ESP32.h | 6 +- .../source/SoftRF/src/platform/ESP8266.cpp | 2 - .../source/SoftRF/src/platform/ESP8266.h | 5 +- .../source/SoftRF/src/platform/RA4M1.h | 6 +- .../source/SoftRF/src/platform/RP2040.cpp | 1 - .../source/SoftRF/src/platform/RP2040.h | 7 +- .../firmware/source/SoftRF/src/ui/Web.cpp | 22 +- .../libraries/WiFiWebServer/.codespellrc | 7 + .../.github/ISSUE_TEMPLATE/bug_report.md | 97 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + .../WiFiWebServer/.github/dependabot.yml | 10 + .../libraries/WiFiWebServer/.github/stale.yml | 31 + .../.github/workflows/auto-github-actions.yml | 12 + .../.github/workflows/check-arduino.yml | 28 + .../.github/workflows/report-size-deltas.yml | 16 + .../.github/workflows/spell-check.yml | 22 + .../source/libraries/WiFiWebServer/.gitignore | 32 + .../libraries/WiFiWebServer/CONTRIBUTING.md | 78 + .../source/libraries/WiFiWebServer/LICENSE | 21 + .../source/libraries/WiFiWebServer/README.md | 1903 +++++++++++++++++ .../libraries/WiFiWebServer/changelog.md | 247 +++ .../libraries/WiFiWebServer/keywords.txt | 211 ++ .../libraries/WiFiWebServer/library.json | 69 + .../WiFiWebServer/library.properties | 12 + .../WiFiWebServer/pics/AdvancedWebServer.png | Bin 0 -> 29330 bytes .../pics/AdvancedWebServer_ESP32_C3.png | Bin 0 -> 23053 bytes .../pics/AdvancedWebServer_ESP32_S3.png | Bin 0 -> 26267 bytes .../AdvancedWebServer_NanoRP2040Connect.png | Bin 0 -> 23668 bytes .../pics/AdvancedWebServer_Portenta_H7.png | Bin 0 -> 34311 bytes ...dWebServer_WiFiMulti_NanoRP2040Connect.png | Bin 0 -> 43032 bytes .../AdvancedWebServer_WiFiMulti_RP2040W.png | Bin 0 -> 43433 bytes .../pics/AdvancedWebServer_v1.0.6.png | Bin 0 -> 19291 bytes .../WiFiWebServer/platformio/platformio.ini | 374 ++++ .../WiFiWebServer/src/Parsing-impl.h | 1473 +++++++++++++ .../source/libraries/WiFiWebServer/src/Uri.h | 75 + .../WiFiWebServer/src/WiFiHttpClient.h | 54 + .../WiFiWebServer/src/WiFiWebServer-impl.h | 1365 ++++++++++++ .../WiFiWebServer/src/WiFiWebServer.h | 57 + .../WiFiWebServer/src/WiFiWebServer.hpp | 706 ++++++ .../src/WiFi_HTTPClient/WiFi_HttpClient.cpp | 1061 +++++++++ .../src/WiFi_HTTPClient/WiFi_HttpClient.h | 576 +++++ .../src/WiFi_HTTPClient/WiFi_URLEncoder.cpp | 107 + .../src/WiFi_HTTPClient/WiFi_URLEncoder.h | 69 + .../WiFi_HTTPClient/WiFi_WebSocketClient.cpp | 459 ++++ .../WiFi_HTTPClient/WiFi_WebSocketClient.h | 144 ++ .../WiFiWebServer/src/libb64/base64.cpp | 87 + .../WiFiWebServer/src/libb64/base64.h | 28 + .../WiFiWebServer/src/libb64/cdecode.c | 147 ++ .../WiFiWebServer/src/libb64/cdecode.h | 54 + .../WiFiWebServer/src/libb64/cencode.c | 149 ++ .../WiFiWebServer/src/libb64/cencode.h | 57 + .../src/utility/ESP_RequestHandlersImpl.h | 338 +++ .../src/utility/RequestHandler.h | 143 ++ .../src/utility/RequestHandlersImpl.h | 249 +++ .../WiFiWebServer/src/utility/RingBuffer.cpp | 138 ++ .../WiFiWebServer/src/utility/RingBuffer.h | 68 + .../WiFiWebServer/src/utility/WiFiDebug.h | 116 + .../src/utility/esp_detail/mimetable.cpp | 151 ++ .../src/utility/esp_detail/mimetable.h | 95 + .../WiFiWebServer/src/utility/mimetable.h | 110 + .../WiFiWebServer/utils/astyle_library.conf | 70 + .../libraries/WiFiWebServer/utils/restyle.sh | 6 + .../libraries/functional-vlpp/.gitignore | 32 + .../libraries/functional-vlpp/CONTRIBUTING.md | 52 + .../source/libraries/functional-vlpp/LICENSE | 22 + .../libraries/functional-vlpp/README.md | 441 ++++ .../libraries/functional-vlpp/keywords.txt | 81 + .../libraries/functional-vlpp/library.json | 22 + .../functional-vlpp/library.properties | 9 + .../functional-vlpp/platformio/platformio.ini | 335 +++ .../libraries/functional-vlpp/src/Basic.h | 743 +++++++ .../libraries/functional-vlpp/src/Function.h | 478 +++++ .../libraries/functional-vlpp/src/Pointer.h | 650 ++++++ .../functional-vlpp/src/functional-vlpp.h | 35 + 77 files changed, 14273 insertions(+), 23 deletions(-) create mode 100644 software/firmware/source/libraries/WiFiWebServer/.codespellrc create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/dependabot.yml create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/stale.yml create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/workflows/auto-github-actions.yml create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/workflows/check-arduino.yml create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/workflows/report-size-deltas.yml create mode 100644 software/firmware/source/libraries/WiFiWebServer/.github/workflows/spell-check.yml create mode 100644 software/firmware/source/libraries/WiFiWebServer/.gitignore create mode 100644 software/firmware/source/libraries/WiFiWebServer/CONTRIBUTING.md create mode 100644 software/firmware/source/libraries/WiFiWebServer/LICENSE create mode 100644 software/firmware/source/libraries/WiFiWebServer/README.md create mode 100644 software/firmware/source/libraries/WiFiWebServer/changelog.md create mode 100644 software/firmware/source/libraries/WiFiWebServer/keywords.txt create mode 100644 software/firmware/source/libraries/WiFiWebServer/library.json create mode 100644 software/firmware/source/libraries/WiFiWebServer/library.properties create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_ESP32_C3.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_ESP32_S3.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_NanoRP2040Connect.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_Portenta_H7.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_WiFiMulti_NanoRP2040Connect.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_WiFiMulti_RP2040W.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_v1.0.6.png create mode 100644 software/firmware/source/libraries/WiFiWebServer/platformio/platformio.ini create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/Parsing-impl.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/Uri.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFiHttpClient.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer-impl.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.hpp create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.cpp create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.cpp create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.cpp create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.cpp create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.c create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.c create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/ESP_RequestHandlersImpl.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandler.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandlersImpl.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.cpp create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/WiFiDebug.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.cpp create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/src/utility/mimetable.h create mode 100644 software/firmware/source/libraries/WiFiWebServer/utils/astyle_library.conf create mode 100644 software/firmware/source/libraries/WiFiWebServer/utils/restyle.sh create mode 100644 software/firmware/source/libraries/functional-vlpp/.gitignore create mode 100644 software/firmware/source/libraries/functional-vlpp/CONTRIBUTING.md create mode 100644 software/firmware/source/libraries/functional-vlpp/LICENSE create mode 100644 software/firmware/source/libraries/functional-vlpp/README.md create mode 100644 software/firmware/source/libraries/functional-vlpp/keywords.txt create mode 100644 software/firmware/source/libraries/functional-vlpp/library.json create mode 100644 software/firmware/source/libraries/functional-vlpp/library.properties create mode 100644 software/firmware/source/libraries/functional-vlpp/platformio/platformio.ini create mode 100644 software/firmware/source/libraries/functional-vlpp/src/Basic.h create mode 100644 software/firmware/source/libraries/functional-vlpp/src/Function.h create mode 100644 software/firmware/source/libraries/functional-vlpp/src/Pointer.h create mode 100644 software/firmware/source/libraries/functional-vlpp/src/functional-vlpp.h diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b262f55c0..cedfb289a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -125,6 +125,7 @@ jobs: arduino --pref "custom_xtal=nodemcuv2_80" --save-prefs ; cd $HOME/.arduino15/packages/esp8266/hardware/esp8266/2.7.0 ; sed -i '52 a #define isFlashInterfacePin(p) ((p) == 6 || (p) == 7 || (p) == 8 || (p) == 9 || (p) == 11)' variants/nodemcu/pins_arduino.h ; + sed -i '57 a \ unsigned long getTimeout () const { return _timeout; }' cores/esp8266/Stream.h ; cd $GITHUB_WORKSPACE ; fi if [[ "$BOARD" =~ "esp32:esp32:esp32:" ]]; then diff --git a/software/firmware/source/SoftRF/src/driver/WiFi.cpp b/software/firmware/source/SoftRF/src/driver/WiFi.cpp index c1f461874..3af5d16fe 100644 --- a/software/firmware/source/SoftRF/src/driver/WiFi.cpp +++ b/software/firmware/source/SoftRF/src/driver/WiFi.cpp @@ -106,8 +106,6 @@ void Raw_Transmit_UDP() } #if defined(USE_ARDUINO_WIFI) -#include - void WiFi_setup() { // Set Hostname. diff --git a/software/firmware/source/SoftRF/src/platform/ESP32.cpp b/software/firmware/source/SoftRF/src/platform/ESP32.cpp index a1e6141c8..0feed58ea 100644 --- a/software/firmware/source/SoftRF/src/platform/ESP32.cpp +++ b/software/firmware/source/SoftRF/src/platform/ESP32.cpp @@ -80,8 +80,6 @@ lmic_pinmap lmic_pins = { .tcxo = LMIC_UNUSED_PIN, }; -WebServer server ( 80 ); - #if !defined(EXCLUDE_LED_RING) #if defined(USE_NEOPIXELBUS_LIBRARY) NeoPixelBus strip(PIX_NUM, SOC_GPIO_PIN_LED); diff --git a/software/firmware/source/SoftRF/src/platform/ESP32.h b/software/firmware/source/SoftRF/src/platform/ESP32.h index 8bf5e4b19..98d2892dc 100644 --- a/software/firmware/source/SoftRF/src/platform/ESP32.h +++ b/software/firmware/source/SoftRF/src/platform/ESP32.h @@ -22,8 +22,10 @@ #include "sdkconfig.h" +#define USE_WIFI_NINA false +#define USE_WIFI_CUSTOM true #include -#include + #include #include #include @@ -238,8 +240,6 @@ extern Adafruit_NeoPixel strip; #include "iomap/WT0132C6.h" #include "iomap/LilyGO_T3C6.h" -extern WebServer server; - enum rst_reason { REASON_DEFAULT_RST = 0, /* normal startup by power on */ REASON_WDT_RST = 1, /* hardware watch dog reset */ diff --git a/software/firmware/source/SoftRF/src/platform/ESP8266.cpp b/software/firmware/source/SoftRF/src/platform/ESP8266.cpp index 68c823e79..6e7601a0c 100644 --- a/software/firmware/source/SoftRF/src/platform/ESP8266.cpp +++ b/software/firmware/source/SoftRF/src/platform/ESP8266.cpp @@ -52,8 +52,6 @@ Exp_SoftwareSerial swSer(SOC_GPIO_PIN_GNSS_RX, SOC_GPIO_PIN_GNSS_TX, false, 256) SoftwareSerial swSer; #endif -ESP8266WebServer server ( 80 ); - // Parameter 1 = number of pixels in strip // Parameter 2 = Arduino pin number (most are valid) // Parameter 3 = pixel type flags, add together as needed: diff --git a/software/firmware/source/SoftRF/src/platform/ESP8266.h b/software/firmware/source/SoftRF/src/platform/ESP8266.h index 32f780cad..23b0d23ea 100644 --- a/software/firmware/source/SoftRF/src/platform/ESP8266.h +++ b/software/firmware/source/SoftRF/src/platform/ESP8266.h @@ -20,10 +20,12 @@ #ifndef PLATFORM_ESP8266_H #define PLATFORM_ESP8266_H +#define USE_WIFI_NINA false +#define USE_WIFI_CUSTOM true #include + #include #include -#include #define USE_EXP_SW_SERIAL @@ -92,7 +94,6 @@ extern "C" { #include } -extern ESP8266WebServer server; #if defined(USE_EXP_SW_SERIAL) extern Exp_SoftwareSerial swSer; #else diff --git a/software/firmware/source/SoftRF/src/platform/RA4M1.h b/software/firmware/source/SoftRF/src/platform/RA4M1.h index 82b6c4b9f..952d17f6b 100644 --- a/software/firmware/source/SoftRF/src/platform/RA4M1.h +++ b/software/firmware/source/SoftRF/src/platform/RA4M1.h @@ -133,7 +133,9 @@ struct rst_info { #elif defined(ARDUINO_UNOR4_WIFI) #define USE_ARDUINO_WIFI #define EXCLUDE_OTA -#define EXCLUDE_WEBUI /* TODO */ +#define USE_WIFI_NINA false +#define USE_WIFI_CUSTOM true +#include #define Serial_setDebugOutput(x) ({}) #endif @@ -160,7 +162,7 @@ struct rst_info { #define EXCLUDE_NRF905 // - kb #define EXCLUDE_UATM // - kb #define EXCLUDE_MAVLINK // - kb -//#define EXCLUDE_EGM96 // - kb +#define EXCLUDE_EGM96 // - kb #define EXCLUDE_LED_RING // - kb #define EXCLUDE_SOUND diff --git a/software/firmware/source/SoftRF/src/platform/RP2040.cpp b/software/firmware/source/SoftRF/src/platform/RP2040.cpp index 99299935a..389439dd0 100644 --- a/software/firmware/source/SoftRF/src/platform/RP2040.cpp +++ b/software/firmware/source/SoftRF/src/platform/RP2040.cpp @@ -122,7 +122,6 @@ Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIX_NUM, SOC_GPIO_PIN_LED, char UDPpacketBuffer[4]; // Dummy definition to satisfy build sequence #else #include "../driver/WiFi.h" -WebServer server ( 80 ); #define isTimeToAP() (millis() - AP_clients_TimeMarker > 1000) static unsigned long AP_clients_TimeMarker = 0; diff --git a/software/firmware/source/SoftRF/src/platform/RP2040.h b/software/firmware/source/SoftRF/src/platform/RP2040.h index cb61630d6..ea91947ba 100644 --- a/software/firmware/source/SoftRF/src/platform/RP2040.h +++ b/software/firmware/source/SoftRF/src/platform/RP2040.h @@ -225,11 +225,12 @@ struct rst_info { #endif #if defined(ARDUINO_RASPBERRY_PI_PICO_W) -#include -#include +#define USE_WIFI_NINA false +#define USE_WIFI_CUSTOM true +#include #define Serial_setDebugOutput(x) ({}) #define WIFI_STA_TIMEOUT 20000 -extern WebServer server; + /* Experimental */ #define ENABLE_PROL //#define ENABLE_BT_VOICE diff --git a/software/firmware/source/SoftRF/src/ui/Web.cpp b/software/firmware/source/SoftRF/src/ui/Web.cpp index 83d838d33..340d740a1 100644 --- a/software/firmware/source/SoftRF/src/ui/Web.cpp +++ b/software/firmware/source/SoftRF/src/ui/Web.cpp @@ -55,6 +55,9 @@ static const char Logo[] PROGMEM = { #include "jquery_min_js.h" +#include +WiFiWebServer server ( 80 ); + byte getVal(char c) { if(c >= '0' && c <= '9') @@ -800,7 +803,7 @@ void handleRoot() { #endif /* ENABLE_RECORDER */ /* SoC specific part 1 */ - if (SoC->id != SOC_RP2040) { + if (SoC->id != SOC_RP2040 && SoC->id != SOC_RA4M1) { snprintf_P ( offset, size, PSTR("\ ")); len = strlen(offset); @@ -1164,6 +1167,10 @@ void Web_setup() server.on ( "/inline", []() { server.send ( 200, "text/plain", "this works as well" ); } ); + + server.onNotFound ( handleNotFound ); + +#if !defined(EXCLUDE_OTA) server.on("/firmware", HTTP_GET, [](){ SoC->swSer_enableRx(false); server.sendHeader(String(F("Connection")), String(F("close"))); @@ -1226,7 +1233,6 @@ void Web_setup() ); SoC->swSer_enableRx(true); }); - server.onNotFound ( handleNotFound ); server.on("/update", HTTP_POST, [](){ SoC->swSer_enableRx(false); @@ -1263,15 +1269,19 @@ void Web_setup() } yield(); }); +#endif /* EXCLUDE_OTA */ /* FLASH memory usage optimization */ -#if !defined(ARDUINO_ARCH_RP2040) && !defined(CONFIG_IDF_TARGET_ESP32C6) +#if !defined(ARDUINO_ARCH_RP2040) && \ + !defined(CONFIG_IDF_TARGET_ESP32C6) && \ + !defined(ARDUINO_ARCH_RENESAS) + server.on ( "/logo.png", []() { server.send_P ( 200, "image/png", Logo, sizeof(Logo) ); } ); -#endif /* ARDUINO_ARCH_RP2040 CONFIG_IDF_TARGET_ESP32C6 */ +#endif /* ARDUINO_ARCH_RP2040 CONFIG_IDF_TARGET_ESP32C6 ARDUINO_ARCH_RENESAS */ -#if !defined(ARDUINO_ARCH_RP2040) +#if !defined(ARDUINO_ARCH_RP2040) && !defined(ARDUINO_ARCH_RENESAS) server.on ( "/jquery.min.js", []() { PGM_P content = jquery_min_js_gz; @@ -1290,7 +1300,7 @@ void Web_setup() } while (bytes_left > 0) ; } ); -#endif /* ARDUINO_ARCH_RP2040 */ +#endif /* ARDUINO_ARCH_RP2040 ARDUINO_ARCH_RENESAS */ #if defined(ENABLE_RECORDER) server.on("/flights", HTTP_GET, Handle_Flight_Download); diff --git a/software/firmware/source/libraries/WiFiWebServer/.codespellrc b/software/firmware/source/libraries/WiFiWebServer/.codespellrc new file mode 100644 index 000000000..00fe36261 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.codespellrc @@ -0,0 +1,7 @@ +# See: https://github.com/codespell-project/codespell#using-a-config-file +[codespell] +# In the event of a false positive, add the problematic word, in all lowercase, to a comma-separated list here: +ignore-words-list = , +check-filenames = +check-hidden = +skip = ./.git,./src,./examples,./Packages_Patches,./LibraryPatches diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/bug_report.md b/software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..c8cdfff68 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,97 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +### Describe the bug + +A clear and concise description of what the bug is. + +### Steps to Reproduce + +Steps to reproduce the behavior. Including the [MRE](https://stackoverflow.com/help/minimal-reproducible-example) sketches + +### Expected behavior + +A clear and concise description of what you expected to happen. + +### Actual behavior + +A clear and concise description of what you expected to happen. + +### Debug and AT-command log (if applicable) + +A clear and concise description of what you expected to happen. + +### Screenshots + +If applicable, add screenshots to help explain your problem. + +### Information + +Please ensure to specify the following: + +* Arduino IDE version (e.g. 1.8.19) or Platform.io version +* Board Core Version (e.g. Arduino SAMDUE core v1.6.12, ESP8266 core v3.0.2, ArduinoCore-mbed v3.4.1, etc.) +* Contextual information (e.g. what you were trying to achieve) +* Simplest possible steps to reproduce +* Anything that might be relevant in your opinion, such as: + * Operating system (Windows, Ubuntu, etc.) and the output of `uname -a` + * Network configuration + + +### Example + +``` +Arduino IDE version: 1.8.19 +RASPBERRY_PI_PICO board +ArduinoCore-mbed v3.4.1 +OS: Ubuntu 20.04 LTS +Linux xy-Inspiron-3593 5.15.0-53-generic #59~20.04.1-Ubuntu SMP Thu Oct 20 15:10:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux + +Context: +I encountered a crash while using this library +Steps to reproduce: +1. ... +2. ... +3. ... +4. ... +``` + +### Additional context + +Add any other context about the problem here. + +--- + +### Sending Feature Requests + +Feel free to post feature requests. It's helpful if you can explain exactly why the feature would be useful. + +There are usually some outstanding feature requests in the [existing issues list](https://github.com/khoih-prog/WiFiWebServer/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement), feel free to add comments to them. + +--- + +### Sending Pull Requests + +Pull Requests with changes and fixes are also welcome! + +Please use the `astyle` to reformat the updated library code as follows (demo for Ubuntu Linux) + +1. Change directory to the library GitHub + +``` +xy@xy-Inspiron-3593:~$ cd Arduino/xy/WiFiWebServer_GitHub/ +xy@xy-Inspiron-3593:~/Arduino/xy/WiFiWebServer_GitHub$ +``` + +2. Issue astyle command + +``` +xy@xy-Inspiron-3593:~/Arduino/xy/WiFiWebServer_GitHub$ bash utils/restyle.sh +``` + diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/feature_request.md b/software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/dependabot.yml b/software/firmware/source/libraries/WiFiWebServer/.github/dependabot.yml new file mode 100644 index 000000000..03600dd7d --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/dependabot.yml @@ -0,0 +1,10 @@ +# See: https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#about-the-dependabotyml-file +version: 2 + +updates: + # Configure check for outdated GitHub Actions actions in workflows. + # See: https://docs.github.com/en/github/administering-a-repository/keeping-your-actions-up-to-date-with-dependabot + - package-ecosystem: github-actions + directory: / # Check the repository's workflows under /.github/workflows/ + schedule: + interval: daily diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/stale.yml b/software/firmware/source/libraries/WiFiWebServer/.github/stale.yml new file mode 100644 index 000000000..7d1411351 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/stale.yml @@ -0,0 +1,31 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +daysUntilStale: 60 +daysUntilClose: 14 +limitPerRun: 30 +staleLabel: stale +exemptLabels: + - pinned + - security + - "to be implemented" + - "for reference" + - "move to PR" + - "enhancement" + +only: issues +onlyLabels: [] +exemptProjects: false +exemptMilestones: false +exemptAssignees: false + +markComment: > + [STALE_SET] This issue has been automatically marked as stale because it has not had + recent activity. It will be closed in 14 days if no further activity occurs. Thank you + for your contributions. + +unmarkComment: > + [STALE_CLR] This issue has been removed from the stale queue. Please ensure activity to keep it opening the future. + +closeComment: > + [STALE_DEL] This stale issue has been automatically closed. Thank you for your contributions. + diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/workflows/auto-github-actions.yml b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/auto-github-actions.yml new file mode 100644 index 000000000..9d0fc4ed4 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/auto-github-actions.yml @@ -0,0 +1,12 @@ +name: auto-github-actions +on: [push] +jobs: + check-bats-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '14' + - run: npm install -g bats + - run: bats -v diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/workflows/check-arduino.yml b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/check-arduino.yml new file mode 100644 index 000000000..3e0d26c9c --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/check-arduino.yml @@ -0,0 +1,28 @@ +name: Check Arduino + +# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows +on: + push: + pull_request: + schedule: + # Run every Tuesday at 8 AM UTC to catch breakage caused by new rules added to Arduino Lint. + - cron: "0 8 * * TUE" + workflow_dispatch: + repository_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Arduino Lint + uses: arduino/arduino-lint-action@v1 + with: + compliance: specification + library-manager: update + # Always use this setting for official repositories. Remove for 3rd party projects. + official: true + project-type: library diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/workflows/report-size-deltas.yml b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/report-size-deltas.yml new file mode 100644 index 000000000..827a89ed1 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/report-size-deltas.yml @@ -0,0 +1,16 @@ +name: Report Size Deltas + +on: + schedule: + - cron: '*/5 * * * *' + +jobs: + report: + runs-on: ubuntu-latest + + steps: + - name: Comment size deltas reports to PRs + uses: arduino/report-size-deltas@v1 + with: + # The name of the workflow artifact created by the "Compile Examples" workflow + sketches-reports-source: sketches-reports diff --git a/software/firmware/source/libraries/WiFiWebServer/.github/workflows/spell-check.yml b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/spell-check.yml new file mode 100644 index 000000000..6ad2f61fa --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.github/workflows/spell-check.yml @@ -0,0 +1,22 @@ +name: Spell Check + +on: + pull_request: + push: + schedule: + # run every Tuesday at 3 AM UTC + - cron: "0 3 * * 2" + workflow_dispatch: + repository_dispatch: + +jobs: + spellcheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + # See: https://github.com/codespell-project/actions-codespell/blob/master/README.md + - name: Spell check + uses: codespell-project/actions-codespell@master diff --git a/software/firmware/source/libraries/WiFiWebServer/.gitignore b/software/firmware/source/libraries/WiFiWebServer/.gitignore new file mode 100644 index 000000000..259148fa1 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/.gitignore @@ -0,0 +1,32 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/software/firmware/source/libraries/WiFiWebServer/CONTRIBUTING.md b/software/firmware/source/libraries/WiFiWebServer/CONTRIBUTING.md new file mode 100644 index 000000000..2e96e98a8 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/CONTRIBUTING.md @@ -0,0 +1,78 @@ +## Contributing to WiFiWebServer + +### Reporting Bugs + +Please report bugs in [WiFiWebServer](https://github.com/khoih-prog/WiFiWebServer/issues/new) if you find them. + +However, before reporting a bug please check through the following: + +* [Existing Open Issues](https://github.com/khoih-prog/WiFiWebServer/issues) - someone might have already encountered this. + +If you don't find anything, please [open a new issue](https://github.com/khoih-prog/WiFiWebServer/issues/new). + +### How to submit a bug report + +Please ensure to specify the following: + +* Arduino IDE version (e.g. 1.8.19) or Platform.io version +* Board Core Version (e.g. Arduino SAMDUE core v1.6.12, ESP8266 core v3.0.2, ArduinoCore-mbed v3.4.1, etc.) +* Contextual information (e.g. what you were trying to achieve) +* Simplest possible steps to reproduce +* Anything that might be relevant in your opinion, such as: + * Operating system (Windows, Ubuntu, etc.) and the output of `uname -a` + * Network configuration + + +### Example + +``` +Arduino IDE version: 1.8.19 +RASPBERRY_PI_PICO board +ArduinoCore-mbed v3.4.1 +OS: Ubuntu 20.04 LTS +Linux xy-Inspiron-3593 5.15.0-53-generic #59~20.04.1-Ubuntu SMP Thu Oct 20 15:10:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux + +Context: +I encountered a crash while using this library +Steps to reproduce: +1. ... +2. ... +3. ... +4. ... +``` + +### Additional context + +Add any other context about the problem here. + +--- + +### Sending Feature Requests + +Feel free to post feature requests. It's helpful if you can explain exactly why the feature would be useful. + +There are usually some outstanding feature requests in the [existing issues list](https://github.com/khoih-prog/WiFiWebServer/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement), feel free to add comments to them. + +--- + +### Sending Pull Requests + +Pull Requests with changes and fixes are also welcome! + +Please use the `astyle` to reformat the updated library code as follows (demo for Ubuntu Linux) + +1. Change directory to the library GitHub + +``` +xy@xy-Inspiron-3593:~$ cd Arduino/xy/WiFiWebServer_GitHub/ +xy@xy-Inspiron-3593:~/Arduino/xy/WiFiWebServer_GitHub$ +``` + +2. Issue astyle command + +``` +xy@xy-Inspiron-3593:~/Arduino/xy/WiFiWebServer_GitHub$ bash utils/restyle.sh +``` + + + diff --git a/software/firmware/source/libraries/WiFiWebServer/LICENSE b/software/firmware/source/libraries/WiFiWebServer/LICENSE new file mode 100644 index 000000000..4a9150f70 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Khoi Hoang + +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/software/firmware/source/libraries/WiFiWebServer/README.md b/software/firmware/source/libraries/WiFiWebServer/README.md new file mode 100644 index 000000000..6bda42800 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/README.md @@ -0,0 +1,1903 @@ +## WiFiWebServer + +[![arduino-library-badge](https://www.ardu-badge.com/badge/WiFiWebServer.svg?)](https://www.ardu-badge.com/WiFiWebServer) +[![GitHub release](https://img.shields.io/github/release/khoih-prog/WiFiWebServer.svg)](https://github.com/khoih-prog/WiFiWebServer/releases) +[![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/khoih-prog/WiFiWebServer/blob/master/LICENSE) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](#Contributing) +[![GitHub issues](https://img.shields.io/github/issues/khoih-prog/WiFiWebServer.svg)](http://github.com/khoih-prog/WiFiWebServer/issues) + +Donate to my libraries using BuyMeACoffee + + + + +--- +--- + +## Table of Contents + +* [Why do we need this WiFiWebServer library](#why-do-we-need-this-wifiwebserver-library) + * [Features](#features) + * [Currently Supported Boards](#currently-supported-boards) + * [Currently supported WiFi shields/modules](#currently-supported-wifi-shieldsmodules) +* [Changelog](changelog.md) +* [Prerequisites](#prerequisites) +* [Installation](#installation) + * [Use Arduino Library Manager](#use-arduino-library-manager) + * [Manual Install](#manual-install) + * [VS Code & PlatformIO](#vs-code--platformio) +* [Packages' Patches](#packages-patches) + * [1. For Adafruit nRF52840 and nRF52832 boards](#1-for-adafruit-nRF52840-and-nRF52832-boards) + * [2. For Teensy boards](#2-for-teensy-boards) + * [3. For Arduino SAM DUE boards](#3-for-arduino-sam-due-boards) + * [4. For Arduino SAMD boards](#4-for-arduino-samd-boards) + * [For core version v1.8.10+](#for-core-version-v1810) + * [For core version v1.8.9-](#for-core-version-v189-) + * [5. For Adafruit SAMD boards](#5-for-adafruit-samd-boards) + * [6. For Seeeduino SAMD boards](#6-for-seeeduino-samd-boards) + * [7. For STM32 boards](#7-for-stm32-boards) + * [7.1. For STM32 boards to use LAN8720](#71-for-stm32-boards-to-use-lan8720) + * [7.2. For STM32 boards to use Serial1](#72-for-stm32-boards-to-use-serial1) + * [8. For RP2040-based boards using Earle Philhower arduino-pico core](#8-for-rp2040-based-boards-using-earle-philhower-arduino-pico-core) + * [8.1. To use BOARD_NAME](#81-to-use-board_name) + * [8.2. To avoid compile error relating to microsecondsToClockCycles](#82-to-avoid-compile-error-relating-to-microsecondstoclockcycles) + * [9. For Portenta_H7 boards using Arduino IDE in Linux](#9-for-portenta_h7-boards-using-arduino-ide-in-linux) + * [10. For RTL8720DN boards using AmebaD core](#10-for-rtl8720dn-boards-using-amebad-core) + * [11. For SAMD21 and SAMD51 boards using ArduinoCore-fab-sam core](#11-For-SAMD21-and-SAMD51-boards-using-ArduinoCore-fab-sam-core) + * [12. For Seeeduino RP2040 boards](#12-For-Seeeduino-RP2040-boards) + * [13. For Seeeduino nRF52840 boards](#13-For-Seeeduino-nRF52840-boards) +* [How to configure to use different WiFi Libraries](#how-to-configure-to-use-different-wifi-libraries) + * [1. Modify pin-to-pin connection in WiFiNINA_Generic library](#1-modify-pin-to-pin-connection-in-wifinina_generic-library) + * [2. How to select which built-in WiFi or shield to use](#2-how-to-select-which-built-in-wifi-or-shield-to-use) + * [3. Important](#3-important) +* [Usage](#usage) + * [Class Constructor](#class-constructor) + * [Basic Operations](#basic-operations) + * [Advanced Options](#advanced-options) + * [Other Function Calls](#other-function-calls) +* [Examples](#examples) + * [Original Examples](#original-examples) + * [ 1. AdvancedWebServer](examples/AdvancedWebServer) + * [ 2. AP_SimpleWebServer](examples/AP_SimpleWebServer) + * [ 3. HelloServer](examples/HelloServer) + * [ 4. HelloServer2](examples/HelloServer2) + * [ 5. HttpBasicAuth](examples/HttpBasicAuth) + * [ 6. **MQTTClient_Auth**](examples/MQTTClient_Auth) + * [ 7. **MQTTClient_Basic**](examples/MQTTClient_Basic) + * [ 8. **MQTT_ThingStream**](examples/MQTT_ThingStream) + * [ 9. PostServer](examples/PostServer) + * [10. ScanNetworks](examples/ScanNetworks) + * [11. SimpleAuthentication](examples/SimpleAuthentication) + * [12. UdpNTPClient](examples/UdpNTPClient) + * [13. UdpSendReceive](examples/UdpSendReceive) + * [14. WebClient](examples/WebClient) + * [15. WebClientRepeating](examples/WebClientRepeating) + * [16. WebServer](examples/WebServer) + * [17. WiFiUdpNtpClient](examples/WiFiUdpNtpClient) + * [18. multiFileProject](examples/multiFileProject) **New** + * [HTTP and WebSocket Client New Examples](#http-and-websocket-client-new-examples) + * [ 1. BasicAuthGet](examples/HTTPClient/BasicAuthGet) + * [ 2. CustomHeader](examples/HTTPClient/CustomHeader) + * [ 3. DweetGet](examples/HTTPClient/DweetGet) + * [ 4. DweetPost](examples/HTTPClient/DweetPost) + * [ 5. HueBlink](examples/HTTPClient/HueBlink) + * [ 6. node_test_server](examples/HTTPClient/node_test_server) + * [ 7. PostWithHeaders](examples/HTTPClient/PostWithHeaders) + * [ 8. SimpleDelete](examples/HTTPClient/SimpleDelete) + * [ 9. SimpleGet](examples/HTTPClient/SimpleGet) + * [10. SimpleHTTPExample](examples/HTTPClient/SimpleHTTPExample) + * [11. SimplePost](examples/HTTPClient/SimplePost) + * [12. SimplePut](examples/HTTPClient/SimplePut) + * [13. SimpleWebSocket](examples/HTTPClient/SimpleWebSocket) + * [WiFiMulti Examples](#WiFiMulti-examples) **New** + * [ 1. AdvancedWebServer_WiFiMulti](examples/WiFiMulti/AdvancedWebServer_WiFiMulti) + * [ 2. MQTTClient_Auth_WiFiMulti](examples/WiFiMulti/MQTTClient_Auth_WiFiMulti) + * [ 3. MQTTClient_Basic_WiFiMulti](examples/WiFiMulti/MQTTClient_Basic_WiFiMulti) + * [ 4. MQTT_ThingStream_WiFiMulti](examples/WiFiMulti/MQTT_ThingStream_WiFiMulti) + * [ 5. WiFiUdpNtpClient_WiFiMulti](examples/WiFiMulti/WiFiUdpNtpClient_WiFiMulti) + * [ 6. WebClient_WiFiMulti](examples/WiFiMulti/WebClient_WiFiMulti) + * [ 7. WebClientRepeating_WiFiMulti](examples/WiFiMulti/WebClientRepeating_WiFiMulti) + * [ 8. WebServer_WiFiMulti](examples/WiFiMulti/WebServer_WiFiMulti) +* [Example AdvancedWebServer](#example-advancedwebserver) + * [1. File AdvancedWebServer.ino](#1-file-advancedwebserverino) + * [2. File defines.h](#2-file-definesh) +* [Debug Terminal Output Samples](#debug-terminal-output-samples) + * [1. AdvancedWebServer on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library](#1-advancedwebserver-on-arduino-samd_nano_33_iot-using-wifinina_generic-library) + * [2. SimpleWebSocket on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library](#2-simplewebsocket-on-arduino-samd_nano_33_iot-using-wifinina_generic-library) + * [3. SimpleHTTPExample on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library](#3-simplehttpexample-on-arduino-samd_nano_33_iot-using-wifinina_generic-library) + * [4. DweetPost on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library](#4-dweetpost-on-arduino-samd_nano_33_iot-using-wifinina_generic-library) + * [5. DweetGet on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library](#5-dweetget-on-arduino-samd_nano_33_iot-using-wifinina_generic-library) + * [6. MQTTClient_Auth on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library](#6-mqttclient_auth-on-arduino-samd_nano_33_iot-using-wifinina_generic-library) + * [7. MQTT_ThingStream on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library](#7-mqtt_thingstream-on-arduino-samd_nano_33_iot-using-wifinina_generic-library) + * [8. WebClientRepeating on RASPBERRY_PI_PICO with Custom WiFi using Custom WiFi Library](#8-webclientrepeating-on-raspberry_pi_pico-with-custom-wifi-using-custom-wifi-library) + * [9. AdvancedWebServer on Arduino Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library](#9-advancedwebserver-on-arduino-nano-rp2040-connect-with-wifinina-using-wifinina_generic-library) + * [10. SimpleHTTPExample on ESP32_DEV](#10-simplehttpexample-on-esp32_dev) + * [11. AdvancedWebServer on PORTENTA_H7_M7 with Portenta_H7 WiFi](#11-advancedwebserver-on-portenta_h7_m7-with-portenta_h7-wifi) + * [12. MQTTClient_Auth on ESP32_DEV](#12-mqttclient_auth-on-portenta_h7_m7-with-portenta_h7-wifi) + * [13. WebClientRepeating on ESP32_DEV](#13-webclientrepeating-on-portenta_h7_m7-with-portenta_h7-wifi) + * [14. AdvancedWebServer on ESP32C3_DEV with ESP WiFi](#14-AdvancedWebServer-on-ESP32C3_DEV-with-ESP-WiFi) + * [15. AdvancedWebServer on ESP32S3_DEV with ESP WiFi](#15-AdvancedWebServer-on-ESP32S3_DEV-with-ESP-WiFi) + * [16. AdvancedWebServer_WiFiMulti on Nano RP2040 Connect with WiFiNINA](#16-AdvancedWebServer_WiFiMulti-on-Nano-RP2040-Connect-with-WiFiNINA) + * [17. MQTTClient_Auth_WiFiMulti on Nano RP2040 Connect with WiFiNINA](#17-MQTTClient_Auth_WiFiMulti-on-Nano-RP2040-Connect-with-WiFiNINA) + * [18. WiFiUdpNTPClient_WiFiMulti on Nano RP2040 Connect with WiFiNINA](#18-WiFiUdpNTPClient_WiFiMulti-on-Nano-RP2040-Connect-with-WiFiNINA) + * [19. AdvancedWebServer_WiFiMulti on RASPBERRY_PI_PICO_W](#19-AdvancedWebServer_WiFiMulti-on-RASPBERRY_PI_PICO_W) +* [Debug](#debug) +* [Troubleshooting](#troubleshooting) +* [Issues](#issues) +* [TO DO](#to-do) +* [DONE](#done) +* [Contributions and Thanks](#contributions-and-thanks) +* [Contributing](#contributing) +* [License](#license) +* [Copyright](#copyright) + +--- +--- + +### Why do we need this [WiFiWebServer library](https://github.com/khoih-prog/WiFiWebServer) + +#### Features + +This [WiFiWebServer library](https://github.com/khoih-prog/WiFiWebServer) is a simple yet complete WebServer library for **AVR, Portenta_H7, Teensy, SAM DUE, Arduino SAMD21, Adafruit SAMD21/SAMD51, Adafruit nRF52, ESP32/ESP8266, STM32F/L/H/G/WB/MP1, etc. boards, using WiFi modules/shields (WiFiNINA, WiFi101, U-Blox W101, W102, ESP8266/ESP32-AT, etc.)**. + +The functions are similar and compatible to those of [`ESP32 WebServer`](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer) and [`ESP8266WebServer`](https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer) libraries to make life much easier to port sketches from ESP8266/ESP32. + +This [**WiFiWebServer library**](https://github.com/khoih-prog/WiFiWebServer), from v1.1.0, also provides high-level **HTTP and WebSocket Client** with the functions are similar and compatible to those of [**ArduinoHttpClient Library**](https://github.com/arduino-libraries/ArduinoHttpClient) + +The library provides supports to: + +1. WiFi Client, STA and AP mode +2. TCP Server and Client +3. UDP Server and Client +4. HTTP Server and Client +5. HTTP GET and POST requests, provides argument parsing, handles one client at a time. +6. **High-level HTTP (GET, POST, PUT, PATCH, DELETE) and WebSocket Client**. From v1.1.0. + +It is based on and modified from: + +1. [Ivan Grokhotkov's ESP8266WebServer](https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer) +2. [Ivan Grokhotkov's ESP32 WebServer](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer) +3. [ArduinoHttpClient Library](https://github.com/arduino-libraries/ArduinoHttpClient) + +The WiFiWebServer class found in `WiFiWebServer.h` header, is a simple web server that knows how to handle HTTP requests such as GET and POST and can only support one client at a time. + +The newly-created WiFiMulti-related examples in [WiFiMulti](https://github.com/khoih-prog/WiFiWebServer/tree/master/examples/WiFiMulti) demonstrate how to use the new [WiFiMulti_Generic](https://github.com/khoih-prog/WiFiMulti_Generic) library to connect to the best of **multi-WiFi APs**, with **auto-checking / auto-reconnecting** features when WiFi connection is lost. + +--- + +#### Currently Supported Boards + +This [**WiFiWebServer library**](https://github.com/khoih-prog/WiFiWebServer) currently supports these following boards: + + 1. SAM DUE + + 2. SAMD21 + + - Arduino: ZERO, MKR, NANO_33_IOT, etc. + - Adafruit SAMD21 (M0) : ItsyBitsy M0, Feather M0, Feather M0 Express, Metro M0 Express, Circuit Playground Express, Trinket M0, PIRkey, HalloWing M0, Crickit M0, etc. + - Seeeduino: LoRaWAN, Zero, Femto M0, XIAO M0, Wio GPS Board, etc. + + 3. SAMD51 + + - Adafruit SAMD51 (M4) : Metro M4, Grand Central M4, ItsyBitsy M4, Feather M44 Express, Trellis M4, Metro M4 AirLift lite, MONSTER M4SK Express, Hallowing EM4 xpress, etc. + - Seeeduino: Wio Terminal, Grove UI Wireless + + 4. Teensy (4.1, 4.0, 3.6, 3.5, 3,2, 3.1, 3.0, LC) + 5. All STM32F/L/H/G/WB/MP1 with more than 32KB flash memory. + 6. AVR Mega1280, 2560, ADK, 32U4, 16U4, etc. using Arduino, Adafruit or Sparkfun core. To use patch for `ArduinoSTL` library. + + 7. RP2040-based boards, such as **Nano RP2040 Connect**, or **RASPBERRY_PI_PICO_W with CYW43439 WiFi**, using [**Arduino-mbed RP2040** core](https://github.com/arduino/ArduinoCore-mbed) or [**Earle Philhower's arduino-pico** core](https://github.com/earlephilhower/arduino-pico) + + 8. RP2040-based boards, such as **RASPBERRY_PI_PICO, ADAFRUIT_FEATHER_RP2040 and GENERIC_RP2040**, using [**Arduino-mbed RP2040** core](https://github.com/arduino/ArduinoCore-mbed) or [**Earle Philhower's arduino-pico** core](https://github.com/earlephilhower/arduino-pico). + + 9. **ESP32** + + - ESP32 boards, such as `ESP32_DEV`, etc. + - ESP32S2-based boards, such as `ESP32S2_DEV`, `ESP32_S2 Saola`, etc. + - ESP32C3-based boards, such as `ESP32C3_DEV`, etc. **New** + - ESP32_S3 (ESP32S3_DEV, ESP32_S3_BOX, UM TINYS3, UM PROS3, UM FEATHERS3, etc.) **New** + +10. **ESP8266** + +11. **Portenta_H7** + +12. **nRF52 boards**, such as **AdaFruit Feather nRF52832, nRF52840 Express, BlueFruit Sense, Itsy-Bitsy nRF52840 Express, Metro nRF52840 Express, NINA_B302_ublox, NINA_B112_ublox, etc.** + +13. Arduino `megaAVR` boards using Arduino core + + - UNO WiFi Rev2 : `WiFi101` **New** + - Nano Every : `WiFiEspAT` **New** + +14. CO2 Ampel + + - SAMD : `WiFi101` **New** + +15. STM32 using `STM32duino Maple` core. **New** + +16. Sparkfun SAMD + +- SAMD21, SAMD51 : `WiFiEspAT` **New** + +17. Industruino SAMD + + - D21G : WiFiEspAT **New** + +18. Tlera Corp STM32WB boards + + - Firefly-WB55RG, Nucleo-WB55RG, etc. : `WiFiEspAT` **New** + +19. Maixduino boards + + - Sipeed Maixduino, etc. : `WiFiEspAT` **New** + +20. `RTL8720DN` using Realtek `AmebaD` core **New** + +21. Arduino, Sparkfun, Adafruit, etc. AVR boards (Mega, 32U4, etc.). To use patch for `ArduinoSTL` library **New** + + - Arduino Uno / Mega / Duemilanove / Diecimila / LilyPad / Mini / Fio / Nano, etc. + - **Arduino ATMega 16U4, 32U4** such as AVR Leonardo, Leonardo ETH, YUN, Esplora, LILYPAD_USB, AVR_ROBOT_CONTROL, AVR_ROBOT_MOTOR, AVR_INDUSTRIAL101, etc. + - **Adafruit ATMega 32U4** such as AVR_FLORA8, AVR_FEATHER32U4, AVR_CIRCUITPLAY, AVR_ITSYBITSY32U4_5V, AVR_ITSYBITSY32U4_3V, AVR_BLUEFRUITMICRO, AVR_ADAFRUIT32U4, etc. + - **Adafruit ATMega 328(P)** such as AVR_METRO, AVR_FEATHER328P, AVR_PROTRINKET5, AVR_PROTRINKET3, AVR_PROTRINKET5FTDI, AVR_PROTRINKET3FTDI, etc. + - **Generic or Sparkfun AVR ATmega_32U4** such as **AVR_MAKEYMAKEY, AVR_PROMICRO, etc.** + - **Generic or Sparkfun AVR ATmega_328(P)** such as **ARDUINO_REDBOT, ARDUINO_AVR_DIGITAL_SANDBOX, etc.** + - **Generic or Sparkfun AVR ATmega128RFA1** such as **ATMEGA128RFA1_DEV_BOARD, etc.** + +--- + +#### Currently supported WiFi shields/modules + +1. WiFiNINA using [`WiFiNINA_Generic library`](https://github.com/khoih-prog/WiFiNINA_Generic) +2. WiFi101 using [`WiFi101_Generic library`](https://github.com/khoih-prog/WiFi101_Generic) **New** +3. u-blox W101, W102 using [`WiFiNINA_Generic library`](https://github.com/khoih-prog/WiFiNINA_Generic) +4. ESP8266-AT command using [`WiFiEspAT library`](https://github.com/jandrassy/WiFiEspAT) +5. ESP8266/ESP32-AT command using [`ESP_AT_Lib library`](https://github.com/khoih-prog/ESP_AT_Lib) +6. Built-in WiFi of ESP32, ESP8266 +7. Built-in WiFi of Portenta_H7 +8. Built-in CYW43439 WiFi of RASPBERRY_PI_PICO_W **New** + +---- +--- + +## Prerequisites + + 1. [`Arduino IDE 1.8.19+` for Arduino](https://github.com/arduino/Arduino). [![GitHub release](https://img.shields.io/github/release/arduino/Arduino.svg)](https://github.com/arduino/Arduino/releases/latest) + 2. [`ESP32 Core 2.0.5+`](https://github.com/espressif/arduino-esp32) for ESP32-based boards. [![Latest release](https://img.shields.io/github/release/espressif/arduino-esp32.svg)](https://github.com/espressif/arduino-esp32/releases/latest/) + 3. [`ESP8266 Core 3.0.2+`](https://github.com/esp8266/Arduino) for ESP8266-based boards. [![Latest release](https://img.shields.io/github/release/esp8266/Arduino.svg)](https://github.com/esp8266/Arduino/releases/latest/). + 4. [`Arduino AVR core 1.8.6+`](https://github.com/arduino/ArduinoCore-avr) for Arduino (Use Arduino Board Manager) for AVR boards. [![GitHub release](https://img.shields.io/github/release/arduino/ArduinoCore-avr.svg)](https://github.com/arduino/ArduinoCore-avr/releases/latest) + 5. [`Teensy core v1.57+`](https://www.pjrc.com/teensy/td_download.html) for Teensy (4.1, 4.0, 3.6, 3.5, 3,2, 3.1, 3.0) boards. + 6. [`Arduino SAM DUE core v1.6.12+`](https://github.com/arduino/ArduinoCore-sam) for SAM DUE ARM Cortex-M3 boards. + 7. [`Arduino SAMD core 1.8.13+`](https://github.com/arduino/ArduinoCore-samd) for SAMD ARM Cortex-M0+ boards. [![GitHub release](https://img.shields.io/github/release/arduino/ArduinoCore-samd.svg)](https://github.com/arduino/ArduinoCore-samd/releases/latest) + 8. [`Adafruit SAMD core 1.7.11+`](https://github.com/adafruit/ArduinoCore-samd) for SAMD ARM Cortex-M0+ and M4 boards (Nano 33 IoT, etc.). [![GitHub release](https://img.shields.io/github/release/adafruit/ArduinoCore-samd.svg)](https://github.com/adafruit/ArduinoCore-samd/releases/latest) + 9. [`Seeeduino SAMD core 1.8.3+`](https://github.com/Seeed-Studio/ArduinoCore-samd) for SAMD21/SAMD51 boards (XIAO M0, Wio Terminal, etc.). [![Latest release](https://img.shields.io/github/release/Seeed-Studio/ArduinoCore-samd.svg)](https://github.com/Seeed-Studio/ArduinoCore-samd/releases/latest/) +10. [`Adafruit nRF52 v1.3.0+`](https://github.com/adafruit/Adafruit_nRF52_Arduino) for nRF52 boards such as Adafruit NRF52840_FEATHER, NRF52832_FEATHER, NRF52840_FEATHER_SENSE, NRF52840_ITSYBITSY, NRF52840_CIRCUITPLAY, NRF52840_CLUE, NRF52840_METRO, NRF52840_PCA10056, PARTICLE_XENON, **NINA_B302_ublox**, etc. [![GitHub release](https://img.shields.io/github/release/adafruit/Adafruit_nRF52_Arduino.svg)](https://github.com/adafruit/Adafruit_nRF52_Arduino/releases/latest) +11. [`Arduino Core for STM32 v2.3.0+`](https://github.com/stm32duino/Arduino_Core_STM32) for STM32F/L/H/G/WB/MP1 boards. [![GitHub release](https://img.shields.io/github/release/stm32duino/Arduino_Core_STM32.svg)](https://github.com/stm32duino/Arduino_Core_STM32/releases/latest) +12. [`Earle Philhower's arduino-pico core v2.6.3+`](https://github.com/earlephilhower/arduino-pico) for RP2040-based boards such as **RASPBERRY_PI_PICO, RASPBERRY_PI_PICO_W, ADAFRUIT_FEATHER_RP2040 and GENERIC_RP2040**, etc. [![GitHub release](https://img.shields.io/github/release/earlephilhower/arduino-pico.svg)](https://github.com/earlephilhower/arduino-pico/releases/latest) +13. [`ArduinoCore-mbed mbed_rp2040, mbed_nano, mbed_portenta core 3.4.1+`](https://github.com/arduino/ArduinoCore-mbed) for Arduino (Use Arduino Board Manager) **Portenta_H7, RP2040-based boards, such as Nano_RP2040_Connect, RASPBERRY_PI_PICO**. [![GitHub release](https://img.shields.io/github/release/arduino/ArduinoCore-mbed.svg)](https://github.com/arduino/ArduinoCore-mbed/releases/latest) +14. [`Arduino megaAVR core 1.8.7+`](https://github.com/arduino/ArduinoCore-megaavr/releases) for Arduino megaAVR boards such as **UNO_WIFI_REV2, NANO_EVERY** + +15. [`Functional-Vlpp library v1.0.2+`](https://github.com/khoih-prog/functional-vlpp) to use server's lambda function. To install, check [![arduino-library-badge](https://www.ardu-badge.com/badge/Functional-Vlpp.svg?)](https://www.ardu-badge.com/Functional-Vlpp) +16. [`WiFiNINA_Generic library v1.8.15-1+`](https://github.com/khoih-prog/WiFiNINA_Generic) if using WiFiNINA. To install, check [![arduino-library-badge](https://www.ardu-badge.com/badge/WiFiNINA_Generic.svg?)](https://www.ardu-badge.com/WiFiNINA_Generic). +17. [`ESP_AT_Lib library v1.4.1+`](https://github.com/khoih-prog/ESP_AT_Lib) if using ESP8288/ESP32-AT shields. To install, check [![arduino-library-badge](https://www.ardu-badge.com/badge/ESP_AT_Lib.svg?)](https://www.ardu-badge.com/ESP_AT_Lib). +18. [`WiFi101_Generic library v1.0.0+`](https://github.com/khoih-prog/WiFi101_Generic) to use SAMD MKR1000, etc. boards with WiFi101 for sending larger data. To install, check [![arduino-library-badge](https://www.ardu-badge.com/badge/WiFi101_Generic.svg?)](https://www.ardu-badge.com/WiFi101_Generic). +19. [`WiFiEspAT library v1.4.1+`](https://github.com/jandrassy/WiFiEspAT) if using ESP8288/ESP32-AT shields. [![GitHub release](https://img.shields.io/github/release/jandrassy/WiFiEspAT.svg)](https://github.com/jandrassy/WiFiEspAT/releases/latest) +20. [`WiFiMulti_Generic library v1.2.2+`](https://github.com/khoih-prog/WiFiMulti_Generic) to use WiFiMulti function. To install, check [![arduino-library-badge](https://www.ardu-badge.com/badge/WiFiMulti_Generic.svg?)](https://www.ardu-badge.com/WiFiMulti_Generic). **New** + + +--- + +## Installation + +### Use Arduino Library Manager +The best and easiest way is to use `Arduino Library Manager`. Search for [**WiFiWebServer**](https://github.com/khoih-prog/WiFiWebServer), then select / install the latest version. +You can also use this link [![arduino-library-badge](https://www.ardu-badge.com/badge/WiFiWebServer.svg?)](https://www.ardu-badge.com/WiFiWebServer) for more detailed instructions. + +### Manual Install + +1. Navigate to [**WiFiWebServer**](https://github.com/khoih-prog/WiFiWebServer) page. +2. Download the latest release `WiFiWebServer-master.zip`. +3. Extract the zip file to `WiFiWebServer-master` directory +4. Copy the whole `WiFiWebServer-master` folder to Arduino libraries' directory such as `~/Arduino/libraries/`. + +### VS Code & PlatformIO: + +1. Install [VS Code](https://code.visualstudio.com/) +2. Install [PlatformIO](https://platformio.org/platformio-ide) +3. Install [**WiFiWebServer** library](https://registry.platformio.org/libraries/khoih-prog/WiFiWebServer) by using [Library Manager](https://registry.platformio.org/libraries/khoih-prog/WiFiWebServer/installation). Search for **WiFiWebServer** in [Platform.io Author's Libraries](https://platformio.org/lib/search?query=author:%22Khoi%20Hoang%22) +4. Use included [platformio.ini](platformio/platformio.ini) file from examples to ensure that all dependent libraries will installed automatically. Please visit documentation for the other options and examples at [Project Configuration File](https://docs.platformio.org/page/projectconf.html) + +--- +--- + +### Packages' Patches + +#### 1. For Adafruit nRF52840 and nRF52832 boards + +**To be able to compile, run and automatically detect and display BOARD_NAME on nRF52840/nRF52832 boards**, you have to copy the whole [nRF52 Packages_Patches](Packages_Patches/adafruit/hardware/nrf52/1.3.0) directory into Adafruit nRF52 directory (~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0). + +Supposing the Adafruit nRF52 version is 1.3.0. These files must be copied into the directory: +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/platform.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/boards.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/cores/nRF5/Udp.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/cores/nRF5/Print.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/cores/nRF5/Print.cpp` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/variants/NINA_B302_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/variants/NINA_B302_ublox/variant.cpp` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/variants/NINA_B112_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/variants/NINA_B112_ublox/variant.cpp` +- **`~/.arduino15/packages/adafruit/hardware/nrf52/1.3.0/cores/nRF5/Udp.h`** + +Whenever a new version is installed, remember to copy these files into the new version directory. For example, new version is x.yy.z +These files must be copied into the directory: + +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/platform.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/boards.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/cores/nRF5/Udp.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/cores/nRF5/Print.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/cores/nRF5/Print.cpp` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B302_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B302_ublox/variant.cpp` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B112_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B112_ublox/variant.cpp` +- **`~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/cores/nRF5/Udp.h`** + +#### 2. For Teensy boards + + **To be able to compile and run on Teensy boards**, you have to copy the files in [**Packages_Patches for Teensy directory**](Packages_Patches/hardware/teensy/avr) into Teensy hardware directory (./arduino-1.8.19/hardware/teensy/avr/boards.txt). + +Supposing the Arduino version is 1.8.19. These files must be copied into the directory: + +- `./arduino-1.8.19/hardware/teensy/avr/boards.txt` +- `./arduino-1.8.19/hardware/teensy/avr/cores/teensy/Stream.h` +- `./arduino-1.8.19/hardware/teensy/avr/cores/teensy3/Stream.h` +- `./arduino-1.8.19/hardware/teensy/avr/cores/teensy4/Stream.h` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +These files must be copied into the directory: + +- `./arduino-x.yy.zz/hardware/teensy/avr/boards.txt` +- `./arduino-x.yy.zz/hardware/teensy/avr/cores/teensy/Stream.h` +- `./arduino-x.yy.zz/hardware/teensy/avr/cores/teensy3/Stream.h` +- `./arduino-x.yy.zz/hardware/teensy/avr/cores/teensy4/Stream.h` + +#### 3. For Arduino SAM DUE boards + + **To be able to compile and run on SAM DUE boards**, you have to copy the whole [SAM DUE](Packages_Patches/arduino/hardware/sam/1.6.12) directory into Arduino sam directory (~/.arduino15/packages/arduino/hardware/sam/1.6.12). + +Supposing the Arduino SAM core version is 1.6.12. This file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/sam/1.6.12/platform.txt` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/sam/x.yy.zz/platform.txt` + +#### 4. For Arduino SAMD boards + + ***To be able to compile, run and automatically detect and display BOARD_NAME on Arduino SAMD (Nano-33-IoT, etc) boards***, you have to copy the whole [Arduino SAMD Packages_Patches](Packages_Patches/arduino/hardware/samd/1.8.13) directory into Arduino SAMD directory (~/.arduino15/packages/arduino/hardware/samd/1.8.13). + +#### For core version v1.8.10+ + +Supposing the Arduino SAMD version is 1.8.13. Now only one file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/1.8.13/platform.txt` + +Whenever a new version is installed, remember to copy this files into the new version directory. For example, new version is x.yy.zz + +This file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/x.yy.zz/platform.txt` + +#### For core version v1.8.9- + +Supposing the Arduino SAMD version is 1.8.9. These files must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/1.8.9/platform.txt` +- ***`~/.arduino15/packages/arduino/hardware/samd/1.8.9/cores/arduino/Arduino.h`*** + +Whenever a new version is installed, remember to copy these files into the new version directory. For example, new version is x.yy.z + +These files must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/x.yy.z/platform.txt` +- ***`~/.arduino15/packages/arduino/hardware/samd/x.yy.z/cores/arduino/Arduino.h`*** + + This is mandatory to fix the ***notorious Arduino SAMD compiler error***. See [Improve Arduino compatibility with the STL (min and max macro)](https://github.com/arduino/ArduinoCore-samd/pull/399) + +``` + ...\arm-none-eabi\include\c++\7.2.1\bits\stl_algobase.h:243:56: error: macro "min" passed 3 arguments, but takes just 2 + min(const _Tp& __a, const _Tp& __b, _Compare __comp) +``` + +Whenever the above-mentioned compiler error issue is fixed with the new Arduino SAMD release, you don't need to copy the `Arduino.h` file anymore. + +#### 5. For Adafruit SAMD boards + + ***To be able to compile, run and automatically detect and display BOARD_NAME on Adafruit SAMD (Itsy-Bitsy M4, etc) boards***, you have to copy the whole [Adafruit SAMD Packages_Patches](Packages_Patches/adafruit/hardware/samd/1.7.11) directory into Adafruit samd directory (~/.arduino15/packages/adafruit/hardware/samd/1.7.11). + +Supposing the Adafruit SAMD core version is 1.7.11. These files must be copied into the directory: + +- `~/.arduino15/packages/adafruit/hardware/samd/1.7.11/platform.txt` +- `~/.arduino15/packages/adafruit/hardware/samd/1.7.11/cores/arduino/Print.h` +- `~/.arduino15/packages/adafruit/hardware/samd/1.7.11/cores/arduino/Print.cpp` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +These files must be copied into the directory: + +- `~/.arduino15/packages/adafruit/hardware/samd/x.yy.zz/platform.txt` +- `~/.arduino15/packages/adafruit/hardware/samd/x.yy.zz/cores/arduino/Print.h` +- `~/.arduino15/packages/adafruit/hardware/samd/x.yy.zz/cores/arduino/Print.cpp` + +#### 6. For Seeeduino SAMD boards + + ***To be able to compile, run and automatically detect and display BOARD_NAME on Seeeduino SAMD (XIAO M0, Wio Terminal, etc) boards***, you have to copy the whole [Seeeduino SAMD Packages_Patches](Packages_Patches/Seeeduino/hardware/samd/1.8.3) directory into Seeeduino samd directory (~/.arduino15/packages/Seeeduino/hardware/samd/1.8.3). + +Supposing the Seeeduino SAMD core version is 1.8.3. These files must be copied into the directory: + +- `~/.arduino15/packages/Seeeduino/hardware/samd/1.8.3/platform.txt` +- `~/.arduino15/packages/Seeeduino/hardware/samd/1.8.3/cores/arduino/Arduino.h` +- `~/.arduino15/packages/Seeeduino/hardware/samd/1.8.3/cores/arduino/Print.h` +- `~/.arduino15/packages/Seeeduino/hardware/samd/1.8.3/cores/arduino/Print.cpp` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +These files must be copied into the directory: + +- `~/.arduino15/packages/Seeeduino/hardware/samd/x.yy.zz/platform.txt` +- `~/.arduino15/packages/Seeeduino/hardware/samd/x.yy.zz/cores/arduino/Arduino.h` +- `~/.arduino15/packages/Seeeduino/hardware/samd/x.yy.zz/cores/arduino/Print.h` +- `~/.arduino15/packages/Seeeduino/hardware/samd/x.yy.zz/cores/arduino/Print.cpp` + +#### 7. For STM32 boards + +#### 7.1 For STM32 boards to use LAN8720 + +For `Generic STM32F4 series` boards, such as `STM32F407VE`, using `LAN8720`, please use STM32 core `v2.2.0` as breaking core `v2.3.0` creates the compile error. + +--- + +To use LAN8720 on some STM32 boards + +- **Nucleo-144 (F429ZI, NUCLEO_F746NG, NUCLEO_F746ZG, NUCLEO_F756ZG)** +- **Discovery (DISCO_F746NG)** +- **STM32F4 boards (BLACK_F407VE, BLACK_F407VG, BLACK_F407ZE, BLACK_F407ZG, BLACK_F407VE_Mini, DIYMORE_F407VGT, FK407M1)** + +you have to copy the files [stm32f4xx_hal_conf_default.h](Packages_Patches/STM32/hardware/stm32/2.2.0/system/STM32F4xx) and [stm32f7xx_hal_conf_default.h](Packages_Patches/STM32/hardware/stm32/2.2.0/system/STM32F7xx) into STM32 stm32 directory (~/.arduino15/packages/STM32/hardware/stm32/2.2.0/system) to overwrite the old files. + +Supposing the STM32 stm32 core version is 2.2.0. These files must be copied into the directory: + +- `~/.arduino15/packages/STM32/hardware/stm32/2.2.0/system/STM32F4xx/stm32f4xx_hal_conf_default.h` for STM32F4. +- `~/.arduino15/packages/STM32/hardware/stm32/2.2.0/system/STM32F7xx/stm32f7xx_hal_conf_default.h` for Nucleo-144 STM32F7. + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz, +these files must be copied into the corresponding directory: + +- `~/.arduino15/packages/STM32/hardware/stm32/x.yy.zz/system/STM32F4xx/stm32f4xx_hal_conf_default.h` +- `~/.arduino15/packages/STM32/hardware/stm32/x.yy.zz/system/STM32F7xx/stm32f7xx_hal_conf_default.h` + + +#### 7.2 For STM32 boards to use Serial1 + +**To use Serial1 on some STM32 boards without Serial1 definition (Nucleo-144 NUCLEO_F767ZI, Nucleo-64 NUCLEO_L053R8, etc.) boards**, you have to copy the files [STM32 variant.h](Packages_Patches/STM32/hardware/stm32/2.3.0) into STM32 stm32 directory (~/.arduino15/packages/STM32/hardware/stm32/2.3.0). You have to modify the files corresponding to your boards, this is just an illustration how to do. + +Supposing the STM32 stm32 core version is 2.3.0. These files must be copied into the directory: + +- `~/.arduino15/packages/STM32/hardware/stm32/2.3.0/variants/STM32F7xx/F765Z(G-I)T_F767Z(G-I)T_F777ZIT/NUCLEO_F767ZI/variant.h` for Nucleo-144 NUCLEO_F767ZI. +- `~/.arduino15/packages/STM32/hardware/stm32/2.3.0/variants/STM32L0xx/L052R(6-8)T_L053R(6-8)T_L063R8T/NUCLEO_L053R8/variant.h` for Nucleo-64 NUCLEO_L053R8. + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz, +these files must be copied into the corresponding directory: + +- `~/.arduino15/packages/STM32/hardware/stm32/x.yy.zz/variants/STM32F7xx/F765Z(G-I)T_F767Z(G-I)T_F777ZIT/NUCLEO_F767ZI/variant.h` +- `~/.arduino15/packages/STM32/hardware/stm32/x.yy.zz/variants/STM32L0xx/L052R(6-8)T_L053R(6-8)T_L063R8T/NUCLEO_L053R8/variant.h` + +#### 8. For RP2040-based boards using [Earle Philhower arduino-pico core](https://github.com/earlephilhower/arduino-pico) + +#### 8.1 To use BOARD_NAME + + **To be able to automatically detect and display BOARD_NAME on RP2040-based boards (RASPBERRY_PI_PICO, ADAFRUIT_FEATHER_RP2040, GENERIC_RP2040, etc) boards**, you have to copy the file [RP2040 platform.txt](Packages_Patches/rp2040/hardware/rp2040/1.4.0) into rp2040 directory (~/.arduino15/packages/rp2040/hardware/rp2040/1.4.0). + +Supposing the rp2040 core version is 1.4.0. This file must be copied into the directory: + +- `~/.arduino15/packages/rp2040/hardware/rp2040/1.4.0/platform.txt` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied into the directory: + +- `~/.arduino15/packages/rp2040/hardware/rp2040/x.yy.zz/platform.txt` + +With core after v1.5.0, this step is not necessary anymore thanks to the PR [Add -DBOARD_NAME="{build.board}" #136](https://github.com/earlephilhower/arduino-pico/pull/136). + +#### 8.2 To avoid compile error relating to microsecondsToClockCycles + +Some libraries, such as [Adafruit DHT-sensor-library](https://github.com/adafruit/DHT-sensor-library), require the definition of microsecondsToClockCycles(). **To be able to compile and run on RP2040-based boards**, you have to copy the files in [**RP2040 Arduino.h**](Packages_Patches/rp2040/hardware/rp2040/1.4.0/cores/rp2040/Arduino.h) into rp2040 directory (~/.arduino15/packages/rp2040/hardware/rp2040/1.4.0). + +Supposing the rp2040 core version is 1.4.0. This file must be copied to replace: + +- `~/.arduino15/packages/rp2040/hardware/rp2040/1.4.0/cores/rp2040/Arduino.h` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied to replace: + +- `~/.arduino15/packages/rp2040/hardware/rp2040/x.yy.zz/cores/rp2040/Arduino.h` + +With core after v1.5.0, this step is not necessary anymore thanks to the PR [Add defs for compatibility #142](https://github.com/earlephilhower/arduino-pico/pull/142). + + +#### 9. For Portenta_H7 boards using Arduino IDE in Linux + + **To be able to upload firmware to Portenta_H7 using Arduino IDE in Linux (Ubuntu, etc.)**, you have to copy the file [portenta_post_install.sh](Packages_Patches/arduino/hardware/mbed_portenta/3.4.1/portenta_post_install.sh) into mbed_portenta directory (~/.arduino15/packages/arduino/hardware/mbed_portenta/3.4.1/portenta_post_install.sh). + + Then run the following command using `sudo` + +``` +$ cd ~/.arduino15/packages/arduino/hardware/mbed_portenta/3.4.1 +$ chmod 755 portenta_post_install.sh +$ sudo ./portenta_post_install.sh +``` + +This will create the file `/etc/udev/rules.d/49-portenta_h7.rules` as follows: + +``` +# Portenta H7 bootloader mode UDEV rules + +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="035b", GROUP="plugdev", MODE="0666" +``` + +Supposing the ArduinoCore-mbed core version is 3.4.1. Now only one file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/mbed_portenta/3.4.1/portenta_post_install.sh` + +Whenever a new version is installed, remember to copy this files into the new version directory. For example, new version is x.yy.zz + +This file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/mbed_portenta/x.yy.zz/portenta_post_install.sh` + + +#### 10. For RTL8720DN boards using AmebaD core + + To avoid compile error relating to PROGMEM, you have to copy the file [Realtek AmebaD core pgmspace.h](Packages_Patches/realtek/hardware/AmebaD/3.1.4/cores/ambd/avr/pgmspace.h) into Realtek AmebaD directory (~/.arduino15/packages/realtek/hardware/AmebaD/3.1.4/cores/ambd/avr/pgmspace.h). + +Supposing the Realtek AmebaD core version is 3.1.4. This file must be copied into the directory: + +- `~/.arduino15/packages/realtek/hardware/AmebaD/3.1.4/cores/ambd/avr/pgmspace.h` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied into the directory: + +- `~/.arduino15/packages/realtek/hardware/AmebaD/x.yy.zz/cores/ambd/avr/pgmspace.h` + + +#### 11. For SAMD21 and SAMD51 boards using ArduinoCore-fab-sam core + + To avoid compile error relating to SAMD21/SAMD51, you have to copy the file [ArduinoCore-fab-sam core pgmspace.h](Packages_Patches/Fab_SAM_Arduino/hardware/samd/1.9.0/boards.txt) into `ArduinoCore-fab-sam` samd directory (~/.arduino15/packages/Fab_SAM_Arduino/hardware/samd/1.9.0/boards.txt). + +Supposing the `ArduinoCore-fab-sam` samd core version is 1.9.0. This file must be copied into the directory: + +- `~/.arduino15/packages/Fab_SAM_Arduino/hardware/samd/1.9.0/boards.txt` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied into the directory: + +- `~/.arduino15/packages/Fab_SAM_Arduino/hardware/samd/x.yy.zz/boards.txt` + + +#### 12. For Seeeduino RP2040 boards + + ***To be able to compile, run and automatically detect and display BOARD_NAME on Seeeduino RP2040 (XIAO RP2040, Wio RP2040 Mini) boards***, you have to copy the whole [Seeeduino RP2040 Packages_Patches](Packages_Patches/Seeeduino/hardware/rp2040/2.7.2) directory into Seeeduino samd directory (~/.arduino15/packages/Seeeduino/hardware/rp2040/2.7.2). + +Supposing the Seeeduino RP2040 core version is 2.7.2. These files must be copied into the directory: + +- `~/.arduino15/packages/Seeeduino/hardware/rp2040/2.7.2/boards.txt` +- `~/.arduino15/packages/Seeeduino/hardware/rp2040/2.7.2/variants/Seeed_XIAO_RP2040/pins_arduino.h` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +These files must be copied into the directory: + +- `~/.arduino15/packages/Seeeduino/hardware/samd/x.yy.zz/boards.txt` +- `~/.arduino15/packages/Seeeduino/hardware/samd/x.yy.zz/variants/Seeed_XIAO_RP2040/pins_arduino.h` + + +--- + +#### 13. For Seeeduino nRF52840 boards + +**To be able to compile and run on Xiao nRF52840 boards**, you have to copy the whole [nRF52 1.0.0](Packages_Patches/Seeeduino/hardware/nrf52/1.0.0) directory into Seeeduino nRF52 directory (~/.arduino15/packages/Seeeduino/hardware/nrf52/1.0.0). + +Supposing the Seeeduino nRF52 version is 1.0.0. These files must be copied into the directory: + +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/1.0.0/platform.txt`** +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/1.0.0/cores/nRF5/Print.h`** +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/1.0.0/cores/nRF5/Print.cpp`** +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/1.0.0/cores/nRF5/Udp.h`** + +Whenever a new version is installed, remember to copy these files into the new version directory. For example, new version is x.yy.z +These files must be copied into the directory: + +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/x.yy.z/platform.txt`** +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/x.yy.z/cores/nRF5/Print.h`** +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/x.yy.z/cores/nRF5/Print.cpp`** +- **`~/.arduino15/packages/Seeeduino/hardware/nrf52/x.yy.z/cores/nRF5/Udp.h`** + + +--- +--- + +### How to configure to use different WiFi Libraries + +#### 1. Modify pin-to-pin connection in WiFiNINA_Generic library + +Please change the pin-to-pin connection in `~/Arduino/libraries/src/WiFiNINA_Pinout_Generic.h` to match actual connection if using WiFiNINA with [`WiFiNINA_Generic library`](https://github.com/khoih-prog/WiFiNINA_Generic). + +For example + +```cpp +#elif ( defined(NRF52840_FEATHER) || defined(NRF52832_FEATHER) || defined(NRF52_SERIES) || defined(ARDUINO_NRF52_ADAFRUIT) || \ + defined(NRF52840_FEATHER_SENSE) || defined(NRF52840_ITSYBITSY) || defined(NRF52840_CIRCUITPLAY) || defined(NRF52840_CLUE) || \ + defined(NRF52840_METRO) || defined(NRF52840_PCA10056) || defined(PARTICLE_XENON) || defined(NINA_B302_ublox) || defined(NINA_B112_ublox) ) + + #warning You have to modify pin usage according to actual connection for NRF528XX + // To define pin out for WiFiNINA here + + //#define PINS_COUNT (60u) + //NINA + #define NINA_GPIO0 (26u) //26 + #define NINA_RESETN (27u) + #define NINA_ACK (28u) + + #define SPIWIFI_SS 24 //PIN_SPI1_SS //24 + #define SPIWIFI_ACK 28 //NINA_ACK //28 + #define SPIWIFI_RESET 27 //NINA_RESETN //27 +``` + +#### 2. How to select which built-in WiFi or shield to use + +- To use W102-based WiFiNINA, define in the sketch: + +```cpp +#define USE_WIFI_NINA true +``` + +- To use built-in WiFi101 or shield: + +```cpp +#define USE_WIFI_NINA false +#define USE_WIFI101 true +``` + +- To use MKR1000 with built-in WiFi101: + +```cpp +// Don't care false or true +#define USE_WIFI_NINA false +``` + +- For boards other than MKR1000, to use another WiFi library with the standard **WiFi.h**, such as [`WiFiEspAT library`](https://github.com/jandrassy/WiFiEspAT) library + +```cpp +#define USE_WIFI_NINA false +``` + +- To use another WiFi library without the standard **WiFi.h** + +For example, WiFi_XYZ library uses **WiFi_XYZ.h** + +```cpp +#define USE_WIFI_NINA false +#define USE_WIFI_CUSTOM true + +... +//Must be placed before #include +#include +#include +``` + +#### 3. Important + +- The **WiFiEsp, WiFi_Link libraries are not supported**. Don't use unless you know how to modify those libraries. +- Requests to support for any custom WiFi library will be ignored. **Use at your own risk**. + +--- +--- + +### Usage + +#### Class Constructor + +```cpp + WiFiWebServer server(80); +``` + +Creates the WiFiWebServer class object. + +*Parameters:* + +host port number: ``int port`` (default is the standard HTTP port 80) + +--- + +#### Basic Operations + +**Starting the server** + +```cpp + void begin(); +``` + +**Handling incoming client requests** + +```cpp + void handleClient(); +``` + +**Disabling the server** + +```cpp + void close(); + void stop(); +``` + +Both methods function the same + +**Client request handlers** + +```cpp + void on(); + void addHandler(); + void onNotFound(); + void onFileUpload(); +``` + +Example: + +```cpp + server.on("/", handlerFunction); + server.onNotFound(handlerFunction); // called when handler is not assigned + server.onFileUpload(handlerFunction); // handle file uploads +``` + +**Sending responses to the client** + +```cpp + void send(); + void send_P(); +``` + +`Parameters:` + +`code` - HTTP response code, can be `200` or `404`, etc. + +`content_type` - HTTP content type, like `"text/plain"` or `"image/png"`, etc. + +`content` - actual content body + +--- + +#### Advanced Options + +**Getting information about request arguments** + +```cpp + const String & arg(); + const String & argName(); + int args(); + bool hasArg(); +``` + +`Function usage:` + +`arg` - get request argument value, use `arg("plain")` to get POST body + +`argName` - get request argument name + +`args` - get arguments count + +`hasArg` - check if argument exist + +**Getting information about request headers** + +```cpp + const String & header(); + const String & headerName(); + const String & hostHeader(); + int headers(); + bool hasHeader(); +``` + +`Function usage:` + +`header` - get request header value + +`headerName` - get request header name + +`hostHeader` - get request host header if available, else empty string + +`headers` - get header count + +`hasHeader` - check if header exist + +**Authentication** + +```cpp + bool authenticate(); + void requestAuthentication(); +``` + +`Function usage:` + +`authenticate` - server authentication, returns true if client is authenticated else false + +`requestAuthentication` - sends authentication failure response to the client + +`Example Usage:` + +```cpp + + if(!server.authenticate(username, password)) + { + server.requestAuthentication(); + } +``` + +--- + +#### Other Function Calls + +```cpp + const String& uri(); // get the current uri + HTTPMethod method(); // get the current method + WiFiClient client(); // get the current client + HTTPUpload& upload(); // get the current upload + + void setContentLength(); // set content length + void sendHeader(); // send HTTP header + void sendContent(); // send content + void sendContent_P(); + void collectHeaders(); // set the request headers to collect + void serveStatic(); + + size_t streamFile(); +``` + +--- +--- + +### Examples: + +#### Original Examples + + 1. [AdvancedWebServer](examples/AdvancedWebServer) + 2. [AP_SimpleWebServer](examples/AP_SimpleWebServer) + 3. [HelloServer](examples/HelloServer) + 4. [HelloServer2](examples/HelloServer2) + 5. [HttpBasicAuth](examples/HttpBasicAuth) + 6. [MQTTClient_Auth](examples/MQTTClient_Auth) + 7. [MQTTClient_Basic](examples/MQTTClient_Basic) + 8. [MQTT_ThingStream](examples/MQTT_ThingStream) + 9. [PostServer](examples/PostServer) +10. [ScanNetworks](examples/ScanNetworks) +11. [SimpleAuthentication](examples/SimpleAuthentication) +12. [UdpNTPClient](examples/UdpNTPClient) +13. [UdpSendReceive](examples/UdpSendReceive) +14. [WebClient](examples/WebClient) +15. [WebClientRepeating](examples/WebClientRepeating) +16. [WebServer](examples/WebServer) +17. [WiFiUdpNtpClient](examples/WiFiUdpNtpClient) +18. [multiFileProject](examples/multiFileProject) **New** + +#### HTTP and WebSocket Client New Examples + + 1. [BasicAuthGet](examples/HTTPClient/BasicAuthGet) + 2. [CustomHeader](examples/HTTPClient/CustomHeader) + 3. [DweetGet](examples/HTTPClient/DweetGet) + 4. [DweetPost](examples/HTTPClient/DweetPost) + 5. [HueBlink](examples/HTTPClient/HueBlink) + 6. [node_test_server](examples/HTTPClient/node_test_server) + 7. [PostWithHeaders](examples/HTTPClient/PostWithHeaders) + 8. [SimpleDelete](examples/HTTPClient/SimpleDelete) + 9. [SimpleGet](examples/HTTPClient/SimpleGet) +10. [SimpleHTTPExample](examples/HTTPClient/SimpleHTTPExample) +11. [SimplePost](examples/HTTPClient/SimplePost) +12. [SimplePut](examples/HTTPClient/SimplePut) +13. [SimpleWebSocket](examples/HTTPClient/SimpleWebSocket) + +#### WiFiMulti Examples + + 1. [AdvancedWebServer_WiFiMulti](examples/WiFiMulti/AdvancedWebServer_WiFiMulti) + 2. [MQTTClient_Auth_WiFiMulti](examples/WiFiMulti/MQTTClient_Auth_WiFiMulti) + 3. [MQTTClient_Basic_WiFiMulti](examples/WiFiMulti/MQTTClient_Basic_WiFiMulti) + 4. [MQTT_ThingStream_WiFiMulti](examples/WiFiMulti/MQTT_ThingStream_WiFiMulti) + 5. [WiFiUdpNtpClient_WiFiMulti](examples/WiFiMulti/WiFiUdpNtpClient_WiFiMulti) + 6. [WebClient_WiFiMulti](examples/WiFiMulti/WebClient_WiFiMulti) + 7. [WebClientRepeating_WiFiMulti](examples/WiFiMulti/WebClientRepeating_WiFiMulti) + 8. [WebServer_WiFiMulti](examples/WiFiMulti/WebServer_WiFiMulti) + +--- + +### Example [AdvancedWebServer](examples/AdvancedWebServer) + +#### 1. File [AdvancedWebServer.ino](examples/AdvancedWebServer/AdvancedWebServer.ino) + + +https://github.com/khoih-prog/WiFiWebServer/blob/9094e545cd4da8007fd321212a27a36edd2d3da2/examples/AdvancedWebServer/AdvancedWebServer.ino#L40-L327 + +#### 2. File [defines.h](examples/AdvancedWebServer/defines.h) + + +https://github.com/khoih-prog/WiFiWebServer/blob/9094e545cd4da8007fd321212a27a36edd2d3da2/examples/AdvancedWebServer/defines.h#L12-L409 + +--- +--- + +### Debug Terminal Output Samples + +#### 1. AdvancedWebServer on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library + +The following are debug terminal output and screen shot when running example [**AdvancedWebServer**](examples/AdvancedWebServer) on **SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library** + +

+ +

+ +```cpp +Starting AdvancedServer on SAMD_NANO_33_IOT +WiFiWebServer v1.10.1 +[NN] =============================== +[NN] +Used/default SPI pinout: +[NN] MOSI: 11 +[NN] MISO: 12 +[NN] SCK: 13 +[NN] SS: 10 +[NN] =============================== +[NN] +Used/default NINA pinout: +[NN] NINA_GPIO0: 26 +[NN] NINA_RESETN/SPIWIFI_RESET: 27 +[NN] NINA_ACK: 28 +[NN] SS: 10 +[NN] =============================== +[NN] +Actual final pinout to used: +[NN] SPIWIFI_SS: 24 +[NN] SLAVESELECT/SPIWIFI_SS: 24 +[NN] SLAVEREADY/SPIWIFI_ACK/NINA_ACK: 28 +[NN] SLAVERESET/SPIWIFI_RESET/NINA_RESETN: 27 +[NN] =============================== +Connecting to WPA SSID: HueNet1 +HTTP server started @ 192.168.2.118 +H[WIFI] String Len = 0, extend to 2048 +WiFiWebServer::handleClient: New Client +method: GET +url: / +search: +headerName: Host +headerValue: 192.168.2.118 +headerName: Connection +headerValue: keep-alive +headerName: Cache-Control +headerValue: max-age=0 +headerName: DNT +headerValue: 1 +headerName: Upgrade-Insecure-Requests +headerValue: 1 +headerName: User-Agent +headerValue: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 +headerName: Accept +headerValue: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 +headerName: Accept-Encoding +headerValue: gzip, deflate +headerName: Accept-Language +headerValue: en-GB,en-US;q=0.9,en;q=0.8 +headerName: Cookie +headerValue: NINASESSIONID=0 +args: +args count: 0 +args: +args count: 0 +Request: / +Arguments: +Final list of key/value pairs: +WiFiWebServer::_handleRequest handle +WiFiWebServer::send1: len = 330 +content = WiFiNINA SAMD_NANO_33_IOT

Hello from WiFiNINA

on SAMD_NANO_33_IOT

Uptime: 00:00:20

+WiFiWebServer::_prepareHeader sendHeader Conn close +WiFiWebServer::send1: write header = HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 330 +Connection: close + + +WiFiWebServer::sendContent: Client.write content: WiFiNINA SAMD_NANO_33_IOT

Hello from WiFiNINA

on SAMD_NANO_33_IOT

Uptime: 00:00:20

+WiFiWebServer::_handleRequest OK +WiFiWebServer::handleClient: Client disconnected +WiFiWebServer::handleClient: Don't keepCurrentClient +WiFiWebServer::handleClient: Client disconnected +WiFiWebServer::handleClient: New Client +method: GET +url: /test.svg +search: +headerName: Host +headerValue: 192.168.2.118 +headerName: Connection +headerValue: keep-alive +headerName: User-Agent +headerValue: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 +headerName: DNT +headerValue: 1 +headerName: Accept +headerValue: image/webp,image/apng,image/*,*/*;q=0.8 +headerName: Referer +headerValue: http://192.168.2.118/ +headerName: Accept-Encoding +headerValue: gzip, deflate +headerName: Accept-Language +headerValue: en-GB,en-US;q=0.9,en;q=0.8 +headerName: Cookie +headerValue: NINASESSIONID=0 +args: +args count: 0 +args: +args count: 0 +Request: /test.svg +Arguments: +Final list of key/value pairs: +WiFiWebServer::_handleRequest handle +WiFiWebServer::send1: len = 1946 +content = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +WiFiWebServer::_prepareHeader sendHeader Conn close +WiFiWebServer::send1: write header = HTTP/1.1 200 OK +Content-Type: image/svg+xml +Content-Length: 1946 +Connection: close +``` + +--- + +#### 2. SimpleWebSocket on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library + +The terminal output of **SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library** running [SimpleWebSocket example](examples/HTTPClient/SimpleWebSocket) to demonstrate newly-added WebSocket Client feature. + +```cpp +Starting SimpleWebSocket on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +WiFiWebServer v1.10.1 +Connecting to SSID: HueNet1 +You're connected to the network, IP = 192.168.2.98 +SSID: HueNet1, Signal strength (RSSI):-24 dBm +starting WebSocket client +Sending Hello 0 +Sending Hello 1 +Received a message: +0 => Hello from SimpleWebSocket on SAMD_NANO_33_IOT, millis = 6642 +Sending Hello 2 +Received a message: +1 => Hello from SimpleWebSocket on SAMD_NANO_33_IOT, millis = 11648 +Sending Hello 3 +Received a message: +2 => Hello from SimpleWebSocket on SAMD_NANO_33_IOT, millis = 16655 +Sending Hello 4 +Received a message: +3 => Hello from SimpleWebSocket on SAMD_NANO_33_IOT, millis = 21661 +Sending Hello 5 +Received a message: +4 => Hello from SimpleWebSocket on SAMD_NANO_33_IOT, millis = 26668 +Sending Hello 6 +Received a message: +5 => Hello from SimpleWebSocket on SAMD_NANO_33_IOT, millis = 31675 +``` + +--- + +#### 3. SimpleHTTPExample on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library + +The terminal output of **SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library** running [SimpleHTTPExample example](examples/HTTPClient/SimpleHTTPExample) to demonstrate newly-added HTTP Client feature. + +```cpp +Starting SimpleHTTPExample on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +WiFiWebServer v1.10.1 +Connecting to SSID: HueNet1 +You're connected to the network, IP = 192.168.2.98 +SSID: HueNet1, Signal strength (RSSI):-21 dBm +startedRequest ok +Got status code: 200 +Content length is: 2263 +Body returned follows: + + `:;;;,` .:;;:. + .;;;;;;;;;;;` :;;;;;;;;;;: TM + `;;;;;;;;;;;;;;;` :;;;;;;;;;;;;;;; + :;;;;;;;;;;;;;;;;;; `;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;; .;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;:` `;;;;;;;;; ,;;;;;;;;.` .;;;;;;;; + .;;;;;;, :;;;;;;; .;;;;;;; ;;;;;;; + ;;;;;; ;;;;;;; ;;;;;;, ;;;;;;. + ,;;;;; ;;;;;;.;;;;;;` ;;;;;; + ;;;;;. ;;;;;;;;;;;` ``` ;;;;;` + ;;;;; ;;;;;;;;;, ;;; .;;;;; +`;;;;: `;;;;;;;; ;;; ;;;;; +,;;;;` `,,,,,,,, ;;;;;;; .,,;;;,,, ;;;;; +:;;;;` .;;;;;;;; ;;;;;, :;;;;;;;; ;;;;; +:;;;;` .;;;;;;;; `;;;;;; :;;;;;;;; ;;;;; +.;;;;. ;;;;;;;. ;;; ;;;;; + ;;;;; ;;;;;;;;; ;;; ;;;;; + ;;;;; .;;;;;;;;;; ;;; ;;;;;, + ;;;;;; `;;;;;;;;;;;; ;;;;; + `;;;;;, .;;;;;; ;;;;;;; ;;;;;; + ;;;;;;: :;;;;;;. ;;;;;;; ;;;;;; + ;;;;;;;` .;;;;;;;, ;;;;;;;; ;;;;;;;: + ;;;;;;;;;:,:;;;;;;;;;: ;;;;;;;;;;:,;;;;;;;;;; + `;;;;;;;;;;;;;;;;;;;. ;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;; :;;;;;;;;;;;;;;;;: + ,;;;;;;;;;;;;;, ;;;;;;;;;;;;;; + .;;;;;;;;;` ,;;;;;;;;: + + + + + ;;; ;;;;;` ;;;;: .;; ;; ,;;;;;, ;;. `;, ;;;; + ;;; ;;:;;; ;;;;;; .;; ;; ,;;;;;: ;;; `;, ;;;:;; + ,;:; ;; ;; ;; ;; .;; ;; ,;, ;;;,`;, ;; ;; + ;; ;: ;; ;; ;; ;; .;; ;; ,;, ;;;;`;, ;; ;;. + ;: ;; ;;;;;: ;; ;; .;; ;; ,;, ;;`;;;, ;; ;;` + ,;;;;; ;;`;; ;; ;; .;; ;; ,;, ;; ;;;, ;; ;; + ;; ,;, ;; .;; ;;;;;: ;;;;;: ,;;;;;: ;; ;;, ;;;;;; + ;; ;; ;; ;;` ;;;;. `;;;: ,;;;;;, ;; ;;, ;;;; + +``` + +--- + +#### 4. DweetPost on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library + +The terminal output of **SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library** running [DweetPost example](examples/HTTPClient/DweetPost) to demonstrate newly-added HTTP Client feature. + +```cpp +Starting DweetPost on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +WiFiWebServer v1.10.1 +Connecting to SSID: HueNet1 +You're connected to the network, IP = 192.168.2.98 +SSID: HueNet1, Signal strength (RSSI):-22 dBm +making POST request +Status code: 200 +Response: {"this":"succeeded","by":"dweeting","the":"dweet","with":{"thing":"Hello-from-SAMD_NANO_33_IOT","created":"2020-11-17T19:55:37.378Z","content":{"sensorValue":581},"transaction":"f968ee5f-35b5-4984-ac3d-34d93fdaddbe"}} +Wait ten seconds + +making POST request +Status code: 200 +Response: {"this":"succeeded","by":"dweeting","the":"dweet","with":{"thing":"Hello-from-SAMD_NANO_33_IOT","created":"2020-11-17T19:55:48.925Z","content":{"sensorValue":570},"transaction":"68ee52e0-22a3-4af2-96cd-aaa53587b314"}} +Wait ten seconds +``` + +--- + +#### 5. DweetGet on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library + +The terminal output of **SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library** running [DweetGet example](examples/HTTPClient/DweetGet) to demonstrate newly-added HTTP Client feature. + +```cpp +Starting DweetGet on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +WiFiWebServer v1.10.1 +Connecting to SSID: HueNet1 +You're connected to the network, IP = 192.168.2.98 +SSID: HueNet1, Signal strength (RSSI):-25 dBm +Making GET request +Status code: 200 +Response: {"this":"succeeded","by":"getting","the":"dweets","with":[{"thing":"Hello-from-SAMD_NANO_33_IOT","created":"2020-11-17T20:06:18.905Z","content":{"sensorValue":567}}]} +"sensorValue":567 +Value string: 567 +Actual value: 567 +Wait ten seconds +Making GET request +Status code: 200 +Response: {"this":"succeeded","by":"getting","the":"dweets","with":[{"thing":"Hello-from-SAMD_NANO_33_IOT","created":"2020-11-17T20:06:18.905Z","content":{"sensorValue":567}}]} +"sensorValue":567 +Value string: 567 +Actual value: 567 +Wait ten seconds +``` + +--- + +#### 6. MQTTClient_Auth on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library + +The terminal output of **SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library** running [MQTTClient_Auth example](examples/MQTTClient_Auth) to demonstrate newly-added MQTT Client feature. + + +```cpp +Starting MQTTClient_Auth on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +WiFiWebServer v1.10.1 +Please upgrade the firmware +Connecting to WPA SSID: HueNet1 +Connected! IP address: 192.168.2.98 +Attempting MQTT connection to broker.emqx.io...connected +Message Send : MQTT_Pub => Hello from MQTTClient_Auth on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +Message arrived [MQTT_Pub] Hello from MQTTClient_Auth on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +Message Send : MQTT_Pub => Hello from MQTTClient_Auth on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +Message arrived [MQTT_Pub] Hello from MQTTClient_Auth on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +``` + +--- + +#### 7. MQTT_ThingStream on Arduino SAMD_NANO_33_IOT using WiFiNINA_Generic Library + +The terminal output of **SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library** running [MQTT_ThingStream example](examples/MQTT_ThingStream) to demonstrate newly-added MQTT Client feature. + + +```cpp +Start MQTT_ThingStream on SAMD_NANO_33_IOT +Starting MQTTClient_Auth on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +WiFiWebServer v1.10.1 +Please upgrade the firmware +Connecting to WPA SSID: HueNet1 +Connected! IP address: 192.168.2.98 +*************************************** +STM32_Pub +*************************************** +Attempting MQTT connection to broker.emqx.io +...connected +Published connection message successfully! +Subscribed to: STM32_Sub +MQTT Message Send : STM32_Pub => Hello from MQTT_ThingStream on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +MQTT Message receive [STM32_Pub] Hello from MQTT_ThingStream on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +MQTT Message Send : STM32_Pub => Hello from MQTT_ThingStream on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +MQTT Message receive [STM32_Pub] Hello from MQTT_ThingStream on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +MQTT Message Send : STM32_Pub => Hello from MQTT_ThingStream on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library +MQTT Message receive [STM32_Pub] Hello from MQTT_ThingStream on SAMD_NANO_33_IOT with WiFiNINA using WiFiNINA_Generic Library + +``` + +--- + +#### 8. WebClientRepeating on RASPBERRY_PI_PICO with Custom WiFi using Custom WiFi Library + +The terminal output of **RASPBERRY_PI_PICO with Custom WiFi (ESP8266-AT) using Custom WiFi (WiFiEspAT) Library** running [WebClientRepeating example](examples/WebClientRepeating) to demonstrate new RP2040-based board using [**Earle Philhower's arduino-pico** core](https://github.com/earlephilhower/arduino-pico) + +```cpp +Starting WebClientRepeating on RASPBERRY_PI_PICO with Custom WiFi using Custom WiFi Library +WiFiWebServer v1.10.1 +WiFi shield init done +Connecting to SSID: HueNet1 +You're connected to the network, IP = 192.168.2.76 +SSID: HueNet1, Signal strength (RSSI):-29 dBm +Connecting... +HTTP/1.1 200 OK +Server: nginx/1.4.2 +Date: Wed, 26 May 2021 03:56:08 GMT +Content-Type: text/plain +Content-Length: 2263 +Last-Modified: Wed, 02 Oct 2013 13:46:47 GMT +Connection: close +Vary: Accept-Encoding +ETag: "524c23c7-8d7" +Accept-Ranges: bytes + + + `:;;;,` .:;;:. + .;;;;;;;;;;;` :;;;;;;;;;;: TM + `;;;;;;;;;;;;;;;` :;;;;;;;;;;;;;;; + :;;;;;;;;;;;;;;;;;; `;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;; .;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;:` `;;;;;;;;; ,;;;;;;;;.` .;;;;;;;; + .;;;;;;, :;;;;;;; .;;;;;;; ;;;;;;; + ;;;;;; ;;;;;;; ;;;;;;, ;;;;;;. + ,;;;;; ;;;;;;.;;;;;;` ;;;;;; + ;;;;;. ;;;;;;;;;;;` ``` ;;;;;` + ;;;;; ;;;;;;;;;, ;;; .;;;;; +`;;;;: `;;;;;;;; ;;; ;;;;; +,;;;;` `,,,,,,,, ;;;;;;; .,,;;;,,, ;;;;; +:;;;;` .;;;;;;;; ;;;;;, :;;;;;;;; ;;;;; +:;;;;` .;;;;;;;; `;;;;;; :;;;;;;;; ;;;;; +.;;;;. ;;;;;;;. ;;; ;;;;; + ;;;;; ;;;;;;;;; ;;; ;;;;; + ;;;;; .;;;;;;;;;; ;;; ;;;;;, + ;;;;;; `;;;;;;;;;;;; ;;;;; + `;;;;;, .;;;;;; ;;;;;;; ;;;;;; + ;;;;;;: :;;;;;;. ;;;;;;; ;;;;;; + ;;;;;;;` .;;;;;;;, ;;;;;;;; ;;;;;;;: + ;;;;;;;;;:,:;;;;;;;;;: ;;;;;;;;;;:,;;;;;;;;;; + `;;;;;;;;;;;;;;;;;;;. ;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;; :;;;;;;;;;;;;;;;;: + ,;;;;;;;;;;;;;, ;;;;;;;;;;;;;; + .;;;;;;;;;` ,;;;;;;;;: + + + + + ;;; ;;;;;` ;;;;: .;; ;; ,;;;;;, ;;. `;, ;;;; + ;;; ;;:;;; ;;;;;; .;; ;; ,;;;;;: ;;; `;, ;;;:;; + ,;:; ;; ;; ;; ;; .;; ;; ,;, ;;;,`;, ;; ;; + ;; ;: ;; ;; ;; ;; .;; ;; ,;, ;;;;`;, ;; ;;. + ;: ;; ;;;;;: ;; ;; .;; ;; ,;, ;;`;;;, ;; ;;` + ,;;;;; ;;`;; ;; ;; .;; ;; ,;, ;; ;;;, ;; ;; + ;; ,;, ;; .;; ;;;;;: ;;;;;: ,;;;;;: ;; ;;, ;;;;;; + ;; ;; ;; ;;` ;;;;. `;;;: ,;;;;;, ;; ;;, ;;;; +``` + +--- + +#### 9. AdvancedWebServer on Arduino Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library + +The following are debug terminal output and screen shot when running example [**AdvancedWebServer**](examples/AdvancedWebServer) on **Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library** + +

+ +

+ + +```cpp +Starting AdvancedServer on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +WiFiWebServer v1.10.1 +Connecting to WPA SSID: HueNet1 +HTTP server started @ 192.168.2.130 +H[WIFI] handleClient: New Client +[WIFI] method: GET +[WIFI] url: / +[WIFI] search: +[WIFI] headerName: Host +[WIFI] headerValue: 192.168.2.130 +[WIFI] headerName: Connection +[WIFI] headerValue: keep-alive +[WIFI] headerName: Cache-Control +[WIFI] headerValue: max-age=0 +[WIFI] headerName: Upgrade-Insecure-Requests +[WIFI] headerValue: 1 +[WIFI] headerName: DNT +[WIFI] headerValue: 1 +[WIFI] headerName: User-Agent +[WIFI] headerValue: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 +[WIFI] headerName: Accept +[WIFI] headerValue: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 +[WIFI] headerName: Referer +[WIFI] headerValue: http://192.168.2.130/ +[WIFI] headerName: Accept-Encoding +[WIFI] headerValue: gzip, deflate +[WIFI] headerName: Accept-Language +[WIFI] headerValue: en-GB,en-US;q=0.9,en;q=0.8,vi;q=0.7 +[WIFI] args: +[WIFI] args count: 0 +[WIFI] args: +[WIFI] args count: 0 +[WIFI] Request: / +[WIFI] Arguments: +[WIFI] Final list of key/value pairs: +[WIFI] _handleRequest handle +[WIFI] send1: len = 392 +[WIFI] content = Nano RP2040 Connect

Hello from Nano RP2040 Connect

running WiFiWebServer

on WiFiNINA using WiFiNINA_Generic Library

Uptime: 0 d 00:00:12

+[WIFI] _prepareHeader sendHeader Conn close +[WIFI] send1: write header = HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 392 +Connection: close +[WIFI] sendContent: Client.write content: Nano RP2040 Connect

Hello from Nano RP2040 Connect

running WiFiWebServer

on WiFiNINA using WiFiNINA_Generic Library

Uptime: 0 d 00:00:12

+[WIFI] _handleRequest OK +[WIFI] handleClient: Client disconnected +[WIFI] handleClient: Don't keepCurrentClient +[WIFI] handleClient: Client disconnected +[WIFI] handleClient: New Client +[WIFI] method: GET +[WIFI] url: /test.svg +[WIFI] search: +[WIFI] headerName: Host +[WIFI] headerValue: 192.168.2.130 +[WIFI] headerName: Connection +[WIFI] headerValue: keep-alive +[WIFI] headerName: User-Agent +[WIFI] headerValue: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 +[WIFI] headerName: DNT +[WIFI] headerValue: 1 +[WIFI] headerName: Accept +[WIFI] headerValue: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 +[WIFI] headerName: Referer +[WIFI] headerValue: http://192.168.2.130/ +[WIFI] headerName: Accept-Encoding +[WIFI] headerValue: gzip, deflate +[WIFI] headerName: Accept-Language +[WIFI] headerValue: en-GB,en-US;q=0.9,en;q=0.8,vi;q=0.7 +[WIFI] args: +[WIFI] args count: 0 +[WIFI] args: +[WIFI] args count: 0 +[WIFI] Request: /test.svg +[WIFI] Arguments: +[WIFI] Final list of key/value pairs: +[WIFI] _handleRequest handle +[WIFI] send1: len = 1954 +[WIFI] content = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[WIFI] _prepareHeader sendHeader Conn close +[WIFI] send1: write header = HTTP/1.1 200 OK +Content-Type: image/svg+xml +Content-Length: 1954 +Connection: close + +``` + +--- + +#### 10. SimpleHTTPExample on ESP32_DEV + +The terminal output of **ESP32_DEV** running [SimpleHTTPExample example](examples/HTTPClient/SimpleHTTPExample) to demonstrate newly-added HTTP Client feature. + +```cpp +Starting SimpleHTTPExample on ESP32_DEV with ESP WiFi using WiFi Library +WiFiWebServer v1.10.1 +Connecting to SSID: HueNet1 +You're connected to the network, IP = 192.168.2.80 +SSID: HueNet1, Signal strength (RSSI):-23 dBm +startedRequest ok +Got status code: 200 +Content length is: 2263 +Body returned follows: + + `:;;;,` .:;;:. + .;;;;;;;;;;;` :;;;;;;;;;;: TM + `;;;;;;;;;;;;;;;` :;;;;;;;;;;;;;;; + :;;;;;;;;;;;;;;;;;; `;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;; .;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;:` `;;;;;;;;; ,;;;;;;;;.` .;;;;;;;; + .;;;;;;, :;;;;;;; .;;;;;;; ;;;;;;; + ;;;;;; ;;;;;;; ;;;;;;, ;;;;;;. + ,;;;;; ;;;;;;.;;;;;;` ;;;;;; + ;;;;;. ;;;;;;;;;;;` ``` ;;;;;` + ;;;;; ;;;;;;;;;, ;;; .;;;;; +`;;;;: `;;;;;;;; ;;; ;;;;; +,;;;;` `,,,,,,,, ;;;;;;; .,,;;;,,, ;;;;; +:;;;;` .;;;;;;;; ;;;;;, :;;;;;;;; ;;;;; +:;;;;` .;;;;;;;; `;;;;;; :;;;;;;;; ;;;;; +.;;;;. ;;;;;;;. ;;; ;;;;; + ;;;;; ;;;;;;;;; ;;; ;;;;; + ;;;;; .;;;;;;;;;; ;;; ;;;;;, + ;;;;;; `;;;;;;;;;;;; ;;;;; + `;;;;;, .;;;;;; ;;;;;;; ;;;;;; + ;;;;;;: :;;;;;;. ;;;;;;; ;;;;;; + ;;;;;;;` .;;;;;;;, ;;;;;;;; ;;;;;;;: + ;;;;;;;;;:,:;;;;;;;;;: ;;;;;;;;;;:,;;;;;;;;;; + `;;;;;;;;;;;;;;;;;;;. ;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;; :;;;;;;;;;;;;;;;;: + ,;;;;;;;;;;;;;, ;;;;;;;;;;;;;; + .;;;;;;;;;` ,;;;;;;;;: + + + + + ;;; ;;;;;` ;;;;: .;; ;; ,;;;;;, ;;. `;, ;;;; + ;;; ;;:;;; ;;;;;; .;; ;; ,;;;;;: ;;; `;, ;;;:;; + ,;:; ;; ;; ;; ;; .;; ;; ,;, ;;;,`;, ;; ;; + ;; ;: ;; ;; ;; ;; .;; ;; ,;, ;;;;`;, ;; ;;. + ;: ;; ;;;;;: ;; ;; .;; ;; ,;, ;;`;;;, ;; ;;` + ,;;;;; ;;`;; ;; ;; .;; ;; ,;, ;; ;;;, ;; ;; + ;; ,;, ;; .;; ;;;;;: ;;;;;: ,;;;;;: ;; ;;, ;;;;;; + ;; ;; ;; ;;` ;;;;. `;;;: ,;;;;;, ;; ;;, ;;;; +Got status code: 200 +Content length is: 2263 +``` + +--- + + +#### 11. AdvancedWebServer on PORTENTA_H7_M7 with Portenta_H7 WiFi + +The following are debug terminal output and screen shot when running example [**AdvancedWebServer**](examples/AdvancedWebServer) on **PORTENTA_H7_M7 with Portenta_H7 WiFi** + +

+ +

+ + +```cpp +Starting AdvancedServer on PORTENTA_H7_M7 with Portenta_H7 WiFi +WiFiWebServer v1.10.1 +Connecting to WPA SSID: HueNet1 +HTTP server started @ 192.168.2.138 +H[WIFI] String Len = 0, extend to 2048 +HHHHHHHHH HHHHHHH +``` + +--- + +#### 12. MQTTClient_Auth on PORTENTA_H7_M7 with Portenta_H7 WiFi + +The terminal output of **PORTENTA_H7_M7 with Portenta_H7 WiFi** running [MQTTClient_Auth example](examples/MQTTClient_Auth) to demonstrate MQTT Client feature. + + +```cpp +Starting MQTTClient_Auth on PORTENTA_H7_M7 with Portenta_H7 WiFi +WiFiWebServer v1.10.1 +Connecting to SSID: HueNet1 +Connected! IP address: 192.168.2.130 +Attempting MQTT connection to broker.emqx.io...connected +Message Send : MQTT_Pub => Hello from MQTTClient_Auth on PORTENTA_H7_M7 with Portenta_H7 WiFi +Message arrived [MQTT_Pub] Hello from MQTTClient_Auth on PORTENTA_H7_M7 with Portenta_H7 WiFi +Message Send : MQTT_Pub => Hello from MQTTClient_Auth on PORTENTA_H7_M7 with Portenta_H7 WiFi +Message arrived [MQTT_Pub] Hello from MQTTClient_Auth on PORTENTA_H7_M7 with Portenta_H7 WiFi +``` + +--- + + +#### 13. WebClientRepeating on PORTENTA_H7_M7 with Portenta_H7 WiFi + +The terminal output of **PORTENTA_H7_M7 with Portenta_H7 WiFi** running [WebClientRepeating example](examples/WebClientRepeating). + + +```cpp +Starting WebClientRepeating on PORTENTA_H7_M7 with Portenta_H7 WiFi +WiFiWebServer v1.10.1 +Connecting to SSID: HueNet1 +You're connected to the network, IP = 192.168.2.130 +SSID: HueNet1, Signal strength (RSSI):-33 dBm +Connecting... +HTTP/1.1 200 OK +Server: nginx/1.4.2 +Date: Wed, 08 Sep 2021 01:29:41 GMT +Content-Type: text/plain +Content-Length: 2263 +Last-Modified: Wed, 02 Oct 2013 13:46:47 GMT +Connection: close +Vary: Accept-Encoding +ETag: "524c23c7-8d7" +Accept-Ranges: bytes + + + `:;;;,` .:;;:. + .;;;;;;;;;;;` :;;;;;;;;;;: TM + `;;;;;;;;;;;;;;;` :;;;;;;;;;;;;;;; + :;;;;;;;;;;;;;;;;;; `;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;; .;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;:` `;;;;;;;;; ,;;;;;;;;.` .;;;;;;;; + .;;;;;;, :;;;;;;; .;;;;;;; ;;;;;;; + ;;;;;; ;;;;;;; ;;;;;;, ;;;;;;. + ,;;;;; ;;;;;;.;;;;;;` ;;;;;; + ;;;;;. ;;;;;;;;;;;` ``` ;;;;;` + ;;;;; ;;;;;;;;;, ;;; .;;;;; +`;;;;: `;;;;;;;; ;;; ;;;;; +,;;;;` `,,,,,,,, ;;;;;;; .,,;;;,,, ;;;;; +:;;;;` .;;;;;;;; ;;;;;, :;;;;;;;; ;;;;; +:;;;;` .;;;;;;;; `;;;;;; :;;;;;;;; ;;;;; +.;;;;. ;;;;;;;. ;;; ;;;;; + ;;;;; ;;;;;;;;; ;;; ;;;;; + ;;;;; .;;;;;;;;;; ;;; ;;;;;, + ;;;;;; `;;;;;;;;;;;; ;;;;; + `;;;;;, .;;;;;; ;;;;;;; ;;;;;; + ;;;;;;: :;;;;;;. ;;;;;;; ;;;;;; + ;;;;;;;` .;;;;;;;, ;;;;;;;; ;;;;;;;: + ;;;;;;;;;:,:;;;;;;;;;: ;;;;;;;;;;:,;;;;;;;;;; + `;;;;;;;;;;;;;;;;;;;. ;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;; :;;;;;;;;;;;;;;;;: + ,;;;;;;;;;;;;;, ;;;;;;;;;;;;;; + .;;;;;;;;;` ,;;;;;;;;: + + + + + ;;; ;;;;;` ;;;;: .;; ;; ,;;;;;, ;;. `;, ;;;; + ;;; ;;:;;; ;;;;;; .;; ;; ,;;;;;: ;;; `;, ;;;:;; + ,;:; ;; ;; ;; ;; .;; ;; ,;, ;;;,`;, ;; ;; + ;; ;: ;; ;; ;; ;; .;; ;; ,;, ;;;;`;, ;; ;;. + ;: ;; ;;;;;: ;; ;; .;; ;; ,;, ;;`;;;, ;; ;;` + ,;;;;; ;;`;; ;; ;; .;; ;; ,;, ;; ;;;, ;; ;; + ;; ,;, ;; .;; ;;;;;: ;;;;;: ,;;;;;: ;; ;;, ;;;;;; + ;; ;; ;; ;;` ;;;;. `;;;: ,;;;;;, ;; ;;, ;;;; +``` + +--- + + +#### 14. AdvancedWebServer on ESP32C3_DEV with ESP WiFi + +The following are debug terminal output and screen shot when running example [**AdvancedWebServer**](examples/AdvancedWebServer) on **PORTENTA_H7_M7 with Portenta_H7 WiFi** + +

+ +

+ + +```cpp +Starting AdvancedWebServer on ESP32C3_DEV with ESP WiFi using WiFi Library +WiFiWebServer v1.10.1 +Connecting to WPA SSID: HueNet1 +HTTP server started @ 192.168.2.86 +HH +``` + +--- + +#### 15. AdvancedWebServer on ESP32S3_DEV with ESP WiFi + +The following are debug terminal output and screen shot when running example [**AdvancedWebServer**](examples/AdvancedWebServer) on **PORTENTA_H7_M7 with Portenta_H7 WiFi** + +

+ +

+ + +```cpp +Starting AdvancedWebServer on ESP32S3_DEV with ESP WiFi using WiFi Library +WiFiWebServer v1.10.1 +Connecting to WPA SSID: HueNet1 +HTTP server started @ 192.168.2.86 +HH +``` + +--- + +#### 16. AdvancedWebServer_WiFiMulti on Nano RP2040 Connect with WiFiNINA + +The following are debug terminal output and screen shot when running example [**AdvancedWebServer_WiFiMulti**](examples/WiFiMulti/AdvancedWebServer_WiFiMulti) on **Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library** + +

+ +

+ + +```cpp +Starting AdvancedWebServer_WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +WiFiMulti_Generic v1.2.2 +WiFiWebServer v1.10.1 +Connecting WiFi... +WiFi connected, IP address: 192.168.2.113 +You're connected to the network, IP = 192.168.2.113 +SSID: HueNet1, Signal strength (RSSI):-20 dBm +HTTP server started @ 192.168.2.113 +H +``` + +--- + +#### 17. MQTTClient_Auth_WiFiMulti on Nano RP2040 Connect with WiFiNINA + +The following are debug terminal output and screen shot when running example [**MQTTClient_Auth_WiFiMulti**](examples/WiFiMulti/MQTTClient_Auth_WiFiMulti) on **Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library** + +```cpp +Starting MQTTClient_Auth_WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +WiFiMulti_Generic v1.2.2 +WiFiWebServer v1.10.1 +Connecting WiFi... +WiFi connected, IP address: 192.168.2.113 +You're connected to the network, IP = 192.168.2.113 +SSID: HueNet1, Signal strength (RSSI):-18 dBm +Attempting MQTT connection to broker.emqx.io...connected +Published connection message successfully! +Subscribed to: Nano RP2040 ConnectSub +H +MQTT Message Send : Nano RP2040 ConnectPub => Hello from MQTTClient_Auth__WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +Message arrived [Nano RP2040 ConnectPub] Hello from MQTTClient_Auth__WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +HH +MQTT Message Send : Nano RP2040 ConnectPub => Hello from MQTTClient_Auth__WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +H +Message arrived [Nano RP2040 ConnectPub] Hello from MQTTClient_Auth__WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +HH +MQTT Message Send : Nano RP2040 ConnectPub => Hello from MQTTClient_Auth__WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +H +Message arrived [Nano RP2040 ConnectPub] Hello from MQTTClient_Auth__WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +``` + +--- + +#### 18. WiFiUdpNTPClient_WiFiMulti on Nano RP2040 Connect with WiFiNINA + +The following are debug terminal output and screen shot when running example [**WiFiUdpNTPClient_WiFiMulti**](examples/WiFiMulti/WiFiUdpNTPClient_WiFiMulti) on **Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library** + +```cpp +Starting WiFiUdpNTPClient_WiFiMulti on Nano RP2040 Connect with WiFiNINA using WiFiNINA_Generic Library +WiFiMulti_Generic v1.2.2 +WiFiWebServer v1.10.1 +Connecting WiFi... +WiFi connected, IP address: 192.168.2.113 +You're connected to the network, IP = 192.168.2.113 +SSID: HueNet1, Signal strength (RSSI):-17 dBm +Starting connection to server... +Listening on port 2390 +HHH +Packet received +Seconds since Jan 1 1900 = 3869665072 +Unix time = 1660676272 +The UTC time is 18:57:52 +HH +Packet received +Seconds since Jan 1 1900 = 3869665132 +Unix time = 1660676332 +The UTC time is 18:58:52 +HH +``` + +--- + +#### 19. AdvancedWebServer_WiFiMulti on RASPBERRY_PI_PICO_W + +The following are debug terminal output and screen shot when running example [**AdvancedWebServer_WiFiMulti**](examples/WiFiMulti/AdvancedWebServer_WiFiMulti) on **RASPBERRY_PI_PICO_W with CYW43439 WiFi** + +

+ +

+ + +```cpp +Starting AdvancedWebServer_WiFiMulti on RASPBERRY_PI_PICO_W with RP2040W CYW43439 WiFi +WiFiMulti_Generic v1.2.2 +WiFiWebServer v1.10.1 +Connecting WiFi... + +WiFi connected, IP address: 192.168.2.180 +You're connected to the network, IP = 192.168.2.180 +SSID: HueNet1, Signal strength (RSSI):0 dBm +HTTP server started @ 192.168.2.180 +HH +``` + +--- +--- + +### Debug + +Debug is enabled by default on Serial. Debug Level from 0 to 4. To disable, change the _WIFI_LOGLEVEL_ and _WIFININA_LOGLEVEL_ to 0 + +```cpp +// Use this to output debug msgs to Serial +#define DEBUG_WIFI_WEBSERVER_PORT Serial + +// Debug Level from 0 to 4 +#define _WIFI_LOGLEVEL_ 1 +#define _WIFININA_LOGLEVEL_ 1 +``` + +--- + +## Troubleshooting + +If you get compilation errors, more often than not, you may need to install a newer version of the board's core, applying Libraries' Patches, Packages' Patches or this library latest version. + +--- +--- + +### Issues + +Submit issues to: [WiFiWebServer issues](https://github.com/khoih-prog/WiFiWebServer/issues) + +--- + +### TO DO + +1. Bug Searching and Killing +2. Add SSL/TLS Client and Server support +3. Support more types of boards using WiFiNINA and other WiFi shields. +4. Add support to megaAVR boards using [MegaCoreX core](https://github.com/MCUdude/MegaCoreX) + + +### DONE + + 1. Add support to Arduino SAMD21, Adafruit SAMD21/SAMD51, Seeeduino SAMD21/SAMD51. + 2. Add support to nRF52. + 3. Add support to SAM DUE. + 4. Add support to all STM32F/L/H/G/WB/MP1. + 5. Add support to WiFiNINA using [**WiFiNINA_Generic library**](https://github.com/khoih-prog/WiFiNINA_Generic). + 6. Add support to [**ESP_AT_Lib library**](https://github.com/khoih-prog/ESP_AT_Lib). + 7. Add support to [`WiFi101 library`](https://www.arduino.cc/en/Reference/WiFi101). + 8. Add support to [`WiFiEspAT library`](https://github.com/jandrassy/WiFiEspAT). + 9. Add support to PROGMEM-related commands, such as sendContent_P() and send_P() +10. Add **High-level HTTP (GET, POST, PUT, PATCH, DELETE) and WebSocket Client** +11. Add support to **Arduino Nano RP2040 Connect** using [**Arduino mbed OS for Nano boards**](https://github.com/arduino/ArduinoCore-mbed). +12. Add support to RP2040-based boards, such as **RASPBERRY_PI_PICO, ADAFRUIT_FEATHER_RP2040 and GENERIC_RP2040**, using [**Earle Philhower's arduino-pico** core](https://github.com/earlephilhower/arduino-pico). +13. Add support to RP2040-based boards, such as **RASPBERRY_PI_PICO, ADAFRUIT_FEATHER_RP2040 and GENERIC_RP2040**, using [**Arduino-mbed mbed_rp2040** core](https://github.com/arduino/ArduinoCore-mbed). +14. Add support to **Portenta_H7 boards**, using [**Arduino-mbed mbed_portenta** core](https://github.com/arduino/ArduinoCore-mbed). +15. Reduce usage of Arduino String with std::string +16. Optimize library code and examples by using **reference-passing instead of value-passing**. +17. Add support to new **ESP32-S3** and **ESP32_C3** +18. Add support to megaAVR boards (UNO_WIFI_REV2, NANO_EVERY) using [Arduino megaAVR core](https://github.com/arduino/ArduinoCore-megaavr) +19. Rewrite library and add example [multiFileProject](examples/multiFileProject) to demo for multiple-file project to fix `multiple-definitions` linker error +20. Add [WiFiMulti_Generic](https://github.com/khoih-prog/WiFiMulti_Generic) library support +21. Add many WiFiMulti-related examples in [WiFiMulti](https://github.com/khoih-prog/WiFiWebServer/tree/master/examples/WiFiMulti) +22. Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi with [**Earle Philhower's arduino-pico core** v2.4.0+](https://github.com/earlephilhower/arduino-pico) +23. Better workaround for RP2040W `WiFi.status()` bug using `ping()` to local gateway +24. Add new features, such as `CORS` +25. Use `allman astyle` and add `utils` +26. Using new [`WiFi101_Generic library`](https://github.com/khoih-prog/WiFi101_Generic) for sending larger data + +--- +--- + +## Contributions and Thanks + +1. Based on and modified from [**Ivan Grokhotkov's ESP8266WebServer**](https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer) +2. Thanks to good work of [Miguel Alexandre Wisintainer](https://github.com/tcpipchip) for initiating, inspriring, working with, developing, debugging and testing. Without that, support to nRF52, especially **U-Blox B302 running as nRF52840 and U-Blox B112 running as nRF52832**, has never been started and finished. See [u-blox nina b](https://github.com/khoih-prog/WiFiNINA_Generic/issues/1) +3. [Adrian McEwen](https://github.com/amcewen) for [HttpClient Library](https://github.com/amcewen/HttpClient) on which the [ArduinoHttpClient Library](https://github.com/arduino-libraries/ArduinoHttpClient) and this [EthernetWebServer library](https://github.com/khoih-prog/EthernetWebServer) are relied. +4. [RQnet](https://github.com/RQnet) to report issue [Decoding Error. two times called urlDecode in Parsing-impl.h. #17](https://github.com/khoih-prog/WiFiWebServer/issues/17) leading to version v1.6.3 to fix the decoding error bug when using special `&` in data fields. + + + + + + + + +
igrr
⭐️⭐️ Ivan Grokhotkov

amcewen
⭐️ Adrian McEwen

tcpipchip
⭐️ Miguel Wisintainer

RQnet
RQnet

+ +--- + +## Contributing + +If you want to contribute to this project: +- Report bugs and errors +- Ask for enhancements +- Create issues and pull requests +- Tell other people about this library + +--- + +### License + +- The library is licensed under [MIT](https://github.com/khoih-prog/WiFiWebServer/blob/master/LICENSE) + +--- + +## Copyright + +Copyright (c) 2020- Khoi Hoang + + diff --git a/software/firmware/source/libraries/WiFiWebServer/changelog.md b/software/firmware/source/libraries/WiFiWebServer/changelog.md new file mode 100644 index 000000000..68c489a3d --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/changelog.md @@ -0,0 +1,247 @@ +## WiFiWebServer Changelog + +[![arduino-library-badge](https://www.ardu-badge.com/badge/WiFiWebServer.svg?)](https://www.ardu-badge.com/WiFiWebServer) +[![GitHub release](https://img.shields.io/github/release/khoih-prog/WiFiWebServer.svg)](https://github.com/khoih-prog/WiFiWebServer/releases) +[![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/khoih-prog/WiFiWebServer/blob/master/LICENSE) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](#Contributing) +[![GitHub issues](https://img.shields.io/github/issues/khoih-prog/WiFiWebServer.svg)](http://github.com/khoih-prog/WiFiWebServer/issues) + +Donate to my libraries using BuyMeACoffee + + + + +--- +--- + +## Table of Contents + +* [Changelog](#changelog) + * [Releases v1.10.1](#releases-v1101) + * [Releases v1.10.0](#releases-v1100) + * [Releases v1.9.5](#releases-v195) + * [Releases v1.9.4](#releases-v194) + * [Releases v1.9.3](#releases-v193) + * [Releases v1.9.2](#releases-v192) + * [Releases v1.9.1](#releases-v191) + * [Releases v1.9.0](#releases-v190) + * [Releases v1.8.0](#releases-v180) + * [Releases v1.7.0](#releases-v170) + * [Releases v1.6.3](#releases-v163) + * [Releases v1.6.2](#releases-v162) + * [Releases v1.6.1](#releases-v161) + * [Releases v1.6.0](#releases-v160) + * [Releases v1.5.4](#releases-v154) + * [Releases v1.5.3](#releases-v153) + * [Releases v1.5.2](#releases-v152) + * [Releases v1.5.1](#releases-v151) + * [Major Releases v1.5.0](#major-releases-v150) + * [Releases v1.4.2](#releases-v142) + * [Releases v1.4.1](#releases-v141) + * [Major Releases v1.4.0](#major-releases-v140) + * [Releases v1.3.1](#releases-v131) + * [Releases v1.3.0](#releases-v130) + * [Major Releases v1.2.0](#major-releases-v120) + * [Releases v1.1.1](#releases-v111) + * [Major Releases v1.1.0](#major-releases-v110) + * [Releases v1.0.7](#releases-v107) + * [Releases v1.0.6](#releases-v106) + * [Releases v1.0.5](#releases-v105) + * [Releases v1.0.4](#releases-v104) + * [Releases v1.0.3](#releases-v103) + * [Releases v1.0.2](#releases-v102) + * [Releases v1.0.1](#releases-v101) + * [Initial Releases v1.0.0](#initial-releases-v100) + +--- +--- + +## Changelog + +### Releases v1.10.1 + +1. Using new [`WiFi101_Generic library`](https://github.com/khoih-prog/WiFi101_Generic) for sending larger data +2. Update `Packages' Patches` + +### Releases v1.10.0 + +1. Add new features, such as `CORS`, etc. +2. Update code and examples +3. Use `allman astyle` and add `utils` +4. Update `Packages' Patches` + +### Releases v1.9.5 + +1. Restore support to Teensy, etc. +2. Fix bug in examples + +### Releases v1.9.4 + +1. Restore support to ESP32 and ESP8266. Check [Problem using ESP8266 nodeMCU 1.0 #20](https://github.com/khoih-prog/WiFiWebServer/issues/20) + +### Releases v1.9.3 + +1. Better workaround for RP2040W `WiFi.status()` bug using `ping()` to local gateway +2. Update WiFiMulti-related examples + +### Releases v1.9.2 + +1. Workaround for RP2040W WiFi.status() bug +2. Update WiFiMulti-related examples + +### Releases v1.9.1 + +1. Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW43439 WiFi + +### Releases v1.9.0 + +1. Add support to RASPBERRY_PI_PICO_W using CYW43439 WiFi +2. Update `Packages' Patches` + +### Releases v1.8.0 + +1. Add [WiFiMulti_Generic](https://github.com/khoih-prog/WiFiMulti_Generic) library support +2. Add many WiFiMulti-related examples in [WiFiMulti](https://github.com/khoih-prog/WiFiWebServer/tree/master/examples/WiFiMulti) +3. Update `Packages' Patches` + +### Releases v1.7.0 + +1. Fix issue with Portenta_H7 core v2.7.2+. Check [[Portenta_H7] WiFi WebServer extremely slow from core v2.7.2 - v3.0.1 #441](https://github.com/arduino/ArduinoCore-mbed/issues/441) +2. Rewrite to avoid `multiple-definitions` linker error for multiple-file project +3. Add example [multiFileProject](examples/multiFileProject) to demo how to avoid `multiple-definitions` linker error for multiple-file project +4. Update `Packages' Patches` + +### Releases v1.6.3 + +1. Fix decoding error bug when using special `&` in data fields. Check [Decoding Error. two times called urlDecode in Parsing-impl.h. #17](https://github.com/khoih-prog/WiFiWebServer/issues/17) +2. Update `Packages' Patches` + + +### Releases v1.6.2 + +1. Add support to megaAVR boards (UNO_WIFI_REV2, NANO_EVERY) using [Arduino megaAVR core](https://github.com/arduino/ArduinoCore-megaavr) +2. Update `Packages' Patches` + +### Releases v1.6.1 + +1. Fix issue in v1.6.0 + +### Releases v1.6.0 + +1. Add support to new ESP32-S3 and ESP32_C3 +2. Update `Packages' Patches` + +### Releases v1.5.4 + +1. Fix libb64 `fallthrough` compile warning +2. Fix bug not supporting ESP32/ESP8266 boards. +3. Fix bug for WiFi other than WiFiNINA + +### Releases v1.5.3 + +1. Fix authenticate issue caused by libb64 + +### Releases v1.5.2 + +1. Fix wrong http status header bug. Check [fix for wrong http status header #42](https://github.com/khoih-prog/EthernetWebServer/pull/42) + +### Releases v1.5.1 + +1. Fix bug related to String in library and examples + +### Major Releases v1.5.0 + +1. Reduce usage of Arduino String with std::string +2. Optimize library code and examples by using **reference-passing instead of value-passing**. +3. Update `Packages' Patches` +4. Add more ESP32/ESP8266 supporting code + +### Releases v1.4.2 + +1. Update `platform.ini` and `library.json` to use original `khoih-prog` instead of `khoih.prog` after PIO fix +2. Update `Packages' Patches` + +### Releases v1.4.1 + +1. Change option for PIO `lib_compat_mode` from default `soft` to `strict` to minimize compile error in crosss-platform +2. Update `Packages' Patches` for many boards + +### Major Releases v1.4.0 + +1. Add support to **Portenta_H7** using [**Arduino mbed_portenta core**](https://github.com/arduino/ArduinoCore-mbed). +2. Update `Packages' Patches` for **Portenta_H7** + +### Releases v1.3.1 + +1. Add support to ESP32/ESP8266 to use in some rare use-cases +2. Update `Packages' Patches` +3. Split `changelog.md` from `README.md` + +### Releases v1.3.0 + +1. Add support to Adafruit nRF52 core v0.22.0+ +2. Add support to Raytac MDBT50Q_RX Dongle +3. Update `Packages' Patches` + +### Major Releases v1.2.0 + +1. Add support to **Arduino Nano RP2040 Connect** using [**Arduino mbed OS for Nano boards**](https://github.com/arduino/ArduinoCore-mbed). +2. Add support to RP2040-based boards, such as **RASPBERRY_PI_PICO, ADAFRUIT_FEATHER_RP2040 and GENERIC_RP2040**, using [**Earle Philhower's arduino-pico** v1.5.1+ core](https://github.com/earlephilhower/arduino-pico). +3. Add support to RP2040-based boards, such as **RASPBERRY_PI_PICO, ADAFRUIT_FEATHER_RP2040 and GENERIC_RP2040**, using [**Arduino-mbed RP2040** v2.1.0+ core](https://github.com/arduino/ArduinoCore-mbed). +4. Add to examples the support to ESP32-AT/ESP8266-AT WiFi, using [`WiFiEspAT library`](https://github.com/jandrassy/WiFiEspAT) +5. Fix bugs +6. Update `Packages' Patches` + +### Releases v1.1.1 + +1. Clean-up all compiler warnings possible. +2. Add MQTT examples +3. Add Version String + +### Major Releases v1.1.0 + +1. Add high-level **HTTP and WebSockets Client** by merging [ArduinoHttpClient Library](https://github.com/arduino-libraries/ArduinoHttpClient) +2. Add many more examples for HTTP and WebSockets Client. + +### Releases v1.0.7 + +1. Add support to **PROGMEM-related commands, such as sendContent_P() and send_P()** +2. Update Platform.ini to support **PlatformIO 5.x owner-based dependency declaration.** +3. Clean up code. +4. Update examples. + +#### Releases v1.0.6 + +1. Add support to all **STM32F/L/H/G/WB/MP1** boards. +2. Add support to **Seeeduino SAMD21/SAMD51** boards. +3. Restructure examples. Clean-up code. + +#### Releases v1.0.5 + +1. Fix bug not closing client and releasing socket exposed in NINA Firmware v1.4.0. +2. Enhance examples. + +#### Releases v1.0.4 + +1. Add support to boards using **WiFi101 built-in or shield**. For example MKR1000, Teensy, Mega, etc.. +2. Support any future custom WiFi library that meets the no-compiling-error requirements. + +#### Releases v1.0.3 + +1. Add support to **nRF52** boards, such as **AdaFruit Feather nRF52832, nRF52840 Express, BlueFruit Sense, Itsy-Bitsy nRF52840 Express, Metro nRF52840 Express, NINA_B302_ublox, etc.** + +#### Releases v1.0.2 + +1. Add support to **SAM51 (Itsy-Bitsy M4, Metro M4, Grand Central M4, Feather M4 Express, etc.) and SAM DUE**. + +#### Releases v1.0.1 + +1. Use new [`WiFiNINA_Generic library`](https://github.com/khoih-prog/WiFiNINA_Generic) to provide support to many more boards running WiFiNINA. + +The original WiFiNINA library only supports **Nano-33 IoT**, Arduino MKR WiFi 1010, Arduino MKR VIDOR 4000 and Arduino UNO WiFi Rev.2. + +#### Initial Releases v1.0.0 + +This is simple yet complete WebServer library for `AVR Mega, Teensy, SAMD21, STM32, etc.` boards running WiFi modules/shields (WiFiNINA U-Blox W101, W102, etc.). **The functions are similar and compatible to ESP8266/ESP32 WebServer libraries** to make life much easier to port sketches from ESP8266/ESP32. + + diff --git a/software/firmware/source/libraries/WiFiWebServer/keywords.txt b/software/firmware/source/libraries/WiFiWebServer/keywords.txt new file mode 100644 index 000000000..e2ee7b54b --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/keywords.txt @@ -0,0 +1,211 @@ +####################################### +# Datatypes (KEYWORD1) +####################################### + +####################### +# WiFiWebServer +####################### + +WiFiWebServer KEYWORD1 +HTTPMethod KEYWORD1 +HTTPUploadStatus KEYWORD1 +HTTPClientStatus KEYWORD1 +HTTPAuthMethod KEYWORD1 +HTTPUpload KEYWORD1 +RequestHandler KEYWORD1 +FunctionRequestHandler KEYWORD1 +StaticRequestHandler KEYWORD1 +WiFi_RingBuffer KEYWORD1 + +WWString KEYWORD1 + +####################### +# WiFiHttpClient +####################### + +WiFiHttpClient KEYWORD1 + +########################## +# WiFiWebSocketClient +########################## + +WiFiWebSocketClient KEYWORD1 + +########################## +# WiFiURLEncoderClass +########################## + +WiFiURLEncoderClass KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +####################### +# WiFiWebServer +####################### + +begin KEYWORD2 +handleClient KEYWORD2 +close KEYWORD2 +stop KEYWORD2 +authenticate KEYWORD2 +requestAuthentication KEYWORD2 +on KEYWORD2 +addHandler KEYWORD2 +onNotFound KEYWORD2 +onFileUpload KEYWORD2 +uri KEYWORD2 +method KEYWORD2 +client KEYWORD2 +upload KEYWORD2 +arg KEYWORD2 +argName KEYWORD2 +args KEYWORD2 +hasArg KEYWORD2 +collectHeaders KEYWORD2 +header KEYWORD2 +headerName KEYWORD2 +headers KEYWORD2 +hasHeader KEYWORD2 +hostHeader KEYWORD2 +send KEYWORD2 +send_P KEYWORD2 +void sendContent_P KEYWORD2 +setContentLength KEYWORD2 +sendHeader KEYWORD2 +sendContent KEYWORD2 +urlDecode KEYWORD2 +streamFile KEYWORD2 + +####################### +# Parsing-impl +####################### +readBytesWithTimeout KEYWORD2 +_parseRequest KEYWORD2 +_collectHeader KEYWORD2 +_parseArguments KEYWORD2 +_uploadWriteByte KEYWORD2 +_uploadReadByte KEYWORD2 +_parseForm KEYWORD2 +urlDecode KEYWORD2 +_parseFormUploadAborted KEYWORD2 + +####################### +# RequestHandler +####################### +canHandle KEYWORD2 +canUpload KEYWORD2 +handle KEYWORD2 +upload KEYWORD2 +next KEYWORD2 +getContentType KEYWORD2 + +####################### +# WiFi_RingBuffer +####################### +reset KEYWORD2 +init KEYWORD2 +push KEYWORD2 +getPos KEYWORD2 +endsWith KEYWORD2 +getStr KEYWORD2 +getStrN KEYWORD2 + +####################### +# WiFiHttpClient +####################### + +beginRequest KEYWORD2 +endRequest KEYWORD2 +beginBody KEYWORD2 +get KEYWORD2 +post KEYWORD2 +put KEYWORD2 +patch KEYWORD2 +del KEYWORD2 +startRequest KEYWORD2 +sendHeader KEYWORD2 +sendBasicAuth KEYWORD2 +responseStatusCode KEYWORD2 +headerAvailable KEYWORD2 +readHeaderValue KEYWORD2 +readHeader KEYWORD2 +skipResponseHeaders KEYWORD2 +endOfHeadersReached KEYWORD2 +endOfBodyReached KEYWORD2 +endOfStream KEYWORD2 +completed KEYWORD2 +contentLength KEYWORD2 +isResponseChunked KEYWORD2 +responseBody KEYWORD2 +connectionKeepAlive KEYWORD2 +noDefaultRequestHeaders KEYWORD2 +write KEYWORD2 +available KEYWORD2 +read KEYWORD2 +peek KEYWORD2 +flush KEYWORD2 +connect KEYWORD2 +stop KEYWORD2 +connected KEYWORD2 +httpResponseTimeout KEYWORD2 +setHttpResponseTimeout KEYWORD2 + +########################## +# WiFiWebSocketClient +########################## + +begin KEYWORD2 +beginMessage KEYWORD2 +endMessage KEYWORD2 +parseMessage KEYWORD2 +messageType KEYWORD2 +isFinal KEYWORD2 +readString KEYWORD2 +ping KEYWORD2 +write KEYWORD2 +available KEYWORD2 +read KEYWORD2 +peek KEYWORD2 + +########################## +# WiFiURLEncoderClass +########################## + +encode KEYWORD2 + + +####################################### +# Constants (LITERAL1) +####################################### + +WIFI_WEBSERVER_VERSION LITERAL1 +WIFI_WEBSERVER_VERSION_MAJOR LITERAL1 +WIFI_WEBSERVER_VERSION_MINOR LITERAL1 +WIFI_WEBSERVER_VERSION_PATCH LITERAL1 +WIFI_WEBSERVER_VERSION_INT LITERAL1 + +HTTP_ANY LITERAL1 +HTTP_GET LITERAL1 +HTTP_HEAD LITERAL1 +HTTP_POST LITERAL1 +HTTP_PUT LITERAL1 +HTTP_PATCH LITERAL1 +HTTP_DELETE LITERAL1 +HTTP_OPTIONS LITERAL1 + +UPLOAD_FILE_START LITERAL1 +UPLOAD_FILE_WRITE LITERAL1 +UPLOAD_FILE_END LITERAL1 +UPLOAD_FILE_ABORTED LITERAL1 + +HC_NONE LITERAL1 +HC_WAIT_READ LITERAL1 +HC_WAIT_CLOSE LITERAL1 + +BASIC_AUTH LITERAL1 +DIGEST_AUTH LITERAL1 + +AUTHORIZATION_HEADER LITERAL1 + diff --git a/software/firmware/source/libraries/WiFiWebServer/library.json b/software/firmware/source/libraries/WiFiWebServer/library.json new file mode 100644 index 000000000..f79f2a11a --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/library.json @@ -0,0 +1,69 @@ +{ + "name": "WiFiWebServer", + "version": "1.10.1", + "keywords": "wifi, wi-fi, WebServer, WiFiNINA, WiFi101, ESP32, Portenta-H7, SAM-DUE, SAMD, STM32, nRF52, rpi-pico, rp2040, HTTP-Client, WebSocket-Client, server, client, websocket, wifi-multi, mega-avr", + "description": "Simple WiFiWebServer, HTTP Client and WebSocket Client library for AVR Mega, megaAVR, Portenta_H7, Teensy, SAM DUE, SAMD21, SAMD51, STM32F/L/H/G/WB/MP1, nRF52, RP2040-based (Nano-RP2040-Connect, RASPBERRY_PI_PICO, RASPBERRY_PI_PICO_W, ESP32/ESP8266, etc.) boards using WiFi, such as WiFiNINA, WiFi101, CYW43439, U-Blox W101, W102, ESP8266/ESP32-AT modules/shields, with functions similar to those of ESP8266/ESP32 WebServer libraries", + "authors": + { + "name": "Khoi Hoang", + "url": "https://github.com/khoih-prog", + "maintainer": true + }, + "repository": + { + "type": "git", + "url": "https://github.com/khoih-prog/WiFiWebServer" + }, + "homepage": "https://github.com/khoih-prog/WiFiWebServer", + "export": { + "exclude": [ + "linux", + "extras", + "tests" + ] + }, + "dependencies": + [ + { + "owner": "khoih-prog", + "name": "WiFiNINA_Generic", + "version": ">=1.8.15-1", + "platforms": ["*"] + }, + { + "owner": "khoih-prog", + "name": "WiFi101_Generic", + "version": ">=1.0.0", + "platforms": ["*"] + }, + { + "owner": "khoih-prog", + "name": "Functional-Vlpp", + "version": ">=1.0.2", + "platforms": ["*"] + }, + { + "owner": "khoih-prog", + "name": "WiFiMulti_Generic", + "version": ">=1.2.2", + "platforms": ["*"] + }, + { + "owner": "khoih-prog", + "name": "ESP_AT_Lib", + "version": ">=1.4.1", + "platforms": ["*"] + }, + { + "owner": "jandrassy", + "name": "WiFiEspAT", + "version": ">=1.4.1", + "platforms": ["*"] + } + ], + "license": "MIT", + "frameworks": "*", + "platforms": "*", + "examples": "examples/*/*/*.ino", + "headers": ["WiFiWebServer.h", "WiFiWebServer.hpp", "WiFiHttpClient.h"] +} diff --git a/software/firmware/source/libraries/WiFiWebServer/library.properties b/software/firmware/source/libraries/WiFiWebServer/library.properties new file mode 100644 index 000000000..ac7ea6071 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/library.properties @@ -0,0 +1,12 @@ +name=WiFiWebServer +version=1.10.1 +author=Khoi Hoang +license=MIT +maintainer=Khoi Hoang +sentence=Simple WiFiWebServer, HTTP Client and WebSocket Client library for AVR Mega, megaAVR, Portenta_H7, Teensy, SAM DUE, SAMD21, SAMD51, STM32F/L/H/G/WB/MP1, nRF52, RP2040-based (Nano-RP2040-Connect, RASPBERRY_PI_PICO, RASPBERRY_PI_PICO_W, ESP32/ESP8266, etc.) boards using WiFi, such as WiFiNINA, WiFi101, CYW43439, U-Blox W101, W102, ESP8266/ESP32-AT modules/shields, with functions similar to those of ESP8266/ESP32 WebServer libraries. +paragraph=The WebServer supports HTTP GET and POST requests, provides argument parsing, handles one client at a time. It now provides HTTP Client and WebSocket Client. Now using WiFiMulti_Generic library +category=Communication +url=https://github.com/khoih-prog/WiFiWebServer +architectures=* +depends=Functional-Vlpp, WiFiNINA_Generic, WiFi101_Generic, ESP_AT_Lib, WiFiEspAT, WiFiMulti_Generic +includes=WiFiWebServer.h, WiFiWebServer.hpp, WiFiHttpClient.h diff --git a/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer.png b/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer.png new file mode 100644 index 0000000000000000000000000000000000000000..eb05b090b082695eb3ff4fe40418786e85e84d0d GIT binary patch literal 29330 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 Cy}f4u literal 0 HcmV?d00001 diff --git a/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_ESP32_C3.png b/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_ESP32_C3.png new file mode 100644 index 0000000000000000000000000000000000000000..bfb33238deda56a508a8be45769f118330f5a60c GIT binary patch literal 23053 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«Kic003(pEX#|i-s=It$0AJt|cw+Ex?SPgKmR!&?}40q>$#2cJ_ z>ALXX+Oa7+5WkJ*WLOLh8ozoiAJW^7=UfC1M;$@aW`R~~v!(9hnsU_wt#St`^SSAc zwhW;0ad%ifs!kl3H_7GFip8uDJk_Y1Q66OMWz{VbDUhUXJC?PTVZeAl#2S%GLjO_<;)t`TFGiA zXdY|~cRvP7)l)tn`j_T}4R=sWTE5(fohdKr0k!Wowl5Q6ikd$KEyz8%wV6u;)I$r6 z_C3mOZiE{?x&f`dHTI73Mr8vTva)2R86R@)B%hTS`p_uW8|FxrnxaV9-jIw*$ir5;C8v9*)D_U1Ho&G$P?Y@04(_&YYAN?HJ~$ z+6;!M6AZrdHaEN7nwJS7lmB`^8c%ps!XaZWP;c*4$K~0i zyX%E5J}Gg&pIu>%5smK;mTM#}4WpYPEkk}k*>qyD6*C)Xmj5sbEqM^Yb0MlR)uMOm zEnRFd6ih*T9Nig#V^l|)KwcF9;5QpQ@pr7KycO*y;e)gO)Dv#NxHvB1pu8P4OkfLG zEaBa?t`^?#+ovi9zPX}iX)>Bc8=#!D`TIII&)`nGnk;15gKQsk%^P!}aF z38k^V{uzU|L8P#}P-{12=SvxgBCHWwOT9iWROfYD!(=JU*u-V|6anST(%C38`jfx0 zU~;FP$sK6(^_7%Qfz1hfjSocSXMywfBYyUP9#3+NtcIq^S(*uckMz!VuEu9&IwKcW zu{Lsw0~~2UAeMPARmk(+e-TWhq<}5zN9aybzahH*z5k*(pmP%spB1>303Y= z{i%VjnbL%uoY=CH<&_y%C3|a9sjEz8_-W*YeSo}f3m4V|Vl)H8dr4J5<<$3MYcIz4 z4gAbgTm2a61=^8B$rw$*Y&KB(HZwyH(@Tw7X1@ci)O~T#h*HqIg&TMI66-pwtwFcq z!ev`oIGBgrTU`0P#-b)CgwLCVla7_`uc-Djn}^}uP7ExYRA6ky@^0}BbNf{_;{~AE z&kI+KDQ56zDctszhvH1^sYP=jqxLa-(Bf!=gLj}D{8yc56t6b^dNg73Noe)gw?zQ{SDzs{Fv!6*|o$l z+hs#G{Dow3dCE@lj-q^&@D7Q+j006=E7XPpOcl70={)2tE1NU7avZ_gb$c#^@w6x6 zre0y%*^lSlg~s%r&1pw^*fZSta=F*wGTnEvfW7|r-GhgtPCm&;p_ZTK%Wb3_22-xt z-GjdA4O)2)SW|lP8(jSSCe((Qac?ZmgXtgg=fC9ulE~gK^*cHeowC<3rBee3TQSjF zL96<19KlBSLyn`PjnWHMw3YkvFpMeh6*4l2&P0Wh?f5v=CsD_^klp3W&lNIwUt_oe ze_*#K(;r4abH@L)N<3O~7=+u&R$H(|n4D7Vvy3Afu1_^Da{}B3UTi~r%`U_kIb%Mx?1`uJC>Q>f(pa+5`MPMKYzY39QCq-jty6kq4_(yH8-WymK&uG zb9U^z^}e&5MF`LI9Ggfn${ZJHkEkMC4EXjMQy9N-UI#oxevCI@Yw6c|ao51BJw<^{ z&?mB#y0MdSwc%xj_mEzfN)Ko58%NswZ*ATwLj8*u|trMW`_I zW-#1s+6muGVigOx>_=(>{M*cmFULX=wp*O7E&LtTd)s2! zTcTQ(%JS*7ddAz?#v`~V;V0KR1INJp=t&!7Hy7*l2XM}o4ACV5M5-f@nLbGNGl6EL zc)S<^^Wh#o3RiS@0*&{RPtx{z_GWg&*g(-bt=c+JE6bs9LCy*|U7N08Wqpv&Uo*SB zJ0sJTAlfYXRb?bvUx&jOYi#U5H{!K|Vgi2Kk<~}=T`Cgi z^k71mPwm3g_uDMKEz$S02q8bSm?qhm@9ryM+ZpK+n{DYK0WgV>r&IX;*{O~;Dy5j5jqVm zG1*buuC|8>U^rPMmms&W9qZ%Pk3o=TRVfsUE+M!!x#fE13r)l4BwRsoNpi$hv(&BS zy^f@Z+gWaL=`59H31PN74`s>kP$9Y#Cica=IQMEb$a-CB!_WF?r`y<(_%#tGGp$^Y zyd8ix?-K-U@n;D-~6@mOZf+8qDEU{k*M%aIrn3 zx|&$wp`9*&Qc}FZu^~m~)t|b$Cu`nvDUX*hl19lXJyds6GtZnI(?g=OG&(&YDl#ai zCyPndUJX@y(YICar1RzPl|{e^Bc1l+=0{&qsq7tUIA86M44yqBTN^1QYSV~Qcgw=H zN|U0(D;iQrC+8cp5fsNoWwHA6D2;M)vo3;nSSdXUhbWO#Uaw{;1?&dX4feg($E+p3>FAlg&DYMP_MO3|R zeN6j66{m8EhFLM5)b>-3lkV@-r1%$m-{N~wd;QYizl-{YL;ZxlRnaGfy%>t)Jj|gQ zmcwsWI)YQf=VG6Ju&Gk(6@}Xctb4&kPusyORPNGoNEZgB0jCu&uY_pP+$Rc;-db}c z;&uo=S#Gha&D78t5-QtN>h0#L)Z~pSW6o~uil$}B4TnC8{xeBLO|%BqTX}__ioye( zQ(CyLUdip$IZIGo?wBcXa`-B))S82d;8}MmxU%BW`I!=5eX#^r*JP|5)BaC{*2!P` zt>o&Ps0wXFF-mgBvao^9mW*rF_hC?Tap1v;qFm=bh&Jk&I-Kamy`xz;2 z|KpQ&GXp}4sb-Bk!S8!QZ5m9PSM|l_5&o@ave->!cESFreHXyinZj6I>mXvdpObD= zXOC_qNL%AwLE=2FP)i;%Ef5n9&3#>HVezmHR(NZqo5^N4MUnYc^$pZiOb`wB0aZz$ zmD2h3gy~eSPne2J_}I6kh@rhF=y^@Ns;@lqT%RrGtDDQTj2Gvp zX&s2D>Zz%6{w}uaj4ZsJ38x{^YvqD{Ml^R3ij&rubZtU8q$VoRz^aMTxz%9BLHLT) z$oMlzQzcp#y&74-HC|q;nJTwwR8d)y7yGBNGViPO!h^3^^qlr#KV5C!-cJk$Z$+34 z4On!oCaKq#C6Xg;he}DgQB}`#ot1m!Ax96n%X_9Qvk&a`C!Re^I+;X)sR8dsS)#M0 zkaq9cTe+4{dqyM1f+xFo1Nz3I_7d@L`vPTTrK+N7o<*)) z4QM~}VCf#|aVe5DK)Q;A##{didU%soAy`I}w5w9>IEYn6QFzcLR-hro`yexDgkPew zi@>{9=`=Re0iqJ_;I*x@(vV)gvj5!a` zW;g8d1>gSQUVg7=@J_dRZ9(rlZ|o%JSL`0E6`cH9vaURy@%7>4y`C+U!V=l1iOmKb zW(2vnfdH6JxC5plbzG`NSe@mU?u-gC-6fQ#<7uRiP0Fs(=;l)N%^N%vNrQjH2h_JD z7ZYRkSiNhthkxi-uE~K-39Sjk7#2jD6zt@tYd~E8?}{=f+sZ!>a0OUmT8^7Um)HQ3r<#pmGi!v>i^yYwJrjRRzCyUhW)GTmJ@xA6=G<2!sd71D8*Sz}2 z%a0sDh_2F6lKXyaB|A|ScHK;v4v*R%TZg{>Mpx9Yevw99mQi<*IKGC-K%yc6W;LPbJvA_-;BA$N) zF~7$2-m@2(oceR*-m?TkQwDT|DhhcAN|l^6Zv|LVDVpISNQ--F%mSg+)m~VUc>#IY zS5+mJnSj;m_t~jD{J#|+l5+p#ef#b)_TsRDXjLcu`8lv551U-BEi+Jqs^yhde=Ww8 z)!FcS<91y`kDb3EQg0W)p3C#ppDo#VoZjt+wu@K?ve!JPQ9-AqrT5oN->c0bPSjqM zX#dY9-Da0J;d45|^9%TkwbQksPzlZ!^eFyA^XJtZ+_V{2oqKI@b!7%aBvZQM-a!*% zvr}Vs%P!CPeD%)FM*ske?B72EBA~2#@pJZ!l^W?BkRJ7zQS{j~MpWg;tuB~ge(|8e zG;w>_Y)8n2@YIYE=q|$6dNXWZT)_%@TfLe`g$Jyp+_bEo0isZFBo}t4QtZ z0k>o0vnb0-^!vuq%?gttyKrRYpz3p*WkVOyfD_Z$#y)t)`j17Z9!;f_#yw6Yz)4gg z+URJUa%8-;HlBIvMb1dW?ax=MI~|8iKE%}8Kl-VbVJ)B@C#E+6kj`k(7*2v!YM8O& z`y*rXvD=Os?dhGhNw+%Vlh^wnJ+ZitPbV{%iwP0F zuOet?Az)!1p|F|bdotJKK+g=;lDupwoKB^g-p{A65I@To^r6|^aUsJMaGLSv z_$r)ox@Re~q(ZN?DE$TXD)FzUuff6?t#v?DoU{!rSK9(&vMrW6iIIuXxV@eEAJgJ1 zkk*i!hB4Cq!6RjNOFpHSc+&EHdvCb)1NvH1AKTe4LGF?GTY%Zh9oMXOmj*8FXwkyP zZ~OX*p}F3~t!i)-jk;!Zt2_4tA09>&yZ_juNh{HS)vb$w%rWE94zY&9D25<-biI4U zNZxM@s;u>gc=>3Kev#(A{qifaj-svJx?Lw!u=V~ehLl+c;r?HB4Q4Gw?IPaU|5}n} z^V?CwnbsuIcTeC^8#i_F_J*)BGuhWEto|Aqm96I&9sp&z<*3?IfAerCi-*^Gzc>1S zwmqtgn9IxU<{wd`A6!^2K7;q-8b zi6u`a1M4l;IW3K>JxbDqt4R~JTkGcL2p%o9l5ms`92xD%ctC~&hHwbhz zvBA#X!)M7Ipd*JV6R1qi`#CxQPurg#8_O)k_G1#Di`0oe_2)Cv4)6&jVEzGQW~}!Y zF{JdDW(ICfH`@;YgC>1qnd%osTGj7Kq^>9Y0fc!J7xWA4(@Tq2#*o|I-$%vEGAyOrsD#V5;5`M(;Q3q_k0mPPCrI7DtE2^8P1ueN6@F@;K-Ad ziokv4Ao}g5g+TNTESIvj*d8u*Tj{a$&v@^+2F1%0uZswL^MvThyEay;wKs4|J( zPL1i*7_IeB;A6w(9CF)Y3_W_sfx);@PNS4a=Ge$Z#?Bo>thsPxX>cP*g|Dl)>b`U# zBSAdqxR5cjR?!>@rReaGIlsZ)ZeqDzvAI(rpEM;tvSw5}-T4{rtl5>`U6L_hIr~H2 z_<5EPXQ>};t|8!Z@vQKGgON-rY6(cf_!7t7$W}6h3z7mI`$oR}_?P4$8C>?Eo}R9M z85f5UM2d+E8IJ%q)(vaM@FWKciEKfjz>XfIzxiH(n)T|k%k(}aMI^CWU#WrA z_y?W$HGg$tSRb^1Rwi;~4v!;R`a}~-!ndVqA_H>8{~MeAU!*!sww&Pe{n@(vp4^}g ze7RQ&Aax#S8qR{ZAliI}sIiBDw7J#F(o};pLlrusm39aJ|HZ`r@7(zRAnX6%CjXPl dU%yAY%ItKsJPip2e=h?l$-kDXlz9{Se*k}H^{@Z{ literal 0 HcmV?d00001 diff --git a/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_NanoRP2040Connect.png b/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_NanoRP2040Connect.png new file mode 100644 index 0000000000000000000000000000000000000000..a71307a50ea8ca6826d654fffa4bac510530ac55 GIT binary patch literal 23668 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 literal 0 HcmV?d00001 diff --git a/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_WiFiMulti_NanoRP2040Connect.png b/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_WiFiMulti_NanoRP2040Connect.png new file mode 100644 index 0000000000000000000000000000000000000000..cb4150be5c5c92c8ca71b8c93a1dd7fb8a339127 GIT binary patch literal 43032 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 literal 0 HcmV?d00001 diff --git a/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_WiFiMulti_RP2040W.png b/software/firmware/source/libraries/WiFiWebServer/pics/AdvancedWebServer_WiFiMulti_RP2040W.png new file mode 100644 index 0000000000000000000000000000000000000000..cbf3e7a94e7c87b6fbb48fe99158a08090fe4f1c GIT binary patch literal 43433 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.0.2 +; WiFiNINA_Generic@>=1.8.15-1 +; WiFi101_Generic@>=1.0.0 +; WiFiMulti_Generic@>=1.2.2 +; ESP_AT_Lib@>=1.4.1 +; PlatformIO 5.x + khoih-prog/Functional-Vlpp@>=1.0.2 + khoih-prog/WiFiNINA_Generic@>=1.8.15-1 + khoih-prog/WiFi101_Generic@>=1.0.0 + khoih-prog/WiFiMulti_Generic@>=1.2.2 + khoih-prog/ESP_AT_Lib@>=1.4.1 + +build_flags = +; set your debug output (default=Serial) +; -D DEBUG_ESP_PORT=Serial +; comment the following line to enable WiFi debugging +; -D NDEBUG + +[env:ESP8266] +platform = espressif8266 +framework = arduino +; ============================================================ +; Board configuration +; choose your board by uncommenting one of the following lines +; ============================================================ +;board = gen4iod +;board = huzzah +;board = oak +;board = esp_wroom_02 +;board = espduino +;board = espectro +;board = espino +;board = espresso_lite_v1 +;board = espresso_lite_v2 +;board = esp12e +;board = esp01_1m +;board = esp01 +;board = esp07 +;board = esp8285 +;board = heltec_wifi_kit_8 +;board = inventone +;board = nodemcu +board = nodemcuv2 +;board = modwifi +;board = phoenix_v1 +;board = phoenix_v2 +;board = sparkfunBlynk +;board = thing +;board = thingdev +;board = esp210 +;board = espinotee +;board = d1 +;board = d1_mini +;board = d1_mini_lite +;board = d1_mini_pro +;board = wifi_slot +;board = wifiduino +;board = wifinfo +;board = wio_link +;board = wio_node +;board = xinabox_cw01 +;board = esp32doit-devkit-v1 + +[env:ESP32] +platform = espressif32 +framework = arduino +; ============================================================ +; Board configuration +; choose your board by uncommenting one of the following lines +; ============================================================ +;board = esp32cam +;board = alksesp32 +;board = featheresp32 +;board = espea32 +;board = bpi-bit +;board = d-duino-32 +board = esp32doit-devkit-v1 +;board = pocket_32 +;board = fm-devkit +;board = pico32 +;board = esp32-evb +;board = esp32-gateway +;board = esp32-pro +;board = esp32-poe +;board = oroca_edubot +;board = onehorse32dev +;board = lopy +;board = lopy4 +;board = wesp32 +;board = esp32thing +;board = sparkfun_lora_gateway_1-channel +;board = ttgo-lora32-v1 +;board = ttgo-t-beam +;board = turta_iot_node +;board = lolin_d32 +;board = lolin_d32_pro +;board = lolin32 +;board = wemosbat +;board = widora-air +;board = xinabox_cw02 +;board = iotbusio +;board = iotbusproteus +;board = nina_w10 + +[env:SAMD] +platform = atmelsam +framework = arduino +; ============================================================ +; Choose your board by uncommenting one of the following lines +; ============================================================ +; ============================================================ +; Board configuration Adafruit SAMD +; ============================================================ + +;board = adafruit_feather_m0 +;board = adafruit_feather_m0_express +;board = adafruit_metro_m0 +;board = adafruit_circuitplayground_m0 +;board = adafruit_gemma_m0 +;board = adafruit_trinket_m0 +;board = adafruit_itsybitsy_m0 +;board = adafruit_pirkey +;board = adafruit_hallowing +;board = adafruit_crickit_m0 +;board = adafruit_metro_m4 +;board = adafruit_grandcentral_m4 +board = adafruit_itsybitsy_m4 +;board = adafruit_feather_m4 +;board = adafruit_trellis_m4 +;board = adafruit_pyportal_m4 +;board = adafruit_pyportal_m4_titano +;board = adafruit_pybadge_m4 +;board = adafruit_metro_m4_airliftlite +;board = adafruit_pygamer_m4 +;board = adafruit_pygamer_advance_m4 +;board = adafruit_pybadge_airlift_m4 +;board = adafruit_monster_m4sk +;board = adafruit_hallowing_m4 + +; ============================================================ +; Board configuration Arduino SAMD and SAM +; ============================================================ + +;board = arduino_zero_edbg +;board = arduino_zero_native +;board = mkr1000 +;board = mkrzero +;board = mkrwifi1010 +;board = nano_33_iot +;board = mkrfox1200 +;board = mkrwan1300 +;board = mkrwan1310 +;board = mkrgsm1400 +;board = mkrnb1500 +;board = mkrvidor4000 +;board = adafruit_circuitplayground_m0 +;board = mzero_pro_bl_dbg +;board = mzero_pro_bl +;board = mzero_bl +;board = tian +;board = tian_cons +;board = arduino_due_x_dbg +;board = arduino_due_x + +; ============================================================ +; Board configuration Seeeduino SAMD +; ============================================================ + +;board = seeed_wio_terminal +;board = Seeed_femto_m0 +;board = seeed_XIAO_m0 +;board = Wio_Lite_MG126 +;board = WioGPS +;board = zero +;board = rolawan +;board = seeed_grove_ui_wireless + + +[env:NRF52] +platform = nordicnrf52 +framework = arduino +; ============================================================ +; Board configuration Adafruit nRF52 +; choose your board by uncommenting one of the following lines +; ============================================================ +;board = feather52832 +board = feather52840 +;board = feather52840sense +;board = itsybitsy52840 +;board = cplaynrf52840 +;board = cluenrf52840 +;board = metro52840 +;board = pca10056 +;board = particle_xenon +;board = mdbt50qrx +;board = ninab302 +;board = ninab112 + +[env:STM32] +platform = ststm32 +framework = arduino + +; ============================================================ +; Choose your board by uncommenting one of the following lines +; ============================================================ + +; ============================================================ +; Board configuration Nucleo-144 +; ============================================================ + +;board = nucleo_f207zg +;board = nucleo_f429zi +;board = nucleo_f746zg +;board = nucleo_f756zg +;board = nucleo_f767zi +;board = nucleo_h743zi +;board = nucleo_l496zg +;board = nucleo_l496zg-p +;board = nucleo_l4r5zi +;board = nucleo_l4r5zi-p + +; ============================================================ +; Board configuration Nucleo-64 +; ============================================================ + +;board = nucleo_f030r8 +;board = nucleo_f072rb + +;board = nucleo_f091rc +;board = nucleo_f103rb +;board = nucleo_f302r8 +;board = nucleo_f303re +;board = nucleo_f401re +;board = nucleo_f411re +;board = nucleo_f446re +;board = nucleo_g071rb +;board = nucleo_g431rb +;board = nucleo_g474re +;board = nucleo_l053r8 +;board = nucleo_l073rz +;board = nucleo_l152re +;board = nucleo_l433rc_p +;board = nucleo_l452re +;board = nucleo_l452re-p +;board = nucleo_l476rg +;board = pnucleo_wb55rg + +; ============================================================ +; Board configuration Nucleo-32 +; ============================================================ + +;board = nucleo_f031k6 +;board = nucleo_l031k6 +;board = nucleo_l412kb +;board = nucleo_l432lc +;board = nucleo_f303k8 +;board = nucleo_g431kb + +; ============================================================ +; Board configuration Discovery Boards +; ============================================================ + +;board = disco_f030r8 +;board = disco_f072rb +;board = disco_f030r8 +;board = disco_f100rb +;board = disco_f407vg +;board = disco_f413zh +;board = disco_f746ng +;board = disco_g0316 +;board = disco_l475vg_iot +;board = disco_f072cz-lrwan1 + +; ============================================================ +; Board configuration STM32MP1 Boards +; ============================================================ + +;board = stm32mp157a-dk1 +;board = stm32mp157c-dk2 + +; ============================================================ +; Board configuration Generic Boards +; ============================================================ + +;board = bluepill_f103c6 +;board = bluepill_f103c8 +;board = blackpill_f103c8 +;board = stm32f103cx +;board = stm32f103rx +;board = stm32f103tx +;board = stm32f103vx +;board = stm32f103zx +;board = stm32f103zet6 +;board = maplemini_f103cb +;board = blackpill_f303cc +;board = black_f407ve +;board = black_f407vg +;board = black_f407ze +;board = black_f407zg +;board = blue_f407ve_mini +;board = blackpill_f401cc +;board = blackpill_f411ce +;board = coreboard_f401rc +;board = feather_f405 + +[env:portenta_h7_m7] +platform = ststm32 +board = portenta_h7_m7 +framework = arduino + +[env:portenta_h7_m4] +platform = ststm32 +board = portenta_h7_m4 +framework = arduino + +[env:pico] +; ============================================================ +; Just a sample +; You have to research and fix if there is issue +; ============================================================ +platform = raspberrypi +board = pico +framework = arduino +upload_protocol = picotool + +; ============================================================ +; Board configuration Many more Boards to be filled +; ============================================================ + diff --git a/software/firmware/source/libraries/WiFiWebServer/src/Parsing-impl.h b/software/firmware/source/libraries/WiFiWebServer/src/Parsing-impl.h new file mode 100644 index 000000000..f926a1108 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/Parsing-impl.h @@ -0,0 +1,1473 @@ +/********************************************************************************************************************************* + Parsing-impl.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + **********************************************************************************************************************************/ + +#pragma once + +#ifndef Parsing_Impl_H +#define Parsing_Impl_H + +#include + +#include "WiFiWebServer.hpp" + +#ifndef WEBSERVER_MAX_POST_ARGS + #define WEBSERVER_MAX_POST_ARGS 32 +#endif + +//////////////////////////////////////// + +// KH +#if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +static bool readBytesWithTimeout(WiFiClient& client, size_t maxLength, String& data, int timeout_ms) +{ + if (!data.reserve(maxLength + 1)) + return false; + + data[0] = 0; // data.clear()?? + + while (data.length() < maxLength) + { + int tries = timeout_ms; + size_t avail; + + while (!(avail = client.available()) && tries--) + delay(1); + + if (!avail) + break; + + if (data.length() + avail > maxLength) + avail = maxLength - data.length(); + + while (avail--) + data += (char)client.read(); + } + + return data.length() == maxLength; +} + +//////////////////////////////////////// + +#else + +//////////////////////////////////////// + +#if !WIFI_USE_PORTENTA_H7 + +static char* readBytesWithTimeout(WiFiClient& client, size_t maxLength, size_t& dataLength, int timeout_ms) +{ + char *buf = nullptr; + dataLength = 0; + + while (dataLength < maxLength) + { + int tries = timeout_ms; + size_t newLength; + + while (!(newLength = client.available()) && tries--) + delay(1); + + if (!newLength) + { + break; + } + + if (!buf) + { + buf = (char *) malloc(newLength + 1); + + if (!buf) + { + return nullptr; + } + } + else + { + char* newBuf = (char *) realloc(buf, dataLength + newLength + 1); + + if (!newBuf) + { + free(buf); + return nullptr; + } + + buf = newBuf; + } + + client.readBytes(buf + dataLength, newLength); + dataLength += newLength; + buf[dataLength] = '\0'; + } + + return buf; +} +#endif // #if !WIFI_USE_PORTENTA_H7 + +#endif // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +bool WiFiWebServer::_parseRequest(WiFiClient& client) +{ + // Read the first line of HTTP request + String req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + //reset header value + for (int i = 0; i < _headerKeysCount; ++i) + { + _currentHeaders[i].value = String(); + } + + // First line of HTTP request looks like "GET /path HTTP/1.1" + // Retrieve the "/path" part by finding the spaces + int addr_start = req.indexOf(' '); + int addr_end = req.indexOf(' ', addr_start + 1); + + if (addr_start == -1 || addr_end == -1) + { + WS_LOGDEBUG1(F("_parseRequest: Invalid request: "), req); + return false; + } + + String methodStr = req.substring(0, addr_start); + String url = req.substring(addr_start + 1, addr_end); + String versionEnd = req.substring(addr_end + 8); + _currentVersion = atoi(versionEnd.c_str()); + String searchStr = ""; + int hasSearch = url.indexOf('?'); + + if (hasSearch != -1) + { + searchStr = url.substring(hasSearch + 1); + url = url.substring(0, hasSearch); + } + + _currentUri = url; + _chunked = false; + + HTTPMethod method = HTTP_GET; + +#if USE_NEW_WEBSERVER_VERSION + + if (methodStr == "HEAD") + { + method = HTTP_HEAD; + } + else if (methodStr == "POST") + { + method = HTTP_POST; + } + else if (methodStr == "DELETE") + { + method = HTTP_DELETE; + } + else if (methodStr == "OPTIONS") + { + method = HTTP_OPTIONS; + } + else if (methodStr == "PUT") + { + method = HTTP_PUT; + } + else if (methodStr == "PATCH") + { + method = HTTP_PATCH; + } + +#else // #if USE_NEW_WEBSERVER_VERSION + + if (methodStr == "POST") + { + method = HTTP_POST; + } + else if (methodStr == "DELETE") + { + + method = HTTP_DELETE; + } + else if (methodStr == "OPTIONS") + { + method = HTTP_OPTIONS; + } + else if (methodStr == "PUT") + { + method = HTTP_PUT; + } + else if (methodStr == "PATCH") + { + method = HTTP_PATCH; + } + +#endif // #if USE_NEW_WEBSERVER_VERSION + + _currentMethod = method; + + WS_LOGDEBUG1(F("method: "), methodStr); + WS_LOGDEBUG1(F("url: "), url); + WS_LOGDEBUG1(F("search: "), searchStr); + + //attach handler + RequestHandler* handler = nullptr; + + for (handler = _firstHandler; handler; handler = handler->next()) + { + if (handler->canHandle(_currentMethod, _currentUri)) + break; + } + + _currentHandler = handler; + + String formData; + + // below is needed only when POST type request + if (method == HTTP_POST || method == HTTP_PUT || method == HTTP_PATCH || method == HTTP_DELETE) + { + String boundaryStr; + String headerName; + String headerValue; + + bool isForm = false; + bool isEncoded = false; + uint32_t contentLength = 0; + + //parse headers + while (1) + { + req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (req == "") + break;//no more headers + + int headerDiv = req.indexOf(':'); + + if (headerDiv == -1) + { + break; + } + + headerName = req.substring(0, headerDiv); + headerValue = req.substring(headerDiv + 1); + + headerValue.trim(); + _collectHeader(headerName.c_str(), headerValue.c_str()); + + WS_LOGDEBUG1(F("headerName: "), headerName); + WS_LOGDEBUG1(F("headerValue: "), headerValue); + + if (headerName.equalsIgnoreCase("Content-Type")) + { +#if (ESP32 || ESP8266) + using namespace mime_esp; +#else + using namespace mime; +#endif + + if (headerValue.startsWith(mimeTable[txt].mimeType)) + { + isForm = false; + } + else if (headerValue.startsWith("application/x-www-form-urlencoded")) + { + isForm = false; + isEncoded = true; + } + else if (headerValue.startsWith("multipart/")) + { + boundaryStr = headerValue.substring(headerValue.indexOf('=') + 1); + boundaryStr.replace("\"", ""); + isForm = true; + } + } + else if (headerName.equalsIgnoreCase("Content-Length")) + { + contentLength = headerValue.toInt(); + _clientContentLength = headerValue.toInt(); + } + else if (headerName.equalsIgnoreCase("Host")) + { + _hostHeader = headerValue; + } + } + + //KH +#if USE_NEW_WEBSERVER_VERSION + + //////////////////////////////////////// + + String plainBuf; + + if ( !isForm + && // read content into plainBuf + ( !readBytesWithTimeout(client, contentLength, plainBuf, HTTP_MAX_POST_WAIT) + || (plainBuf.length() < contentLength) + ) + ) + { + return false; + } + + if (isEncoded) + { + // isEncoded => !isForm => plainBuf is not empty + // add plainBuf in search str + if (searchStr.length()) + searchStr += '&'; + + searchStr += plainBuf; + } + + // parse searchStr for key/value pairs + _parseArguments(searchStr); + + if (!isForm) + { + if (contentLength) + { + // add key=value: plain={body} (post json or other data) + RequestArgument& arg = _currentArgs[_currentArgCount++]; + arg.key = F("plain"); + arg.value = plainBuf; + } + } + else + { + // isForm is true + // here: content is not yet read (plainBuf is still empty) + if (!_parseForm(client, boundaryStr, contentLength)) + { + return false; + } + } + } + else + { + String headerName; + String headerValue; + + //parse headers + while (1) + { + req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (req == "") + break;//no more headers + + int headerDiv = req.indexOf(':'); + + if (headerDiv == -1) + { + break; + } + + headerName = req.substring(0, headerDiv); + headerValue = req.substring(headerDiv + 2); + _collectHeader(headerName.c_str(), headerValue.c_str()); + + WS_LOGDEBUG1(F("headerName:"), headerName); + WS_LOGDEBUG1(F("headerValue:"), headerValue); + + if (headerName.equalsIgnoreCase(F("Host"))) + { + _hostHeader = headerValue; + } + } + + _parseArguments(searchStr); + } + + client.flush(); + + WS_LOGDEBUG1(F("Request:"), url); + WS_LOGDEBUG1(F("Arguments:"), searchStr); + WS_LOGDEBUG (F("Final list of key/value pairs:")); + + for (int i = 0; i < _currentArgCount; i++) + { + WS_LOGDEBUG1("key:", _currentArgs[i].key.c_str()); + WS_LOGDEBUG1("value:", _currentArgs[i].value.c_str()); + } + + return true; + + //////////////////////////////////////// + +#else // #if USE_NEW_WEBSERVER_VERSION + + //////////////////////////////////////// + + (void) isEncoded; + + if (isForm) + { + _parseArguments(searchStr); + + if (!_parseForm(client, boundaryStr, contentLength)) + { + return false; + } + } + } + else + { + String headerName; + String headerValue; + + //parse headers + while (1) + { + req = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (req == "") + break;//no more headers + + int headerDiv = req.indexOf(':'); + + if (headerDiv == -1) + { + break; + } + + headerName = req.substring(0, headerDiv); + headerValue = req.substring(headerDiv + 2); + _collectHeader(headerName.c_str(), headerValue.c_str()); + + WS_LOGDEBUG1(F("headerName: "), headerName); + WS_LOGDEBUG1(F("headerValue: "), headerValue); + + if (headerName == "Host") + { + _hostHeader = headerValue; + } + } + + _parseArguments(searchStr); + } + + client.flush(); + + WS_LOGDEBUG1(F("Request: "), url); + WS_LOGDEBUG1(F("Arguments: "), searchStr); + + return true; + +#endif // #if USE_NEW_WEBSERVER_VERSION +} + +//////////////////////////////////////// + +bool WiFiWebServer::_collectHeader(const char* headerName, const char* headerValue) +{ + for (int i = 0; i < _headerKeysCount; i++) + { + if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) + { + _currentHeaders[i].value = headerValue; + return true; + } + } + + return false; +} + +//////////////////////////////////////// + +#if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +struct storeArgHandler +{ + void operator() (String& key, String& value, const String& data, int equal_index, int pos, int key_end_pos, + int next_index) + { + key = WiFiWebServer::urlDecode(data.substring(pos, key_end_pos)); + + if ((equal_index != -1) && ((equal_index < next_index - 1) || (next_index == -1))) + value = WiFiWebServer::urlDecode(data.substring(equal_index + 1, next_index)); + } +}; + +//////////////////////////////////////// + +struct nullArgHandler +{ + void operator() (String& key, String& value, const String& data, int equal_index, int pos, int key_end_pos, + int next_index) + { + (void)key; + (void)value; + (void)data; + (void)equal_index; + (void)pos; + (void)key_end_pos; + (void)next_index; + // do nothing + } +}; + +//////////////////////////////////////// + +void WiFiWebServer::_parseArguments(const String& data) +{ + if (_currentArgs) + delete[] _currentArgs; + + _currentArgs = 0; + + if (data.length() == 0) + { + _currentArgCount = 0; + _currentArgs = new RequestArgument[1]; + + return; + } + + _currentArgCount = 1; + + for (int i = 0; i < (int)data.length(); ) + { + i = data.indexOf('&', i); + + if (i == -1) + break; + + ++i; + ++_currentArgCount; + } + + _currentArgs = new RequestArgument[_currentArgCount + 1]; + + int pos = 0; + int iarg; + + for (iarg = 0; iarg < _currentArgCount;) + { + int equal_sign_index = data.indexOf('=', pos); + int next_arg_index = data.indexOf('&', pos); + + if ((equal_sign_index == -1) || ((equal_sign_index > next_arg_index) && (next_arg_index != -1))) + { + if (next_arg_index == -1) + break; + + pos = next_arg_index + 1; + + continue; + } + + RequestArgument& arg = _currentArgs[iarg]; + arg.key = urlDecode(data.substring(pos, equal_sign_index)); + arg.value = urlDecode(data.substring(equal_sign_index + 1, next_arg_index)); + + ++iarg; + + if (next_arg_index == -1) + break; + + pos = next_arg_index + 1; + } + + _currentArgCount = iarg; +} + +//////////////////////////////////////// + +void WiFiWebServer::_uploadWriteByte(uint8_t b) +{ + if (_currentUpload->currentSize == HTTP_UPLOAD_BUFLEN) + { + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + + _currentUpload->totalSize += _currentUpload->currentSize; + _currentUpload->currentSize = 0; + } + + _currentUpload->buf[_currentUpload->currentSize++] = b; +} + +//////////////////////////////////////// + +int WiFiWebServer::_uploadReadByte(WiFiClient& client) +{ + int res = client.read(); + + if (res < 0) + { + // keep trying until you either read a valid byte or timeout + unsigned long startMillis = millis(); + unsigned long timeoutIntervalMillis = client.getTimeout(); + bool timedOut = false; + + for (;;) + { + if (!client.connected()) + return -1; + + // loosely modeled after blinkWithoutDelay pattern + while (!timedOut && !client.available() && client.connected()) + { + delay(2); + timedOut = (millis() - startMillis) >= timeoutIntervalMillis; + } + + res = client.read(); + + if (res >= 0) + { + return res; // exit on a valid read + } + + timedOut = (millis() - startMillis) >= timeoutIntervalMillis; + + if (timedOut) + { + return res; // exit on a timeout + } + } + } + + return res; +} + +//////////////////////////////////////// + +#else // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +void WiFiWebServer::_parseArguments(const String& data) +{ + + WS_LOGDEBUG1(F("args: "), data); + + if (_currentArgs) + delete[] _currentArgs; + + _currentArgs = 0; + + if (data.length() == 0) + { + _currentArgCount = 0; + _currentArgs = new RequestArgument[1]; + + return; + } + + _currentArgCount = 1; + + for (int i = 0; i < (int)data.length(); ) + { + i = data.indexOf('&', i); + + if (i == -1) + break; + + ++i; + ++_currentArgCount; + } + + WS_LOGDEBUG1(F("args count: "), _currentArgCount); + + _currentArgs = new RequestArgument[_currentArgCount + 1]; + + int pos = 0; + int iarg; + + for (iarg = 0; iarg < _currentArgCount;) + { + int equal_sign_index = data.indexOf('=', pos); + int next_arg_index = data.indexOf('&', pos); + + WS_LOGDEBUG1(F("pos: "), pos); + WS_LOGDEBUG1(F("=@ "), equal_sign_index); + WS_LOGDEBUG1(F(" &@ "), next_arg_index); + + + if ((equal_sign_index == -1) || ((equal_sign_index > next_arg_index) && (next_arg_index != -1))) + { + WS_LOGDEBUG1(F("arg missing value: "), iarg); + + if (next_arg_index == -1) + break; + + pos = next_arg_index + 1; + + continue; + } + + RequestArgument& arg = _currentArgs[iarg]; + arg.key = data.substring(pos, equal_sign_index); + arg.value = data.substring(equal_sign_index + 1, next_arg_index); + + WS_LOGDEBUG1(F("arg: "), iarg); + WS_LOGDEBUG1(F("key: "), arg.key); + WS_LOGDEBUG1(F("value: "), arg.value); + + ++iarg; + + if (next_arg_index == -1) + break; + + pos = next_arg_index + 1; + } + + _currentArgCount = iarg; + + WS_LOGDEBUG1(F("args count: "), _currentArgCount); +} + +//////////////////////////////////////// + +void WiFiWebServer::_uploadWriteByte(uint8_t b) +{ + if (_currentUpload.currentSize == HTTP_UPLOAD_BUFLEN) + { + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, _currentUpload); + + _currentUpload.totalSize += _currentUpload.currentSize; + _currentUpload.currentSize = 0; + } + + _currentUpload.buf[_currentUpload.currentSize++] = b; +} + +//////////////////////////////////////// + +int WiFiWebServer::_uploadReadByte(WiFiClient& client) +{ + int res = client.read(); + + if (res == -1) + { + while (!client.available() && client.connected()) + yield(); + + res = client.read(); + } + + return res; +} + +//////////////////////////////////////// + +#endif // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +#if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +bool WiFiWebServer::_parseForm(WiFiClient& client, const String& boundary, uint32_t len) +{ + (void) len; + + WS_LOGDEBUG1(F("Parse Form: Boundary: "), boundary); + WS_LOGDEBUG1(F("Length: "), len); + + String line; + int retry = 0; + + do + { + line = client.readStringUntil('\r'); + ++retry; + } while (line.length() == 0 && retry < 3); + + client.readStringUntil('\n'); + + //start reading the form + if (line == ("--" + boundary)) + { + if (_postArgs) + delete[] _postArgs; + + _postArgs = new RequestArgument[WEBSERVER_MAX_POST_ARGS]; + _postArgsLen = 0; + + while (1) + { + String argName; + String argValue; + String argType; + String argFilename; + + bool argIsFile = false; + + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (line.length() > 19 && line.substring(0, 19).equalsIgnoreCase(F("Content-Disposition"))) + { + int nameStart = line.indexOf('='); + + if (nameStart != -1) + { + argName = line.substring(nameStart + 2); + nameStart = argName.indexOf('='); + + if (nameStart == -1) + { + argName = argName.substring(0, argName.length() - 1); + } + else + { + argFilename = argName.substring(nameStart + 2, argName.length() - 1); + argName = argName.substring(0, argName.indexOf('"')); + argIsFile = true; + + WS_LOGDEBUG1(F("PostArg FileName: "), argFilename); + + //use GET to set the filename if uploading using blob + if (argFilename == F("blob") && hasArg("filename")) + argFilename = arg("filename"); + } + + WS_LOGDEBUG1(F("PostArg Name: "), argName); + +#if (ESP32 || ESP8266) + using namespace mime_esp; +#else + using namespace mime; +#endif + + argType = mimeTable[txt].mimeType; + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (line.length() > 12 && line.substring(0, 12).equalsIgnoreCase("Content-Type")) + { + argType = line.substring(line.indexOf(':') + 2); + //skip next line + client.readStringUntil('\r'); + client.readStringUntil('\n'); + } + + WS_LOGDEBUG1(F("PostArg Type: "), argType); + + if (!argIsFile) + { + while (1) + { + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (line.startsWith("--" + boundary)) + break; + + if (argValue.length() > 0) + argValue += "\n"; + + argValue += line; + } + + WS_LOGDEBUG1(F("PostArg Value: "), argValue); + + RequestArgument& arg = _postArgs[_postArgsLen++]; + arg.key = argName; + arg.value = argValue; + + if (line == ("--" + boundary + "--")) + { + WS_LOGDEBUG(F("Done Parsing POST")); + + break; + } + } + else + { + //_currentUpload.reset(new HTTPUpload()); + if (!_currentUpload) + _currentUpload = new HTTPUpload(); + + _currentUpload->status = UPLOAD_FILE_START; + _currentUpload->name = argName; + _currentUpload->filename = argFilename; + _currentUpload->type = argType; + _currentUpload->totalSize = 0; + _currentUpload->currentSize = 0; + _currentUpload->contentLength = len; + + WS_LOGDEBUG1(F("Start File: "), _currentUpload->filename); + WS_LOGDEBUG1(F("Type: "), _currentUpload->type); + + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + + _currentUpload->status = UPLOAD_FILE_WRITE; + uint8_t argByte = _uploadReadByte(client); + +readfile: + + while (argByte != 0x0D) + { + if (!client.connected()) + return _parseFormUploadAborted(); + + _uploadWriteByte(argByte); + argByte = _uploadReadByte(client); + } + + argByte = _uploadReadByte(client); + + if (!client.connected()) + return _parseFormUploadAborted(); + + if (argByte == 0x0A) + { + argByte = _uploadReadByte(client); + + if (!client.connected()) + return _parseFormUploadAborted(); + + if ((char)argByte != '-') + { + //continue reading the file + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + goto readfile; + } + else + { + argByte = _uploadReadByte(client); + + if (!client.connected()) + return _parseFormUploadAborted(); + + if ((char)argByte != '-') + { + //continue reading the file + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + _uploadWriteByte((uint8_t)('-')); + goto readfile; + } + } + +#define USING_VLA true + +#if USING_VLA + // Better compiler warning than risk of fragmented heap + uint8_t endBuf[boundary.length()]; +#else + // No compiler warning, but risk of fragmented heap + uint8_t* endBuf = new uint8_t[boundary.length()]; + + if (!endBuf) + { + return false; + } + +#endif + + client.readBytes(endBuf, boundary.length()); + + if (strstr((const char*)endBuf, boundary.c_str()) != NULL) + { + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + + _currentUpload->totalSize += _currentUpload->currentSize; + _currentUpload->status = UPLOAD_FILE_END; + + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + + WS_LOGDEBUG1(F("End File: "), _currentUpload->filename); + WS_LOGDEBUG1(F("Type: "), _currentUpload->type); + WS_LOGDEBUG1(F("Size: "), _currentUpload->totalSize); + + line = client.readStringUntil(0x0D); + client.readStringUntil(0x0A); + + if (line == "--") + { + WS_LOGDEBUG(F("Done Parsing POST")); + + break; + } + + continue; + } + else + { + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + _uploadWriteByte((uint8_t)('-')); + _uploadWriteByte((uint8_t)('-')); + + uint32_t i = 0; + + while (i < boundary.length()) + { + _uploadWriteByte(endBuf[i++]); + } + + argByte = _uploadReadByte(client); + goto readfile; + } + +#if !USING_VLA + + if (endBuf) + delete [] endBuf; + +#endif + } + else + { + _uploadWriteByte(0x0D); + goto readfile; + } + + break; + } + } + } + } + + int iarg; + int totalArgs = ((WEBSERVER_MAX_POST_ARGS - _postArgsLen) < _currentArgCount) ? (WEBSERVER_MAX_POST_ARGS - _postArgsLen) + : _currentArgCount; + + for (iarg = 0; iarg < totalArgs; iarg++) + { + RequestArgument& arg = _postArgs[_postArgsLen++]; + arg.key = _currentArgs[iarg].key; + arg.value = _currentArgs[iarg].value; + } + + if (_currentArgs) + delete[] _currentArgs; + + _currentArgs = new RequestArgument[_postArgsLen]; + + for (iarg = 0; iarg < _postArgsLen; iarg++) + { + RequestArgument& arg = _currentArgs[iarg]; + arg.key = _postArgs[iarg].key; + arg.value = _postArgs[iarg].value; + } + + _currentArgCount = iarg; + + if (_postArgs) + { + delete[] _postArgs; + _postArgs = nullptr; + _postArgsLen = 0; + } + + return true; + } + + WS_LOGDEBUG1(F("Error: line: "), line); + + return false; +} + +bool WiFiWebServer::_parseFormUploadAborted() +{ + _currentUpload->status = UPLOAD_FILE_ABORTED; + + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, *_currentUpload); + + return false; +} + +//////////////////////////////////////// + +#else // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +bool WiFiWebServer::_parseForm(WiFiClient& client, const String& boundary, uint32_t len) +{ + WS_LOGDEBUG1(F("Parse Form: Boundary: "), boundary); + WS_LOGDEBUG1(F("Length: "), len); + WS_LOGERROR1(F("Parse Form: Boundary: "), boundary); + WS_LOGERROR1(F("Length: "), len); + + String line; + int retry = 0; + + do + { + line = client.readStringUntil('\r'); + ++retry; + } while (line.length() == 0 && retry < 3); + + client.readStringUntil('\n'); + + //start reading the form + if (line == ("--" + boundary)) + { + RequestArgument* postArgs = new RequestArgument[32]; + int postArgsLen = 0; + + while (1) + { + String argName; + String argValue; + String argType; + String argFilename; + + bool argIsFile = false; + + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (line.startsWith("Content-Disposition")) + { + int nameStart = line.indexOf('='); + + if (nameStart != -1) + { + argName = line.substring(nameStart + 2); + nameStart = argName.indexOf('='); + + if (nameStart == -1) + { + argName = argName.substring(0, argName.length() - 1); + } + else + { + argFilename = argName.substring(nameStart + 2, argName.length() - 1); + argName = argName.substring(0, argName.indexOf('"')); + argIsFile = true; + + WS_LOGDEBUG1(F("PostArg FileName: "), argFilename); + + //use GET to set the filename if uploading using blob + if (argFilename == "blob" && hasArg("filename")) + argFilename = arg("filename"); + } + + WS_LOGDEBUG1(F("PostArg Name: "), argName); + + argType = "text/plain"; + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (line.startsWith("Content-Type")) + { + argType = line.substring(line.indexOf(':') + 2); + //skip next line + client.readStringUntil('\r'); + client.readStringUntil('\n'); + } + + WS_LOGDEBUG1(F("PostArg Type: "), argType); + + if (!argIsFile) + { + while (1) + { + line = client.readStringUntil('\r'); + client.readStringUntil('\n'); + + if (line.startsWith("--" + boundary)) + break; + + if (argValue.length() > 0) + argValue += "\n"; + + argValue += line; + } + + WS_LOGDEBUG1(F("PostArg Value: "), argValue); + + RequestArgument& arg = postArgs[postArgsLen++]; + arg.key = argName; + arg.value = argValue; + + if (line == ("--" + boundary + "--")) + { + WS_LOGDEBUG(F("Done Parsing POST")); + + break; + } + } + else + { + _currentUpload.status = UPLOAD_FILE_START; + _currentUpload.name = argName; + _currentUpload.filename = argFilename; + _currentUpload.type = argType; + _currentUpload.totalSize = 0; + _currentUpload.currentSize = 0; + + WS_LOGDEBUG1(F("Start File: "), _currentUpload.filename); + WS_LOGDEBUG1(F("Type: "), _currentUpload.type); + + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, _currentUpload); + + _currentUpload.status = UPLOAD_FILE_WRITE; + uint8_t argByte = _uploadReadByte(client); + +readfile: + + while (argByte != 0x0D) + { + if (!client.connected()) + return _parseFormUploadAborted(); + + _uploadWriteByte(argByte); + argByte = _uploadReadByte(client); + } + + argByte = _uploadReadByte(client); + + if (!client.connected()) + return _parseFormUploadAborted(); + + if (argByte == 0x0A) + { + argByte = _uploadReadByte(client); + + if (!client.connected()) + return _parseFormUploadAborted(); + + if ((char)argByte != '-') + { + //continue reading the file + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + goto readfile; + } + else + { + argByte = _uploadReadByte(client); + + if (!client.connected()) + return _parseFormUploadAborted(); + + if ((char)argByte != '-') + { + //continue reading the file + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + _uploadWriteByte((uint8_t)('-')); + goto readfile; + } + } + +#define USING_VLA true + +#if USING_VLA + // Better compiler warning than risk of fragmented heap + uint8_t endBuf[boundary.length()]; +#else + // No compiler warning, but risk of fragmented heap + uint8_t* endBuf = new uint8_t[boundary.length()]; + + if (!endBuf) + { + return false; + } + +#endif + + client.readBytes(endBuf, boundary.length()); + + if (strstr((const char*)endBuf, boundary.c_str()) != NULL) + { + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, _currentUpload); + + _currentUpload.totalSize += _currentUpload.currentSize; + _currentUpload.status = UPLOAD_FILE_END; + + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, _currentUpload); + + WS_LOGDEBUG1(F("End File: "), _currentUpload.filename); + WS_LOGDEBUG1(F("Type: "), _currentUpload.type); + WS_LOGDEBUG1(F("Size: "), _currentUpload.totalSize); + + line = client.readStringUntil(0x0D); + client.readStringUntil(0x0A); + + if (line == "--") + { + WS_LOGDEBUG(F("Done Parsing POST")); + + break; + } + + continue; + } + else + { + _uploadWriteByte(0x0D); + _uploadWriteByte(0x0A); + _uploadWriteByte((uint8_t)('-')); + _uploadWriteByte((uint8_t)('-')); + + uint32_t i = 0; + + while (i < boundary.length()) + { + _uploadWriteByte(endBuf[i++]); + } + + argByte = _uploadReadByte(client); + goto readfile; + } + +#if !USING_VLA + + if (endBuf) + delete [] endBuf; + +#endif + } + else + { + _uploadWriteByte(0x0D); + goto readfile; + } + + break; + } + } + } + } + + int iarg; + int totalArgs = ((32 - postArgsLen) < _currentArgCount) ? (32 - postArgsLen) : _currentArgCount; + + for (iarg = 0; iarg < totalArgs; iarg++) + { + RequestArgument& arg = postArgs[postArgsLen++]; + arg.key = _currentArgs[iarg].key; + arg.value = _currentArgs[iarg].value; + } + + if (_currentArgs) + delete[] _currentArgs; + + _currentArgs = new RequestArgument[postArgsLen]; + + for (iarg = 0; iarg < postArgsLen; iarg++) + { + RequestArgument& arg = _currentArgs[iarg]; + arg.key = postArgs[iarg].key; + arg.value = postArgs[iarg].value; + } + + _currentArgCount = iarg; + + if (postArgs) + delete[] postArgs; + + return true; + } + + WS_LOGDEBUG1(F("Error: line: "), line); + + return false; +} + +//////////////////////////////////////// + +bool WiFiWebServer::_parseFormUploadAborted() +{ + _currentUpload.status = UPLOAD_FILE_ABORTED; + + if (_currentHandler && _currentHandler->canUpload(_currentUri)) + _currentHandler->upload(*this, _currentUri, _currentUpload); + + return false; +} + +//////////////////////////////////////// + +#endif // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +String WiFiWebServer::urlDecode(const String& text) +{ + String decoded = ""; + char temp[] = "0x00"; + unsigned int len = text.length(); + unsigned int i = 0; + + 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 += decodedChar; + } + + return decoded; +} + +//////////////////////////////////////// + +#endif // Parsing_Impl_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/Uri.h b/software/firmware/source/libraries/WiFiWebServer/src/Uri.h new file mode 100644 index 000000000..c34784dec --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/Uri.h @@ -0,0 +1,75 @@ +/**************************************************************************************************************************** + Uri.h - Dead simple HTTP WebClient. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#ifndef URI_H +#define URI_H + +#include +#include + +//////////////////////////////////////// + +class Uri +{ + protected: + const String _uri; + + public: + Uri(const char *uri) : _uri(uri) {} + Uri(const String &uri) : _uri(uri) {} + Uri(const __FlashStringHelper *uri) : _uri(String(uri)) {} + virtual ~Uri() {} + + //////////////////////////////////////// + + virtual Uri* clone() const + { + return new Uri(_uri); + }; + + //////////////////////////////////////// + + virtual void initPathArgs(__attribute__((unused)) std::vector &pathArgs) {} + + //////////////////////////////////////// + + virtual inline bool canHandle(const String &requestUri, __attribute__((unused)) std::vector &pathArgs) + { + return _uri == requestUri; + } +}; + +#endif diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFiHttpClient.h b/software/firmware/source/libraries/WiFiWebServer/src/WiFiHttpClient.h new file mode 100644 index 000000000..a713bbc6b --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFiHttpClient.h @@ -0,0 +1,54 @@ +/**************************************************************************************************************************** + WiFiHttpClient.h - Dead simple HTTP WebClient. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +// Library to simplify HTTP fetching on Arduino +// (c) Copyright Arduino. 2016 +// Released under Apache License, version 2.0 + +#pragma once + +#ifndef WiFiHttpClient_H +#define WiFiHttpClient_H + +#include + +#include "utility/WiFiDebug.h" +#include "WiFi_HTTPClient/WiFi_HttpClient.h" +#include "WiFi_HTTPClient/WiFi_WebSocketClient.h" +#include "WiFi_HTTPClient/WiFi_URLEncoder.h" + +#endif // WiFiHttpClient_H + diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer-impl.h b/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer-impl.h new file mode 100644 index 000000000..f0fdfeac7 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer-impl.h @@ -0,0 +1,1365 @@ +/********************************************************************************************************************************* + WiFiWebServer-impl.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + **********************************************************************************************************************************/ + +#pragma once + +#ifndef WiFiWebServer_Impl_H +#define WiFiWebServer_Impl_H + +#include +#include + +#include "WiFiWebServer.hpp" +#include "utility/RequestHandlersImpl.h" +#include "utility/WiFiDebug.h" +#include "utility/mimetable.h" + +const char * AUTHORIZATION_HEADER = "Authorization"; + +// New to use WWString + +//////////////////////////////////////// + +WWString fromString(const String& str) +{ + return str.c_str(); +} + +//////////////////////////////////////// + +WWString fromString(const String&& str) +{ + return str.c_str(); +} + +//////////////////////////////////////// + +String fromWWString(const WWString& str) +{ + return str.c_str(); +} + +//////////////////////////////////////// + +String fromWWString(const WWString&& str) +{ + return str.c_str(); +} + +//////////////////////////////////////// +//////////////////////////////////////// + +#if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +#if (ESP32 || ESP8266) +WiFiWebServer::WiFiWebServer(IPAddress addr, int port) + : _corsEnabled(false) + , _server(addr, port) + , _currentMethod(HTTP_ANY) + , _currentVersion(0) + , _currentStatus(HC_NONE) + , _statusChange(0) + , _nullDelay(true) + , _currentHandler(nullptr) + , _firstHandler(nullptr) + , _lastHandler(nullptr) + , _currentArgCount(0) + , _currentArgs(nullptr) + , _postArgsLen(0) + , _postArgs(nullptr) + , _headerKeysCount(0) + , _currentHeaders(nullptr) + , _contentLength(0) + , _clientContentLength(0) + , _chunked(false) +{ +} +#endif + +//////////////////////////////////////// + +WiFiWebServer::WiFiWebServer(int port) + : _corsEnabled(false) + , _server(port) + , _currentMethod(HTTP_ANY) + , _currentVersion(0) + , _currentStatus(HC_NONE) + , _statusChange(0) + , _nullDelay(true) + , _currentHandler(nullptr) + , _firstHandler(nullptr) + , _lastHandler(nullptr) + , _currentArgCount(0) + , _currentArgs(nullptr) + , _postArgsLen(0) + , _postArgs(nullptr) + , _headerKeysCount(0) + , _currentHeaders(nullptr) + , _contentLength(0) + , _clientContentLength(0) + , _chunked(false) +{ +} + +//////////////////////////////////////// + +WiFiWebServer::~WiFiWebServer() +{ +#if (ESP32 || ESP8266) + _server.close(); +#endif + + if (_currentHeaders) + delete[]_currentHeaders; + + RequestHandler* handler = _firstHandler; + + while (handler) + { + RequestHandler* next = handler->next(); + delete handler; + handler = next; + } +} + +//////////////////////////////////////// + +void WiFiWebServer::begin() +{ + close(); + _server.begin(); + +#if (ESP32 || ESP8266) + _server.setNoDelay(true); +#endif +} + +//////////////////////////////////////// + +void WiFiWebServer::begin(uint16_t port) +{ + close(); + _server.begin(port); + +#if (ESP32 || ESP8266) + _server.setNoDelay(true); +#endif +} + +//////////////////////////////////////// + +#else // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +WiFiWebServer::WiFiWebServer(int port) + : _corsEnabled(false) + , _server(port) + , _currentMethod(HTTP_ANY) + , _currentVersion(0) + , _currentHandler(nullptr) + , _firstHandler(nullptr) + , _lastHandler(nullptr) + , _currentArgCount(0) + , _currentArgs(nullptr) + , _headerKeysCount(0) + , _currentHeaders(nullptr) + , _contentLength(0) + , _chunked(false) +{ +} + +//////////////////////////////////////// + +WiFiWebServer::~WiFiWebServer() +{ + if (_currentHeaders) + delete[]_currentHeaders; + + _headerKeysCount = 0; + RequestHandler* handler = _firstHandler; + + while (handler) + { + RequestHandler* next = handler->next(); + delete handler; + handler = next; + } + + close(); +} + +//////////////////////////////////////// + +void WiFiWebServer::begin() +{ + _currentStatus = HC_NONE; + _server.begin(); + + if (!_headerKeysCount) + collectHeaders(0, 0); +} + +//////////////////////////////////////// + +#endif // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +bool WiFiWebServer::authenticate(const char * username, const char * password) +{ + if (hasHeader(AUTHORIZATION_HEADER)) + { + String authReq = header(AUTHORIZATION_HEADER); + + 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 = String(); + + return false; + } + + char *encoded = new char[base64_encode_expected_len(toencodeLen) + 1]; + + if (encoded == NULL) + { + authReq = String(); + delete[] toencode; + + return false; + } + + sprintf(toencode, "%s:%s", username, password); + + if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equals(encoded)) + { + authReq = String(); + delete[] toencode; + delete[] encoded; + + return true; + } + + delete[] toencode; + delete[] encoded; + } + + authReq = String(); + } + + return false; +} + +//////////////////////////////////////// + +void WiFiWebServer::requestAuthentication() +{ + sendHeader("WWW-Authenticate", "Basic realm=\"Login Required\""); + send(401); +} + +//////////////////////////////////////// + +void WiFiWebServer::on(const String &uri, WiFiWebServer::THandlerFunction handler) +{ + on(uri, HTTP_ANY, handler); +} + +//////////////////////////////////////// + +void WiFiWebServer::on(const String &uri, HTTPMethod method, WiFiWebServer::THandlerFunction fn) +{ + on(uri, method, fn, _fileUploadHandler); +} + +//////////////////////////////////////// + +void WiFiWebServer::on(const String &uri, HTTPMethod method, WiFiWebServer::THandlerFunction fn, + WiFiWebServer::THandlerFunction ufn) +{ + _addRequestHandler(new FunctionRequestHandler(fn, ufn, uri, method)); +} + +//////////////////////////////////////// + +void WiFiWebServer::addHandler(RequestHandler* handler) +{ + _addRequestHandler(handler); +} + +//////////////////////////////////////// + +void WiFiWebServer::_addRequestHandler(RequestHandler* handler) +{ + if (!_lastHandler) + { + _firstHandler = handler; + _lastHandler = handler; + } + else + { + _lastHandler->next(handler); + _lastHandler = handler; + } +} + +//////////////////////////////////////// + +#if USE_NEW_WEBSERVER_VERSION + +void WiFiWebServer::handleClient() +{ + if (_currentStatus == HC_NONE) + { + WiFiClient client = _server.available(); + + if (!client) + { + if (_nullDelay) + { + delay(1); + } + + return; + } + + WS_LOGDEBUG(F("handleClient: New Client")); + + _currentClient = client; + _currentStatus = HC_WAIT_READ; + _statusChange = millis(); + } + + bool keepCurrentClient = false; + bool callYield = false; + + if (_currentClient.connected() || _currentClient.available()) + { + switch (_currentStatus) + { + case HC_NONE: + // No-op to avoid C++ compiler warning + break; + + case HC_WAIT_READ: + + // Wait for data from client to become available + if (_currentClient.available()) + { + if (_parseRequest(_currentClient)) + { + _currentClient.setTimeout(HTTP_MAX_SEND_WAIT); + _contentLength = CONTENT_LENGTH_NOT_SET; + _handleRequest(); + +#if USE_WIFI_NINA || ( defined(ARDUINO_NANO_RP2040_CONNECT) || defined(ARDUINO_SAMD_NANO_33_IOT) ) + + // Fix for issue with Chrome based browsers: https://github.com/espressif/arduino-esp32/issues/3652 + // Remove this will hang boards using WiFNINA, such as + // Nano_RP2040_Connect with arduino_pico core, Nano_33_IoT + if (_currentClient.connected()) + { + _currentStatus = HC_WAIT_CLOSE; + _statusChange = millis(); + keepCurrentClient = true; + } + +#endif + } + } + else + { + // !_currentClient.available() + if (millis() - _statusChange <= HTTP_MAX_DATA_WAIT) + { + keepCurrentClient = true; + } + + callYield = true; + } + + break; + + case HC_WAIT_CLOSE: + + // Wait for client to close the connection + if (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT) + { + keepCurrentClient = true; + callYield = true; + } + } + } + + if (!keepCurrentClient) + { + WS_LOGDEBUG(F("handleClient: Don't keepCurrentClient")); + _currentClient = WiFiClient(); + _currentStatus = HC_NONE; + // KH + //_currentUpload.reset(); + } + + if (callYield) + { + yield(); + } + +#if (USE_WIFI_NINA || WIFI_USE_PORTENTA_H7) + // KH, fix bug relating to New NINA FW 1.4.0. Have to close the connection + _currentClient.stop(); + WS_LOGDEBUG(F("handleClient: Client disconnected")); +#endif +} + +//////////////////////////////////////// + +#else // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +// KH, rewritten for Portenta H7 from v1.4.0 +void WiFiWebServer::handleClient() +{ + if (_currentStatus == HC_NONE) + { + WiFiClient client = _server.available(); + + if (!client) + { + return; + } + + WS_LOGDEBUG(F("handleClient: New Client")); + + _currentClient = client; + _currentStatus = HC_WAIT_READ; + _statusChange = millis(); + } + + if (!_currentClient.connected()) + { + _currentStatus = HC_NONE; + + goto stopClient; + } + + // Wait for data from client to become available + if (_currentStatus == HC_WAIT_READ) + { + if (!_currentClient.available()) + { + if (millis() - _statusChange > HTTP_MAX_DATA_WAIT) + { + WS_LOGDEBUG(F("handleClient: HTTP_MAX_DATA_WAIT Timeout")); + + _currentStatus = HC_NONE; + + goto stopClient; + } + + yield(); + return; + } + + WS_LOGDEBUG(F("handleClient: Parsing Request")); + + if (!_parseRequest(_currentClient)) + { + WS_LOGDEBUG(F("handleClient: Can't parse request")); + + _currentStatus = HC_NONE; + + goto stopClient; + } + + _currentClient.setTimeout(HTTP_MAX_SEND_WAIT); + _contentLength = CONTENT_LENGTH_NOT_SET; + + _handleRequest(); + + if (!_currentClient.connected()) + { + WS_LOGINFO(F("handleClient: Connection closed")); + + _currentStatus = HC_NONE; + + goto stopClient; + } + else + { + _currentStatus = HC_WAIT_CLOSE; + _statusChange = millis(); + return; + } + } + + if (_currentStatus == HC_WAIT_CLOSE) + { + if (millis() - _statusChange > HTTP_MAX_CLOSE_WAIT) + { + _currentStatus = HC_NONE; + + WS_LOGDEBUG(F("handleClient: HTTP_MAX_CLOSE_WAIT Timeout")); + + yield(); + } + else + { + yield(); + return; + } + } + +stopClient: + +#if (USE_WIFI_NINA || WIFI_USE_PORTENTA_H7) + // To be used with New NINA FW 1.4.0 and Portenta_H7 WiFi. Have to close the connection + _currentClient.stop(); + WS_LOGDEBUG(F("handleClient: Client disconnected")); +#endif +} + +#endif // #if USE_NEW_WEBSERVER_VERSION + +//////////////////////////////////////// + +void WiFiWebServer::close() +{ +#if (ESP32 || ESP8266) + _server.close(); +#endif + + _currentStatus = HC_NONE; + + if (!_headerKeysCount) + collectHeaders(0, 0); +} + +//////////////////////////////////////// + +void WiFiWebServer::stop() +{ + close(); +} + +//////////////////////////////////////// + +void WiFiWebServer::sendHeader(const String& name, const String& value, bool first) +{ + WWString headerLine = fromString(name); + + headerLine += ": "; + headerLine += fromString(value); + headerLine += RETURN_NEWLINE; + + if (first) + { + _responseHeaders = fromWWString(headerLine + fromString(_responseHeaders)); + } + else + { + _responseHeaders = fromWWString(fromString(_responseHeaders) + headerLine); + } +} + +//////////////////////////////////////// + +void WiFiWebServer::setContentLength(size_t contentLength) +{ + _contentLength = contentLength; +} + +//////////////////////////////////////// + +void WiFiWebServer::_prepareHeader(String& response, int code, const char* content_type, size_t contentLength) +{ + WWString aResponse = fromString(response); + + aResponse = "HTTP/1." + fromString(String(_currentVersion)) + " "; + aResponse += fromString(String(code)); + aResponse += " "; + aResponse += fromString(_responseCodeToString(code)); + aResponse += RETURN_NEWLINE; + +#if (ESP32 || ESP8266) + using namespace mime_esp; +#else + using namespace mime; +#endif + + if (!content_type) + content_type = mimeTable[html].mimeType; + + sendHeader("Content-Type", content_type, true); + + if (_contentLength == CONTENT_LENGTH_NOT_SET) + { + sendHeader("Content-Length", String(contentLength)); + } + else if (_contentLength != CONTENT_LENGTH_UNKNOWN) + { + sendHeader("Content-Length", String(_contentLength)); + } + else if (_contentLength == CONTENT_LENGTH_UNKNOWN && _currentVersion) + { + //HTTP/1.1 or above client + //let's do chunked + _chunked = true; + sendHeader("Accept-Ranges", "none"); + sendHeader("Transfer-Encoding", "chunked"); + } + + if (_corsEnabled) + { + sendHeader("Access-Control-Allow-Origin", "*"); + sendHeader("Access-Control-Allow-Methods", "*"); + sendHeader("Access-Control-Allow-Headers", "*"); + } + + WS_LOGDEBUG(F("_prepareHeader sendHeader Conn close")); + + sendHeader("Connection", "close"); + + aResponse += fromString(_responseHeaders); + aResponse += RETURN_NEWLINE; + + response = fromWWString(aResponse); + + _responseHeaders = String(""); +} + +//////////////////////////////////////// + +void WiFiWebServer::_prepareHeader(WWString& response, int code, const char* content_type, size_t contentLength) +{ + response = "HTTP/1." + fromString(String(_currentVersion)) + " "; + response += fromString(String(code)); + response += " "; + response += fromString(_responseCodeToString(code)); + response += RETURN_NEWLINE; + +#if (ESP32 || ESP8266) + using namespace mime_esp; +#else + using namespace mime; +#endif + + if (!content_type) + content_type = mimeTable[html].mimeType; + + sendHeader("Content-Type", content_type, true); + + if (_contentLength == CONTENT_LENGTH_NOT_SET) + { + sendHeader("Content-Length", String(contentLength)); + } + else if (_contentLength != CONTENT_LENGTH_UNKNOWN) + { + sendHeader("Content-Length", String(_contentLength)); + } + else if (_contentLength == CONTENT_LENGTH_UNKNOWN && _currentVersion) + { + //HTTP/1.1 or above client + //let's do chunked + _chunked = true; + sendHeader("Accept-Ranges", "none"); + sendHeader("Transfer-Encoding", "chunked"); + } + else if (_contentLength != CONTENT_LENGTH_UNKNOWN) + { + sendHeader("Content-Length", String(_contentLength)); + } + else if (_contentLength == CONTENT_LENGTH_UNKNOWN && _currentVersion) + { + //HTTP/1.1 or above client + //let's do chunked + _chunked = true; + sendHeader("Accept-Ranges", "none"); + sendHeader("Transfer-Encoding", "chunked"); + } + + if (_corsEnabled) + { + sendHeader("Access-Control-Allow-Origin", "*"); + sendHeader("Access-Control-Allow-Methods", "*"); + sendHeader("Access-Control-Allow-Headers", "*"); + } + + WS_LOGDEBUG(F("_prepareHeader sendHeader Conn close")); + + sendHeader("Connection", "close"); + + response += fromString(_responseHeaders); + response += RETURN_NEWLINE; + + _responseHeaders = String(""); +} + +//////////////////////////////////////// + +void WiFiWebServer::send(int code, const char* content_type, const String& content) +{ + WWString header; + + _prepareHeader(header, code, content_type, content.length()); + + _currentClient.write((const uint8_t *)header.c_str(), header.length()); + + if (content.length()) + { + sendContent(content, content.length()); + } +} + +//////////////////////////////////////// + +void WiFiWebServer::send(int code, char* content_type, const String& content, size_t contentLength) +{ + WWString header; + + char type[64]; + + memccpy((void*)type, content_type, 0, sizeof(type)); + _prepareHeader(header, code, (const char* )type, contentLength); + + _currentClient.write((const uint8_t *) header.c_str(), header.length()); + + if (contentLength) + { + sendContent(content, contentLength); + } +} + +//////////////////////////////////////// + +void WiFiWebServer::send(int code, char* content_type, const String& content) +{ + send(code, (const char*)content_type, content); +} + +//////////////////////////////////////// + +void WiFiWebServer::send(int code, const String& content_type, const String& content) +{ + send(code, (const char*)content_type.c_str(), content); +} + +//////////////////////////////////////// + +// KH New + +void WiFiWebServer::send(int code, const char* content_type, const char* content) +{ + send(code, content_type, content, content ? strlen(content) : 0); +} + +//////////////////////////////////////// + +void WiFiWebServer::send(int code, const char* content_type, const char* content, size_t contentLength) +{ + String header; + + _prepareHeader(header, code, content_type, contentLength); + + _currentClient.write((const uint8_t *) header.c_str(), header.length()); + + if (contentLength) + { + sendContent(content, contentLength); + } +} + +//////////////////////////////////////// + +void WiFiWebServer::sendContent(const char* content, size_t contentLength) +{ + const char * footer = RETURN_NEWLINE; + + if (_chunked) + { + char chunkSize[11]; + + WS_LOGDEBUG1(F("sendContent_char: _chunked, _currentVersion ="), _currentVersion); + + sprintf(chunkSize, "%x%s", contentLength, footer); + _currentClient.write(chunkSize, strlen(chunkSize)); + } + + _currentClient.write(content, contentLength); + + if (_chunked) + { + _currentClient.write(footer, 2); + + if (contentLength == 0) + { + _chunked = false; + } + } +} + +//////////////////////////////////////// + +void WiFiWebServer::sendContent(const String& content) +{ + sendContent(content.c_str(), content.length()); +} + +//////////////////////////////////////// + +void WiFiWebServer::sendContent(const String& content, size_t contentLength) +{ + sendContent(content.c_str(), contentLength); +} + +//////////////////////////////////////// + +// KH, Restore PROGMEM commands +void WiFiWebServer::send_P(int code, PGM_P content_type, PGM_P content) +{ + size_t contentLength = 0; + + if (content != NULL) + { + contentLength = strlen_P(content); + } + + String header; + char type[64]; + + memccpy_P((void*)type, (PGM_VOID_P)content_type, 0, sizeof(type)); + _prepareHeader(header, code, (const char* )type, contentLength); + +#if !( defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_H7_M4) ) + WS_LOGDEBUG1(F("send_P: len = "), contentLength); + WS_LOGDEBUG1(F("content = "), content); + WS_LOGDEBUG1(F("send_P: hdrlen = "), header.length()); + WS_LOGDEBUG1(F("header = "), header); +#endif + + _currentClient.write((const uint8_t *) header.c_str(), header.length()); + + if (contentLength) + { + sendContent_P(content); + } +} + +//////////////////////////////////////// + +void WiFiWebServer::send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength) +{ + WWString header; + + char type[64]; + + memccpy_P((void*)type, (PGM_VOID_P)content_type, 0, sizeof(type)); + _prepareHeader(header, code, (const char* )type, contentLength); + +#if !( defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_H7_M4) ) + WS_LOGDEBUG1(F("send_P: len = "), contentLength); + WS_LOGDEBUG1(F("content = "), content); + WS_LOGDEBUG1(F("send_P: hdrlen = "), header.length()); + WS_LOGDEBUG1(F("header = "), fromWWString(header)); +#endif + + _currentClient.write((const uint8_t *) header.c_str(), header.length()); + + if (contentLength) + { + sendContent_P(content, contentLength); + } +} + +//////////////////////////////////////// + +void WiFiWebServer::sendContent_P(PGM_P content) +{ + sendContent_P(content, strlen_P(content)); +} + +//////////////////////////////////////// + +void WiFiWebServer::sendContent_P(PGM_P content, size_t contentLength) +{ + const char * footer = RETURN_NEWLINE; + + if (_chunked) + { + char chunkSize[11]; + + WS_LOGDEBUG1(F("sendContent_P: _chunked, _currentVersion ="), _currentVersion); + + sprintf(chunkSize, "%x%s", contentLength, footer); + _currentClient.write(chunkSize, strlen(chunkSize)); + } + + uint8_t* _sendContentBuffer = new uint8_t[SENDCONTENT_P_BUFFER_SZ]; + + if (_sendContentBuffer) + { + uint16_t count = contentLength / SENDCONTENT_P_BUFFER_SZ; + uint16_t remainder = contentLength % SENDCONTENT_P_BUFFER_SZ; + uint16_t i = 0; + + for (i = 0; i < count; i++) + { + /* code */ + memcpy_P(_sendContentBuffer, &content[i * SENDCONTENT_P_BUFFER_SZ], SENDCONTENT_P_BUFFER_SZ); + _currentClient.write(_sendContentBuffer, SENDCONTENT_P_BUFFER_SZ); + } + + memcpy_P(_sendContentBuffer, &content[i * SENDCONTENT_P_BUFFER_SZ], remainder); + _currentClient.write(_sendContentBuffer, remainder); + + delete [] _sendContentBuffer; + } + else + { + WS_LOGERROR1(F("sendContent_P: Error, can't allocate _sendContentBuffer, Sz ="), SENDCONTENT_P_BUFFER_SZ); + + return; + } + + if (_chunked) + { + _currentClient.write(footer, 2); + + _chunked = false; + } +} + +//////////////////////////////////////// + +#if (ESP32 || ESP8266) + +#include "FS.h" + +//////////////////////////////////////// + +void WiFiWebServer::serveStatic(const char* uri, FS& fs, const char* path, const char* cache_header) +{ + _addRequestHandler(new StaticFileRequestHandler(fs, path, uri, cache_header)); +} + +//////////////////////////////////////// + +void WiFiWebServer::_streamFileCore(const size_t fileSize, const String & fileName, const String & contentType, + const int code) +{ +#if (ESP32 || ESP8266) + using namespace mime_esp; +#else + using namespace mime; +#endif + + setContentLength(fileSize); + + if (fileName.endsWith(String(FPSTR(mimeTable[gz].endsWith))) && + contentType != String(FPSTR(mimeTable[gz].mimeType)) && + contentType != String(FPSTR(mimeTable[none].mimeType))) + { + sendHeader(F("Content-Encoding"), F("gzip")); + } + + send(code, contentType, emptyString); +} + +#endif + +//////////////////////////////////////// + +String WiFiWebServer::arg(const String& name) +{ + for (int i = 0; i < _currentArgCount; ++i) + { + if ( _currentArgs[i].key == name ) + return _currentArgs[i].value; + } + + return String(); +} + +//////////////////////////////////////// + +String WiFiWebServer::arg(int i) +{ + if (i < _currentArgCount) + return _currentArgs[i].value; + + return String(); +} + +//////////////////////////////////////// + +String WiFiWebServer::argName(int i) +{ + if (i < _currentArgCount) + return _currentArgs[i].key; + + return String(); +} + +//////////////////////////////////////// + +int WiFiWebServer::args() +{ + return _currentArgCount; +} + +//////////////////////////////////////// + +bool WiFiWebServer::hasArg(const String& name) +{ + for (int i = 0; i < _currentArgCount; ++i) + { + if (_currentArgs[i].key == name) + return true; + } + + return false; +} + +//////////////////////////////////////// + +String WiFiWebServer::header(const String& name) +{ + for (int i = 0; i < _headerKeysCount; ++i) + { + if (_currentHeaders[i].key == name) + return _currentHeaders[i].value; + } + + return String(); +} + +//////////////////////////////////////// + +void WiFiWebServer::collectHeaders(const char* headerKeys[], const size_t headerKeysCount) +{ + _headerKeysCount = headerKeysCount + 1; + + if (_currentHeaders) + delete[]_currentHeaders; + + _currentHeaders = new RequestArgument[_headerKeysCount]; + _currentHeaders[0].key = AUTHORIZATION_HEADER; + + for (int i = 1; i < _headerKeysCount; i++) + { + _currentHeaders[i].key = headerKeys[i - 1]; + } +} + +//////////////////////////////////////// + +String WiFiWebServer::header(int i) +{ + if (i < _headerKeysCount) + return _currentHeaders[i].value; + + return String(); +} + +//////////////////////////////////////// + +String WiFiWebServer::headerName(int i) +{ + if (i < _headerKeysCount) + return _currentHeaders[i].key; + + return String(); +} + +//////////////////////////////////////// + +int WiFiWebServer::headers() +{ + return _headerKeysCount; +} + +//////////////////////////////////////// + +bool WiFiWebServer::hasHeader(const String& name) +{ + for (int i = 0; i < _headerKeysCount; ++i) + { + if ((_currentHeaders[i].key == name) && (_currentHeaders[i].value.length() > 0)) + return true; + } + + return false; +} + +//////////////////////////////////////// + +String WiFiWebServer::hostHeader() +{ + return _hostHeader; +} + +//////////////////////////////////////// + +void WiFiWebServer::onFileUpload(THandlerFunction fn) +{ + _fileUploadHandler = fn; +} + +//////////////////////////////////////// + +void WiFiWebServer::onNotFound(THandlerFunction fn) +{ + _notFoundHandler = fn; +} + +//////////////////////////////////////// + +void WiFiWebServer::_handleRequest() +{ + bool handled = false; + + if (!_currentHandler) + { + WS_LOGDEBUG(F("_handleRequest: request handler not found")); + } + else + { + WS_LOGDEBUG(F("_handleRequest handle")); + + handled = _currentHandler->handle(*this, _currentMethod, _currentUri); + + if (!handled) + { + WS_LOGDEBUG(F("_handleRequest: _handleRequest failed")); + } + else + { + WS_LOGDEBUG(F("_handleRequest OK")); + } + } + + if (!handled && _notFoundHandler) + { + WS_LOGDEBUG(F("_handleRequest: Call _notFoundHandler")); + + _notFoundHandler(); + handled = true; + } + + if (!handled) + { +#if (ESP32 || ESP8266) + using namespace mime_esp; +#else + using namespace mime; +#endif + + WS_LOGDEBUG(F("_handleRequest: Send Not found")); + + send(404, mimeTable[html].mimeType, String("Not found: ") + _currentUri); + handled = true; + } + + if (handled) + { + WS_LOGDEBUG(F("_handleRequest: _finalizeResponse")); + + _finalizeResponse(); + } + +#if WIFI_USE_PORTENTA_H7 + WS_LOGDEBUG(F("_handleRequest: Clear _currentUri")); + //_currentUri = String(); + WS_LOGDEBUG(F("_handleRequest: Done Clear _currentUri")); +#else + _responseHeaders = String(""); +#endif +} + +//////////////////////////////////////// + +void WiFiWebServer::_finalizeResponse() +{ + if (_chunked) + { + sendContent(String()); + } +} + +//////////////////////////////////////// + +String WiFiWebServer::_responseCodeToString(int code) +{ + switch (code) + { + case 100: + return F("Continue"); + + case 101: + return F("Switching Protocols"); + + case 200: + return F("OK"); + + case 201: + return F("Created"); + + case 202: + return F("Accepted"); + + case 203: + return F("Non-Authoritative Information"); + + case 204: + return F("No Content"); + + case 205: + return F("Reset Content"); + + case 206: + return F("Partial Content"); + + case 300: + return F("Multiple Choices"); + + case 301: + return F("Moved Permanently"); + + case 302: + return F("Found"); + + case 303: + return F("See Other"); + + case 304: + return F("Not Modified"); + + case 305: + return F("Use Proxy"); + + case 307: + return F("Temporary Redirect"); + + case 400: + return F("Bad Request"); + + case 401: + return F("Unauthorized"); + + case 402: + return F("Payment Required"); + + case 403: + return F("Forbidden"); + + case 404: + return F("Not Found"); + + case 405: + return F("Method Not Allowed"); + + case 406: + return F("Not Acceptable"); + + case 407: + return F("Proxy Authentication Required"); + + case 408: + return F("Request Time-out"); + + case 409: + return F("Conflict"); + + case 410: + return F("Gone"); + + case 411: + return F("Length Required"); + + case 412: + return F("Precondition Failed"); + + case 413: + return F("Request Entity Too Large"); + + case 414: + return F("Request-URI Too Large"); + + case 415: + return F("Unsupported Media Type"); + + case 416: + return F("Requested range not satisfiable"); + + case 417: + return F("Expectation Failed"); + + case 500: + return F("Internal Server Error"); + + case 501: + return F("Not Implemented"); + + case 502: + return F("Bad Gateway"); + + case 503: + return F("Service Unavailable"); + + case 504: + return F("Gateway Time-out"); + + case 505: + return F("HTTP Version not supported"); + + default: + return ""; + } +} + +//////////////////////////////////////// + +#endif // WiFiWebServer_Impl_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.h b/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.h new file mode 100644 index 000000000..d286d30bf --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.h @@ -0,0 +1,57 @@ +/********************************************************************************************************************************* + WiFiWebServer.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + **********************************************************************************************************************************/ + +#pragma once + +#ifndef WiFiWebServer_H +#define WiFiWebServer_H + +#ifndef WIFI_WEBSERVER_VERSION + #define WIFI_WEBSERVER_VERSION "WiFiWebServer v1.10.1" + + #define WIFI_WEBSERVER_VERSION_MAJOR 1 + #define WIFI_WEBSERVER_VERSION_MINOR 10 + #define WIFI_WEBSERVER_VERSION_PATCH 1 + + #define WIFI_WEBSERVER_VERSION_INT 1010001 +#endif + +#include "WiFiWebServer.hpp" +#include "WiFiWebServer-impl.h" +#include "Parsing-impl.h" + + +#endif // WiFiWebServer_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.hpp b/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.hpp new file mode 100644 index 000000000..cde6f93c2 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFiWebServer.hpp @@ -0,0 +1,706 @@ +/********************************************************************************************************************************* + WiFiWebServer.hpp - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + **********************************************************************************************************************************/ + +#pragma once + +#ifndef WiFiWebServer_HPP +#define WiFiWebServer_HPP + +#define USE_NEW_WEBSERVER_VERSION true + +//////////////////////////////////////// + +#if ( defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_H7_M4) ) + #if defined(WIFI_USE_PORTENTA_H7) + #undef WIFI_USE_PORTENTA_H7 + #endif + #define WIFI_USE_PORTENTA_H7 true + + #if defined(USE_NEW_WEBSERVER_VERSION) + #undef USE_NEW_WEBSERVER_VERSION + #endif + #define USE_NEW_WEBSERVER_VERSION false + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use mbed-portenta architecture for PORTENTA_H7 from WiFiWebServer + + #undef _WIFI_LOGLEVEL_ + // Somehow Portenta_H7 with latest core hangs if printing too much + #define _WIFI_LOGLEVEL_ 1 + #endif + +//////////////////////////////////////// + +#elif ( defined(ARDUINO_SAMD_ZERO) || defined(ARDUINO_SAMD_MKR1000) || defined(ARDUINO_SAMD_MKRWIFI1010) \ + || defined(ARDUINO_SAMD_NANO_33_IOT) || defined(ARDUINO_SAMD_MKRFox1200) || defined(ARDUINO_SAMD_MKRWAN1300) || defined(ARDUINO_SAMD_MKRWAN1310) \ + || defined(ARDUINO_SAMD_MKRGSM1400) || defined(ARDUINO_SAMD_MKRNB1500) || defined(ARDUINO_SAMD_MKRVIDOR4000) || defined(__SAMD21G18A__) \ + || defined(ARDUINO_SAMD_CIRCUITPLAYGROUND_EXPRESS) || defined(__SAMD21E18A__) || defined(__SAMD51__) || defined(__SAMD51J20A__) || defined(__SAMD51J19A__) \ + || defined(__SAMD51G19A__) || defined(__SAMD51P19A__) || defined(__SAMD21G18A__) ) + #if defined(WIFI_USE_SAMD) + #undef WIFI_USE_SAMD + #endif + #define WIFI_USE_SAMD true + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use SAMD architecture from WiFiWebServer + #endif + +//////////////////////////////////////// + +#elif (defined(NRF52840_FEATHER) || defined(NRF52832_FEATHER) || defined(NRF52_SERIES) || defined(ARDUINO_NRF52_ADAFRUIT) || \ + defined(NRF52840_FEATHER_SENSE) || defined(NRF52840_ITSYBITSY) || defined(NRF52840_CIRCUITPLAY) || \ + defined(NRF52840_CLUE) || defined(NRF52840_METRO) || defined(NRF52840_PCA10056) || defined(PARTICLE_XENON) || \ + defined(MDBT50Q_RX) || defined(NINA_B302_ublox) || defined(NINA_B112_ublox) ) + #if defined(WIFI_USE_NRF528XX) + #undef WIFI_USE_NRF528XX + #endif + #define WIFI_USE_NRF528XX true + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use nFR52 architecture from WiFiWebServer + #endif + + #include + +//////////////////////////////////////// + +#elif ( defined(ARDUINO_SAM_DUE) || defined(__SAM3X8E__) ) + #if defined(WIFI_USE_SAM_DUE) + #undef WIFI_USE_SAM_DUE + #endif + #define WIFI_USE_SAM_DUE true + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use SAM_DUE architecture from WiFiWebServer + #endif + +//////////////////////////////////////// + +#elif ( defined(STM32F0) || defined(STM32F1) || defined(STM32F2) || defined(STM32F3) ||defined(STM32F4) || defined(STM32F7) || \ + defined(STM32L0) || defined(STM32L1) || defined(STM32L4) || defined(STM32H7) ||defined(STM32G0) || defined(STM32G4) || \ + defined(STM32WB) || defined(STM32MP1) ) && !( defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_H7_M4) ) + #if (_WIFI_LOGLEVEL_ > 2) + #warning STM32F/L/H/G/WB/MP1 board selected + #endif + + #if defined(WIFI_USE_STM32) + #undef WIFI_USE_STM32 + #endif + #define WIFI_USE_STM32 true + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use STM32 architecture from WiFiWebServer + #endif + +//////////////////////////////////////// + +#elif ( defined(ARDUINO_NANO_RP2040_CONNECT) || defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_RASPBERRY_PI_PICO) || \ + defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) || defined(ARDUINO_GENERIC_RP2040) || defined(ARDUINO_RASPBERRY_PI_PICO_W) ) + + #if (_WIFI_LOGLEVEL_ > 2) + #if defined(ARDUINO_RASPBERRY_PI_PICO_W) + #warning RASPBERRY_PI_PICO_W board using CYW4343 WiFi selected + #else + #warning RP2040-based board selected + #endif + #endif + + #if defined(WIFI_USE_RP2040) + #undef WIFI_USE_RP2040 + #endif + #define WIFI_USE_RP2040 true + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use RP2040 architecture from WiFiWebServer + #endif + +//////////////////////////////////////// + +#elif ( defined(ARDUINO_AVR_UNO_WIFI_REV2) || defined(ARDUINO_AVR_NANO_EVERY) ) + + #include "ArduinoSTL.h" + + #if defined(WIFI_USE_MEGA_AVR) + #undef WIFI_USE_MEGA_AVR + #endif + #define WIFI_USE_MEGA_AVR true + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use Arduino megaAVR architecture from WiFiWebServer + #endif + +//////////////////////////////////////// + +#elif ( defined(__AVR_ATmega4809__) || defined(ARDUINO_AVR_ATmega4809) || defined(ARDUINO_AVR_ATmega4808) || \ + defined(ARDUINO_AVR_ATmega3209) || defined(ARDUINO_AVR_ATmega3208) || defined(ARDUINO_AVR_ATmega1609) || \ + defined(ARDUINO_AVR_ATmega1608) || defined(ARDUINO_AVR_ATmega809) || defined(ARDUINO_AVR_ATmega808) ) + + #if defined(WIFI_USE_MEGACOREX) + #undef WIFI_USE_MEGACOREX + #endif + #define WIFI_USE_MEGACOREX true + #error megaAVR architecture and MegaCoreX from WiFiWebServer not supported yet + +//////////////////////////////////////// + +#elif (ESP32) + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use ESP32 from WiFiWebServer + #endif + +//////////////////////////////////////// + +#elif (ESP8266) + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use ESP8266 from WiFiWebServer + #endif + +//////////////////////////////////////// + +#elif defined(CORE_TEENSY) + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use Teensy from WiFiWebServer + #endif + +//////////////////////////////////////// + +#else + + #warning Unknown or unsupported board + +#endif + +//////////////////////////////////////// + +// To support lambda function in class +#include + +#if !defined(USE_WIFI_NINA) + #define USE_WIFI_NINA true +#endif + +//////////////////////////////////////// + +// Default to use +#if !defined(USE_WIFI101_GENERIC) + #define USE_WIFI101_GENERIC true +#endif + +// Modify to use new WiFiNINA_Generic library to support boards besides Nano-33 IoT, MKRWiFi1010, Adafruit MetroM4, etc. +#if USE_WIFI_NINA + #include + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use WiFiNINA_Generic from WiFiWebServer + #endif +#elif USE_WIFI101 + #if USE_WIFI101_GENERIC + #include + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use WiFi101_Generic from WiFiWebServer + #endif + #else + #include + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use WiFi101 from WiFiWebServer + #endif + #endif + +#elif USE_WIFI_CUSTOM + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use Custom WiFi for WiFiWebServer + #endif +#else + #include + + #if (_WIFI_LOGLEVEL_ > 2) + #warning Use WiFi.h from WiFiWebServer + #endif +#endif + +//////////////////////////////////////// + +#include "utility/mimetable.h" +#include "utility/RingBuffer.h" + +//////////////////////////////////////// + +// KH, For PROGMEM commands +// ESP32/ESP8266 includes by default, and memccpy_P was already defined there +#if !(ESP32 || ESP8266 || defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_H7_M4)) + #include + #define memccpy_P(dest, src, c, n) memccpy((dest), (src), (c), (n)) +#endif + +//////////////////////////////////////// + +// Permit redefinition of SENDCONTENT_P_BUFFER_SZ in sketch, default is 4K, minimum is 256 bytes +#ifndef SENDCONTENT_P_BUFFER_SZ + #define SENDCONTENT_P_BUFFER_SZ 4096 + + #if (_WIFI_LOGLEVEL_ > 2) + #warning SENDCONTENT_P_BUFFER_SZ using default 4 Kbytes + #endif +#else + #if (SENDCONTENT_P_BUFFER_SZ < 256) + #undef SENDCONTENT_P_BUFFER_SZ + #define SENDCONTENT_P_BUFFER_SZ 256 + + #if (_WIFI_LOGLEVEL_ > 2) + #warning SENDCONTENT_P_BUFFER_SZ reset to min 256 bytes + #endif + #endif +#endif + +//////////////////////////////////////// + +#ifndef PGM_VOID_P + #define PGM_VOID_P const void * +#endif + +//////////////////////////////////////// + +enum HTTPMethod +{ + HTTP_ANY, + HTTP_GET, + HTTP_HEAD, + HTTP_POST, + HTTP_PUT, + HTTP_PATCH, + HTTP_DELETE, + HTTP_OPTIONS +}; + +enum HTTPUploadStatus +{ + UPLOAD_FILE_START, + UPLOAD_FILE_WRITE, + UPLOAD_FILE_END, + UPLOAD_FILE_ABORTED +}; + +enum HTTPClientStatus +{ + HC_NONE, + HC_WAIT_READ, + HC_WAIT_CLOSE +}; + +enum HTTPAuthMethod +{ + BASIC_AUTH, + DIGEST_AUTH +}; + +//////////////////////////////////////// + +#define HTTP_DOWNLOAD_UNIT_SIZE 1460 + +// Permit user to increase HTTP_UPLOAD_BUFLEN larger than default 2K +//#define HTTP_UPLOAD_BUFLEN 2048 +#if !defined(HTTP_UPLOAD_BUFLEN) + #define HTTP_UPLOAD_BUFLEN 2048 +#endif + +#define HTTP_MAX_DATA_WAIT 5000 //ms to wait for the client to send the request +#define HTTP_MAX_POST_WAIT 5000 //ms to wait for POST data to arrive +#define HTTP_MAX_SEND_WAIT 5000 //ms to wait for data chunk to be ACKed +#define HTTP_MAX_CLOSE_WAIT 2000 //ms to wait for the client to close the connection + +#define CONTENT_LENGTH_UNKNOWN ((size_t) -1) +#define CONTENT_LENGTH_NOT_SET ((size_t) -2) + +//////////////////////////////////////// + +#define RETURN_NEWLINE "\r\n" + +#include +#include + +typedef std::string WWString; + +//////////////////////////////////////// + +#include "Uri.h" + +//////////////////////////////////////// + +class WiFiWebServer; + +typedef struct +{ + HTTPUploadStatus status; + String filename; + String name; + String type; + size_t totalSize; // file size + size_t currentSize; // size of data currently in buf + size_t contentLength; // size of entire post request, file size + headers and other request data. + uint8_t buf[HTTP_UPLOAD_BUFLEN]; +} HTTPUpload; + +//////////////////////////////////////// + +#include "utility/RequestHandler.h" + +#if (ESP32 || ESP8266) + #include "FS.h" +#endif + +//////////////////////////////////////// +//////////////////////////////////////// + +class WiFiWebServer +{ + public: + +#if (ESP32 || ESP8266) + WiFiWebServer(IPAddress addr, int port = 80); +#endif + + WiFiWebServer(int port = 80); + virtual ~WiFiWebServer(); + + virtual void begin(); + +#if USE_NEW_WEBSERVER_VERSION + virtual void begin(uint16_t port); +#endif + + virtual void handleClient(); + + virtual void close(); + void stop(); + + bool authenticate(const char * username, const char * password); + void requestAuthentication(); + + typedef vl::Func THandlerFunction; + //typedef std::function THandlerFunction; + //typedef void (*THandlerFunction)(void); + + void on(const String &uri, THandlerFunction handler); + void on(const String &uri, HTTPMethod method, THandlerFunction fn); + void on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn); + void addHandler(RequestHandler* handler); + void onNotFound(THandlerFunction fn); //called when handler is not assigned + void onFileUpload(THandlerFunction fn); //handle file uploads + + //////////////////////////////////////// + + inline String uri() + { + return _currentUri; + } + + //////////////////////////////////////// + + inline HTTPMethod method() + { + return _currentMethod; + } + + //////////////////////////////////////// + + virtual inline WiFiClient client() + { + return _currentClient; + } + + //////////////////////////////////////// + +#if USE_NEW_WEBSERVER_VERSION + inline HTTPUpload& upload() + { + return *_currentUpload; + } +#else + inline HTTPUpload& upload() + { + return _currentUpload; + } +#endif + + //////////////////////////////////////// + + String pathArg(unsigned int i); // get request path argument by number + + String arg(const String& name); // get request argument value by name + String arg(int i); // get request argument value by number + String argName(int i); // get request argument name by number + + int args(); // get arguments count + bool hasArg(const String& name); // check if argument exists + void collectHeaders(const char* headerKeys[], const size_t headerKeysCount); // set the request headers to collect + String header(const String& name); // get request header value by name + String header(int i); // get request header value by number + String headerName(int i); // get request header name by number + int headers(); // get header count + bool hasHeader(const String& name); // check if header exists + + //////////////////////////////////////// + + inline int clientContentLength() + { + return _clientContentLength; + } + + //////////////////////////////////////// + + String hostHeader(); // get request host header if available or empty String if not + + // send response to the client + // code - HTTP response code, can be 200 or 404 + // content_type - HTTP content type, like "text/plain" or "image/png" + // content - actual content body + void send(int code, const char* content_type = NULL, const String& content = String("")); + void send(int code, char* content_type, const String& content); + void send(int code, const String& content_type, const String& content); + + void send(int code, char* content_type, const String& content, size_t contentLength); + void send(int code, const char* content_type, const char* content); + void send(int code, const char* content_type, const char* content, size_t contentLength); + + //////////////////////////////////////// + + inline void enableDelay(bool value) + { + _nullDelay = value; + } + + //////////////////////////////////////// + + inline void enableCORS(bool value = true) + { + _corsEnabled = value; + } + + //////////////////////////////////////// + + inline void enableCrossOrigin(bool value = true) + { + enableCORS(value); + } + + //////////////////////////////////////// + + void setContentLength(size_t contentLength); + void sendHeader(const String& name, const String& value, bool first = false); + void sendContent(const String& content); + void sendContent(const String& content, size_t contentLength); + + // New + void sendContent(const char* content, size_t contentLength); + ////// + + // KH, Restore PROGMEM commands + void send_P(int code, PGM_P content_type, PGM_P content); + void send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength); + + void sendContent_P(PGM_P content); + void sendContent_P(PGM_P content, size_t contentLength); + ////// + + static String urlDecode(const String& text); + + //////////////////////////////////////// + +#if !(ESP32 || ESP8266) + template size_t streamFile(T &file, const String& contentType) + { + using namespace mime; + setContentLength(file.size()); + + if (String(file.name()).endsWith(mimeTable[gz].endsWith) && contentType != mimeTable[gz].mimeType && contentType != mimeTable[none].mimeType) + { + sendHeader("Content-Encoding", "gzip"); + } + + send(200, contentType, ""); + + return _currentClient.write(file); + } + + //////////////////////////////////////// + +#else + + //////////////////////////////////////// + + void serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_header = NULL ); // serve static pages from file system + + // Handle a GET request by sending a response header and stream file content to response body + //template + //size_t streamFile(T &file, const String& contentType) + //{ + // return streamFile(file, contentType, HTTP_GET); + //} + + // Implement GET and HEAD requests for files. + // Stream body on HTTP_GET but not on HTTP_HEAD requests. + template + size_t streamFile(T &file, const String& contentType, const int code = 200) + { + _streamFileCore(file.size(), file.name(), contentType, code); + + return _currentClient.write(file); + } + + //////////////////////////////////////// + +#endif + + protected: + + //////////////////////////////////////// + + virtual size_t _currentClientWrite(const char* buffer, size_t length) + { + return _currentClient.write( buffer, length ); + } + + //////////////////////////////////////// + +#if (ESP32 || ESP8266) + virtual size_t _currentClientWrite_P(PGM_P buffer, size_t length) + { + return _currentClient.write_P( buffer, length ); + } +#endif + + //////////////////////////////////////// + + void _addRequestHandler(RequestHandler* handler); + void _handleRequest(); + void _finalizeResponse(); + bool _parseRequest(WiFiClient& client); + +#if USE_NEW_WEBSERVER_VERSION + void _parseArguments(const String& data); + int _parseArgumentsPrivate(const String& data, vl::Func handler); + bool _parseForm(WiFiClient& client, const String& boundary, uint32_t len); +#else + void _parseArguments(const String& data); + bool _parseForm(WiFiClient& client, const String& boundary, uint32_t len); +#endif + + static String _responseCodeToString(int code); + bool _parseFormUploadAborted(); + void _uploadWriteByte(uint8_t b); + int _uploadReadByte(WiFiClient& client); + void _prepareHeader(String& response, int code, const char* content_type, size_t contentLength); + void _prepareHeader(WWString& response, int code, const char* content_type, size_t contentLength); + bool _collectHeader(const char* headerName, const char* headerValue); + +#if (ESP32 || ESP8266) + void _streamFileCore(const size_t fileSize, const String & fileName, const String & contentType, const int code = 200); + + //////////////////////////////////////// + + template + size_t _customClientWrite(T &file) + { + char buffer[256]; + size_t contentLength = 0; + size_t bytesRead = 0; + + // read up to sizeof(buffer) bytes + while ((bytesRead = file.readBytes(buffer, sizeof(buffer))) > 0) + { + _currentClient.write(buffer, bytesRead); + contentLength += bytesRead; + } + + return contentLength; + } + + //////////////////////////////////////// + +#endif + + struct RequestArgument + { + String key; + String value; + }; + + bool _corsEnabled; + + WiFiServer _server; + + WiFiClient _currentClient; + HTTPMethod _currentMethod; + String _currentUri; + uint8_t _currentVersion; + HTTPClientStatus _currentStatus; + unsigned long _statusChange; + + bool _nullDelay; + + RequestHandler* _currentHandler = nullptr; + RequestHandler* _firstHandler = nullptr; + RequestHandler* _lastHandler = nullptr; + THandlerFunction _notFoundHandler; + THandlerFunction _fileUploadHandler; + + int _currentArgCount; + RequestArgument* _currentArgs = nullptr; + +#if USE_NEW_WEBSERVER_VERSION + HTTPUpload* _currentUpload = nullptr; + int _postArgsLen; + RequestArgument* _postArgs = nullptr; +#else + HTTPUpload _currentUpload; +#endif + + int _headerKeysCount; + RequestArgument* _currentHeaders = nullptr; + size_t _contentLength; + int _clientContentLength; // "Content-Length" from header of incoming POST or GET request + String _responseHeaders; + String _hostHeader; + bool _chunked; +}; + +#endif // WiFiWebServer_HPP diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.cpp b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.cpp new file mode 100644 index 000000000..55bcc61f4 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.cpp @@ -0,0 +1,1061 @@ +/**************************************************************************************************************************** + WiFi_HttpClient.cpp - Dead simple HTTP WebClient. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +// Class to simplify HTTP fetching on Arduino +// (c) Copyright 2010-2011 MCQN Ltd +// Released under Apache License, version 2.0 + +#define _WIFI_LOGLEVEL_ 0 + + +#include "WiFi_HTTPClient/WiFi_HttpClient.h" +#include "libb64/base64.h" + +#include "utility/WiFiDebug.h" + +//////////////////////////////////////// + +// Initialize constants +const char* WiFiHttpClient::kUserAgent = "Arduino/2.2.0"; +const char* WiFiHttpClient::kContentLengthPrefix = HTTP_HEADER_CONTENT_LENGTH ": "; +const char* WiFiHttpClient::kTransferEncodingChunked = HTTP_HEADER_TRANSFER_ENCODING ": " HTTP_HEADER_VALUE_CHUNKED; + +//////////////////////////////////////// + +WiFiHttpClient::WiFiHttpClient(Client& aClient, const char* aServerName, uint16_t aServerPort) + : iClient(&aClient), iServerName(aServerName), iServerAddress(), iServerPort(aServerPort), + iConnectionClose(true), iSendDefaultRequestHeaders(true) +{ + resetState(); +} + +//////////////////////////////////////// + +WiFiHttpClient::WiFiHttpClient(Client& aClient, const String& aServerName, uint16_t aServerPort) + : WiFiHttpClient(aClient, aServerName.c_str(), aServerPort) +{ +} + +//////////////////////////////////////// + +WiFiHttpClient::WiFiHttpClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort) + : iClient(&aClient), iServerName(NULL), iServerAddress(aServerAddress), iServerPort(aServerPort), + iConnectionClose(true), iSendDefaultRequestHeaders(true) +{ + resetState(); +} + +//////////////////////////////////////// + +void WiFiHttpClient::resetState() +{ + iState = eIdle; + iStatusCode = 0; + iContentLength = kNoContentLengthHeader; + + iBodyLengthConsumed = 0; + iContentLengthPtr = kContentLengthPrefix; + iTransferEncodingChunkedPtr = kTransferEncodingChunked; + + iIsChunked = false; + iChunkLength = 0; + iHttpResponseTimeout = kHttpResponseTimeout; +} + +//////////////////////////////////////// + +void WiFiHttpClient::stop() +{ + iClient->stop(); + resetState(); +} + +//////////////////////////////////////// + +void WiFiHttpClient::connectionKeepAlive() +{ + iConnectionClose = false; +} + +//////////////////////////////////////// + +void WiFiHttpClient::noDefaultRequestHeaders() +{ + iSendDefaultRequestHeaders = false; +} + +//////////////////////////////////////// + +void WiFiHttpClient::beginRequest() +{ + iState = eRequestStarted; +} + +//////////////////////////////////////// + +int WiFiHttpClient::startRequest(const char* aURLPath, const char* aHttpMethod, + const char* aContentType, int aContentLength, const byte aBody[]) +{ + if (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk) + { + flushClientRx(); + + resetState(); + } + + tHttpState initialState = iState; + + if ((eIdle != iState) && (eRequestStarted != iState)) + { + return HTTP_ERROR_API; + } + + if (iConnectionClose || !iClient->connected()) + { + if (iServerName) + { + if (! ( iClient->connect(iServerName, iServerPort) > 0) ) + { + WS_LOGDEBUG(F("WiFiHttpClient::startRequest: Connection failed")); + + return HTTP_ERROR_CONNECTION_FAILED; + } + } + else + { + if (! ( iClient->connect(iServerAddress, iServerPort) > 0) ) + { + WS_LOGDEBUG(F("WiFiHttpClient::startRequest: Connection failed")); + + return HTTP_ERROR_CONNECTION_FAILED; + } + } + } + else + { + WS_LOGDEBUG(F("WiFiHttpClient::startRequest: Connection already open")); + } + + // Now we're connected, send the first part of the request + int ret = sendInitialHeaders(aURLPath, aHttpMethod); + + if (HTTP_SUCCESS == ret) + { + if (aContentType) + { + sendHeader(HTTP_HEADER_CONTENT_TYPE, aContentType); + } + + if (aContentLength > 0) + { + sendHeader(HTTP_HEADER_CONTENT_LENGTH, aContentLength); + } + + bool hasBody = (aBody && aContentLength > 0); + + if (initialState == eIdle || hasBody) + { + // This was a simple version of the API, so terminate the headers now + finishHeaders(); + } + + // else we'll call it in endRequest or in the first call to print, etc. + + if (hasBody) + { + write(aBody, aContentLength); + } + } + + return ret; +} + +//////////////////////////////////////// + +int WiFiHttpClient::sendInitialHeaders(const char* aURLPath, const char* aHttpMethod) +{ + WS_LOGDEBUG(F("WiFiHttpClient::startRequest: Connected")); + + // Send the HTTP command, i.e. "GET /somepath/ HTTP/1.0" + iClient->print(aHttpMethod); + iClient->print(" "); + + iClient->print(aURLPath); + iClient->println(" HTTP/1.1"); + + if (iSendDefaultRequestHeaders) + { + // The host header, if required + if (iServerName) + { + iClient->print("Host: "); + iClient->print(iServerName); + + if (iServerPort != kHttpPort) + { + iClient->print(":"); + iClient->print(iServerPort); + } + + iClient->println(); + } + + // And user-agent string + sendHeader(HTTP_HEADER_USER_AGENT, kUserAgent); + ////// + } + + if (iConnectionClose) + { + // Tell the server to + // close this connection after we're done + sendHeader(HTTP_HEADER_CONNECTION, "close"); + } + + // Everything has gone well + iState = eRequestStarted; + + return HTTP_SUCCESS; +} + +//////////////////////////////////////// + +void WiFiHttpClient::sendHeader(const char* aHeader) +{ + iClient->println(aHeader); +} + +//////////////////////////////////////// + +void WiFiHttpClient::sendHeader(const char* aHeaderName, const char* aHeaderValue) +{ + iClient->print(aHeaderName); + iClient->print(": "); + iClient->println(aHeaderValue); +} + +//////////////////////////////////////// + +void WiFiHttpClient::sendHeader(const char* aHeaderName, const int aHeaderValue) +{ + iClient->print(aHeaderName); + iClient->print(": "); + iClient->println(aHeaderValue); +} + +//////////////////////////////////////// + +void WiFiHttpClient::sendBasicAuth(const char* aUser, const char* aPassword) +{ + // Send the initial part of this header line + iClient->print("Authorization: Basic "); + // Now Base64 encode "aUser:aPassword" and send that + // This seems trickier than it should be but it's mostly to avoid either + // (a) some arbitrarily sized buffer which hopes to be big enough, or + // (b) allocating and freeing memory + // ...so we'll loop through 3 bytes at a time, outputting the results as we + // go. + // In Base64, each 3 bytes of unencoded data become 4 bytes of encoded data + unsigned char input[3]; + unsigned char output[5]; // Leave space for a '\0' terminator so we can easily print + + int userLen = strlen(aUser); + int passwordLen = strlen(aPassword); + int inputOffset = 0; + + for (int i = 0; i < (userLen + 1 + passwordLen); i++) + { + // Copy the relevant input byte into the input + if (i < userLen) + { + input[inputOffset++] = aUser[i]; + } + else if (i == userLen) + { + input[inputOffset++] = ':'; + } + else + { + input[inputOffset++] = aPassword[i - (userLen + 1)]; + } + + // See if we've got a chunk to encode + if ( (inputOffset == 3) || (i == userLen + passwordLen) ) + { + // We've either got to a 3-byte boundary, or we've reached then end + base64_encode(input, inputOffset, output, 4); + // NUL-terminate the output string + output[4] = '\0'; + // And write it out + iClient->print((char*)output); + // FIXME We might want to fill output with '=' characters if base64_encode doesn't + // FIXME do it for us when we're encoding the final chunk + inputOffset = 0; + } + } + + // And end the header we've sent + iClient->println(); +} + +//////////////////////////////////////// + +void WiFiHttpClient::finishHeaders() +{ + iClient->println(); + iState = eRequestSent; +} + +//////////////////////////////////////// + +void WiFiHttpClient::flushClientRx() +{ + while (iClient->available()) + { + iClient->read(); + } +} + +//////////////////////////////////////// + +void WiFiHttpClient::endRequest() +{ + beginBody(); +} + +//////////////////////////////////////// + +void WiFiHttpClient::beginBody() +{ + if (iState < eRequestSent) + { + // We still need to finish off the headers + finishHeaders(); + } + + // else the end of headers has already been sent, so nothing to do here +} + +//////////////////////////////////////// + +int WiFiHttpClient::get(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_GET); +} + +//////////////////////////////////////// + +int WiFiHttpClient::get(const String& aURLPath) +{ + return get(aURLPath.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::post(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_POST); +} + +//////////////////////////////////////// + +int WiFiHttpClient::post(const String& aURLPath) +{ + return post(aURLPath.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::post(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return post(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::post(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return post(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::post(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_POST, aContentType, aContentLength, aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::put(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_PUT); +} + +//////////////////////////////////////// + +int WiFiHttpClient::put(const String& aURLPath) +{ + return put(aURLPath.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::put(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return put(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::put(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return put(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::put(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_PUT, aContentType, aContentLength, aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::patch(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_PATCH); +} + +//////////////////////////////////////// + +int WiFiHttpClient::patch(const String& aURLPath) +{ + return patch(aURLPath.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::patch(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return patch(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::patch(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return patch(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::patch(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_PATCH, aContentType, aContentLength, aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::del(const char* aURLPath) +{ + return startRequest(aURLPath, HTTP_METHOD_DELETE); +} + +//////////////////////////////////////// + +int WiFiHttpClient::del(const String& aURLPath) +{ + return del(aURLPath.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::del(const char* aURLPath, const char* aContentType, const char* aBody) +{ + return del(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::del(const String& aURLPath, const String& aContentType, const String& aBody) +{ + return del(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); +} + +//////////////////////////////////////// + +int WiFiHttpClient::del(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) +{ + return startRequest(aURLPath, HTTP_METHOD_DELETE, aContentType, aContentLength, aBody); +} + +//////////////////////////////////////// + +int WiFiHttpClient::responseStatusCode() +{ + if (iState < eRequestSent) + { + return HTTP_ERROR_API; + } + + // The first line will be of the form Status-Line: + // HTTP-Version SP Status-Code SP Reason-Phrase CRLF + // Where HTTP-Version is of the form: + // HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT + + int c = '\0'; + + do + { + // Make sure the status code is reset, and likewise the state. This + // lets us easily cope with 1xx informational responses by just + // ignoring them really, and reading the next line for a proper response + iStatusCode = 0; + iState = eRequestSent; + + unsigned long timeoutStart = millis(); + + // Psuedo-regexp we're expecting before the status-code + const char* statusPrefix = "HTTP/*.* "; + const char* statusPtr = statusPrefix; + + // Whilst we haven't timed out & haven't reached the end of the headers + while ((c != '\n') && ( (millis() - timeoutStart) < iHttpResponseTimeout )) + { + if (available()) + { + c = read(); + + // KH test + WS_LOGDEBUG1(F("WiFiHttpClient::startRequest: c ="), String(c)); + ////// + + if (c != -1) + { + switch (iState) + { + case eRequestSent: + + // We haven't reached the status code yet + if ( (*statusPtr == '*') || (*statusPtr == c) ) + { + // This character matches, just move along + statusPtr++; + + if (*statusPtr == '\0') + { + // We've reached the end of the prefix + iState = eReadingStatusCode; + } + } + else + { + return HTTP_ERROR_INVALID_RESPONSE; + } + + break; + + case eReadingStatusCode: + if (isdigit(c)) + { + // This assumes we won't get more than the 3 digits we + // want + iStatusCode = iStatusCode * 10 + (c - '0'); + } + else + { + // We've reached the end of the status code + // We could sanity check it here or double-check for ' ' + // rather than anything else, but let's be lenient + iState = eStatusCodeRead; + } + + break; + + case eStatusCodeRead: + // We're just waiting for the end of the line now + break; + + default: + break; + }; + + // We read something, reset the timeout counter + timeoutStart = millis(); + } + } + else + { + // We haven't got any data, so let's pause to allow some to + // arrive + delay(kHttpWaitForDataDelay); + } + } + + if ( (c == '\n') && (iStatusCode < 200 && iStatusCode != 101) ) + { + // We've reached the end of an informational status line + c = '\0'; // Clear c so we'll go back into the data reading loop + } + } + + // If we've read a status code successfully but it's informational (1xx) + // loop back to the start + while ( (iState == eStatusCodeRead) && (iStatusCode < 200 && iStatusCode != 101) ); + + if ( (c == '\n') && (iState == eStatusCodeRead) ) + { + // We've read the status-line successfully + return iStatusCode; + } + else if (c != '\n') + { + // We must've timed out before we reached the end of the line + return HTTP_ERROR_TIMED_OUT; + } + else + { + // This wasn't a properly formed status line, or at least not one we + // could understand + return HTTP_ERROR_INVALID_RESPONSE; + } +} + +//////////////////////////////////////// + +int WiFiHttpClient::skipResponseHeaders() +{ + // Just keep reading until we finish reading the headers or time out + unsigned long timeoutStart = millis(); + + // Whilst we haven't timed out & haven't reached the end of the headers + while ((!endOfHeadersReached()) && ( (millis() - timeoutStart) < iHttpResponseTimeout )) + { + if (available()) + { + (void)readHeader(); + // We read something, reset the timeout counter + timeoutStart = millis(); + } + else + { + // We haven't got any data, so let's pause to allow some to + // arrive + delay(kHttpWaitForDataDelay); + } + } + + if (endOfHeadersReached()) + { + // Success + return HTTP_SUCCESS; + } + else + { + // We must've timed out + return HTTP_ERROR_TIMED_OUT; + } +} + +//////////////////////////////////////// + +bool WiFiHttpClient::endOfHeadersReached() +{ + return (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk); +}; + +//////////////////////////////////////// + +int WiFiHttpClient::contentLength() +{ + // skip the response headers, if they haven't been read already + if (!endOfHeadersReached()) + { + skipResponseHeaders(); + } + + return iContentLength; +} + +//////////////////////////////////////// + +String WiFiHttpClient::responseBody() +{ + int bodyLength = contentLength(); + String response; + + WS_LOGDEBUG1(F("WiFiHttpClient::responseBody => bodyLength ="), String(bodyLength)); + + if (bodyLength > 0) + { + // try to reserve bodyLength bytes + if (response.reserve(bodyLength) == 0) + { + // String reserve failed + return String((const char*)NULL); + } + } + + // keep on timedRead'ing, until: + // - we have a content length: body length equals consumed or no bytes + // available + // - no content length: no bytes are available + while (iBodyLengthConsumed != bodyLength) + { + // KH test + int c = timedRead(); + //int c = iClient->read(); + + WS_LOGDEBUG1(F("WiFiHttpClient::responseBody => c ="), String(c)); + ////// + + if (c == -1) + { + // read timed out, done + break; + } + + if (!response.concat((char)c)) + { + // adding char failed + return String((const char*)NULL); + } + } + + if (bodyLength > 0 && (unsigned int)bodyLength != response.length()) + { + // failure, we did not read in reponse content length bytes + return String((const char*)NULL); + } + + return response; +} + +//////////////////////////////////////// + +bool WiFiHttpClient::endOfBodyReached() +{ + if (endOfHeadersReached() && (contentLength() != kNoContentLengthHeader)) + { + // We've got to the body and we know how long it will be + return (iBodyLengthConsumed >= contentLength()); + } + + return false; +} + +//////////////////////////////////////// + +int WiFiHttpClient::available() +{ + if (iState == eReadingChunkLength) + { + while (iClient->available()) + { + char c = iClient->read(); + + if (c == '\n') + { + iState = eReadingBodyChunk; + break; + } + else if (c == '\r') + { + // no-op + } + else if (isHexadecimalDigit(c)) + { + char digit[2] = {c, '\0'}; + + iChunkLength = (iChunkLength * 16) + strtol(digit, NULL, 16); + } + } + } + + if (iState == eReadingBodyChunk && iChunkLength == 0) + { + iState = eReadingChunkLength; + } + + if (iState == eReadingChunkLength) + { + return 0; + } + + int clientAvailable = iClient->available(); + + if (iState == eReadingBodyChunk) + { + return min(clientAvailable, iChunkLength); + } + else + { + return clientAvailable; + } +} + +//////////////////////////////////////// + +int WiFiHttpClient::read() +{ + if (iIsChunked && !available()) + { + return -1; + } + + int ret = iClient->read(); + + if (ret >= 0) + { + if (endOfHeadersReached() && iContentLength > 0) + { + // We're outputting the body now and we've seen a Content-Length header + // So keep track of how many bytes are left + iBodyLengthConsumed++; + } + + if (iState == eReadingBodyChunk) + { + iChunkLength--; + + if (iChunkLength == 0) + { + iState = eReadingChunkLength; + } + } + } + + return ret; +} + +//////////////////////////////////////// + +bool WiFiHttpClient::headerAvailable() +{ + // clear the currently store header line + iHeaderLine = ""; + + while (!endOfHeadersReached()) + { + // read a byte from the header + int c = readHeader(); + + if (c == '\r' || c == '\n') + { + if (iHeaderLine.length()) + { + // end of the line, all done + break; + } + else + { + // ignore any CR or LF characters + continue; + } + } + + // append byte to header line + iHeaderLine += (char)c; + } + + return (iHeaderLine.length() > 0); +} + +//////////////////////////////////////// + +String WiFiHttpClient::readHeaderName() +{ + int colonIndex = iHeaderLine.indexOf(':'); + + if (colonIndex == -1) + { + return ""; + } + + return iHeaderLine.substring(0, colonIndex); +} + +//////////////////////////////////////// + +String WiFiHttpClient::readHeaderValue() +{ + int colonIndex = iHeaderLine.indexOf(':'); + int startIndex = colonIndex + 1; + + if (colonIndex == -1) + { + return ""; + } + + // trim any leading whitespace + while (startIndex < (int)iHeaderLine.length() && isSpace(iHeaderLine[startIndex])) + { + startIndex++; + } + + return iHeaderLine.substring(startIndex); +} + +//////////////////////////////////////// + +int WiFiHttpClient::read(uint8_t *buf, size_t size) +{ + int ret = iClient->read(buf, size); + + if (endOfHeadersReached() && iContentLength > 0) + { + // We're outputting the body now and we've seen a Content-Length header + // So keep track of how many bytes are left + if (ret >= 0) + { + iBodyLengthConsumed += ret; + } + } + + return ret; +} + +//////////////////////////////////////// + +int WiFiHttpClient::readHeader() +{ + char c = read(); + + if (endOfHeadersReached()) + { + // We've passed the headers, but rather than return an error, we'll just + // act as a slightly less efficient version of read() + return c; + } + + // Whilst reading out the headers to whoever wants them, we'll keep an + // eye out for the "Content-Length" header + switch (iState) + { + case eStatusCodeRead: + + // We're at the start of a line, or somewhere in the middle of reading + // the Content-Length prefix + if (*iContentLengthPtr == c) + { + // This character matches, just move along + iContentLengthPtr++; + + if (*iContentLengthPtr == '\0') + { + // We've reached the end of the prefix + iState = eReadingContentLength; + // Just in case we get multiple Content-Length headers, this + // will ensure we just get the value of the last one + iContentLength = 0; + iBodyLengthConsumed = 0; + } + } + else if (*iTransferEncodingChunkedPtr == c) + { + // This character matches, just move along + iTransferEncodingChunkedPtr++; + + if (*iTransferEncodingChunkedPtr == '\0') + { + // We've reached the end of the Transfer Encoding: chunked header + iIsChunked = true; + iState = eSkipToEndOfHeader; + } + } + else if (((iContentLengthPtr == kContentLengthPrefix) && (iTransferEncodingChunkedPtr == kTransferEncodingChunked)) + && (c == '\r')) + { + // We've found a '\r' at the start of a line, so this is probably + // the end of the headers + iState = eLineStartingCRFound; + } + else + { + // This isn't the Content-Length or Transfer Encoding chunked header, skip to the end of the line + iState = eSkipToEndOfHeader; + } + + break; + + case eReadingContentLength: + if (isdigit(c)) + { + iContentLength = iContentLength * 10 + (c - '0'); + } + else + { + // We've reached the end of the content length + // We could sanity check it here or double-check for "\r\n" + // rather than anything else, but let's be lenient + iState = eSkipToEndOfHeader; + } + + break; + + case eLineStartingCRFound: + if (c == '\n') + { + if (iIsChunked) + { + iState = eReadingChunkLength; + iChunkLength = 0; + } + else + { + iState = eReadingBody; + } + } + + break; + + default: + // We're just waiting for the end of the line now + break; + }; + + if ( (c == '\n') && !endOfHeadersReached() ) + { + // We've got to the end of this line, start processing again + iState = eStatusCodeRead; + iContentLengthPtr = kContentLengthPrefix; + iTransferEncodingChunkedPtr = kTransferEncodingChunked; + } + + // And return the character read to whoever wants it + return c; +} diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.h b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.h new file mode 100644 index 000000000..00541a30e --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_HttpClient.h @@ -0,0 +1,576 @@ +/**************************************************************************************************************************** + WiFi_HttpClient.h - Dead simple HTTP WebClient. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +// Class to simplify HTTP fetching on Arduino +// (c) Copyright MCQN Ltd. 2010-2012 +// Released under Apache License, version 2.0 + +#pragma once + +#ifndef WiFi_HttpClient_H +#define WiFi_HttpClient_H + +#include +#include +#include "Client.h" + +#include "utility/WiFiDebug.h" + +//////////////////////////////////////// + +static const int HTTP_SUCCESS = 0; + +// The end of the headers has been reached. This consumes the '\n' +// Could not connect to the server +static const int HTTP_ERROR_CONNECTION_FAILED = -1; + +// This call was made when the WiFiHttpClient class wasn't expecting it +// to be called. Usually indicates your code is using the class +// incorrectly +static const int HTTP_ERROR_API = -2; + +// Spent too long waiting for a reply +static const int HTTP_ERROR_TIMED_OUT = -3; + +// The response from the server is invalid, is it definitely an HTTP +// server? +static const int HTTP_ERROR_INVALID_RESPONSE = -4; + +//////////////////////////////////////// + +// Define some of the common methods and headers here +// That lets other code reuse them without having to declare another copy +// of them, so saves code space and RAM +#define HTTP_METHOD_GET "GET" +#define HTTP_METHOD_POST "POST" +#define HTTP_METHOD_PUT "PUT" +#define HTTP_METHOD_PATCH "PATCH" +#define HTTP_METHOD_DELETE "DELETE" +#define HTTP_HEADER_CONTENT_LENGTH "Content-Length" +#define HTTP_HEADER_CONTENT_TYPE "Content-Type" +#define HTTP_HEADER_CONNECTION "Connection" +#define HTTP_HEADER_TRANSFER_ENCODING "Transfer-Encoding" +#define HTTP_HEADER_USER_AGENT "User-Agent" +#define HTTP_HEADER_VALUE_CHUNKED "chunked" + +//////////////////////////////////////// + +// Number of milliseconds that we wait each time there isn't any data +// available to be read (during status code and header processing) +#define kHttpWaitForDataDelay 1000L + +// Number of milliseconds that we'll wait in total without receiveing any +// data before returning HTTP_ERROR_TIMED_OUT (during status code and header +// processing) +#define kHttpResponseTimeout 30000L + +//////////////////////////////////////// + +class WiFiHttpClient : public Client +{ + public: + static const int kNoContentLengthHeader = -1; + static const int kHttpPort = 80; + static const char* kUserAgent; + + // FIXME Write longer API request, using port and user-agent, example + // FIXME Update tempToPachube example to calculate Content-Length correctly + + WiFiHttpClient(Client& aClient, const char* aServerName, uint16_t aServerPort = kHttpPort); + WiFiHttpClient(Client& aClient, const String& aServerName, uint16_t aServerPort = kHttpPort); + WiFiHttpClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort = kHttpPort); + + /** Start a more complex request. + Use this when you need to send additional headers in the request, + but you will also need to call endRequest() when you are finished. + */ + void beginRequest(); + + /** End a more complex request. + Use this when you need to have sent additional headers in the request, + but you will also need to call beginRequest() at the start. + */ + void endRequest(); + + /** Start the body of a more complex request. + Use this when you need to send the body after additional headers + in the request, but can optionally call endRequest() when + you are finished. + */ + void beginBody(); + + /** Connect to the server and start to send a GET request. + @param aURLPath Url to request + @return 0 if successful, else error + */ + int get(const char* aURLPath); + int get(const String& aURLPath); + + /** Connect to the server and start to send a POST request. + @param aURLPath Url to request + @return 0 if successful, else error + */ + int post(const char* aURLPath); + int post(const String& aURLPath); + + /** Connect to the server and send a POST request + with body and content type + @param aURLPath Url to request + @param aContentType Content type of request body + @param aBody Body of the request + @return 0 if successful, else error + */ + int post(const char* aURLPath, const char* aContentType, const char* aBody); + int post(const String& aURLPath, const String& aContentType, const String& aBody); + int post(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); + + /** Connect to the server and start to send a PUT request. + @param aURLPath Url to request + @return 0 if successful, else error + */ + int put(const char* aURLPath); + int put(const String& aURLPath); + + /** Connect to the server and send a PUT request + with body and content type + @param aURLPath Url to request + @param aContentType Content type of request body + @param aBody Body of the request + @return 0 if successful, else error + */ + int put(const char* aURLPath, const char* aContentType, const char* aBody); + int put(const String& aURLPath, const String& aContentType, const String& aBody); + int put(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); + + /** Connect to the server and start to send a PATCH request. + @param aURLPath Url to request + @return 0 if successful, else error + */ + int patch(const char* aURLPath); + int patch(const String& aURLPath); + + /** Connect to the server and send a PATCH request + with body and content type + @param aURLPath Url to request + @param aContentType Content type of request body + @param aBody Body of the request + @return 0 if successful, else error + */ + int patch(const char* aURLPath, const char* aContentType, const char* aBody); + int patch(const String& aURLPath, const String& aContentType, const String& aBody); + int patch(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); + + /** Connect to the server and start to send a DELETE request. + @param aURLPath Url to request + @return 0 if successful, else error + */ + int del(const char* aURLPath); + int del(const String& aURLPath); + + /** Connect to the server and send a DELETE request + with body and content type + @param aURLPath Url to request + @param aContentType Content type of request body + @param aBody Body of the request + @return 0 if successful, else error + */ + int del(const char* aURLPath, const char* aContentType, const char* aBody); + int del(const String& aURLPath, const String& aContentType, const String& aBody); + int del(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); + + /** Connect to the server and start to send the request. + If a body is provided, the entire request (including headers and body) will be sent + @param aURLPath Url to request + @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. + @param aContentType Content type of request body (optional) + @param aContentLength Length of request body (optional) + @param aBody Body of request (optional) + @return 0 if successful, else error + */ + int startRequest(const char* aURLPath, + const char* aHttpMethod, + const char* aContentType = NULL, + int aContentLength = -1, + const byte aBody[] = NULL); + + /** Send an additional header line. This can only be called in between the + calls to beginRequest and endRequest. + @param aHeader Header line to send, in its entirety (but without the + trailing CRLF. E.g. "Authorization: Basic YQDDCAIGES" + */ + void sendHeader(const char* aHeader); + + //////////////////////////////////////// + + void sendHeader(const String& aHeader) + { + sendHeader(aHeader.c_str()); + } + + //////////////////////////////////////// + + /** Send an additional header line. This is an alternate form of + sendHeader() which takes the header name and content as separate strings. + The call will add the ": " to separate the header, so for example, to + send a XXXXXX header call sendHeader("XXXXX", "Something") + @param aHeaderName Type of header being sent + @param aHeaderValue Value for that header + */ + void sendHeader(const char* aHeaderName, const char* aHeaderValue); + + //////////////////////////////////////// + + void sendHeader(const String& aHeaderName, const String& aHeaderValue) + { + sendHeader(aHeaderName.c_str(), aHeaderValue.c_str()); + } + + //////////////////////////////////////// + + /** Send an additional header line. This is an alternate form of + sendHeader() which takes the header name and content separately but where + the value is provided as an integer. + The call will add the ": " to separate the header, so for example, to + send a XXXXXX header call sendHeader("XXXXX", 123) + @param aHeaderName Type of header being sent + @param aHeaderValue Value for that header + */ + void sendHeader(const char* aHeaderName, const int aHeaderValue); + + //////////////////////////////////////// + + void sendHeader(const String& aHeaderName, const int aHeaderValue) + { + sendHeader(aHeaderName.c_str(), aHeaderValue); + } + + //////////////////////////////////////// + + /** Send a basic authentication header. This will encode the given username + and password, and send them in suitable header line for doing Basic + Authentication. + @param aUser Username for the authorization + @param aPassword Password for the user aUser + */ + void sendBasicAuth(const char* aUser, const char* aPassword); + + //////////////////////////////////////// + + void sendBasicAuth(const String& aUser, const String& aPassword) + { + sendBasicAuth(aUser.c_str(), aPassword.c_str()); + } + + //////////////////////////////////////// + + /** Get the HTTP status code contained in the response. + For example, 200 for successful request, 404 for file not found, etc. + */ + int responseStatusCode(); + + /** Check if a header is available to be read. + Use readHeaderName() to read header name, and readHeaderValue() to + read the header value + MUST be called after responseStatusCode() and before contentLength() + */ + bool headerAvailable(); + + /** Read the name of the current response header. + Returns empty string if a header is not available. + */ + String readHeaderName(); + + /** Read the vallue of the current response header. + Returns empty string if a header is not available. + */ + String readHeaderValue(); + + /** Read the next character of the response headers. + This functions in the same way as read() but to be used when reading + through the headers. Check whether or not the end of the headers has + been reached by calling endOfHeadersReached(), although after that point + this will still return data as read() would, but slightly less efficiently + MUST be called after responseStatusCode() and before contentLength() + @return The next character of the response headers + */ + int readHeader(); + + /** Skip any response headers to get to the body. + Use this if you don't want to do any special processing of the headers + returned in the response. You can also use it after you've found all of + the headers you're interested in, and just want to get on with processing + the body. + MUST be called after responseStatusCode() + @return HTTP_SUCCESS if successful, else an error code + */ + int skipResponseHeaders(); + + /** Test whether all of the response headers have been consumed. + @return true if we are now processing the response body, else false + */ + bool endOfHeadersReached(); + + /** Test whether the end of the body has been reached. + Only works if the Content-Length header was returned by the server + @return true if we are now at the end of the body, else false + */ + bool endOfBodyReached(); + + //////////////////////////////////////// + + virtual bool endOfStream() + { + return endOfBodyReached(); + }; + + //////////////////////////////////////// + + virtual bool completed() + { + return endOfBodyReached(); + }; + + //////////////////////////////////////// + + /** Return the length of the body. + Also skips response headers if they have not been read already + MUST be called after responseStatusCode() + @return Length of the body, in bytes, or kNoContentLengthHeader if no + Content-Length header was returned by the server + */ + int contentLength(); + + //////////////////////////////////////// + + /** Returns if the response body is chunked + @return true if response body is chunked, false otherwise + */ + int isResponseChunked() + { + return iIsChunked; + } + + //////////////////////////////////////// + + /** Return the response body as a String + Also skips response headers if they have not been read already + MUST be called after responseStatusCode() + @return response body of request as a String + */ + String responseBody(); + + /** Enables connection keep-alive mode + */ + void connectionKeepAlive(); + + /** Disables sending the default request headers (Host and User Agent) + */ + void noDefaultRequestHeaders(); + + //////////////////////////////////////// + + // Inherited from Print + // Note: 1st call to these indicates the user is sending the body, so if need + // Note: be we should finish the header first + virtual size_t write(uint8_t aByte) + { + if (iState < eRequestSent) + { + finishHeaders(); + }; + + return iClient-> write(aByte); + }; + + //////////////////////////////////////// + + virtual size_t write(const uint8_t *aBuffer, size_t aSize) + { + if (iState < eRequestSent) + { + finishHeaders(); + }; + + return iClient->write(aBuffer, aSize); + }; + + //////////////////////////////////////// + + // Inherited from Stream + virtual int available(); + + /** Read the next byte from the server. + @return Byte read or -1 if there are no bytes available. + */ + virtual int read(); + virtual int read(uint8_t *buf, size_t size); + + //////////////////////////////////////// + + virtual int peek() + { + return iClient->peek(); + }; + + //////////////////////////////////////// + + virtual void flush() + { + iClient->flush(); + }; + + //////////////////////////////////////// + + // Inherited from Client + virtual int connect(IPAddress ip, uint16_t port) + { + return iClient->connect(ip, port); + }; + + //////////////////////////////////////// + + virtual int connect(const char *host, uint16_t port) + { + return iClient->connect(host, port); + }; + + //////////////////////////////////////// + + virtual void stop(); + + //////////////////////////////////////// + + virtual uint8_t connected() + { + return iClient->connected(); + }; + + //////////////////////////////////////// + + virtual operator bool() + { + return bool(iClient); + }; + + //////////////////////////////////////// + + virtual uint32_t httpResponseTimeout() + { + return iHttpResponseTimeout; + }; + + //////////////////////////////////////// + + virtual void setHttpResponseTimeout(uint32_t timeout) + { + iHttpResponseTimeout = timeout; + }; + + //////////////////////////////////////// + + protected: + /** Reset internal state data back to the "just initialised" state + */ + void resetState(); + + /** Send the first part of the request and the initial headers. + @param aURLPath Url to request + @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. + @return 0 if successful, else error + */ + int sendInitialHeaders(const char* aURLPath, + const char* aHttpMethod); + + /* Let the server know that we've reached the end of the headers + */ + void finishHeaders(); + + /** Reading any pending data from the client (used in connection keep alive mode) + */ + void flushClientRx(); + + static const char* kContentLengthPrefix; + static const char* kTransferEncodingChunked; + + //////////////////////////////////////// + + typedef enum + { + eIdle, + eRequestStarted, + eRequestSent, + eReadingStatusCode, + eStatusCodeRead, + eReadingContentLength, + eSkipToEndOfHeader, + eLineStartingCRFound, + eReadingBody, + eReadingChunkLength, + eReadingBodyChunk + } tHttpState; + + //////////////////////////////////////// + + // Client we're using + Client* iClient = nullptr; + // Server we are connecting to + const char* iServerName = nullptr; + IPAddress iServerAddress; + // Port of server we are connecting to + uint16_t iServerPort; + // Current state of the finite-state-machine + tHttpState iState; + // Stores the status code for the response, once known + int iStatusCode; + // Stores the value of the Content-Length header, if present + int iContentLength; + // How many bytes of the response body have been read by the user + int iBodyLengthConsumed; + // How far through a Content-Length header prefix we are + const char* iContentLengthPtr = nullptr; + // How far through a Transfer-Encoding chunked header we are + const char* iTransferEncodingChunkedPtr = nullptr; + // Stores if the response body is chunked + bool iIsChunked; + // Stores the value of the current chunk length, if present + int iChunkLength; + uint32_t iHttpResponseTimeout; + bool iConnectionClose; + bool iSendDefaultRequestHeaders; + String iHeaderLine; +}; + +#endif // WiFi_HttpClient_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.cpp b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.cpp new file mode 100644 index 000000000..4c7d7bc1e --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.cpp @@ -0,0 +1,107 @@ +/**************************************************************************************************************************** + WiFi_URLEncoder.cpp - Dead simple HTTP WebClient. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +// Library to simplify HTTP fetching on Arduino +// (c) Copyright Arduino. 2019 +// Released under Apache License, version 2.0 + +#define _WIFI_LOGLEVEL_ 0 + +#include "utility/WiFiDebug.h" +#include "WiFi_HTTPClient/WiFi_URLEncoder.h" + +//////////////////////////////////////// + +WiFiURLEncoderClass::WiFiURLEncoderClass() +{ +} + +//////////////////////////////////////// +WiFiURLEncoderClass::~WiFiURLEncoderClass() +{ +} + +//////////////////////////////////////// + +String WiFiURLEncoderClass::encode(const char* str) +{ + return encode(str, strlen(str)); +} + +//////////////////////////////////////// + +String WiFiURLEncoderClass::encode(const String& str) +{ + return encode(str.c_str(), str.length()); +} + +//////////////////////////////////////// + +String WiFiURLEncoderClass::encode(const char* str, int length) +{ + String encoded; + + encoded.reserve(length); + + for (int i = 0; i < length; i++) + { + char c = str[i]; + + const char HEX_DIGIT_MAPPER[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + if (isAlphaNumeric(c) || (c == '-') || (c == '.') || (c == '_') || (c == '~')) + { + encoded += c; + } + else + { + char s[4]; + + s[0] = '%'; + s[1] = HEX_DIGIT_MAPPER[(c >> 4) & 0xf]; + s[2] = HEX_DIGIT_MAPPER[(c & 0x0f)]; + s[3] = 0; + + encoded += s; + } + } + + return encoded; +} + +//////////////////////////////////////// + +WiFiURLEncoderClass WiFiURLEncoder; diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.h b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.h new file mode 100644 index 000000000..59655a99d --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_URLEncoder.h @@ -0,0 +1,69 @@ +/**************************************************************************************************************************** + WiFi_URLEncoder.h - Dead simple HTTP WebClient. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +// Library to simplify HTTP fetching on Arduino +// (c) Copyright Arduino. 2019 +// Released under Apache License, version 2.0 + +#pragma once + +#ifndef WiFi_URLEncoder_H +#define WiFi_URLEncoder_H + +#include + +#include "utility/WiFiDebug.h" + +//////////////////////////////////////// + +class WiFiURLEncoderClass +{ + public: + WiFiURLEncoderClass(); + virtual ~WiFiURLEncoderClass(); + + static String encode(const char* str); + static String encode(const String& str); + + private: + static String encode(const char* str, int length); +}; + +//////////////////////////////////////// + +extern WiFiURLEncoderClass WiFiURLEncoder; + +#endif // WiFi_URLEncoder_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.cpp b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.cpp new file mode 100644 index 000000000..5e909bfc3 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.cpp @@ -0,0 +1,459 @@ +/**************************************************************************************************************************** + WiFi_WebSocketClient.cpp - Dead simple HTTP WebSockets Client. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +// (c) Copyright Arduino. 2016 +// Released under Apache License, version 2.0 + +#define _WIFI_LOGLEVEL_ 0 + +#include "libb64/base64.h" + +#include "utility/WiFiDebug.h" +#include "WiFi_HTTPClient/WiFi_WebSocketClient.h" + +//////////////////////////////////////// + +WiFiWebSocketClient::WiFiWebSocketClient(Client& aClient, const char* aServerName, uint16_t aServerPort) + : WiFiHttpClient(aClient, aServerName, aServerPort), + iTxStarted(false), + iRxSize(0) +{ +} + +//////////////////////////////////////// + +WiFiWebSocketClient::WiFiWebSocketClient(Client& aClient, const String& aServerName, uint16_t aServerPort) + : WiFiHttpClient(aClient, aServerName, aServerPort), + iTxStarted(false), + iRxSize(0) +{ +} + +//////////////////////////////////////// + +WiFiWebSocketClient::WiFiWebSocketClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort) + : WiFiHttpClient(aClient, aServerAddress, aServerPort), + iTxStarted(false), + iRxSize(0) +{ +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::begin(const char* aPath) +{ + // start the GET request + beginRequest(); + connectionKeepAlive(); + + int status = get(aPath); + + if (status == 0) + { + uint8_t randomKey[16]; + char base64RandomKey[25]; + + // create a random key for the connection upgrade + for (int i = 0; i < (int)sizeof(randomKey); i++) + { + randomKey[i] = random(0x01, 0xff); + } + + memset(base64RandomKey, 0x00, sizeof(base64RandomKey)); + base64_encode(randomKey, sizeof(randomKey), (unsigned char*)base64RandomKey, sizeof(base64RandomKey)); + + // start the connection upgrade sequence + sendHeader("Upgrade", "websocket"); + sendHeader("Connection", "Upgrade"); + sendHeader("Sec-WebSocket-Key", base64RandomKey); + sendHeader("Sec-WebSocket-Version", "13"); + endRequest(); + + status = responseStatusCode(); + + if (status > 0) + { + skipResponseHeaders(); + } + } + + iRxSize = 0; + + // status code of 101 means success + return (status == 101) ? 0 : status; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::begin(const String& aPath) +{ + return begin(aPath.c_str()); +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::beginMessage(int aType) +{ + if (iTxStarted) + { + // fail TX already started + return 1; + } + + iTxStarted = true; + iTxMessageType = (aType & 0xf); + iTxSize = 0; + + return 0; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::endMessage() +{ + if (!iTxStarted) + { + // fail TX not started + return 1; + } + + // send FIN + the message type (opcode) + WiFiHttpClient::write(0x80 | iTxMessageType); + + // the message is masked (0x80) + // send the length + if (iTxSize < 126) + { + WiFiHttpClient::write(0x80 | (uint8_t)iTxSize); + } + else if (iTxSize < 0xffff) + { + WiFiHttpClient::write(0x80 | 126); + WiFiHttpClient::write((iTxSize >> 8) & 0xff); + WiFiHttpClient::write((iTxSize >> 0) & 0xff); + } + else + { + WiFiHttpClient::write(0x80 | 127); + WiFiHttpClient::write((iTxSize >> 56) & 0xff); + WiFiHttpClient::write((iTxSize >> 48) & 0xff); + WiFiHttpClient::write((iTxSize >> 40) & 0xff); + WiFiHttpClient::write((iTxSize >> 32) & 0xff); + WiFiHttpClient::write((iTxSize >> 24) & 0xff); + WiFiHttpClient::write((iTxSize >> 16) & 0xff); + WiFiHttpClient::write((iTxSize >> 8) & 0xff); + WiFiHttpClient::write((iTxSize >> 0) & 0xff); + } + + uint8_t maskKey[4]; + + // create a random mask for the data and send + for (int i = 0; i < (int)sizeof(maskKey); i++) + { + maskKey[i] = random(0xff); + } + + WiFiHttpClient::write(maskKey, sizeof(maskKey)); + + // mask the data and send + for (int i = 0; i < (int)iTxSize; i++) + { + iTxBuffer[i] ^= maskKey[i % sizeof(maskKey)]; + } + + size_t txSize = iTxSize; + + iTxStarted = false; + iTxSize = 0; + + return (WiFiHttpClient::write(iTxBuffer, txSize) == txSize) ? 0 : 1; +} + +//////////////////////////////////////// + +size_t WiFiWebSocketClient::write(uint8_t aByte) +{ + return write(&aByte, sizeof(aByte)); +} + +//////////////////////////////////////// + +size_t WiFiWebSocketClient::write(const uint8_t *aBuffer, size_t aSize) +{ + if (iState < eReadingBody) + { + // have not upgraded the connection yet + return WiFiHttpClient::write(aBuffer, aSize); + } + + if (!iTxStarted) + { + // fail TX not started + return 0; + } + + // check if the write size, fits in the buffer + if ((iTxSize + aSize) > sizeof(iTxBuffer)) + { + aSize = sizeof(iTxSize) - iTxSize; + } + + // copy data into the buffer + memcpy(iTxBuffer + iTxSize, aBuffer, aSize); + + iTxSize += aSize; + + return aSize; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::parseMessage() +{ + flushRx(); + + // make sure 2 bytes (opcode + length) + // are available + if (WiFiHttpClient::available() < 2) + { + return 0; + } + + // read open code and length + uint8_t opcode = WiFiHttpClient::read(); + int length = WiFiHttpClient::read(); + + if ((opcode & 0x0f) == 0) + { + // continuation, use previous opcode and update flags + iRxOpCode |= opcode; + } + else + { + iRxOpCode = opcode; + } + + iRxMasked = (length & 0x80); + length &= 0x7f; + + // read the RX size + if (length < 126) + { + iRxSize = length; + } + else if (length == 126) + { + iRxSize = (WiFiHttpClient::read() << 8) | WiFiHttpClient::read(); + } + else + { + iRxSize = ((uint64_t)WiFiHttpClient::read() << 56) | + ((uint64_t)WiFiHttpClient::read() << 48) | + ((uint64_t)WiFiHttpClient::read() << 40) | + ((uint64_t)WiFiHttpClient::read() << 32) | + ((uint64_t)WiFiHttpClient::read() << 24) | + ((uint64_t)WiFiHttpClient::read() << 16) | + ((uint64_t)WiFiHttpClient::read() << 8) | + (uint64_t)WiFiHttpClient::read(); + } + + // read in the mask, if present + if (iRxMasked) + { + for (int i = 0; i < (int)sizeof(iRxMaskKey); i++) + { + iRxMaskKey[i] = WiFiHttpClient::read(); + } + } + + iRxMaskIndex = 0; + + if (TYPE_CONNECTION_CLOSE == messageType()) + { + flushRx(); + stop(); + iRxSize = 0; + } + else if (TYPE_PING == messageType()) + { + beginMessage(TYPE_PONG); + + while (available()) + { + write(read()); + } + + endMessage(); + + iRxSize = 0; + } + else if (TYPE_PONG == messageType()) + { + flushRx(); + iRxSize = 0; + } + + return iRxSize; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::messageType() +{ + return (iRxOpCode & 0x0f); +} + +//////////////////////////////////////// + +bool WiFiWebSocketClient::isFinal() +{ + return ((iRxOpCode & 0x80) != 0); +} + +//////////////////////////////////////// + +String WiFiWebSocketClient::readString() +{ + int avail = available(); + String s; + + if (avail > 0) + { + s.reserve(avail); + + for (int i = 0; i < avail; i++) + { + s += (char)read(); + } + } + + return s; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::ping() +{ + uint8_t pingData[16]; + + // create random data for the ping + for (int i = 0; i < (int)sizeof(pingData); i++) + { + pingData[i] = random(0xff); + } + + beginMessage(TYPE_PING); + write(pingData, sizeof(pingData)); + + return endMessage(); +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::available() +{ + if (iState < eReadingBody) + { + return WiFiHttpClient::available(); + } + + return iRxSize; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::read() +{ + byte b; + + if (read(&b, sizeof(b))) + { + return b; + } + + return -1; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::read(uint8_t *aBuffer, size_t aSize) +{ + int readCount = WiFiHttpClient::read(aBuffer, aSize); + + if (readCount > 0) + { + iRxSize -= readCount; + + // unmask the RX data if needed + if (iRxMasked) + { + for (int i = 0; i < (int)aSize; i++, iRxMaskIndex++) + { + aBuffer[i] ^= iRxMaskKey[iRxMaskIndex % sizeof(iRxMaskKey)]; + } + } + } + + return readCount; +} + +//////////////////////////////////////// + +int WiFiWebSocketClient::peek() +{ + int p = WiFiHttpClient::peek(); + + if (p != -1 && iRxMasked) + { + // unmask the RX data if needed + p = (uint8_t)p ^ iRxMaskKey[iRxMaskIndex % sizeof(iRxMaskKey)]; + } + + return p; +} + +//////////////////////////////////////// + +void WiFiWebSocketClient::flushRx() +{ + while (available()) + { + read(); + } +} + +//////////////////////////////////////// + diff --git a/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.h b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.h new file mode 100644 index 000000000..1dbaab7e7 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/WiFi_HTTPClient/WiFi_WebSocketClient.h @@ -0,0 +1,144 @@ +/**************************************************************************************************************************** + WiFi_WebSocketClient.h - Dead simple HTTP WebSockets Client. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +// (c) Copyright Arduino. 2016 +// Released under Apache License, version 2.0 + +#pragma once + +#ifndef WiFi_WebSocketClient_H +#define WiFi_WebSocketClient_H + +#include + +#include "utility/WiFiDebug.h" + +#include "WiFi_HTTPClient/WiFi_HttpClient.h" + +//////////////////////////////////////// + +static const int TYPE_CONTINUATION = 0x0; +static const int TYPE_TEXT = 0x1; +static const int TYPE_BINARY = 0x2; +static const int TYPE_CONNECTION_CLOSE = 0x8; +static const int TYPE_PING = 0x9; +static const int TYPE_PONG = 0xa; + +//////////////////////////////////////// + +class WiFiWebSocketClient : public WiFiHttpClient +{ + public: + WiFiWebSocketClient(Client& aClient, const char* aServerName, uint16_t aServerPort = WiFiHttpClient::kHttpPort); + WiFiWebSocketClient(Client& aClient, const String& aServerName, uint16_t aServerPort = WiFiHttpClient::kHttpPort); + WiFiWebSocketClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort = WiFiHttpClient::kHttpPort); + + /** Start the Web Socket connection to the specified path + @param aURLPath Path to use in request (optional, "/" is used by default) + @return 0 if successful, else error + */ + int begin(const char* aPath = "/"); + int begin(const String& aPath); + + /** Begin to send a message of type (TYPE_TEXT or TYPE_BINARY) + Use the write or Stream API's to set message content, followed by endMessage + to complete the message. + @param aURLPath Path to use in request + @return 0 if successful, else error + */ + int beginMessage(int aType); + + /** Completes sending of a message started by beginMessage + @return 0 if successful, else error + */ + int endMessage(); + + /** Try to parse an incoming messages + @return 0 if no message available, else size of parsed message + */ + int parseMessage(); + + /** Returns type of current parsed message + @return type of current parsedMessage (TYPE_TEXT or TYPE_BINARY) + */ + int messageType(); + + /** Returns if the current message is the final chunk of a split + message + @return true for final message, false otherwise + */ + bool isFinal(); + + /** Read the current messages as a string + @return current message as a string + */ + String readString(); + + /** Send a ping + @return 0 if successful, else error + */ + int ping(); + + // Inherited from Print + virtual size_t write(uint8_t aByte); + virtual size_t write(const uint8_t *aBuffer, size_t aSize); + + // Inherited from Stream + virtual int available(); + /** Read the next byte from the server. + @return Byte read or -1 if there are no bytes available. + */ + virtual int read(); + virtual int read(uint8_t *buf, size_t size); + virtual int peek(); + + private: + void flushRx(); + + private: + bool iTxStarted; + uint8_t iTxMessageType; + uint8_t iTxBuffer[128]; + uint64_t iTxSize; + + uint8_t iRxOpCode; + uint64_t iRxSize; + bool iRxMasked; + int iRxMaskIndex; + uint8_t iRxMaskKey[4]; +}; + +#endif // WiFi_WebSocketClient_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.cpp b/software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.cpp new file mode 100644 index 000000000..febd2d35d --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.cpp @@ -0,0 +1,87 @@ +/**************************************************************************************************************************** + base64.cpp - cpp source to a base64 encoding algorithm implementation + + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + *****************************************************************************************************************************/ + +#include "base64.h" + +/* Simple test program + #include + void main() + { + char* in = "amcewen"; + char out[22]; + + b64_encode(in, 15, out, 22); + out[21] = '\0'; + + printf(out); + } +*/ + +int base64_encode(const unsigned char* aInput, int aInputLen, unsigned char* aOutput, int aOutputLen) +{ + // Work out if we've got enough space to encode the input + // Every 6 bits of input becomes a byte of output + if (aOutputLen < (aInputLen * 8) / 6) + { + // FIXME Should we return an error here, or just the length + return (aInputLen * 8) / 6; + } + + // If we get here we've got enough space to do the encoding + + const char* b64_dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + if (aInputLen == 3) + { + aOutput[0] = b64_dictionary[aInput[0] >> 2]; + aOutput[1] = b64_dictionary[(aInput[0] & 0x3) << 4 | (aInput[1] >> 4)]; + aOutput[2] = b64_dictionary[(aInput[1] & 0x0F) << 2 | (aInput[2] >> 6)]; + aOutput[3] = b64_dictionary[aInput[2] & 0x3F]; + } + else if (aInputLen == 2) + { + aOutput[0] = b64_dictionary[aInput[0] >> 2]; + aOutput[1] = b64_dictionary[(aInput[0] & 0x3) << 4 | (aInput[1] >> 4)]; + aOutput[2] = b64_dictionary[(aInput[1] & 0x0F) << 2]; + aOutput[3] = '='; + } + else if (aInputLen == 1) + { + aOutput[0] = b64_dictionary[aInput[0] >> 2]; + aOutput[1] = b64_dictionary[(aInput[0] & 0x3) << 4]; + aOutput[2] = '='; + aOutput[3] = '='; + } + else + { + // Break the input into 3-byte chunks and process each of them + int i; + + for (i = 0; i < aInputLen / 3; i++) + { + base64_encode(&aInput[i * 3], 3, &aOutput[i * 4], 4); + } + + if (aInputLen % 3 > 0) + { + // It doesn't fit neatly into a 3-byte chunk, so process what's left + base64_encode(&aInput[i * 3], aInputLen % 3, &aOutput[i * 4], aOutputLen - (i * 4)); + } + } + + return ((aInputLen + 2) / 3) * 4; +} + diff --git a/software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.h b/software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.h new file mode 100644 index 000000000..b0017f7a2 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/libb64/base64.h @@ -0,0 +1,28 @@ +/**************************************************************************************************************************** + base64.h - c source to a base64 encoding algorithm implementation + + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + *****************************************************************************************************************************/ + +#pragma once + +// Reintroduce to prevent duplication compile error if other lib/core already has LIB64 +// pragma once can't prevent that +#ifndef base64_h +#define base64_h + +int base64_encode(const unsigned char* aInput, int aInputLen, unsigned char* aOutput, int aOutputLen); + + +#endif // base64_h + diff --git a/software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.c b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.c new file mode 100644 index 000000000..79fb395c1 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.c @@ -0,0 +1,147 @@ +/**************************************************************************************************************************** + cdecoder.c - c source to a base64 decoding algorithm implementation + + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + *****************************************************************************************************************************/ + +#if !(ESP32 || ESP8266) + +#include "cdecode.h" + +int base64_decode_value(int value_in) +{ + static const char decoding[] = + { + 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, + -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51 + }; + + static const char decoding_size = sizeof(decoding); + value_in -= 43; + + if (value_in < 0 || value_in > decoding_size) + return -1; + + return decoding[(int)value_in]; +} + +void base64_init_decodestate(base64_decodestate* state_in) +{ + state_in->step = step_a; + state_in->plainchar = 0; +} + +int base64_decode_block(const char* code_in, const int length_in, char* plaintext_out, base64_decodestate* state_in) +{ + const char* codechar = code_in; + char* plainchar = plaintext_out; + int fragment; + + *plainchar = state_in->plainchar; + + switch (state_in->step) + { + while (1) + { + case step_a: + do + { + if (codechar == code_in + length_in) + { + state_in->step = step_a; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + + fragment = base64_decode_value(*codechar++); + } while (fragment < 0); + + *plainchar = (fragment & 0x03f) << 2; + + // fall through + + case step_b: + do + { + if (codechar == code_in + length_in) + { + state_in->step = step_b; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + + fragment = base64_decode_value(*codechar++); + } while (fragment < 0); + + *plainchar++ |= (fragment & 0x030) >> 4; + *plainchar = (fragment & 0x00f) << 4; + + // fall through + + case step_c: + do + { + if (codechar == code_in + length_in) + { + state_in->step = step_c; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + + fragment = base64_decode_value(*codechar++); + } while (fragment < 0); + + *plainchar++ |= (fragment & 0x03c) >> 2; + *plainchar = (fragment & 0x003) << 6; + + // fall through + + case step_d: + do + { + if (codechar == code_in + length_in) + { + state_in->step = step_d; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + + fragment = base64_decode_value(*codechar++); + } while (fragment < 0); + + *plainchar++ |= (fragment & 0x03f); + + // fall through + } + } + + /* control should not reach here */ + return plainchar - plaintext_out; +} + +int base64_decode_chars(const char* code_in, const int length_in, char* plaintext_out) +{ + + base64_decodestate _state; + base64_init_decodestate(&_state); + int len = base64_decode_block(code_in, length_in, plaintext_out, &_state); + + if (len > 0) + plaintext_out[len] = 0; + + return len; +} + +#endif diff --git a/software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.h b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.h new file mode 100644 index 000000000..e7fd954fe --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cdecode.h @@ -0,0 +1,54 @@ +/**************************************************************************************************************************** + cdecoder.h - c source to a base64 decoding algorithm implementation + + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + *****************************************************************************************************************************/ + +#pragma once + +// Reintroduce to prevent duplication compile error if other lib/core already has LIB64 +// pragma once can't prevent that +#ifndef BASE64_CDECODE_H +#define BASE64_CDECODE_H + +#define base64_decode_expected_len(n) ((n * 3) / 4) + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum +{ + step_a, step_b, step_c, step_d +} base64_decodestep; + +typedef struct +{ + base64_decodestep step; + char plainchar; +} base64_decodestate; + +void base64_init_decodestate(base64_decodestate* state_in); + +int base64_decode_value(int value_in); + +int base64_decode_block(const char* code_in, const int length_in, char* plaintext_out, base64_decodestate* state_in); + +int base64_decode_chars(const char* code_in, const int length_in, char* plaintext_out); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* BASE64_CDECODE_H */ + diff --git a/software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.c b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.c new file mode 100644 index 000000000..3365dcd24 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.c @@ -0,0 +1,149 @@ +/**************************************************************************************************************************** + cencoder.c - c source to a base64 decoding algorithm implementation + + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + *****************************************************************************************************************************/ + +#if !(ESP32 || ESP8266) + +#include "cencode.h" + +const int CHARS_PER_LINE = 72; + +void base64_init_encodestate(base64_encodestate* state_in) +{ + state_in->step = step_A; + state_in->result = 0; + state_in->stepcount = 0; +} + +char base64_encode_value(char value_in) +{ + static const char* encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + if (value_in > 63) + return '='; + + return encoding[(unsigned int)value_in]; +} + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in) +{ + const char* plainchar = plaintext_in; + const char* const plaintextend = plaintext_in + length_in; + char* codechar = code_out; + char result; + char fragment; + + result = state_in->result; + + switch (state_in->step) + { + while (1) + { + case step_A: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_A; + return codechar - code_out; + } + + fragment = *plainchar++; + result = (fragment & 0x0fc) >> 2; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x003) << 4; + + // fall through + + case step_B: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_B; + return codechar - code_out; + } + + fragment = *plainchar++; + result |= (fragment & 0x0f0) >> 4; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x00f) << 2; + + // fall through + + case step_C: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_C; + return codechar - code_out; + } + + fragment = *plainchar++; + result |= (fragment & 0x0c0) >> 6; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x03f) >> 0; + *codechar++ = base64_encode_value(result); + + ++(state_in->stepcount); + + if (state_in->stepcount == CHARS_PER_LINE / 4) + { + *codechar++ = '\n'; + state_in->stepcount = 0; + } + + // fall through + } + } + + /* control should not reach here */ + return codechar - code_out; +} + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in) +{ + char* codechar = code_out; + + switch (state_in->step) + { + case step_B: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + *codechar++ = '='; + break; + + case step_C: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + break; + + case step_A: + break; + } + + *codechar = 0x00; + + return codechar - code_out; +} + +int base64_encode_chars(const char* plaintext_in, int length_in, char* code_out) +{ + base64_encodestate _state; + base64_init_encodestate(&_state); + int len = base64_encode_block(plaintext_in, length_in, code_out, &_state); + + return len + base64_encode_blockend((code_out + len), &_state); +} + +#endif diff --git a/software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.h b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.h new file mode 100644 index 000000000..ee76d0dd5 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/libb64/cencode.h @@ -0,0 +1,57 @@ +/**************************************************************************************************************************** + cencoder.h - c source to a base64 decoding algorithm implementation + + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + *****************************************************************************************************************************/ + +#pragma once + +// Reintroduce to prevent duplication compile error if other lib/core already has LIB64 +// pragma once can't prevent that +#ifndef BASE64_CENCODE_H +#define BASE64_CENCODE_H + +#define base64_encode_expected_len(n) ((((4 * n) / 3) + 3) & ~3) + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum +{ + step_A, step_B, step_C +} base64_encodestep; + +typedef struct +{ + base64_encodestep step; + char result; + int stepcount; +} base64_encodestate; + +void base64_init_encodestate(base64_encodestate* state_in); + +char base64_encode_value(char value_in); + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in); + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in); + +int base64_encode_chars(const char* plaintext_in, int length_in, char* code_out); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* BASE64_CENCODE_H */ + diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/ESP_RequestHandlersImpl.h b/software/firmware/source/libraries/WiFiWebServer/src/utility/ESP_RequestHandlersImpl.h new file mode 100644 index 000000000..c854e0c0a --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/ESP_RequestHandlersImpl.h @@ -0,0 +1,338 @@ +/**************************************************************************************************************************** + ESP_RequestHandlersImpl.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#pragma once + +#ifndef ESP_RequestHandlersImpl_H +#define ESP_RequestHandlersImpl_H + +#include "RequestHandler.h" +#include "utility/esp_detail/mimetable.h" +#include "FS.h" +#include "WString.h" +#include +#include + +#include "Uri.h" + +#include "utility/WiFiDebug.h" + +//////////////////////////////////////// +//////////////////////////////////////// + +class FunctionRequestHandler : public RequestHandler +{ + public: + + FunctionRequestHandler(WiFiWebServer::THandlerFunction fn, WiFiWebServer::THandlerFunction ufn, const Uri &uri, + const HTTPMethod& method) + : _fn(fn) + , _ufn(ufn) + , _uri(uri.clone()) + , _method(method) + { + _uri->initPathArgs(pathArgs); + } + + ~FunctionRequestHandler() + { + delete _uri; + } + + //////////////////////////////////////// + + bool canHandle(const HTTPMethod& requestMethod, const String& requestUri) override + { + if (_method != HTTP_ANY && _method != requestMethod) + return false; + + return _uri->canHandle(requestUri, pathArgs); + } + + //////////////////////////////////////// + + bool canUpload(const String& requestUri) override + { + if (!_ufn || !canHandle(HTTP_POST, requestUri)) + return false; + + return true; + } + + //////////////////////////////////////// + + bool handle(WiFiWebServer& server, const HTTPMethod& requestMethod, /*const*/ String& requestUri) override + { + WFW_UNUSED(server); + + if (!canHandle(requestMethod, requestUri)) + return false; + + _fn(); + + return true; + } + + //////////////////////////////////////// + + void upload(WiFiWebServer& server, const String& requestUri, const HTTPUpload& upload) override + { + WFW_UNUSED(server); + WFW_UNUSED(upload); + + if (canUpload(requestUri)) + _ufn(); + } + + //////////////////////////////////////// + + protected: + + WiFiWebServer::THandlerFunction _fn; + WiFiWebServer::THandlerFunction _ufn; + + Uri *_uri; + + HTTPMethod _method; +}; + +//////////////////////////////////////// +//////////////////////////////////////// + +class StaticRequestHandler : public RequestHandler +{ + using WebServerType = WiFiWebServer; + + public: + + StaticRequestHandler(FS& fs, const char* path, const char* uri, const char* cache_header) + : _fs(fs) + , _uri(uri) + , _path(path) + , _cache_header(cache_header) + { + _isFile = fs.exists(path); + _baseUriLength = _uri.length(); + } + + //////////////////////////////////////// + + bool canHandle(const HTTPMethod& requestMethod, const String& requestUri) override + { + if (requestMethod != HTTP_GET) + return false; + + if ((_isFile && requestUri != _uri) || !requestUri.startsWith(_uri)) + return false; + + return true; + } + + //////////////////////////////////////// + + bool handle(WiFiWebServer& server, const HTTPMethod& requestMethod, /*const*/ String& requestUri) override + { + if (!canHandle(requestMethod, requestUri)) + return false; + + WS_LOGDEBUG3(F("StaticRequestHandler::handle: request ="), requestUri, F(", _uri ="), _uri); + + String path(_path); + + if (!_isFile) + { + // Base URI doesn't point to a file. + // If a directory is requested, look for index file. + if (requestUri.endsWith("/")) + requestUri += "index.htm"; + + // Append whatever follows this URI in request to get the file path. + path += requestUri.substring(_baseUriLength); + } + + WS_LOGDEBUG3(F("StaticRequestHandler::handle: path ="), path, F(", _isFile ="), _isFile); + + String contentType = getContentType(path); + + using namespace mime_esp; + + // look for gz file, only if the original specified path is not a gz. + // So part only works to send gzip via content encoding when a non compressed is asked for + // if you point the the path to gzip you will serve the gzip as content type "application/x-gzip" + // not text or javascript etc... + if (!path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !_fs.exists(path)) + { + String pathWithGz = path + FPSTR(mimeTable[gz].endsWith); + + if (_fs.exists(pathWithGz)) + path += FPSTR(mimeTable[gz].endsWith); + } + + File f = _fs.open(path, "r"); + + if (!f || !f.available()) + return false; + + if (_cache_header.length() != 0) + server.sendHeader("Cache-Control", _cache_header); + + server.streamFile(f, contentType); + + return true; + } + + //////////////////////////////////////// + + static String getContentType(const String& path) + { + using namespace mime_esp; + + char buff[sizeof(mimeTable[0].mimeType)]; + + // Check all entries but last one for match, return if found + for (size_t i = 0; i < sizeof(mimeTable) / sizeof(mimeTable[0]) - 1; i++) + { + strcpy_P(buff, mimeTable[i].endsWith); + + if (path.endsWith(buff)) + { + strcpy_P(buff, mimeTable[i].mimeType); + + return String(buff); + } + } + + // Fall-through and just return default type + strcpy_P(buff, mimeTable[sizeof(mimeTable) / sizeof(mimeTable[0]) - 1].mimeType); + + return String(buff); + } + + //////////////////////////////////////// + + bool validMethod(HTTPMethod requestMethod) + { + return (requestMethod == HTTP_GET) || (requestMethod == HTTP_HEAD); + } + + //////////////////////////////////////// + + protected: + FS _fs; + String _uri; + String _path; + String _cache_header; + bool _isFile; + size_t _baseUriLength; +}; + +//////////////////////////////////////// +//////////////////////////////////////// + + +class StaticFileRequestHandler : public StaticRequestHandler +{ + using SRH = StaticRequestHandler; + using WebServerType = WiFiWebServer; + + public: + + //////////////////////////////////////// + + StaticFileRequestHandler(FS& fs, const char* path, const char* uri, const char* cache_header) + : + StaticRequestHandler{fs, path, uri, cache_header} + { + File f = SRH::_fs.open(path, "r"); + MD5Builder calcMD5; + calcMD5.begin(); + calcMD5.addStream(f, f.size()); + calcMD5.calculate(); + calcMD5.getBytes(_ETag_md5); + f.close(); + } + + //////////////////////////////////////// + + bool canHandle(const HTTPMethod& requestMethod, const String& requestUri) override + { + return SRH::validMethod(requestMethod) && requestUri == SRH::_uri; + } + + //////////////////////////////////////// + + bool handle(WiFiWebServer& server, const HTTPMethod& requestMethod, const String& requestUri) + { + if (!canHandle(requestMethod, requestUri)) + return false; + + + const String etag = "\"" + base64::encode(_ETag_md5, 16) + "\""; + + if (server.header("If-None-Match") == etag) + { + server.send(304); + return true; + } + + File f = SRH::_fs.open(SRH::_path, "r"); + + if (!f) + return false; + + if (!_isFile) + { + f.close(); + return false; + } + + if (SRH::_cache_header.length() != 0) + server.sendHeader("Cache-Control", SRH::_cache_header); + + server.sendHeader("ETag", etag); + + server.streamFile(f, mime_esp::getContentType(SRH::_path), requestMethod); + return true; + } + + //////////////////////////////////////// + + protected: + uint8_t _ETag_md5[16]; +}; + + +#endif // ESP_RequestHandlersImpl_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandler.h b/software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandler.h new file mode 100644 index 000000000..5e9e2ce7c --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandler.h @@ -0,0 +1,143 @@ +/**************************************************************************************************************************** + RequestHandler.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#pragma once + +#ifndef RequestHandler_H +#define RequestHandler_H + +#ifndef WFW_UNUSED + #define WFW_UNUSED(x) (void)(x) +#endif + +#include "utility/WiFiDebug.h" + +#include + +//////////////////////////////////////// + +class RequestHandler +{ + public: + + virtual ~RequestHandler() { } + + //////////////////////////////////////// + + virtual bool canHandle(const HTTPMethod& method, const String& uri) + { + WFW_UNUSED(method); + WFW_UNUSED(uri); + + return false; + } + + //////////////////////////////////////// + + virtual bool canUpload(const String& uri) + { + WFW_UNUSED(uri); + + return false; + } + + //////////////////////////////////////// + + virtual bool handle(WiFiWebServer& server, const HTTPMethod& requestMethod, /*const*/ String& requestUri) + { + WFW_UNUSED(server); + WFW_UNUSED(requestMethod); + WFW_UNUSED(requestUri); + + return false; + } + + //////////////////////////////////////// + + virtual void upload(WiFiWebServer& server, const String& requestUri, const HTTPUpload& upload) + { + WFW_UNUSED(server); + WFW_UNUSED(requestUri); + WFW_UNUSED(upload); + } + + //////////////////////////////////////// + + RequestHandler* next() + { + return _next; + } + + //////////////////////////////////////// + + void next(RequestHandler* r) + { + _next = r; + } + + //////////////////////////////////////// + + private: + + RequestHandler* _next = nullptr; + + //////////////////////////////////////// + + protected: + std::vector pathArgs; + + //////////////////////////////////////// + + public: + + //////////////////////////////////////// + + const String& pathArg(unsigned int i) + { + if (i < pathArgs.size()) + { + return pathArgs[i]; + } + else + { + WS_LOGERROR3(F("RequestHandler::pathArg: error i ="), i, F(" > pathArgs.size() ="), pathArgs.size()); + + return pathArgs[0]; + } + } +}; + +#endif // RequestHandler_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandlersImpl.h b/software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandlersImpl.h new file mode 100644 index 000000000..24ffe54bf --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/RequestHandlersImpl.h @@ -0,0 +1,249 @@ +/**************************************************************************************************************************** + RequestHandlersImpl.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#pragma once + +#ifndef RequestHandlersImpl_H +#define RequestHandlersImpl_H + +#if !(ESP32 || ESP8266) +#include "RequestHandler.h" +#include "mimetable.h" + +//////////////////////////////////////// +//////////////////////////////////////// + +class FunctionRequestHandler : public RequestHandler +{ + public: + + //////////////////////////////////////// + + FunctionRequestHandler(WiFiWebServer::THandlerFunction fn, WiFiWebServer::THandlerFunction ufn, const String &uri, + const HTTPMethod& method) + : _fn(fn) + , _ufn(ufn) + , _uri(uri) + , _method(method) + { + } + + //////////////////////////////////////// + + bool canHandle(const HTTPMethod& requestMethod, const String& requestUri) override + { + if (_method != HTTP_ANY && _method != requestMethod) + return false; + + if (requestUri == _uri) + return true; + + if (_uri.endsWith("/*")) + { + String _uristart = _uri; + _uristart.replace("/*", ""); + + if (requestUri.startsWith(_uristart)) + return true; + } + + return false; + } + + //////////////////////////////////////// + + bool canUpload(const String& requestUri) override + { + if (!_ufn || !canHandle(HTTP_POST, requestUri)) + return false; + + return true; + } + + //////////////////////////////////////// + + bool handle(WiFiWebServer& server, const HTTPMethod& requestMethod, /*const*/ String& requestUri) override + { + WFW_UNUSED(server); + + if (!canHandle(requestMethod, requestUri)) + return false; + + _fn(); + return true; + } + + //////////////////////////////////////// + + void upload(WiFiWebServer& server, const String& requestUri, const HTTPUpload& upload) override + { + WFW_UNUSED(server); + WFW_UNUSED(upload); + + if (canUpload(requestUri)) + _ufn(); + } + + //////////////////////////////////////// + + protected: + WiFiWebServer::THandlerFunction _fn; + WiFiWebServer::THandlerFunction _ufn; + String _uri; + HTTPMethod _method; +}; + +//////////////////////////////////////// +//////////////////////////////////////// + +class StaticRequestHandler : public RequestHandler +{ + public: + + //////////////////////////////////////// + + bool canHandle(const HTTPMethod& requestMethod, const String& requestUri) override + { + if (requestMethod != HTTP_GET) + return false; + + if ((_isFile && requestUri != _uri) || !requestUri.startsWith(_uri)) + return false; + + return true; + } + + //////////////////////////////////////// + +#if USE_NEW_WEBSERVER_VERSION + + //////////////////////////////////////// + + static String getContentType(const String& path) + { + using namespace mime; + char buff[sizeof(mimeTable[0].mimeType)]; + + // Check all entries but last one for match, return if found + for (size_t i = 0; i < sizeof(mimeTable) / sizeof(mimeTable[0]) - 1; i++) + { + strcpy(buff, mimeTable[i].endsWith); + + if (path.endsWith(buff)) + { + strcpy(buff, mimeTable[i].mimeType); + return String(buff); + } + } + + // Fall-through and just return default type + strcpy(buff, mimeTable[sizeof(mimeTable) / sizeof(mimeTable[0]) - 1].mimeType); + return String(buff); + } + + //////////////////////////////////////// + +#else // #if USE_NEW_WEBSERVER_VERSION + + //////////////////////////////////////// + + static String getContentType(const String& path) + { + if (path.endsWith(".html")) + return "text/html"; + else if (path.endsWith(".htm")) + return "text/html"; + else if (path.endsWith(".css")) + return "text/css"; + else if (path.endsWith(".txt")) + return "text/plain"; + else if (path.endsWith(".js")) + return "application/javascript"; + else if (path.endsWith(".png")) + return "image/png"; + else if (path.endsWith(".gif")) + return "image/gif"; + else if (path.endsWith(".jpg")) + return "image/jpeg"; + else if (path.endsWith(".ico")) + return "image/x-icon"; + else if (path.endsWith(".svg")) + return "image/svg+xml"; + else if (path.endsWith(".ttf")) + return "application/x-font-ttf"; + else if (path.endsWith(".otf")) + return "application/x-font-opentype"; + else if (path.endsWith(".woff")) + return "application/font-woff"; + else if (path.endsWith(".woff2")) + return "application/font-woff2"; + else if (path.endsWith(".eot")) + return "application/vnd.ms-fontobject"; + else if (path.endsWith(".sfnt")) + return "application/font-sfnt"; + else if (path.endsWith(".xml")) + return "text/xml"; + else if (path.endsWith(".pdf")) + return "application/pdf"; + else if (path.endsWith(".zip")) + return "application/zip"; + else if (path.endsWith(".gz")) + return "application/x-gzip"; + else if (path.endsWith(".appcache")) + return "text/cache-manifest"; + + return "application/octet-stream"; + } + + //////////////////////////////////////// + +#endif // #if USE_NEW_WEBSERVER_VERSION + + protected: + + String _uri; + String _path; + String _cache_header; + bool _isFile; + size_t _baseUriLength; +}; + +#else // #if !(ESP32 || ESP8266) +#include "ESP_RequestHandlersImpl.h" +#endif + + +#endif // RequestHandlersImpl_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.cpp b/software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.cpp new file mode 100644 index 000000000..d8af5797c --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.cpp @@ -0,0 +1,138 @@ +/**************************************************************************************************************************** + RingBuffer.cpp - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#include "RingBuffer.h" + +#include + +//////////////////////////////////////// + +WiFi_RingBuffer::WiFi_RingBuffer(unsigned int size) +{ + _size = size; + // add one char to terminate the string + ringBuf = new char[size + 1]; + ringBufEnd = &ringBuf[size]; + init(); +} + +//////////////////////////////////////// + +WiFi_RingBuffer::~WiFi_RingBuffer() {} + +//////////////////////////////////////// + +void WiFi_RingBuffer::reset() +{ + ringBufP = ringBuf; +} + +//////////////////////////////////////// + +void WiFi_RingBuffer::init() +{ + ringBufP = ringBuf; + memset(ringBuf, 0, _size + 1); +} + +//////////////////////////////////////// + +void WiFi_RingBuffer::push(char c) +{ + *ringBufP = c; + ringBufP++; + + if (ringBufP >= ringBufEnd) + ringBufP = ringBuf; +} + +//////////////////////////////////////// + +bool WiFi_RingBuffer::endsWith(const char* str) +{ + int findStrLen = strlen(str); + + // b is the start position into the ring buffer + char* b = ringBufP - findStrLen; + + if (b < ringBuf) + b = b + _size; + + char *p1 = (char*)&str[0]; + char *p2 = p1 + findStrLen; + + for (char *p = p1; p < p2; p++) + { + if (*p != *b) + return false; + + b++; + + if (b == ringBufEnd) + b = ringBuf; + } + + return true; +} + +//////////////////////////////////////// + +void WiFi_RingBuffer::getStr(char * destination, unsigned int skipChars) +{ + unsigned int len = ringBufP - ringBuf - skipChars; + + // copy buffer to destination string + strncpy(destination, ringBuf, len); + + // terminate output string + //destination[len]=0; +} + +//////////////////////////////////////// + +void WiFi_RingBuffer::getStrN(char * destination, unsigned int skipChars, unsigned int num) +{ + unsigned int len = ringBufP - ringBuf - skipChars; + + if (len > num) + len = num; + + // copy buffer to destination string + strncpy(destination, ringBuf, len); + + // terminate output string + //destination[len]=0; +} diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.h b/software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.h new file mode 100644 index 000000000..819e77039 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/RingBuffer.h @@ -0,0 +1,68 @@ +/**************************************************************************************************************************** + RingBuffer.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#pragma once + +#ifndef RingBuffer_H +#define RingBuffer_H + +//////////////////////////////////////// + +class WiFi_RingBuffer +{ + public: + WiFi_RingBuffer(unsigned int size); + ~WiFi_RingBuffer(); + + void reset(); + void init(); + void push(char c); + int getPos(); + bool endsWith(const char* str); + void getStr(char * destination, unsigned int skipChars); + void getStrN(char * destination, unsigned int skipChars, unsigned int num); + + private: + + unsigned int _size; + char* ringBuf; + char* ringBufEnd; + char* ringBufP; +}; + +//////////////////////////////////////// + +#endif // RingBuffer_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/WiFiDebug.h b/software/firmware/source/libraries/WiFiWebServer/src/utility/WiFiDebug.h new file mode 100644 index 000000000..2c07e5a0b --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/WiFiDebug.h @@ -0,0 +1,116 @@ +/**************************************************************************************************************************** + WiFiDebug.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#pragma once + +#ifndef WiFiDebug_H +#define WiFiDebug_H + +#if defined(ARDUINO) + #if ARDUINO >= 100 + #include + #else + #include + #endif +#endif + +#include + +#ifdef DEBUG_WIFI_WEBSERVER_PORT + #define WS_DEBUG_OUTPUT DEBUG_WIFI_WEBSERVER_PORT +#else + #define WS_DEBUG_OUTPUT Serial +#endif + +// Change _WIFI_LOGLEVEL_ to set tracing and logging verbosity +// 0: DISABLED: no logging +// 1: ERROR: errors +// 2: WARN: errors and warnings +// 3: INFO: errors, warnings and informational (default) +// 4: DEBUG: errors, warnings, informational and debug + +#ifndef _WIFI_LOGLEVEL_ + #define _WIFI_LOGLEVEL_ 0 +#endif + +const char WWS_MARK[] = "[WIFI] "; +const char WWS_SPACE[] = " "; +const char WWS_LINE[] = "========================================\n"; + +#define WWS_PRINT_MARK WWS_PRINT(WWS_MARK) +#define WWS_PRINT_SP WWS_PRINT(WWS_SPACE) +#define WWS_PRINT_LINE WWS_PRINT(WWS_LINE) + +#define WWS_PRINT WS_DEBUG_OUTPUT.print +#define WWS_PRINTLN WS_DEBUG_OUTPUT.println + +/////////////////////////////////////// + +#define WS_LOGERROR(x) if(_WIFI_LOGLEVEL_>0) { WWS_PRINT_MARK; WWS_PRINTLN(x); } +#define WS_LOGERROR0(x) if(_WIFI_LOGLEVEL_>0) { WWS_PRINT(x); } +#define WS_LOGERROR1(x,y) if(_WIFI_LOGLEVEL_>0) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINTLN(y); } +#define WS_LOGERROR2(x,y,z) if(_WIFI_LOGLEVEL_>0) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINTLN(z); } +#define WS_LOGERROR3(x,y,z,w) if(_WIFI_LOGLEVEL_>0) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINTLN(w); } +#define WS_LOGERROR5(x,y,z,w, xx, yy) if(_WIFI_LOGLEVEL_>0) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINT(w); WWS_PRINT_SP; WWS_PRINT(xx); WWS_PRINT_SP; WWS_PRINTLN(yy);} + +/////////////////////////////////////// + +#define WS_LOGWARN(x) if(_WIFI_LOGLEVEL_>1) { WWS_PRINT_MARK; WWS_PRINTLN(x); } +#define WS_LOGWARN0(x) if(_WIFI_LOGLEVEL_>1) { WWS_PRINT(x); } +#define WS_LOGWARN1(x,y) if(_WIFI_LOGLEVEL_>1) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINTLN(y); } +#define WS_LOGWARN2(x,y,z) if(_WIFI_LOGLEVEL_>1) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINTLN(z); } +#define WS_LOGWARN3(x,y,z,w) if(_WIFI_LOGLEVEL_>1) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINTLN(w); } +#define WS_LOGWARN5(x,y,z,w, xx, yy) if(_WIFI_LOGLEVEL_>1) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINT(w); WWS_PRINT_SP; WWS_PRINT(xx); WWS_PRINT_SP; WWS_PRINTLN(yy);} + +/////////////////////////////////////// + +#define WS_LOGINFO(x) if(_WIFI_LOGLEVEL_>2) { WWS_PRINT_MARK; WWS_PRINTLN(x); } +#define WS_LOGINFO0(x) if(_WIFI_LOGLEVEL_>2) { WWS_PRINT(x); } +#define WS_LOGINFO1(x,y) if(_WIFI_LOGLEVEL_>2) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINTLN(y); } +#define WS_LOGINFO2(x,y,z) if(_WIFI_LOGLEVEL_>2) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINTLN(z); } +#define WS_LOGINFO3(x,y,z,w) if(_WIFI_LOGLEVEL_>2) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINTLN(w); } +#define WS_LOGINFO5(x,y,z,w, xx, yy) if(_WIFI_LOGLEVEL_>2) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINT(w); WWS_PRINT_SP; WWS_PRINT(xx); WWS_PRINT_SP; WWS_PRINTLN(yy);} + +/////////////////////////////////////// + +#define WS_LOGDEBUG(x) if(_WIFI_LOGLEVEL_>3) { WWS_PRINT_MARK; WWS_PRINTLN(x); } +#define WS_LOGDEBUG0(x) if(_WIFI_LOGLEVEL_>3) { WWS_PRINT(x); } +#define WS_LOGDEBUG1(x,y) if(_WIFI_LOGLEVEL_>3) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINTLN(y); } +#define WS_LOGDEBUG2(x,y,z) if(_WIFI_LOGLEVEL_>3) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINTLN(z); } +#define WS_LOGDEBUG3(x,y,z,w) if(_WIFI_LOGLEVEL_>3) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINTLN(w); } +#define WS_LOGDEBUG5(x,y,z,w, xx, yy) if(_WIFI_LOGLEVEL_>3) { WWS_PRINT_MARK; WWS_PRINT(x); WWS_PRINT_SP; WWS_PRINT(y); WWS_PRINT_SP; WWS_PRINT(z); WWS_PRINT_SP; WWS_PRINT(w); WWS_PRINT_SP; WWS_PRINT(xx); WWS_PRINT_SP; WWS_PRINTLN(yy);} + +#endif // WiFiDebug_H diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.cpp b/software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.cpp new file mode 100644 index 000000000..070f3fb25 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.cpp @@ -0,0 +1,151 @@ +/**************************************************************************************************************************** + mimetable.cpp - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#if (ESP32 || ESP8266) + +#include "mimetable.h" +#include "pgmspace.h" +#include "WString.h" + +//////////////////////////////////////// + +namespace mime_esp +{ +static const char kHtmlSuffix[] PROGMEM = ".html"; +static const char kHtmSuffix[] PROGMEM = ".htm"; +static const char kTxtSuffix[] PROGMEM = ".txt"; + +static const char kCssSuffix[] PROGMEM = ".css"; +static const char kJsSuffix[] PROGMEM = ".js"; +static const char kJsonSuffix[] PROGMEM = ".json"; +static const char kPngSuffix[] PROGMEM = ".png"; +static const char kGifSuffix[] PROGMEM = ".gif"; +static const char kJpgSuffix[] PROGMEM = ".jpg"; +static const char kJpegSuffix[] PROGMEM = ".jpeg"; +static const char kIcoSuffix[] PROGMEM = ".ico"; +static const char kSvgSuffix[] PROGMEM = ".svg"; +static const char kTtfSuffix[] PROGMEM = ".ttf"; +static const char kOtfSuffix[] PROGMEM = ".otf"; +static const char kWoffSuffix[] PROGMEM = ".woff"; +static const char kWoff2Suffix[] PROGMEM = ".woff2"; +static const char kEotSuffix[] PROGMEM = ".eot"; +static const char kSfntSuffix[] PROGMEM = ".sfnt"; +static const char kXmlSuffix[] PROGMEM = ".xml"; +static const char kPdfSuffix[] PROGMEM = ".pdf"; +static const char kZipSuffix[] PROGMEM = ".zip"; +static const char kAppcacheSuffix[] PROGMEM = ".appcache"; + +static const char kGzSuffix[] PROGMEM = ".gz"; +static const char kDefaultSuffix[] PROGMEM = ""; + +//////////////////////////////////////// + +static const char kHtml[] PROGMEM = "text/html"; +static const char kTxt[] PROGMEM = "text/plain"; + +static const char kCss[] PROGMEM = "text/css"; +static const char kJs[] PROGMEM = "application/javascript"; +static const char kJson[] PROGMEM = "application/json"; +static const char kPng[] PROGMEM = "image/png"; +static const char kGif[] PROGMEM = "image/gif"; +static const char kJpg[] PROGMEM = "image/jpeg"; +static const char kJpeg[] PROGMEM = "image/jpeg"; +static const char kIco[] PROGMEM = "image/x-icon"; +static const char kSvg[] PROGMEM = "image/svg+xml"; +static const char kTtf[] PROGMEM = "application/x-font-ttf"; +static const char kOtf[] PROGMEM = "application/x-font-opentype"; +static const char kWoff[] PROGMEM = "application/font-woff"; +static const char kWoff2[] PROGMEM = "application/font-woff2"; +static const char kEot[] PROGMEM = "application/vnd.ms-fontobject"; +static const char kSfnt[] PROGMEM = "application/font-sfnt"; +static const char kXml[] PROGMEM = "text/xml"; +static const char kPdf[] PROGMEM = "application/pdf"; +static const char kZip[] PROGMEM = "application/zip"; +static const char kAppcache[] PROGMEM = "text/cache-manifest"; + +static const char kGz[] PROGMEM = "application/x-gzip"; +static const char kDefault[] PROGMEM = "application/octet-stream"; + +//////////////////////////////////////// + +const Entry mimeTable[maxType] PROGMEM = +{ + { kHtmlSuffix, kHtml }, + { kHtmSuffix, kHtml }, + { kCssSuffix, kCss }, + { kTxtSuffix, kTxt }, + { kJsSuffix, kJs }, + { kJsonSuffix, kJson }, + { kPngSuffix, kPng }, + { kGifSuffix, kGif }, + { kJpgSuffix, kJpg }, + { kIcoSuffix, kIco }, + { kSvgSuffix, kSvg }, + { kTtfSuffix, kTtf }, + { kOtfSuffix, kOtf }, + { kWoffSuffix, kWoff }, + { kWoff2Suffix, kWoff2 }, + { kEotSuffix, kEot }, + { kSfntSuffix, kSfnt }, + { kXmlSuffix, kXml }, + { kPdfSuffix, kPdf }, + { kZipSuffix, kZip }, + { kGzSuffix, kGz }, + { kAppcacheSuffix, kAppcache }, + { kDefaultSuffix, kDefault } +}; + +//////////////////////////////////////// + +String getContentType(const String& path) +{ + for (size_t i = 0; i < maxType; i++) + { + if (path.endsWith(FPSTR(mimeTable[i].endsWith))) + { + return String(FPSTR(mimeTable[i].mimeType)); + } + } + + // Fall-through and just return default type + return String(FPSTR(kDefault)); +} + +//////////////////////////////////////// + +} // namespace mime_esp + +#endif diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.h b/software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.h new file mode 100644 index 000000000..15210d4f6 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/esp_detail/mimetable.h @@ -0,0 +1,95 @@ +/**************************************************************************************************************************** + mimetable.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#pragma once + +#ifndef __ESP_MIMETABLE_H__ +#define __ESP_MIMETABLE_H__ + +#if (ESP32 || ESP8266) + +#include "WString.h" + +//////////////////////////////////////// + +namespace mime_esp +{ + +enum type +{ + html, + htm, + css, + txt, + js, + json, + png, + gif, + jpg, + ico, + svg, + ttf, + otf, + woff, + woff2, + eot, + sfnt, + xml, + pdf, + zip, + gz, + appcache, + none, + maxType +}; + +//////////////////////////////////////// + +struct Entry +{ + const char * endsWith; + const char * mimeType; +}; + +//////////////////////////////////////// + +extern const Entry mimeTable[maxType]; + +String getContentType(const String& path); +} + +#endif // #if (ESP32 || ESP8266) +#endif // #ifndef __ESP_MIMETABLE_H__ diff --git a/software/firmware/source/libraries/WiFiWebServer/src/utility/mimetable.h b/software/firmware/source/libraries/WiFiWebServer/src/utility/mimetable.h new file mode 100644 index 000000000..f3bbf0c80 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/src/utility/mimetable.h @@ -0,0 +1,110 @@ +/**************************************************************************************************************************** + mimetable.h - Dead simple web-server. + For any WiFi shields, such as WiFiNINA W101, W102, W13x, or custom, such as ESP8266/ESP32-AT, Ethernet, etc + + WiFiWebServer is a library for the ESP32-based WiFi shields to run WebServer + Forked and modified from ESP8266 https://github.com/esp8266/Arduino/releases + Forked and modified from Arduino WiFiNINA library https://www.arduino.cc/en/Reference/WiFiNINA + Built by Khoi Hoang https://github.com/khoih-prog/WiFiWebServer + Licensed under MIT license + + Original author: + @file Esp8266WebServer.h + @author Ivan Grokhotkov + + Version: 1.10.1 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 12/02/2020 Initial coding for SAMD21, Nano 33 IoT, etc running WiFiNINA + ... + 1.6.0 K Hoang 13/02/2022 Add support to new ESP32-S3 and ESP32_C3 + 1.6.1 K Hoang 13/02/2022 Fix v1.6.0 issue + 1.6.2 K Hoang 22/02/2022 Add support to megaAVR using Arduino megaAVR core + 1.6.3 K Hoang 02/03/2022 Fix decoding error bug + 1.7.0 K Hoang 05/04/2022 Fix issue with Portenta_H7 core v2.7.2+ + 1.8.0 K Hoang 26/04/2022 Add WiFiMulti library support and examples + 1.9.0 K Hoang 12/08/2022 Add support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.1 K Hoang 13/08/2022 Add WiFiMulti support to RASPBERRY_PI_PICO_W using CYW4343 WiFi + 1.9.2 K Hoang 16/08/2022 Workaround for RP2040W WiFi.status() bug + 1.9.3 K Hoang 16/08/2022 Better workaround for RP2040W WiFi.status() bug using ping() to local gateway + 1.9.4 K Hoang 06/09/2022 Restore support to ESP32 and ESP8266 + 1.9.5 K Hoang 10/09/2022 Restore support to Teensy, etc. Fix bug in examples + 1.10.0 K Hoang 13/11/2022 Add new features, such as CORS. Update code and examples + 1.10.1 K Hoang 24/11/2022 Using new WiFi101_Generic library to send larger data + *****************************************************************************************************************************/ + +#pragma once + +#ifndef __MIMETABLE_H__ +#define __MIMETABLE_H__ + +namespace mime +{ + +enum type +{ + html, + htm, + css, + txt, + js, + json, + png, + gif, + jpg, + ico, + svg, + ttf, + otf, + woff, + woff2, + eot, + sfnt, + xml, + pdf, + zip, + gz, + appcache, + none, + maxType +}; + +struct Entry +{ + const char endsWith[16]; + const char mimeType[32]; +}; + +// Table of extension->MIME strings stored in PROGMEM, needs to be global due to GCC section typing rules +const Entry mimeTable[maxType] = +{ + { ".html", "text/html" }, + { ".htm", "text/html" }, + { ".css", "text/css" }, + { ".txt", "text/plain" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".png", "image/png" }, + { ".gif", "image/gif" }, + { ".jpg", "image/jpeg" }, + { ".ico", "image/x-icon" }, + { ".svg", "image/svg+xml" }, + { ".ttf", "application/x-font-ttf" }, + { ".otf", "application/x-font-opentype" }, + { ".woff", "application/font-woff" }, + { ".woff2", "application/font-woff2" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".sfnt", "application/font-sfnt" }, + { ".xml", "text/xml" }, + { ".pdf", "application/pdf" }, + { ".zip", "application/zip" }, + { ".gz", "application/x-gzip" }, + { ".appcache", "text/cache-manifest" }, + { "", "application/octet-stream" } +}; +//extern const Entry mimeTable[maxType]; +} + + +#endif // __MIMETABLE_H__ diff --git a/software/firmware/source/libraries/WiFiWebServer/utils/astyle_library.conf b/software/firmware/source/libraries/WiFiWebServer/utils/astyle_library.conf new file mode 100644 index 000000000..8a73bc276 --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/utils/astyle_library.conf @@ -0,0 +1,70 @@ +# Code formatting rules for Arduino libraries, modified from for KH libraries: +# +# https://github.com/arduino/Arduino/blob/master/build/shared/examples_formatter.conf +# + +# astyle --style=allman -s2 -t2 -C -S -xW -Y -M120 -f -p -xg -H -xb -c --xC120 -xL *.h *.cpp *.ino + +--mode=c +--lineend=linux +--style=allman + +# -r or -R +#--recursive + +# -c => Converts tabs into spaces +convert-tabs + +# -s2 => 2 spaces indentation +--indent=spaces=2 + +# -t2 => tab =2 spaces +#--indent=tab=2 + +# -C +--indent-classes + +# -S +--indent-switches + +# -xW +--indent-preproc-block + +# -Y => indent classes, switches (and cases), comments starting at column 1 +--indent-col1-comments + +# -M120 => maximum of 120 spaces to indent a continuation line +--max-continuation-indent=120 + +# -xC120 => max‑code‑length will break a line if the code exceeds # characters +--max-code-length=120 + +# -f => +--break-blocks + +# -p => put a space around operators +--pad-oper + +# -xg => Insert space padding after commas +--pad-comma + +# -H => put a space after if/for/while +pad-header + +# -xb => Break one line headers (e.g. if/for/while) +--break-one-line-headers + +# -c => Converts tabs into spaces +#--convert-tabs + +# if you like one-liners, keep them +#keep-one-line-statements + +# -xV +--attach-closing-while + +#unpad-paren + +# -xp +remove-comment-prefix + diff --git a/software/firmware/source/libraries/WiFiWebServer/utils/restyle.sh b/software/firmware/source/libraries/WiFiWebServer/utils/restyle.sh new file mode 100644 index 000000000..bcd846f7a --- /dev/null +++ b/software/firmware/source/libraries/WiFiWebServer/utils/restyle.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +for dir in . ; do + find $dir -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" -o -name "*.ino" \) -exec astyle --suffix=none --options=./utils/astyle_library.conf \{\} \; +done + diff --git a/software/firmware/source/libraries/functional-vlpp/.gitignore b/software/firmware/source/libraries/functional-vlpp/.gitignore new file mode 100644 index 000000000..259148fa1 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/.gitignore @@ -0,0 +1,32 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/software/firmware/source/libraries/functional-vlpp/CONTRIBUTING.md b/software/firmware/source/libraries/functional-vlpp/CONTRIBUTING.md new file mode 100644 index 000000000..65a021b15 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/CONTRIBUTING.md @@ -0,0 +1,52 @@ +## Contributing to functional-vlpp + +### Reporting Bugs + +Please report bugs in Functional-Vlpp if you find them. + +However, before reporting a bug please check through the following: + +* [Existing Open Issues](https://github.com/khoih-prog/functional-vlpp/issues) - someone might have already encountered this. + +If you don't find anything, please [open a new issue](https://github.com/khoih-prog/functional-vlpp/issues/new). + +### How to submit a bug report + +Please ensure to specify the following: + +* Arduino IDE version (e.g. 1.8.13) or Platform.io version +* `SAMD` Core Version (e.g. Arduino SAMD core v1.8.11, Adafruit SAMD core v1.6.5, Seeed Studio SAMD v1.8.1) +* Contextual information (e.g. what you were trying to achieve) +* Simplest possible steps to reproduce +* Anything that might be relevant in your opinion, such as: + * Operating system (Windows, Ubuntu, etc.) and the output of `uname -a` + * Network configuration + + +### Example + +``` +Arduino IDE version: 1.8.13 +Arduino SAMD Core Version 1.8.11 +OS: Ubuntu 20.04 LTS +Linux xy-Inspiron-3593 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux + +Context: +The board couldn't autoreconnect to Local Blynk Server after router power recycling. + +Steps to reproduce: +1. ... +2. ... +3. ... +4. ... +``` + +### Sending Feature Requests + +Feel free to post feature requests. It's helpful if you can explain exactly why the feature would be useful. + +There are usually some outstanding feature requests in the [existing issues list](https://github.com/khoih-prog/functional-vlpp/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement), feel free to add comments to them. + +### Sending Pull Requests + +Pull Requests with changes and fixes are also welcome! diff --git a/software/firmware/source/libraries/functional-vlpp/LICENSE b/software/firmware/source/libraries/functional-vlpp/LICENSE new file mode 100644 index 000000000..cfe22f4c5 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Khoi Hoang + +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/software/firmware/source/libraries/functional-vlpp/README.md b/software/firmware/source/libraries/functional-vlpp/README.md new file mode 100644 index 000000000..828f93231 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/README.md @@ -0,0 +1,441 @@ +## functional-vlpp Library + +[![arduino-library-badge](https://www.ardu-badge.com/badge/Functional-Vlpp.svg?)](https://www.ardu-badge.com/Functional-Vlpp) +[![GitHub release](https://img.shields.io/github/release/khoih-prog/Functional-Vlpp.svg)](https://github.com/khoih-prog/Functional-Vlpp/releases) +[![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/khoih-prog/Functional-Vlpp/blob/master/LICENSE) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](#Contributing) +[![GitHub issues](https://img.shields.io/github/issues/khoih-prog/Functional-Vlpp.svg)](http://github.com/khoih-prog/Functional-Vlpp/issues) + +--- +--- + +## Table of Contents + +* [Why do we need this functional-vlpp library](#why-do-we-need-this-functional-vlpp-library) + * [Features](#features) + * [Currently supported Boards](#currently-supported-boards) +* [Changelog](#changelog) + * [Release v1.0.2](#release-v102) + * [Release v1.0.1](#release-v101) + * [Release v1.0.0](#release-v100) +* [Prerequisites](#prerequisites) +* [Installation](#installation) + * [Use Arduino Library Manager](#use-arduino-library-manager) + * [Manual Install](#manual-install) + * [VS Code & PlatformIO](#vs-code--platformio) +* [Packages' Patches](#packages-patches) + * [1. For Adafruit nRF52840 and nRF52832 boards](#1-for-adafruit-nRF52840-and-nRF52832-boards) + * [2. For Teensy boards](#2-for-teensy-boards) + * [3. For Arduino SAM DUE boards](#3-for-arduino-sam-due-boards) + * [4. For Arduino SAMD boards](#4-for-arduino-samd-boards) + * [For core version v1.8.10+](#for-core-version-v1810) + * [For core version v1.8.9-](#for-core-version-v189-) + * [5. For Adafruit SAMD boards](#5-for-adafruit-samd-boards) + * [6. For Seeeduino SAMD boards](#6-for-seeeduino-samd-boards) + * [7. For STM32 boards](#7-for-stm32-boards) +* [How to use](#how-to-use) +* [Libraries using this Functional-Vlpp library](#libraries-using-this-functional-vlpp-library) +* [How to use in your sketch](#how-to-use-in-your-sketch) +* [Releases](#releases) +* [Issues](#issues) +* [Contributions and Thanks](#contributions-and-thanks) +* [Contributing](#contributing) +* [License](#license) +* [Copyright](#copyright) + +--- +--- + +### Why do we need this [functional-vlpp library](https://github.com/khoih-prog/functional-vlpp) + +#### Features + +The original [**vczh-libraries/Vlpp**](https://github.com/vczh-libraries/Vlpp) provides common C++ construction, including string operation / generic container / linq, function templates to better support **C++ functional programming across platforms**. + +The `functional portion` of the project was then forked and modified to be used for Arduino by [**Marcus Rugger's functional-vlpp library**](https://github.com/marcusrugger/functional-vlpp). + +This [**Functional-Vlpp library**](https://github.com/khoih-prog/Functional-Vlpp) is based on, modified to use and tested working **OK** in other architectures such as **Teensy, SAM-DUE, SAMD, STM32, eps8266, esp32**. + +#### Currently supported Boards + +This [**Functional-Vlpp library**](https://github.com/khoih-prog/Functional-Vlpp) currently supports these following boards: + + 1. **nRF52 boards**, such as **AdaFruit Feather nRF52832, nRF52840 Express, BlueFruit Sense, Itsy-Bitsy nRF52840 Express, Metro nRF52840 Express, NINA_B302_ublox, NINA_B112_ublox, etc.** + + 2. **SAMD21** + - Arduino SAMD21: ZERO, MKRs, NANO_33_IOT, etc. + - Adafruit SAMD21 (M0): ItsyBitsy M0, Feather M0, Feather M0 Express, Metro M0 Express, Circuit Playground Express, Trinket M0, PIRkey, Hallowing M0, Crickit M0, etc. + - Seeeduino: LoRaWAN, Zero, Femto M0, XIAO M0, Wio GPS Board, etc. + + 3. **SAMD51** + - Adafruit SAMD51 (M4): Metro M4, Grand Central M4, ItsyBitsy M4, Feather M4 Express, Trellis M4, Metro M4 AirLift Lite, MONSTER M4SK Express, Hallowing M4, etc. + - Seeeduino: Wio Terminal, Grove UI Wireless + + 4. **SAM DUE** + 5. **Teensy (4.1, 4.0, 3.6, 3.5, 3,2, 3.1, 3.0)** + 6. **STM32F/L/H/G/WB/MP1 boards (with 32+K Flash)** + - Nucleo-144 + - Nucleo-64 + - Discovery + - Generic STM32F0, STM32F1, STM32F2, STM32F3, STM32F4, STM32F7 (with 64+K Flash): x8 and up + - STM32L0, STM32L1, STM32L4 + - STM32G0, STM32G4 + - STM32H7 + - STM32WB + - STM32MP1 + - LoRa boards + - 3-D printer boards + - Generic Flight Controllers + - Midatronics boards + + 7. **ESP32** + 8. **ESP8266** + +--- +--- + +## Changelog + +### Release v1.0.2 + +1. Clear compiler warnings. +2. Add Version String +3. Add Table of Contents + +### Release v1.0.1 + +1. Add support to and tested working **OK** in other architectures such as **Teensy, SAM, SAMD, STM32, eps8266, esp32** besides AVR. + +### Release v1.0.0 + +1. Based on and modified from [**Marcus Rugger's functional-vlpp Library**](https://github.com/marcusrugger/functional-vlpp). + +--- +--- + +## Prerequisites + + 1. [`Arduino IDE 1.8.13+` for Arduino](https://www.arduino.cc/en/Main/Software) + 2. [`Arduino AVR core 1.8.3+`](https://github.com/arduino/ArduinoCore-avr) for Arduino AVR boards. Use Arduino Board Manager to install. [![GitHub release](https://img.shields.io/github/release/arduino/ArduinoCore-avr.svg)](https://github.com/arduino/ArduinoCore-avr/releases/latest) + 3. [`Arduino Core for STM32 v1.9.0+`](https://github.com/stm32duino/Arduino_Core_STM32) for STM32 boards. [![GitHub release](https://img.shields.io/github/release/stm32duino/Arduino_Core_STM32.svg)](https://github.com/stm32duino/Arduino_Core_STM32/releases/latest) + 4. [`Teensy core 1.51+`](https://www.pjrc.com/teensy/td_download.html) for Teensy (4.1, 4.0, 3.6, 3.5, 3,2, 3.1, 3.0, LC) boards + 5. [`Arduino SAM DUE core 1.6.12+`](https://github.com/arduino/ArduinoCore-sam) for SAM DUE ARM Cortex-M3 boards + 6. [`Arduino SAMD core 1.8.11+`](https://www.arduino.cc/en/Guide/ArduinoM0) for SAMD ARM Cortex-M0+ boards. [![GitHub release](https://img.shields.io/github/release/arduino/ArduinoCore-samd.svg)](https://github.com/arduino/ArduinoCore-samd/releases/latest) + 7. [`Adafruit SAMD core 1.6.5+`](https://www.adafruit.com/) for SAMD ARM Cortex-M0+ and M4 boards (Nano 33 IoT, etc.). [![GitHub release](https://img.shields.io/github/release/adafruit/ArduinoCore-samd.svg)](https://github.com/adafruit/ArduinoCore-samd/releases/latest) + 8. [`Seeeduino SAMD core 1.8.1+`](https://github.com/Seeed-Studio/ArduinoCore-samd) for SAMD21/SAMD51 boards (XIAO M0, Wio Terminal, etc.). [![Latest release](https://img.shields.io/github/release/Seeed-Studio/ArduinoCore-samd.svg)](https://github.com/Seeed-Studio/ArduinoCore-samd/releases/latest/) + 9. [`Adafruit nRF52 v0.21.0+`](https://www.adafruit.com) for nRF52 boards such as Adafruit NRF52840_FEATHER, NRF52832_FEATHER, NRF52840_FEATHER_SENSE, NRF52840_ITSYBITSY, NRF52840_CIRCUITPLAY, NRF52840_CLUE, NRF52840_METRO, NRF52840_PCA10056, PARTICLE_XENON, **NINA_B302_ublox, NINA_B112_ublox**, etc. [![GitHub release](https://img.shields.io/github/release/adafruit/Adafruit_nRF52_Arduino.svg)](https://github.com/adafruit/Adafruit_nRF52_Arduino/releases/latest) + +--- + +## Installation + +### Use Arduino Library Manager + +The best and easiest way is to use `Arduino Library Manager`. Search for `Functional-Vlpp`, then select / install the latest version. +You can also use this link [![arduino-library-badge](https://www.ardu-badge.com/badge/Functional-Vlpp.svg?)](https://www.ardu-badge.com/Functional-Vlpp) for more detailed instructions. + +### Manual Install + +1. Navigate to [Functional-Vlpp](https://github.com/khoih-prog/Functional-Vlpp) page. +2. Download the latest release `Functional-Vlpp-master.zip`. +3. Extract the zip file to `Functional-Vlpp-master` directory +4. Copy whole + - `Functional-Vlpp-master` folder to Arduino libraries' directory such as `~/Arduino/libraries/`. + +### VS Code & PlatformIO: + +1. Install [VS Code](https://code.visualstudio.com/) +2. Install [PlatformIO](https://platformio.org/platformio-ide) +3. Install [**Functional-Vlpp** library](https://platformio.org/lib/show/7082/Functional-Vlpp) by using [Library Manager](https://platformio.org/lib/show/7082/Functional-Vlpp/installation). Search for **Functional-Vlpp** in [Platform.io Author's Libraries](https://platformio.org/lib/search?query=author:%22Khoi%20Hoang%22) +4. Use included [platformio.ini](platformio/platformio.ini) file from examples to ensure that all dependent libraries will installed automatically. Please visit documentation for the other options and examples at [Project Configuration File](https://docs.platformio.org/page/projectconf.html) + +--- + +### Packages' Patches + +#### 1. For Adafruit nRF52840 and nRF52832 boards + +**To be able to compile, run and automatically detect and display BOARD_NAME on nRF52840/nRF52832 boards**, you have to copy the whole [nRF52 0.21.0](Packages_Patches/adafruit/hardware/nrf52/0.21.0) directory into Adafruit nRF52 directory (~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0). + +Supposing the Adafruit nRF52 version is 0.21.0. These files must be copied into the directory: +- `~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0/platform.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0/boards.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0/variants/NINA_B302_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0/variants/NINA_B302_ublox/variant.cpp` +- `~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0/variants/NINA_B112_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0/variants/NINA_B112_ublox/variant.cpp` +- **`~/.arduino15/packages/adafruit/hardware/nrf52/0.21.0/cores/nRF5/Udp.h`** + +Whenever a new version is installed, remember to copy these files into the new version directory. For example, new version is x.yy.z +These files must be copied into the directory: + +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/platform.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/boards.txt` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B302_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B302_ublox/variant.cpp` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B112_ublox/variant.h` +- `~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/variants/NINA_B112_ublox/variant.cpp` +- **`~/.arduino15/packages/adafruit/hardware/nrf52/x.yy.z/cores/nRF5/Udp.h`** + +#### 2. For Teensy boards + + **To be able to compile and run on Teensy boards**, you have to copy the files in [**Packages_Patches for Teensy directory**](Packages_Patches/hardware/teensy/avr) into Teensy hardware directory (./arduino-1.8.13/hardware/teensy/avr/boards.txt). + +Supposing the Arduino version is 1.8.13. These files must be copied into the directory: + +- `./arduino-1.8.13/hardware/teensy/avr/boards.txt` +- `./arduino-1.8.13/hardware/teensy/avr/cores/teensy/Stream.h` +- `./arduino-1.8.13/hardware/teensy/avr/cores/teensy3/Stream.h` +- `./arduino-1.8.13/hardware/teensy/avr/cores/teensy4/Stream.h` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +These files must be copied into the directory: + +- `./arduino-x.yy.zz/hardware/teensy/avr/boards.txt` +- `./arduino-x.yy.zz/hardware/teensy/avr/cores/teensy/Stream.h` +- `./arduino-x.yy.zz/hardware/teensy/avr/cores/teensy3/Stream.h` +- `./arduino-x.yy.zz/hardware/teensy/avr/cores/teensy4/Stream.h` + +#### 3. For Arduino SAM DUE boards + + **To be able to compile and run on SAM DUE boards**, you have to copy the whole [SAM DUE](Packages_Patches/arduino/hardware/sam/1.6.12) directory into Arduino sam directory (~/.arduino15/packages/arduino/hardware/sam/1.6.12). + +Supposing the Arduino SAM core version is 1.6.12. This file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/sam/1.6.12/platform.txt` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/sam/x.yy.zz/platform.txt` + +#### 4. For Arduino SAMD boards + + ***To be able to compile without error and automatically detect and display BOARD_NAME on Arduino SAMD (Nano-33-IoT, etc) boards***, you have to copy the whole [Arduino SAMD cores 1.8.10](Packages_Patches/arduino/hardware/samd/1.8.10) directory into Arduino SAMD directory (~/.arduino15/packages/arduino/hardware/samd/1.8.10). + +#### For core version v1.8.10+ + +Supposing the Arduino SAMD version is 1.8.11. Now only one file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/1.8.11/platform.txt` + +Whenever a new version is installed, remember to copy this files into the new version directory. For example, new version is x.yy.zz + +This file must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/x.yy.zz/platform.txt` + +#### For core version v1.8.9- + +Supposing the Arduino SAMD version is 1.8.9. These files must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/1.8.9/platform.txt` +- ***`~/.arduino15/packages/arduino/hardware/samd/1.8.9/cores/arduino/Arduino.h`*** + +Whenever a new version is installed, remember to copy these files into the new version directory. For example, new version is x.yy.z + +These files must be copied into the directory: + +- `~/.arduino15/packages/arduino/hardware/samd/x.yy.z/platform.txt` +- ***`~/.arduino15/packages/arduino/hardware/samd/x.yy.z/cores/arduino/Arduino.h`*** + + This is mandatory to fix the ***notorious Arduino SAMD compiler error***. See [Improve Arduino compatibility with the STL (min and max macro)](https://github.com/arduino/ArduinoCore-samd/pull/399) + +``` + ...\arm-none-eabi\include\c++\7.2.1\bits\stl_algobase.h:243:56: error: macro "min" passed 3 arguments, but takes just 2 + min(const _Tp& __a, const _Tp& __b, _Compare __comp) +``` + +Whenever the above-mentioned compiler error issue is fixed with the new Arduino SAMD release, you don't need to copy the `Arduino.h` file anymore. + +#### 5. For Adafruit SAMD boards + + ***To be able to automatically detect and display BOARD_NAME on Adafruit SAMD (Itsy-Bitsy M4, etc) boards***, you have to copy the file [Adafruit SAMD platform.txt](Packages_Patches/adafruit/hardware/samd/1.6.4) into Adafruit samd directory (~/.arduino15/packages/adafruit/hardware/samd/1.6.4). + +Supposing the Adafruit SAMD core version is 1.6.4. This file must be copied into the directory: + +- `~/.arduino15/packages/adafruit/hardware/samd/1.6.4/platform.txt` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied into the directory: + +- `~/.arduino15/packages/adafruit/hardware/samd/x.yy.zz/platform.txt` + +#### 6. For Seeeduino SAMD boards + + ***To be able to automatically detect and display BOARD_NAME on Seeeduino SAMD (XIAO M0, Wio Terminal, etc) boards***, you have to copy the file [Seeeduino SAMD platform.txt](Packages_Patches/Seeeduino/hardware/samd/1.8.1) into Adafruit samd directory (~/.arduino15/packages/Seeeduino/hardware/samd/1.8.1). + +Supposing the Seeeduino SAMD core version is 1.8.1. This file must be copied into the directory: + +- `~/.arduino15/packages/Seeeduino/hardware/samd/1.8.1/platform.txt` + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz +This file must be copied into the directory: + +- `~/.arduino15/packages/Seeeduino/hardware/samd/x.yy.zz/platform.txt` + +#### 7. For STM32 boards + +**To use Serial1 on some STM32 boards without Serial1 definition (Nucleo-144 NUCLEO_F767ZI, Nucleo-64 NUCLEO_L053R8, etc.) boards**, you have to copy the files [STM32 variant.h](Packages_Patches/STM32/hardware/stm32/1.9.0) into STM32 stm32 directory (~/.arduino15/packages/STM32/hardware/stm32/1.9.0). You have to modify the files corresponding to your boards, this is just an illustration how to do. + +Supposing the STM32 stm32 core version is 1.9.0. These files must be copied into the directory: + +- `~/.arduino15/packages/STM32/hardware/stm32/1.9.0/variants/NUCLEO_F767ZI/variant.h` for Nucleo-144 NUCLEO_F767ZI. +- `~/.arduino15/packages/STM32/hardware/stm32/1.9.0/variants/NUCLEO_L053R8/variant.h` for Nucleo-64 NUCLEO_L053R8. + +Whenever a new version is installed, remember to copy this file into the new version directory. For example, new version is x.yy.zz, +theses files must be copied into the corresponding directory: + +- `~/.arduino15/packages/STM32/hardware/stm32/x.yy.zz/variants/NUCLEO_F767ZI/variant.h` +- `~/.arduino15/packages/STM32/hardware/stm32/x.yy.zz/variants/NUCLEO_L053R8/variant.h` + +--- +--- + +### How to use + +This library can be used as a replacement of C++11 STL `std::function`. + +For example, we can use `typedef vl::Func THandlerFunction` instead of `typedef std::function THandlerFunction` or `typedef void (*THandlerFunction)(void)`. + +Its importance is the ability to be used in complicated function calls, such as private Lambda functions in Classes. + +For example: + +```cpp +class BlynkEthernet + : public BlynkProtocol +{ + typedef BlynkProtocol Base; +public: + +... + +private: + EthernetWebServer *server; + ... + + void startConfigurationMode() + { + ... + if (!server) + server = new EthernetWebServer; + + if (server) + { + server->on("/", [this](){ handleRequest(); }); + server->begin(); + } + ... + } +}; + +``` + +--- +--- + +### Libraries using this [Functional-Vlpp library](https://github.com/khoih-prog/Functional-Vlpp) + +This [**Functional-Vlpp library**](https://github.com/khoih-prog/Functional-Vlpp) has been used sucessfully in : + +1. [ESP8266_AT_WebServer library for Arduino, Teensy, SAM, SAMD, STM32, etc. architectures](https://github.com/khoih-prog/ESP8266_AT_WebServer) +2. [EthernetWebServer library for Arduino, ESP,Teensy, SAM, SAMD, etc. architectures](https://github.com/khoih-prog/EthernetWebServer) +3. [EthernetWebServer_STM32 library for STM32 architectures](https://github.com/khoih-prog/EthernetWebServer_STM32) +4. [Blynk_Esp8266AT_WM library for Arduino, Teensy, SAM, SAMD, STM32, etc. architectures](https://github.com/khoih-prog/Blynk_Esp8266AT_WM) +5. [BlynkEthernet_WM library for Arduino, ESP,Teensy, SAM, SAMD, etc. architectures](https://github.com/khoih-prog/BlynkEthernet_WM) +6. [BlynkEthernet_STM32_WM library for Arduino, Teensy, SAM, SAMD, STM32, etc. architectures](https://github.com/khoih-prog/BlynkEthernet_STM32_WM) +7. [WiFiManager_NINA_Lite library for Arduino, Teensy, SAM, SAMD, STM32, etc. architectures](https://github.com/khoih-prog/WiFiManager_NINA_Lite) +8. [WiFiWebServer library for Arduino, Teensy, SAM, SAMD, STM32, etc. architectures](https://github.com/khoih-prog/WiFiWebServer) + +and many more to come. + +--- +--- + +### How to use in your sketch + +Just include in your sketch and modify as follows + +```cpp +// For this functional-vlpp library +#include + +// Modify from +//typedef std::function THandlerFunction; +// or +//typedef void (*THandlerFunction)(void); +// to this +typedef vl::Func THandlerFunction; +// That's it + +// Keep these the same +void on(const String &uri, THandlerFunction handler); +void on(const String &uri, HTTPMethod method, THandlerFunction fn); +void on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn); +void addHandler(RequestHandler* handler); +void onNotFound(THandlerFunction fn); //called when handler is not assigned +void onFileUpload(THandlerFunction fn); //handle file uploads + +``` +--- +--- + +## Releases + +### Release v1.0.2 + +1. Clear compiler warnings. +2. Add Version String +3. Add Table of Contents + +### Release v1.0.1 + +1. Add support to and tested working **OK** in other architectures such as **Teensy, SAM, SAMD, stm32, eps8266, esp32** besides AVR. + +### Release v1.0.0 + +1. Based on and modified from [Marcus Rugger's functional-vlpp library](https://github.com/marcusrugger/functional-vlpp). + + +--- +--- + +### Issues + +Submit issues to: [WiFiManager_NINA_Lite issues](https://github.com/khoih-prog/Functional-Vlpp/issues) + +--- +--- + +### Contributions and thanks + +1. Based on and modified from [vczh-libraries/Vlpp](https://github.com/vczh-libraries/Vlpp) and [Marcus Rugger functional-vlpp library](https://github.com/marcusrugger/functional-vlpp) + + + + + +
marcusrugger
⭐️ Marcus Rugger

+ +--- + +### Contributing + +If you want to contribute to this project: +- Report bugs and errors +- Ask for enhancements +- Create issues and pull requests +- Tell other people about this library + +--- + +### License + +- The library is licensed under [MIT](https://github.com/khoih-prog/Functional-Vlpp/blob/master/LICENSE) + +--- + +### Copyright + +Copyright 2020- Khoi Hoang diff --git a/software/firmware/source/libraries/functional-vlpp/keywords.txt b/software/firmware/source/libraries/functional-vlpp/keywords.txt new file mode 100644 index 000000000..ef2def378 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/keywords.txt @@ -0,0 +1,81 @@ +####################################### +# Data types (KEYWORD1) +####################################### +NotCopyable KEYWORD1 +RemoveReference KEYWORD1 +RemoveConst KEYWORD1 +RemoveVolatile KEYWORD1 +RemoveCVR KEYWORD1 +ForwardValue KEYWORD1 +TypeTuple KEYWORD1 +Object KEYWORD1 +ObjectBox KEYWORD1 +Nullable KEYWORD1 +BinaryRetriver KEYWORD1 +KeyType KEYWORD1 +POD KEYWORD1 +Interface KEYWORD1 +YesType KEYWORD1 +NoType KEYWORD1 +AcceptType KEYWORD1 +YesNoAnd KEYWORD1 +YesNoOr KEYWORD1 +AcceptValue KEYWORD1 +PointerConvertable KEYWORD1 +ReturnConvertable KEYWORD1 +AcceptAlways KEYWORD1 +RequiresConvertable KEYWORD1 +ReferenceCounterOperator KEYWORD1 +Ptr KEYWORD1 +ComPtr KEYWORD1 +Func KEYWORD1 +Invoker KEYWORD1 +StaticInvoker KEYWORD1 +MemberInvoker KEYWORD1 +ObjectInvoker KEYWORD1 +LambdaRetriveType KEYWORD1 +FunctionObjectRetriveType KEYWORD1 +Binding KEYWORD1 +CR KEYWORD1 +Binder KEYWORD1 +Currier KEYWORD1 +Combining KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +Unbox KEYWORD2 +Equals KEYWORD2 +Equals KEYWORD2 +Value KEYWORD2 +GetKeyValue KEYWORD2 +CreateCounter KEYWORD2 +DeleteReference KEYWORD2 +Inc KEYWORD2 +Dec KEYWORD2 +Counter KEYWORD2 +Cast KEYWORD2 +Obj KEYWORD2 +MakePtr KEYWORD2 +KeyType KEYWORD2 +Invoke KEYWORD2 +Lambda KEYWORD2 +Curry KEYWORD2 + +####################################### +# Literals (LITERAL1) +####################################### + +vint8_t LITERAL1 +vuint8_t LITERAL1 +vint16_t LITERAL1 +vuint16_t LITERAL1 +vint32_t LITERAL1 +vuint32_t LITERAL1 +vint64_t LITERAL1 +vuint64_t LITERAL1 +vint LITERAL1 +vsint LITERAL1 +vuint LITERAL1 +pos_t LITERAL1 diff --git a/software/firmware/source/libraries/functional-vlpp/library.json b/software/firmware/source/libraries/functional-vlpp/library.json new file mode 100644 index 000000000..159e69bda --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/library.json @@ -0,0 +1,22 @@ +{ + "name": "Functional-Vlpp", + "version": "1.0.2", + "keywords": "C++, Function, functional-vlpp, lambda-function, Lambda, Class, AVR, Teensy, SAM, SAMD, stm32, eps8266, esp32, nRF52", + "description": "Provides common C++ construction, including string operation / generic container / linq, function templates to better support C++ functional programming across platforms", + "repository": + { + "type": "git", + "url": "https://github.com/khoih-prog/functional-vlpp" + }, + "homepage": "https://github.com/khoih-prog/functional-vlpp", + "export": { + "exclude": [ + "linux", + "extras", + "tests" + ] + }, + "frameworks": "*", + "platforms": "*", + "examples": "examples/*/*/*.ino" +} diff --git a/software/firmware/source/libraries/functional-vlpp/library.properties b/software/firmware/source/libraries/functional-vlpp/library.properties new file mode 100644 index 000000000..fc221b253 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/library.properties @@ -0,0 +1,9 @@ +name=Functional-Vlpp +version=1.0.2 +author=Khoi Hoang +maintainer=Khoi Hoang +sentence=Provides function templates to better support C++ functional programming across platforms. +paragraph=Provides common C++ construction, including string operation / generic container / linq, function templates to better support C++ functional programming across platforms +category=Other +url=https://github.com/khoih-prog/functional-vlpp +architectures=* diff --git a/software/firmware/source/libraries/functional-vlpp/platformio/platformio.ini b/software/firmware/source/libraries/functional-vlpp/platformio/platformio.ini new file mode 100644 index 000000000..f48cca6c4 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/platformio/platformio.ini @@ -0,0 +1,335 @@ +;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] +; ============================================================ +; chose environment: +; ESP8266 +; ESP32 +; SAMD +; NRF52 +; STM32 +; ============================================================ +;default_envs = ESP8266 +;default_envs = ESP32 +default_envs = SAMD +;default_envs = NRF52 +;default_envs = STM32 + +[env] +; ============================================================ +; Serial configuration +; choose upload speed, serial-monitor speed +; ============================================================ +upload_speed = 921600 +;upload_port = COM11 +;monitor_speed = 9600 +;monitor_port = COM11 + +lib_deps = + + +build_flags = +; set your debug output (default=Serial) +; -D DEBUG_ESP_PORT=Serial +; comment the folowing line to enable WiFi debugging +; -D NDEBUG + +[env:ESP8266] +platform = espressif8266 +framework = arduino +; ============================================================ +; Board configuration +; choose your board by uncommenting one of the following lines +; ============================================================ +;board = gen4iod +;board = huzzah +;board = oak +;board = esp_wroom_02 +;board = espduino +;board = espectro +;board = espino +;board = espresso_lite_v1 +;board = espresso_lite_v2 +;board = esp12e +;board = esp01_1m +;board = esp01 +;board = esp07 +;board = esp8285 +;board = heltec_wifi_kit_8 +;board = inventone +;board = nodemcu +board = nodemcuv2 +;board = modwifi +;board = phoenix_v1 +;board = phoenix_v2 +;board = sparkfunBlynk +;board = thing +;board = thingdev +;board = esp210 +;board = espinotee +;board = d1 +;board = d1_mini +;board = d1_mini_lite +;board = d1_mini_pro +;board = wifi_slot +;board = wifiduino +;board = wifinfo +;board = wio_link +;board = wio_node +;board = xinabox_cw01 +;board = esp32doit-devkit-v1 + +[env:ESP32] +platform = espressif32 +framework = arduino, espidf +; ============================================================ +; Board configuration +; choose your board by uncommenting one of the following lines +; ============================================================ +;board = esp32cam +;board = alksesp32 +;board = featheresp32 +;board = espea32 +;board = bpi-bit +;board = d-duino-32 +board = esp32doit-devkit-v1 +;board = pocket_32 +;board = fm-devkit +;board = pico32 +;board = esp32-evb +;board = esp32-gateway +;board = esp32-pro +;board = esp32-poe +;board = oroca_edubot +;board = onehorse32dev +;board = lopy +;board = lopy4 +;board = wesp32 +;board = esp32thing +;board = sparkfun_lora_gateway_1-channel +;board = ttgo-lora32-v1 +;board = ttgo-t-beam +;board = turta_iot_node +;board = lolin_d32 +;board = lolin_d32_pro +;board = lolin32 +;board = wemosbat +;board = widora-air +;board = xinabox_cw02 +;board = iotbusio +;board = iotbusproteus +;board = nina_w10 + +[env:SAMD] +platform = atmelsam +framework = arduino +; ============================================================ +; Choose your board by uncommenting one of the following lines +; ============================================================ +; ============================================================ +; Board configuration Adafruit SAMD +; ============================================================ + +;board = adafruit_feather_m0 +;board = adafruit_feather_m0_express +;board = adafruit_metro_m0 +;board = adafruit_circuitplayground_m0 +;board = adafruit_gemma_m0 +;board = adafruit_trinket_m0 +;board = adafruit_itsybitsy_m0 +;board = adafruit_pirkey +;board = adafruit_hallowing +;board = adafruit_crickit_m0 +;board = adafruit_metro_m4 +;board = adafruit_grandcentral_m4 +board = adafruit_itsybitsy_m4 +;board = adafruit_feather_m4 +;board = adafruit_trellis_m4 +;board = adafruit_pyportal_m4 +;board = adafruit_pyportal_m4_titano +;board = adafruit_pybadge_m4 +;board = adafruit_metro_m4_airliftlite +;board = adafruit_pygamer_m4 +;board = adafruit_pygamer_advance_m4 +;board = adafruit_pybadge_airlift_m4 +;board = adafruit_monster_m4sk +;board = adafruit_hallowing_m4 + +; ============================================================ +; Board configuration Arduino SAMD and SAM +; ============================================================ + +;board = arduino_zero_edbg +;board = arduino_zero_native +;board = mkr1000 +;board = mkrzero +;board = mkrwifi1010 +;board = nano_33_iot +;board = mkrfox1200 +;board = mkrwan1300 +;board = mkrwan1310 +;board = mkrgsm1400 +;board = mkrnb1500 +;board = mkrvidor4000 +;board = adafruit_circuitplayground_m0 +;board = mzero_pro_bl_dbg +;board = mzero_pro_bl +;board = mzero_bl +;board = tian +;board = tian_cons +;board = arduino_due_x_dbg +;board = arduino_due_x + +; ============================================================ +; Board configuration Seeeduino SAMD +; ============================================================ + +;board = seeed_wio_terminal +;board = Seeed_femto_m0 +;board = seeed_XIAO_m0 +;board = Wio_Lite_MG126 +;board = WioGPS +;board = zero +;board = rolawan +;board = seeed_grove_ui_wireless + + +[env:NRF52] +platform = nordicnrf52 +framework = arduino +; ============================================================ +; Board configuration Adafruit nRF52 +; choose your board by uncommenting one of the following lines +; ============================================================ +;board = feather52832 +board = feather52840 +;board = feather52840sense +;board = itsybitsy52840 +;board = cplaynrf52840 +;board = cluenrf52840 +;board = metro52840 +;board = pca10056 +;board = particle_xenon +;board = mdbt50qrx +;board = ninab302 +;board = ninab112 + +[env:STM32] +platform = ststm32 +framework = arduino + +; ============================================================ +; Choose your board by uncommenting one of the following lines +; ============================================================ + +; ============================================================ +; Board configuration Nucleo-144 +; ============================================================ + +;board = nucleo_f207zg +;board = nucleo_f429zi +;board = nucleo_f746zg +;board = nucleo_f756zg +;board = nucleo_f767zi +;board = nucleo_h743zi +;board = nucleo_l496zg +;board = nucleo_l496zg-p +;board = nucleo_l4r5zi +;board = nucleo_l4r5zi-p + +; ============================================================ +; Board configuration Nucleo-64 +; ============================================================ + +;board = nucleo_f030r8 +;board = nucleo_f072rb + +;board = nucleo_f091rc +;board = nucleo_f103rb +;board = nucleo_f302r8 +;board = nucleo_f303re +;board = nucleo_f401re +;board = nucleo_f411re +;board = nucleo_f446re +;board = nucleo_g071rb +;board = nucleo_g431rb +;board = nucleo_g474re +;board = nucleo_l053r8 +;board = nucleo_l073rz +;board = nucleo_l152re +;board = nucleo_l433rc_p +;board = nucleo_l452re +;board = nucleo_l452re-p +;board = nucleo_l476rg +;board = pnucleo_wb55rg + +; ============================================================ +; Board configuration Nucleo-32 +; ============================================================ + +;board = nucleo_f031k6 +;board = nucleo_l031k6 +;board = nucleo_l412kb +;board = nucleo_l432lc +;board = nucleo_f303k8 +;board = nucleo_g431kb + +; ============================================================ +; Board configuration Discovery Boards +; ============================================================ + +;board = disco_f030r8 +;board = disco_f072rb +;board = disco_f030r8 +;board = disco_f100rb +;board = disco_f407vg +;board = disco_f413zh +;board = disco_f746ng +;board = disco_g0316 +;board = disco_l475vg_iot +;board = disco_f072cz-lrwan1 + +; ============================================================ +; Board configuration STM32MP1 Boards +; ============================================================ + +;board = stm32mp157a-dk1 +;board = stm32mp157c-dk2 + +; ============================================================ +; Board configuration Generic Boards +; ============================================================ + +;board = bluepill_f103c6 +;board = bluepill_f103c8 +;board = blackpill_f103c8 +;board = stm32f103cx +;board = stm32f103rx +;board = stm32f103tx +;board = stm32f103vx +;board = stm32f103zx +;board = stm32f103zet6 +;board = maplemini_f103cb +;board = blackpill_f303cc +;board = black_f407ve +;board = black_f407vg +;board = black_f407ze +;board = black_f407zg +;board = blue_f407ve_mini +;board = blackpill_f401cc +;board = blackpill_f411ce +;board = coreboard_f401rc +;board = feather_f405 + +; ============================================================ +; Board configuration Many more Boards to be filled +; ============================================================ + diff --git a/software/firmware/source/libraries/functional-vlpp/src/Basic.h b/software/firmware/source/libraries/functional-vlpp/src/Basic.h new file mode 100644 index 000000000..250750f82 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/src/Basic.h @@ -0,0 +1,743 @@ +/**************************************************************************************************************************** + Basic.h + + This library provides function templates to better support C++ functional programming across platforms. + Based on Vlpp library (https://github.com/vczh-libraries/Vlpp) + and Marcus Rugger functional-vlpp library (https://github.com/marcusrugger/functional-vlpp) + Built by Khoi Hoang (https://github.com/khoih-prog/functional-vlpp) + Licensed under MIT license + + Original author + Vczh Library++ 3.0 + Developer: Zihan Chen(vczh) + Framework::Basic + + Classes: + NotCopyable : Object inherits from this type cannot be copied + Error : Error, unlike exception, is not encouraged to catch + Object : Base class of all classes + + Macros: + CHECK_ERROR(CONDITION,DESCRIPTION) : Assert, throws an Error if failed + CHECK_FAIL(DESCRIPTION) : Force an assert failure + SCOPE_VARIABLE(TYPE,VARIABLE,VALUE){ ... } : Scoped variable + + Version: 1.0.2 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 13/02/2019 Initial coding, testing and supporting AVR architecture + 1.0.1 K Hoang 01/03/2020 Add support for STM32 and all other architectures. + 1.0.2 K Hoang 21/02/2021 Clear compiler warnings + *****************************************************************************************************************************/ + +#pragma once + +#ifndef VCZH_BASIC +#define VCZH_BASIC + +#ifdef VCZH_CHECK_MEMORY_LEAKS + #define _CRTDBG_MAP_ALLOC + #include + #include + #define VCZH_CHECK_MEMORY_LEAKS_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__) + #define new VCZH_CHECK_MEMORY_LEAKS_NEW +#endif + +#include +#include +#define abstract +#define __thiscall +#define __forceinline inline + +#define _I8_MIN ((vint8_t)0x80) +#define _I8_MAX ((vint8_t)0x7F) +#define _UI8_MAX ((vuint8_t)0xFF) + +#define _I16_MIN ((vint16_t)0x8000) +#define _I16_MAX ((vint16_t)0x7FFF) +#define _UI16_MAX ((vuint16_t)0xFFFF) + +#define _I32_MIN ((vint32_t)0x80000000) +#define _I32_MAX ((vint32_t)0x7FFFFFFF) +#define _UI32_MAX ((vuint32_t)0xFFFFFFFF) + +#define _I64_MIN ((vint64_t)0x8000000000000000L) +#define _I64_MAX ((vint64_t)0x7FFFFFFFFFFFFFFFL) +#define _UI64_MAX ((vuint64_t)0xFFFFFFFFFFFFFFFFL) + +#define L_(x) L__(x) +#define L__(x) L ## x + +namespace vl +{ + typedef int8_t vint8_t; + typedef uint8_t vuint8_t; + typedef int16_t vint16_t; + typedef uint16_t vuint16_t; + typedef int32_t vint32_t; + typedef uint32_t vuint32_t; + typedef int64_t vint64_t; + typedef uint64_t vuint64_t; + + + ///

Signed interface whose size is equal to sizeof(void*). + typedef vint32_t vint; + /// Signed interface whose size is equal to sizeof(void*). + typedef vint32_t vsint; + /// Unsigned interface whose size is equal to sizeof(void*). + typedef vuint32_t vuint; + + /// Signed interger representing position. + typedef vint64_t pos_t; + + #define ITOA_S _itoa_s + #define ITOW_S _itow_s + #define I64TOA_S _i64toa_s + #define I64TOW_S _i64tow_s + #define UITOA_S _ui64toa_s + #define UITOW_S _ui64tow_s + #define UI64TOA_S _ui64toa_s + #define UI64TOW_S _ui64tow_s + + //#define INCRC(x) (__sync_add_and_fetch(x, 1)) + //#define DECRC(x) (__sync_sub_and_fetch(x, 1)) + #define INCRC(x) (++(*x)) + #define DECRC(x) (--(*x)) + + + /*********************************************************************** + NotCopyable Base Class + ***********************************************************************/ + + class NotCopyable + { + private: + NotCopyable(const NotCopyable&) + { + } + NotCopyable& operator=(const NotCopyable&) + { + return *this; + } + public: + NotCopyable() + { + } + }; + + #define SCOPE_VARIABLE(TYPE, VARIABLE, VALUE)\ + if(bool __scope_variable_flag__=true)\ + for(TYPE VARIABLE = VALUE;__scope_variable_flag__;__scope_variable_flag__=false) + + /*********************************************************************** + Type Traits + ***********************************************************************/ + + template + struct RemoveReference + { + typedef T Type; + }; + + template + struct RemoveReference + { + typedef T Type; + }; + + template + struct RemoveReference < T&& > + { + typedef T Type; + }; + + template + struct RemoveConst + { + typedef T Type; + }; + + template + struct RemoveConst + { + typedef T Type; + }; + + template + struct RemoveVolatile + { + typedef T Type; + }; + + template + struct RemoveVolatile + { + typedef T Type; + }; + + template + struct RemoveCVR + { + typedef T Type; + }; + + template + struct RemoveCVR + { + typedef typename RemoveCVR::Type Type; + }; + + template + struct RemoveCVR < T&& > + { + typedef typename RemoveCVR::Type Type; + }; + + template + struct RemoveCVR + { + typedef typename RemoveCVR::Type Type; + }; + + template + struct RemoveCVR + { + typedef typename RemoveCVR::Type Type; + }; + + template + typename RemoveReference::Type&& MoveValue(T&& value) + { + return (typename RemoveReference::Type&&)value; + } + + template + T&& ForwardValue(typename RemoveReference::Type&& value) + { + return (T&&)value; + } + + template + T&& ForwardValue(typename RemoveReference::Type& value) + { + return (T&&)value; + } + + template + struct TypeTuple + { + }; + + /*********************************************************************** + Object Base Class + ***********************************************************************/ + + /// Base type of all classes. + class Object + { + public: + virtual ~Object() + { + } + }; + + /// Type for storing a value to wherever requiring a [T:vl.Ptr`1] to [T:vl.Object]. + /// Type of the value. + template + class ObjectBox : public Object + { + private: + T object; + public: + /// Box a value. + /// The value to box. + ObjectBox(const T& _object) + : object(_object) + { + } + + /// Box a movable value. + /// The value to box. + ObjectBox(T&& _object) + : object(MoveValue(_object)) + { + } + + /// Copy a box. + /// The box. + ObjectBox(const ObjectBox& value) + : object(value.object) + { + } + + /// Move a box. + /// The box. + ObjectBox(ObjectBox&& value) + : object(MoveValue(value.object)) + { + } + + /// Box a value. + /// The boxed value. + /// The value to box. + ObjectBox& operator=(const T& _object) + { + object = _object; + return *this; + } + + /// Copy a box. + /// The boxed value. + /// The box. + ObjectBox& operator=(const ObjectBox& value) + { + object = value.object; + return *this; + } + + /// Move a box. + /// The boxed value. + /// The box. + ObjectBox& operator=(ObjectBox&& value) + { + object = MoveValue(value.object); + return *this; + } + + /// Unbox the value. + /// The original value. + const T& Unbox() + { + return object; + } + }; + + /// Type for optionally storing a value. + /// Type of the value. + template + class Nullable + { + private: + T* object; + public: + /// Create a null value. + Nullable() + : object(0) + { + } + + /// Create a non-null value. + /// The value to copy. + Nullable(const T& value) + : object(new T(value)) + { + } + + /// Create a non-null value. + /// The value to move. + Nullable(T&& value) + : object(new T(MoveValue(value))) + { + } + + /// Copy a nullable value. + /// The nullable value to copy. + Nullable(const Nullable& nullable) + : object(nullable.object ? new T(*nullable.object) : 0) + { + } + + /// Move a nullable value. + /// The nullable value to move. + Nullable(Nullable&& nullable) + : object(nullable.object) + { + nullable.object = 0; + } + + ~Nullable() + { + if (object) + { + delete object; + object = 0; + } + } + + /// Create a non-null value. + /// The created nullable value. + /// The value to copy. + Nullable& operator=(const T& value) + { + if (object) + { + delete object; + object = 0; + } + + object = new T(value); + return *this; + } + + /// Copy a nullable value. + /// The created nullable value. + /// The nullable value to copy. + Nullable& operator=(const Nullable& nullable) + { + if (this != &nullable) + { + if (object) + { + delete object; + object = 0; + } + + if (nullable.object) + { + object = new T(*nullable.object); + } + } + + return *this; + } + + /// Move a nullable value. + /// The created nullable value. + /// The nullable value to move. + Nullable& operator=(Nullable&& nullable) + { + if (this != &nullable) + { + if (object) + { + delete object; + object = 0; + } + + object = nullable.object; + nullable.object = 0; + } + return *this; + } + + static bool Equals(const Nullable& a, const Nullable& b) + { + return + a.object + ? b.object + ? *a.object == *b.object + : false + : b.object + ? false + : true; + } + + static vint Compare(const Nullable& a, const Nullable& b) + { + return + a.object + ? b.object + ? (*a.object == *b.object ? 0 : *a.object < *b.object ? -1 : 1) + : 1 + : b.object + ? -1 + : 0; + } + + bool operator==(const Nullable& nullable)const + { + return Equals(*this, nullable); + } + + bool operator!=(const Nullable& nullable)const + { + return !Equals(*this, nullable); + } + + bool operator<(const Nullable& nullable)const + { + return Compare(*this, nullable) < 0; + } + + bool operator<=(const Nullable& nullable)const + { + return Compare(*this, nullable) <= 0; + } + + bool operator>(const Nullable& nullable)const + { + return Compare(*this, nullable) > 0; + } + + bool operator>=(const Nullable& nullable)const + { + return Compare(*this, nullable) >= 0; + } + + /// Convert the nullable value to a bool value. + /// Returns true if it is not null. + operator bool()const + { + return object != 0; + } + + /// Unbox the value. This operation will cause an access violation of it is null. + /// The original value. + const T& Value()const + { + return *object; + } + }; + + template + union BinaryRetriver + { + T t; + char binary[sizeof(T) > minSize ? sizeof(T) : minSize]; + }; + + /*********************************************************************** + Configuration Type Traits + ***********************************************************************/ + + /// Get the index type of a value for containers. + /// Type of the value. + template + struct KeyType + { + public: + /// The index type of a value for containers. + typedef T Type; + + /// Convert a value to its index type. + /// The corresponding index value. + /// The value. + static T GetKeyValue(const T& value) + { + return value; + } + }; + + /// Test is a type a Plain-Old-Data type for containers. + /// The type to test. + template + struct POD + { + /// Returns true if the type is a Plain-Old-Data type. + static const bool Result = false; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD { + + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + template<>struct POD + { + static const bool Result = true; + }; + + templatestruct POD + { + static const bool Result = true; + }; + + templatestruct POD + { + static const bool Result = true; + }; + + templatestruct POD < T&& > + { + static const bool Result = true; + }; + + templatestruct POD + { + static const bool Result = true; + }; + + templatestruct POD + { + static const bool Result = POD::Result; + }; + + templatestruct POD + { + static const bool Result = POD::Result; + }; + + templatestruct POD + { + static const bool Result = POD::Result; + }; + + templatestruct POD + { + static const bool Result = POD::Result; + }; + + /*********************************************************************** + Interface Class + ***********************************************************************/ + + /// Base type of all interfaces. All interface types are encouraged to be virtual inherited. + class Interface : private NotCopyable + { + public: + virtual ~Interface() + { + } + }; + + /*********************************************************************** + Type Extraction Class + ***********************************************************************/ + + struct YesType {}; + struct NoType {}; + + template + struct AcceptType + { + }; + + template + struct AcceptType + { + typedef T Type; + }; + + template + struct YesNoAnd + { + typedef NoType Type; + }; + + template<> + struct YesNoAnd + { + typedef YesType Type; + }; + + template + struct YesNoOr + { + typedef YesType Type; + }; + + template<> + struct YesNoOr + { + typedef NoType Type; + }; + + template + struct AcceptValue + { + static const bool Result = false; + }; + + template<> + struct AcceptValue + { + static const bool Result = true; + }; + + template + T ValueOf(); + + template + struct PointerConvertable + { + static YesType Test(TTo* value); + static NoType Test(void* value); + + typedef decltype(Test(ValueOf())) YesNoType; + }; + + template + struct ReturnConvertable + { + static YesType Test(TTo&& value); + static NoType Test(...); + + typedef decltype(Test(ValueOf < TFrom&& > ())) YesNoType; + }; + + template + struct ReturnConvertable + { + typedef YesType YesNoType; + }; + + template + struct ReturnConvertable + { + typedef NoType YesNoType; + }; + + template<> + struct ReturnConvertable + { + typedef YesType YesNoType; + }; + + template + struct AcceptAlways + { + typedef T Type; + }; + + template + struct RequiresConvertable + { + static YesType Test(TTo* value); + static NoType Test(void* value); + + typedef decltype(Test((TFrom*)0)) YesNoType; + }; +} // namespace vl + +#endif // VCZH_BASIC diff --git a/software/firmware/source/libraries/functional-vlpp/src/Function.h b/software/firmware/source/libraries/functional-vlpp/src/Function.h new file mode 100644 index 000000000..b3952b61d --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/src/Function.h @@ -0,0 +1,478 @@ +/**************************************************************************************************************************** + Function.h + + This library provides function templates to better support C++ functional programming across platforms. + Based on Vlpp library (https://github.com/vczh-libraries/Vlpp) + and Marcus Rugger functional-vlpp library (https://github.com/marcusrugger/functional-vlpp) + Built by Khoi Hoang (https://github.com/khoih-prog/functional-vlpp) + Licensed under MIT license + + Original author + Vczh Library++ 3.0 + Developer: Zihan Chen(vczh) + Framework::Basic + + Classes: + Func :Function object + + Functions: + Curry :: (A->B) -> A -> B :Currying + Combine :: (A->B) -> (A->C) -> (B->C->D) -> (A->D) :Combine multiple functors using an operator + + Version: 1.0.2 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 13/02/2019 Initial coding, testing and supporting AVR architecture + 1.0.1 K Hoang 01/03/2020 Add support for STM32 and all other architectures. + 1.0.2 K Hoang 21/02/2021 Clear compiler warnings + *****************************************************************************************************************************/ + +#pragma once + +#ifndef VCZH_FUNCTION +#define VCZH_FUNCTION + +#include +#include "Basic.h" +#include "Pointer.h" + +namespace vl +{ + + /*********************************************************************** + vl::Func + ***********************************************************************/ + + template + class Func + { + }; + + namespace internal_invokers + { + template + class Invoker : public Object + { + public: + virtual R Invoke(TArgs&& ...args) = 0; + }; + + //------------------------------------------------------ + + template + class StaticInvoker : public Invoker + { + protected: + R(*function)(TArgs ...args); + + public: + StaticInvoker(R(*_function)(TArgs...)) + : function(_function) + { + } + + R Invoke(TArgs&& ...args)override + { + return function(ForwardValue(args)...); + } + }; + + //------------------------------------------------------ + + template + class MemberInvoker : public Invoker + { + protected: + C* sender; + R(C::*function)(TArgs ...args); + + public: + MemberInvoker(C* _sender, R(C::*_function)(TArgs ...args)) + : sender(_sender) + , function(_function) + { + } + + R Invoke(TArgs&& ...args)override + { + return (sender->*function)(ForwardValue(args)...); + } + }; + + //------------------------------------------------------ + + template + class ObjectInvoker : public Invoker + { + protected: + C function; + + public: + ObjectInvoker(const C& _function) + : function(_function) + { + } + + R Invoke(TArgs&& ...args)override + { + return function(ForwardValue(args)...); + } + }; + + //------------------------------------------------------ + + template + class ObjectInvoker : public Invoker + { + protected: + C function; + + public: + ObjectInvoker(const C& _function) + : function(_function) + { + } + + void Invoke(TArgs&& ...args)override + { + function(ForwardValue(args)...); + } + }; + } + + /// A type representing a function reference. + /// The return type. + /// Types of parameters. + template + class Func : public Object + { + protected: + Ptr> invoker; + public: + typedef R FunctionType(TArgs...); + typedef R ResultType; + + /// Create a null function reference. + Func() + { + } + + /// Copy a function reference. + /// The function reference to copy. + Func(const Func& function) + : invoker(function.invoker) + { + } + + /// Create a reference using a function pointer. + /// The function pointer. + Func(R(*function)(TArgs...)) + { + invoker = new internal_invokers::StaticInvoker(function); + } + + /// Create a reference using a method. + /// Type of the class that has the method. + /// The object that has the method. + /// The function pointer. + template + Func(C* sender, R(C::*function)(TArgs...)) + { + invoker = new internal_invokers::MemberInvoker(sender, function); + } + + /// Create a reference using a function object. + /// Type of the function object. + /// The function object. It could be a lambda expression. + template + Func(const C& function) + { + invoker = new internal_invokers::ObjectInvoker(function); + } + + /// Invoke the function. + /// Returns the function result. + /// Arguments to invoke the function. + R operator()(TArgs ...args)const + { + return invoker->Invoke(ForwardValue(args)...); + } + + Func& operator=(const Func& function) + { + invoker = function.invoker; + return *this; + } + + Func& operator=(const Func&& function) + { + invoker = MoveValue(function.invoker); + return *this; + } + + bool operator==(const Func& function)const + { + return invoker == function.invoker; + } + + bool operator!=(const Func& function)const + { + return invoker != function.invoker; + } + + /// Test is the reference a null reference. + /// Returns true if it is not a null reference. + operator bool()const + { + return invoker; + } + }; + + /*********************************************************************** + vl::function_lambda::LambdaRetriveType + ***********************************************************************/ + + namespace function_lambda + { + template + struct LambdaRetriveType + { + typedef vint Type; + typedef vint FunctionType; + typedef vint ResultType; + }; + + template + struct FunctionObjectRetriveType + { + typedef typename LambdaRetriveType::Type Type; + typedef typename LambdaRetriveType::FunctionType FunctionType; + typedef typename LambdaRetriveType::ResultType ResultType; + typedef typename LambdaRetriveType::ParameterTypes ParameterTypes; + }; + + template + struct LambdaRetriveType + { + typedef Func Type; + typedef R(FunctionType)(TArgs...); + typedef R ResultType; + }; + + template + struct LambdaRetriveType + { + typedef Func Type; + typedef R(FunctionType)(TArgs...); + typedef R ResultType; + }; + + template + struct FunctionObjectRetriveType + { + typedef Func Type; + typedef R(FunctionType)(TArgs...); + typedef R ResultType; + }; + + /// Create a function reference to a function object or a lambda expression, with all type information + /// automatically inferred. You can use the macro called "LAMBDA" to refer to this function. + /// Type of the function object or the lambda expression. + /// The function reference. + /// The function object or the lambda expression. + template + typename LambdaRetriveType::Type Lambda(T functionObject) + { + return functionObject; + } + + /// Create a function reference to a function pointer, with all type information + /// automatically inferred. You can use the macro called "FUNCTION" to refer to this function. + /// Type of the function pointer. + /// The function reference. + /// The function pointer. + template + typename FunctionObjectRetriveType::Type ConvertToFunction(T functionObject) + { + return functionObject; + } + + #define LAMBDA vl::function_lambda::Lambda + #define FUNCTION vl::function_lambda::ConvertToFunction + #define FUNCTION_TYPE(T) typename vl::function_lambda::FunctionObjectRetriveType::Type + #define FUNCTION_RESULT_TYPE(T) typename vl::function_lambda::FunctionObjectRetriveType::ResultType + } // namespace function_lambda + + /*********************************************************************** + vl::function_binding::Binding + ***********************************************************************/ + + namespace function_binding + { + template + struct Binding + { + }; + + template + struct CR + { + typedef const T& Type; + }; + + template + struct CR + { + typedef T& Type; + }; + + template + struct CR + { + typedef const T& Type; + }; + + template + struct CR + { + typedef const T& Type; + }; + + template + struct Binding + { + typedef R FunctionType(T0, TArgs...); + typedef R CurriedType(TArgs...); + typedef T0 FirstParameterType; + + class Binder : public Object + { + protected: + Func target; + T0 firstArgument; + public: + Binder(const Func& _target, T0 _firstArgument) + : target(_target), firstArgument(ForwardValue(_firstArgument)) + { + } + + R operator()(TArgs ...args)const + { + return target(firstArgument, args...); + } + }; + + class Currier : public Object + { + protected: + Func target; + public: + Currier(const Func& _target) + : target(_target) + { + } + + Func operator()(T0 firstArgument)const + { + return Binder(target, firstArgument); + } + }; + }; + } // namespace function_binding + + /// Currize a function. Currizing means to create a new function whose argument is the first argument + /// of the original function. Calling this function will return another function reference whose arguments is all + /// remain arguments of the original function. Calling the returned function will call the original function. + /// Type of the function. + /// The currized function. + /// The function pointer to currize. + template + Func::CurriedType>(typename function_binding::Binding::FirstParameterType)> + Curry(T* function) + { + return typename function_binding::Binding::Currier(function); + } + + /// Currize a function. Currizing means to create a new function whose argument is the first argument + /// of the original function. Calling this function will return another function reference whose arguments is all + /// remain arguments of the original function. Calling the returned function will call the original function. + /// Type of the function. + /// The currized function. + /// The function reference to currize. + template + Func::CurriedType>(typename function_binding::Binding::FirstParameterType)> + Curry(const Func& function) + { + return typename function_binding::Binding::Currier(function); + } + + /*********************************************************************** + vl::function_combining::Combining + ***********************************************************************/ + + namespace function_combining + { + template + class Combining + { + }; + + template + class Combining : public Object + { + protected: + Func function1; + Func function2; + Func converter; + public: + typedef R1 FirstFunctionType(TArgs...); + typedef R2 SecondFunctionType(TArgs...); + typedef R ConverterFunctionType(R1, R2); + typedef R FinalFunctionType(TArgs...); + + Combining(const Func& _function1, const Func& _function2, const Func& _converter) + : function1(_function1), function2(_function2), converter(_converter) + { + } + + R operator()(TArgs&& ...args)const + { + return converter(function1(ForwardValue(args)...), function2(ForwardValue(args)...)); + } + }; + } // namespace function_combining + + /// Combine two functions with a converter function. The two functions to combine should have the same argument types. + /// The converter function will use the return values of the two function to calculate the final value. + /// Type of the first function. + /// Type of the second function. + /// Type of the converter function. + /// A new function whose argument list are the same of the two functions to provide. Calling this function + /// will call function1, function2 and converter in order to calculate the final value. + /// The converter function. + /// The first function. + /// The second function. + template + Func::FinalFunctionType> + Combine(Func converter, Func function1, Func function2) + { + return function_combining::Combining(function1, function2, converter); + } + + /// Use the converter function to create a combiner, who will receive two function and use to create + /// a combined function. This function assumes the result types of the two provided function in the future are the same, + /// and the converter function will not change the result type. + /// Type of the two functions to combine. + /// The combiner. + /// The converter function. + template + Func(Func, Func)> Combiner(const Func::ResultType(typename Func::ResultType, typename Func::ResultType)>& converter) + { + typedef typename Func::ResultType R; + return Curry(Func, Func, Func)>(Combine)(converter); + } +} // namespace vl + +#endif // VCZH_FUNCTION + diff --git a/software/firmware/source/libraries/functional-vlpp/src/Pointer.h b/software/firmware/source/libraries/functional-vlpp/src/Pointer.h new file mode 100644 index 000000000..d035ec6f7 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/src/Pointer.h @@ -0,0 +1,650 @@ +/**************************************************************************************************************************** + Pointer.h + + This library provides function templates to better support C++ functional programming across platforms. + Based on Vlpp library (https://github.com/vczh-libraries/Vlpp) + and Marcus Rugger functional-vlpp library (https://github.com/marcusrugger/functional-vlpp) + Built by Khoi Hoang (https://github.com/khoih-prog/functional-vlpp) + Licensed under MIT license + + Original author + Vczh Library++ 3.0 + Developer: Zihan Chen(vczh) + Framework::Basic + + Classes: + Ptr :Smart pointer + + Version: 1.0.2 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 13/02/2019 Initial coding, testing and supporting AVR architecture + 1.0.1 K Hoang 01/03/2020 Add support for STM32 and all other architectures. + 1.0.2 K Hoang 21/02/2021 Clear compiler warnings + *****************************************************************************************************************************/ + +#pragma once + +#ifndef VCZH_POINTER +#define VCZH_POINTER + +#include "Basic.h" + +namespace vl +{ + + /*********************************************************************** + ReferenceCounterOperator + ***********************************************************************/ + + /// The strategy to get the pointer to the reference counter from an object. If you get the same pointer multiple times + /// from the same object by calling [M:vl.ReferenceCounterOperator`2.CreateCounter], than it is safe to convert a object pointer + /// to a [T:vl.Ptr`1]. Currently for reflectable C++ types which inherit from [T:vl.reflection.DescriptableObject] it is yet. + /// For others it is no. + /// The type of the object. + /// [T:vl.Ptr`1] will always use [T:vl.YesType] as the second type parameter. This parameter is useful + /// when you want to do partial specialization in the SFINAE way. + template + struct ReferenceCounterOperator + { + /// Create a pointer to the reference counter from an object. + /// The pointer to the reference counter. + /// The object. + static __forceinline volatile vint* CreateCounter(T* reference) + { + (void) reference; + return new vint(0); + } + + /// Destroy a pointer to the reference counter from an object. + /// The pointer to the reference counter. + /// The object. + static __forceinline void DeleteReference(volatile vint* counter, void* reference) + { + delete counter; + delete (T*)reference; + } + }; + + /*********************************************************************** + Smart Ptr Class + ***********************************************************************/ + + /// A smart pointer. It is always safe to convert a pointer to an object to a smart pointer once. If you do it multiple times, + /// it may be wrong due to different implementation of [T:vl.ReferenceCounterOperator`2]. In case of wrong, disposing the smart pointer + /// will cause an access violation. + /// The type of the object. + template + class Ptr + { + template + friend class Ptr; + + protected: + typedef void (*Destructor)(volatile vint*, void*); + + volatile vint* counter; + T* reference; + void* originalReference; + Destructor originalDestructor; + + void Inc() + { + if (counter) + { + INCRC(counter); + } + } + + void Dec() + { + if (counter) + { + if (DECRC(counter) == 0) + { + originalDestructor(counter, originalReference); + counter = 0; + reference = 0; + originalReference = 0; + originalDestructor = 0; + } + } + } + + volatile vint* Counter()const + { + return counter; + } + + Ptr(volatile vint* _counter, T* _reference, void* _originalReference, Destructor _originalDestructor) + : counter(_counter) + , reference(_reference) + , originalReference(_originalReference) + , originalDestructor(_originalDestructor) + { + Inc(); + } + + public: + + /// Create a null pointer. + Ptr() + : counter(0) + , reference(0) + , originalReference(0) + , originalDestructor(0) + { + } + + /// Convert a pointer to an object to a smart pointer. + /// The pointer to the object. + Ptr(T* pointer) + : counter(0) + , reference(0) + , originalReference(0) + , originalDestructor(0) + { + if (pointer) + { + counter = ReferenceCounterOperator::CreateCounter(pointer); + reference = pointer; + originalReference = pointer; + originalDestructor = &ReferenceCounterOperator::DeleteReference; + Inc(); + } + } + + /// Copy a smart pointer. + /// The smart pointer to copy. + Ptr(const Ptr& pointer) + : counter(pointer.counter) + , reference(pointer.reference) + , originalReference(pointer.originalReference) + , originalDestructor(pointer.originalDestructor) + { + Inc(); + } + + /// Move a smart pointer. + /// The smart pointer to Move. + Ptr(Ptr&& pointer) + : counter(pointer.counter) + , reference(pointer.reference) + , originalReference(pointer.originalReference) + , originalDestructor(pointer.originalDestructor) + { + pointer.counter = 0; + pointer.reference = 0; + pointer.originalReference = 0; + pointer.originalDestructor = 0; + } + + /// Cast a smart pointer. + /// The type of the object before casting. + /// The smart pointer to cast. + template + Ptr(const Ptr& pointer) + : counter(0) + , reference(0) + , originalReference(0) + , originalDestructor(0) + { + T* converted = pointer.Obj(); + if (converted) + { + counter = pointer.Counter(); + reference = converted; + originalReference = pointer.originalReference; + originalDestructor = pointer.originalDestructor; + Inc(); + } + } + + ~Ptr() + { + Dec(); + } + + /// Cast a smart pointer. + /// The type of the object after casting. + /// The casted smart pointer. Returns null if failed. + template + Ptr Cast()const + { + C* converted = dynamic_cast(reference); + return Ptr((converted ? counter : 0), converted, originalReference, originalDestructor); + } + + /// Convert a pointer to an object to a smart pointer. + /// The converted smart pointer. + /// The pointer to the object. + Ptr& operator=(T* pointer) + { + Dec(); + + if (pointer) + { + counter = ReferenceCounterOperator::CreateCounter(pointer); + reference = pointer; + originalReference = pointer; + originalDestructor = &ReferenceCounterOperator::DeleteReference; + Inc(); + } + else + { + counter = 0; + reference = 0; + originalReference = 0; + originalDestructor = 0; + } + + return *this; + } + + /// Copy a smart pointer. + /// The copied smart pointer. + /// The smart pointer to copy. + Ptr& operator=(const Ptr& pointer) + { + if (this != &pointer) + { + Dec(); + counter = pointer.counter; + reference = pointer.reference; + originalReference = pointer.originalReference; + originalDestructor = pointer.originalDestructor; + Inc(); + } + + return *this; + } + + /// Move a smart pointer. + /// The moved smart pointer. + /// The smart pointer to Move. + Ptr& operator=(Ptr&& pointer) + { + if (this != &pointer) + { + Dec(); + counter = pointer.counter; + reference = pointer.reference; + originalReference = pointer.originalReference; + originalDestructor = pointer.originalDestructor; + + pointer.counter = 0; + pointer.reference = 0; + pointer.originalReference = 0; + pointer.originalDestructor = 0; + } + + return *this; + } + + /// Cast a smart pointer. + /// The type of the object before casting. + /// The smart pointer after casting. + /// The smart pointer to cast. + template + Ptr& operator=(const Ptr& pointer) + { + T* converted = pointer.Obj(); + Dec(); + + if (converted) + { + counter = pointer.counter; + reference = converted; + originalReference = pointer.originalReference; + originalDestructor = pointer.originalDestructor; + Inc(); + } + else + { + counter = 0; + reference = 0; + originalReference = 0; + originalDestructor = 0; + } + + return *this; + } + + bool operator==(const T* pointer)const + { + return reference == pointer; + } + + bool operator!=(const T* pointer)const + { + return reference != pointer; + } + + bool operator>(const T* pointer)const + { + return reference > pointer; + } + + bool operator>=(const T* pointer)const + { + return reference >= pointer; + } + + bool operator<(const T* pointer)const + { + return reference < pointer; + } + + bool operator<=(const T* pointer)const + { + return reference <= pointer; + } + + bool operator==(const Ptr& pointer)const + { + return reference == pointer.reference; + } + + bool operator!=(const Ptr& pointer)const + { + return reference != pointer.reference; + } + + bool operator>(const Ptr& pointer)const + { + return reference > pointer.reference; + } + + bool operator>=(const Ptr& pointer)const + { + return reference >= pointer.reference; + } + + bool operator<(const Ptr& pointer)const + { + return reference < pointer.reference; + } + + bool operator<=(const Ptr& pointer)const + { + return reference <= pointer.reference; + } + + /// Test if it is a null pointer. + /// Returns true if it is not null. + operator bool()const + { + return reference != 0; + } + + /// Get the pointer to the object. + /// The pointer to the object. + T* Obj()const + { + return reference; + } + + /// Get the pointer to the object. + /// The pointer to the object. + T* operator->()const + { + return reference; + } + }; + + /*********************************************************************** + ComPtr + ***********************************************************************/ + + template + class ComPtr + { + protected: + volatile vint* counter; + T* reference; + + void Inc() + { + if (counter) + { + INCRC(counter); + } + } + + void Dec() + { + if (counter) + { + if (DECRC(counter) == 0) + { + delete counter; + reference->Release(); + counter = 0; + reference = 0; + } + } + } + + volatile vint* Counter()const + { + return counter; + } + + ComPtr(volatile vint* _counter, T* _reference) + : counter(_counter) + , reference(_reference) + { + Inc(); + } + + public: + + ComPtr() + { + counter = 0; + reference = 0; + } + + ComPtr(T* pointer) + { + if (pointer) + { + counter = new volatile vint(1); + reference = pointer; + } + else + { + counter = 0; + reference = 0; + } + } + + ComPtr(const ComPtr& pointer) + { + counter = pointer.counter; + reference = pointer.reference; + Inc(); + } + + ComPtr(ComPtr&& pointer) + { + counter = pointer.counter; + reference = pointer.reference; + + pointer.counter = 0; + pointer.reference = 0; + } + + ~ComPtr() + { + Dec(); + } + + ComPtr& operator=(T* pointer) + { + Dec(); + + if (pointer) + { + counter = new vint(1); + reference = pointer; + } + else + { + counter = 0; + reference = 0; + } + return *this; + } + + ComPtr& operator=(const ComPtr& pointer) + { + if (this != &pointer) + { + Dec(); + counter = pointer.counter; + reference = pointer.reference; + Inc(); + } + + return *this; + } + + ComPtr& operator=(ComPtr&& pointer) + { + if (this != &pointer) + { + Dec(); + counter = pointer.counter; + reference = pointer.reference; + + pointer.counter = 0; + pointer.reference = 0; + } + + return *this; + } + + bool operator==(const T* pointer)const + { + return reference == pointer; + } + + bool operator!=(const T* pointer)const + { + return reference != pointer; + } + + bool operator>(const T* pointer)const + { + return reference > pointer; + } + + bool operator>=(const T* pointer)const + { + return reference >= pointer; + } + + bool operator<(const T* pointer)const + { + return reference < pointer; + } + + bool operator<=(const T* pointer)const + { + return reference <= pointer; + } + + bool operator==(const ComPtr& pointer)const + { + return reference == pointer.reference; + } + + bool operator!=(const ComPtr& pointer)const + { + return reference != pointer.reference; + } + + bool operator>(const ComPtr& pointer)const + { + return reference > pointer.reference; + } + + bool operator>=(const ComPtr& pointer)const + { + return reference >= pointer.reference; + } + + bool operator<(const ComPtr& pointer)const + { + return reference < pointer.reference; + } + + bool operator<=(const ComPtr& pointer)const + { + return reference <= pointer.reference; + } + + operator bool()const + { + return reference != 0; + } + + T* Obj()const + { + return reference; + } + + T* operator->()const + { + return reference; + } + }; + + template + Ptr MakePtr(TArgs ...args) + { + return new T(args...); + } + + /*********************************************************************** + Traits + ***********************************************************************/ + + template + struct KeyType> + { + typedef T* Type; + + static T* GetKeyValue(const Ptr& key) + { + return key.Obj(); + } + }; + + template + struct POD> + { + static const bool Result = false; + }; + + template + struct KeyType> + { + typedef T* Type; + + static T* GetKeyValue(const ComPtr& key) + { + return key.Obj(); + } + }; + + template + struct POD> + { + static const bool Result = false; + }; +} // namespace vl + +#endif // VCZH_POINTER diff --git a/software/firmware/source/libraries/functional-vlpp/src/functional-vlpp.h b/software/firmware/source/libraries/functional-vlpp/src/functional-vlpp.h new file mode 100644 index 000000000..b34832df9 --- /dev/null +++ b/software/firmware/source/libraries/functional-vlpp/src/functional-vlpp.h @@ -0,0 +1,35 @@ +/**************************************************************************************************************************** + functional-vlpp.h + + Interface for Classes + + This library provides function templates to better support C++ functional programming across platforms. + Based on Vlpp library (https://github.com/vczh-libraries/Vlpp) + and Marcus Rugger functional-vlpp library (https://github.com/marcusrugger/functional-vlpp) + Built by Khoi Hoang (https://github.com/khoih-prog/functional-vlpp) + Licensed under MIT license + + Original author + Vczh Library++ 3.0 + Developer: Zihan Chen(vczh) + Framework::Basic + + Version: 1.0.2 + + Version Modified By Date Comments + ------- ----------- ---------- ----------- + 1.0.0 K Hoang 13/02/2019 Initial coding, testing and supporting AVR architecture + 1.0.1 K Hoang 01/03/2020 Add support for STM32 and all other architectures. + 1.0.2 K Hoang 21/02/2021 Clear compiler warnings + *****************************************************************************************************************************/ + +#pragma once + +#ifndef FUNCTIONAL_VLPP +#define FUNCTIONAL_VLPP + +#define FUNCTIONAL_VLPP_VERSION "Functional-Vlpp v1.0.2" + +#include "Function.h" + +#endif // FUNCTIONAL_VLPP