text: /****************************************************************************************************************************
    ESP_WiFiManager.h
    For ESP8266 / ESP32 boards
  
    ESP_WiFiManager is a library for the ESP8266/Arduino platform
    (https://github.com/esp8266/Arduino) to enable easy
    configuration and reconfiguration of WiFi credentials using a Captive Portal
    inspired by:
    http://www.esp8266.com/viewtopic.php?f=29&t=2520
    https://github.com/chriscook8/esp-arduino-apboot
    https://github.com/esp8266/Arduino/blob/master/libraries/DNSServer/examples/CaptivePortalAdvanced/
  
    Modified from Tzapu https://github.com/tzapu/WiFiManager
    and from Ken Taylor https://github.com/kentaylor
  
    Built by Khoi Hoang https://github.com/khoih-prog/ESP_WiFiManager
    Licensed under MIT license
    
    Version: 1.8.0
  
    Version Modified By   Date      Comments
    ------- -----------  ---------- -----------
    1.0.0   K Hoang      07/10/2019 Initial coding
    1.0.1   K Hoang      13/12/2019 Fix bug. Add features. Add support for ESP32
    1.0.2   K Hoang      19/12/2019 Fix bug thatkeeps ConfigPortal in endless loop if Portal/Router SSID or Password is NULL.
    1.0.3   K Hoang      05/01/2020 Option not displaying AvailablePages in Info page. Enhance README.md. Modify examples
    1.0.4   K Hoang      07/01/2020 Add RFC952 setHostname feature.
    1.0.5   K Hoang      15/01/2020 Add configurable DNS feature. Thanks to @Amorphous of https://community.blynk.cc
    1.0.6   K Hoang      03/02/2020 Add support for ArduinoJson version 6.0.0+ ( tested with v6.14.1 )
    1.0.7   K Hoang      13/04/2020 Reduce start time, fix SPIFFS bug in examples, update README.md
    1.0.8   K Hoang      10/06/2020 Fix STAstaticIP issue. Restructure code. Add LittleFS support for ESP8266 core 2.7.1+
    1.0.9   K Hoang      29/07/2020 Fix ESP32 STAstaticIP bug. Permit changing from DHCP <-> static IP using Config Portal.
                                    Add, enhance examples (fix MDNS for ESP32)
    1.0.10  K Hoang      08/08/2020 Add more features to Config Portal. Use random WiFi AP channel to avoid conflict.
    1.0.11  K Hoang      17/08/2020 Add CORS feature. Fix bug in softAP, autoConnect, resetSettings.
    1.1.0   K Hoang      28/08/2020 Add MultiWiFi feature to autoconnect to best WiFi at runtime
    1.1.1   K Hoang      30/08/2020 Add setCORSHeader function to allow flexible CORS. Fix typo and minor improvement.
    1.1.2   K Hoang      17/08/2020 Fix bug. Add example.
    1.2.0   K Hoang      09/10/2020 Restore cpp code besides Impl.h code to use if linker error. Fix bug.
    1.3.0   K Hoang      04/12/2020 Add LittleFS support to ESP32 using LITTLEFS Library
    1.4.1   K Hoang      22/12/2020 Fix staticIP not saved. Add functions. Add complex examples. Sync with ESPAsync_WiFiManager
    1.4.2   K Hoang      14/01/2021 Fix examples' bug not using saved WiFi Credentials after losing all WiFi connections.
    1.4.3   K Hoang      23/01/2021 Fix examples' bug not saving Static IP in certain cases.
    1.5.0   K Hoang      12/02/2021 Add support to new ESP32-S2
    1.5.1   K Hoang      26/03/2021 Fix compiler error if setting Compiler Warnings to All. Retest with esp32 core v1.0.6
    1.5.2   K Hoang      08/04/2021 Fix example misleading messages.
    1.5.3   K Hoang      13/04/2021 Add dnsServer error message.
    1.6.0   K Hoang      20/04/2021 Add support to new ESP32-C3 using SPIFFS or EEPROM
    1.6.1   K Hoang      25/04/2021 Fix MultiWiFi bug. Fix captive-portal bug if CP AP address is not default 192.168.4.1
    1.7.0   K Hoang      06/05/2021 Set _timezoneName. Add support to new ESP32-S2 (METRO_ESP32S2, FUNHOUSE_ESP32S2, etc.)
    1.7.1   K Hoang      08/05/2021 Fix Json bug. Fix timezoneName not displayed in Info page.
    1.7.2   K Hoang      08/05/2021 Fix warnings with ESP8266 core v3.0.0
    1.7.3   K Hoang      29/07/2021 Fix MultiWiFi connection issue with ESP32 core v2.0.0-rc1+
    1.7.4   K Hoang      13/08/2021 Add WiFi scanning of hidden SSIDs
    1.7.5   K Hoang      10/10/2021 Update `platform.ini` and `library.json`
    1.7.6   K Hoang      26/11/2021 Auto detect ESP32 core and use either built-in LittleFS or LITTLEFS library
    1.7.7   K Hoang      26/11/2021 Fix compile error for ESP32 core v1.0.5-
    1.7.8   K Hoang      30/11/2021 Fix bug to permit using HTTP port different from 80. Fix bug
    1.8.0  K Hoang       29/12/2021 Fix `multiple-definitions` linker error and weird bug related to src_cpp
   *****************************************************************************************************************************/
  
  #pragma once
  
  #ifndef ESP_WiFiManager_h
  #define ESP_WiFiManager_h
  
  #if !( defined(ESP8266) ||  defined(ESP32) )
    #error This code is intended to run on the ESP8266 or ESP32 platform! Please check your Tools->Board setting.
  #elif ( ARDUINO_ESP32S2_DEV || ARDUINO_FEATHERS2 || ARDUINO_ESP32S2_THING_PLUS || ARDUINO_MICROS2 || \
          ARDUINO_METRO_ESP32S2 || ARDUINO_MAGTAG29_ESP32S2 || ARDUINO_FUNHOUSE_ESP32S2 || \
          ARDUINO_ADAFRUIT_FEATHER_ESP32S2_NOPSRAM )
    #warning Using ESP32_S2. To follow library instructions to install esp32-s2 core and WebServer Patch
    #warning You have to select HUGE APP or 1.9-2.0 MB APP to be able to run Config Portal. Must use PSRAM
    #define USING_ESP32_S2        true
  #elif ( ARDUINO_ESP32C3_DEV )
    #warning Using ESP32_C3. To follow library instructions to install esp32-c3 core. Only SPIFFS and EEPROM OK.
    #warning You have to select Flash size 2MB and Minimal APP (1.3MB + 700KB) for some boards
    #define USING_ESP32_C3        true
  #endif
  
  #define ESP_WIFIMANAGER_VERSION           "ESP_WiFiManager v1.8.0"
  
  #define ESP_WIFIMANAGER_VERSION_MAJOR     1
  #define ESP_WIFIMANAGER_VERSION_MINOR     8
  #define ESP_WIFIMANAGER_VERSION_PATCH     0
  
  #define ESP_WIFIMANAGER_VERSION_INT      1008000
  
  #include "ESP_WiFiManager_Debug.h"
  
  #if ( defined(HTTP_PORT) && (HTTP_PORT < 65536) && (HTTP_PORT > 0) )
    #if (_WIFIMGR_LOGLEVEL_ > 2)
      #warning Using custom HTTP_PORT
    #endif
      
    #define HTTP_PORT_TO_USE     HTTP_PORT
  #else
    #if (_WIFIMGR_LOGLEVEL_ > 2)
      #warning Using default HTTP_PORT = 80
    #endif
    
    #define HTTP_PORT_TO_USE     80
  #endif
  
  //KH, for ESP32
  #ifdef ESP8266
    #include <ESP8266WiFi.h>
    #include <ESP8266WebServer.h>
  #else		//ESP32
    #include <WiFi.h>
    #include <WebServer.h>
  #endif
  
  #include <DNSServer.h>
  #include <memory>
  #undef min
  #undef max
  #include <algorithm>
  
  //KH, for ESP32
  #ifdef ESP8266
    extern "C"
    {
      #include "user_interface.h"
    }
    
    #define ESP_getChipId()   (ESP.getChipId())
  #else		//ESP32
    #include <esp_wifi.h>
    #define ESP_getChipId()   ((uint32_t)ESP.getEfuseMac())
  #endif
  
  // New in v1.4.0
  typedef struct
  {
    IPAddress _ap_static_ip;
    IPAddress _ap_static_gw;
    IPAddress _ap_static_sn;
  
  }  WiFi_AP_IPConfig;
  
  // Thanks to @Amorphous for the feature and code
  // (https://community.blynk.cc/t/esp-wifimanager-for-esp32-and-esp8266/42257/13)
  // To enable to configure from sketch
  #if !defined(USE_CONFIGURABLE_DNS)
    #define USE_CONFIGURABLE_DNS        false
  #endif
  
  typedef struct
  {
    IPAddress _sta_static_ip;
    IPAddress _sta_static_gw;
    IPAddress _sta_static_sn;
    IPAddress _sta_static_dns1;
    IPAddress _sta_static_dns2;
  }  WiFi_STA_IPConfig;
  //////
  
  #define WFM_LABEL_BEFORE 1
  #define WFM_LABEL_AFTER 2
  #define WFM_NO_LABEL 0
  
  /** Handle CORS in pages */
  // Default false for using only whenever necessary to avoid security issue when using CORS (Cross-Origin Resource Sharing)
  #ifndef USING_CORS_FEATURE
    // Contributed by AlesSt (https://github.com/AlesSt) to solve AJAX CORS protection problem of API redirects on client side
    // See more in https://github.com/khoih-prog/ESP_WiFiManager/issues/27 and https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
    #define USING_CORS_FEATURE     false
  #endif
  
  //KH
  //Mofidy HTTP_HEAD to WM_HTTP_HEAD_START to avoid conflict in Arduino esp8266 core 2.6.0+
  const char WM_HTTP_200[] PROGMEM = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
  const char WM_HTTP_HEAD_START[] PROGMEM = "<!DOCTYPE html><html lang='en'><head><meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'/><title>{v}</title>";
  
  // KH, update from v1.0.10
  const char WM_HTTP_STYLE[] PROGMEM = "<style>div{padding:2px;font-size:1em;}body,textarea,input,select{background: 0;border-radius: 0;font: 16px sans-serif;margin: 0}textarea,input,select{outline: 0;font-size: 14px;border: 1px solid #ccc;padding: 8px;width: 90%}.btn a{text-decoration: none}.container{margin: auto;width: 90%}@media(min-width:1200px){.container{margin: auto;width: 30%}}@media(min-width:768px) and (max-width:1200px){.container{margin: auto;width: 50%}}.btn,h2{font-size: 2em}h1{font-size: 3em}.btn{background: #0ae;border-radius: 4px;border: 0;color: #fff;cursor: pointer;display: inline-block;margin: 2px 0;padding: 10px 14px 11px;width: 100%}.btn:hover{background: #09d}.btn:active,.btn:focus{background: #08b}label>*{display: inline}form>*{display: block;margin-bottom: 10px}textarea:focus,input:focus,select:focus{border-color: #5ab}.msg{background: #def;border-left: 5px solid #59d;padding: 1.5em}.q{float: right;width: 64px;text-align: right}.l{background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==') no-repeat left center;background-size: 1em}input[type='checkbox']{float: left;width: 20px}.table td{padding:.5em;text-align:left}.table tbody>:nth-child(2n-1){background:#ddd}fieldset{border-radius:0.5rem;margin:0px;}</style>";
  //////
  
  // KH, update from v1.1.0
  const char WM_HTTP_SCRIPT[] PROGMEM = "<script>function c(l){document.getElementById('s').value=l.innerText||l.textContent;document.getElementById('p').focus();document.getElementById('s1').value=l.innerText||l.textContent;document.getElementById('p1').focus();document.getElementById('timezone').value=timezone.name();}</script>";
  //////
  
  // From v1.0.9 to permit disable or configure NTP from sketch
  #ifndef USE_ESP_WIFIMANAGER_NTP
    // From v1.0.6 to enable NTP config
    #define USE_ESP_WIFIMANAGER_NTP     true
  #endif
  
  #if USE_ESP_WIFIMANAGER_NTP
  
  #include "utils/TZ.h"
  
  const char WM_HTTP_SCRIPT_NTP_MSG[] PROGMEM = "<p>Your Timezone is : <b><label id='timezone' name='timezone'></b><script>document.getElementById('timezone').innerHTML=timezone.name();document.getElementById('timezone').value=timezone.name();</script></p>";
  const char WM_HTTP_SCRIPT_NTP_HIDDEN[] PROGMEM = "<p><input type='hidden' id='timezone' name='timezone'><script>document.getElementById('timezone').innerHTML=timezone.name();document.getElementById('timezone').value=timezone.name();</script></p>";
  
  #if ESP8266
    #if !(USE_CLOUDFLARE_NTP)
      #undef USE_CLOUDFLARE_NTP
      #define USE_CLOUDFLARE_NTP      true
      #warning Forcing USE_CLOUDFLARE_NTP for ESP8266 as low memory can cause blank page
    #endif
  #endif
  
  // To permit disable or configure NTP from sketch
  #ifndef USE_CLOUDFLARE_NTP
    #define USE_CLOUDFLARE_NTP          false
  #endif
  
  #if USE_CLOUDFLARE_NTP
  const char WM_HTTP_SCRIPT_NTP[] PROGMEM = "<script src='https://cdnjs.cloudflare.com/ajax/libs/jstimezonedetect/1.0.7/jstz.min.js'></script><script>var timezone=jstz.determine();console.log('Your CloudFlare timezone is:' + timezone.name());document.getElementById('timezone').innerHTML = timezone.name();</script>";
  #else
  const char WM_HTTP_SCRIPT_NTP[] PROGMEM = "<script>(function(e){var t=function(){'use strict';var e='s',n=function(e){var t=-e.getTimezoneOffset();return t!==null?t:0},r=function(e,t,n){var r=new Date;return e!==undefined&&r.setFullYear(e),r.setDate(n),r.setMonth(t),r},i=function(e){return n(r(e,0,2))},s=function(e){return n(r(e,5,2))},o=function(e){var t=e.getMonth()>7?s(e.getFullYear()):i(e.getFullYear()),r=n(e);return t-r!==0},u=function(){var t=i(),n=s(),r=i()-s();return r<0?t+',1':r>0?n+',1,'+e:t+',0'},a=function(){var e=u();return new t.TimeZone(t.olson.timezones[e])},f=function(e){var t=new Date(2010,6,15,1,0,0,0),n={'America/Denver':new Date(2011,2,13,3,0,0,0),'America/Mazatlan':new Date(2011,3,3,3,0,0,0),'America/Chicago':new Date(2011,2,13,3,0,0,0),'America/Mexico_City':new Date(2011,3,3,3,0,0,0),'America/Asuncion':new Date(2012,9,7,3,0,0,0),'America/Santiago':new Date(2012,9,3,3,0,0,0),'America/Campo_Grande':new Date(2012,9,21,5,0,0,0),'America/Montevideo':new Date(2011,9,2,3,0,0,0),'America/Sao_Paulo':new Date(2011,9,16,5,0,0,0),'America/Los_Angeles':new Date(2011,2,13,8,0,0,0),'America/Santa_Isabel':new Date(2011,3,5,8,0,0,0),'America/Havana':new Date(2012,2,10,2,0,0,0),'America/New_York':new Date(2012,2,10,7,0,0,0),'Asia/Beirut':new Date(2011,2,27,1,0,0,0),'Europe/Helsinki':new Date(2011,2,27,4,0,0,0),'Europe/Istanbul':new Date(2011,2,28,5,0,0,0),'Asia/Damascus':new Date(2011,3,1,2,0,0,0),'Asia/Jerusalem':new Date(2011,3,1,6,0,0,0),'Asia/Gaza':new Date(2009,2,28,0,30,0,0),'Africa/Cairo':new Date(2009,3,25,0,30,0,0),'Pacific/Auckland':new Date(2011,8,26,7,0,0,0),'Pacific/Fiji':new Date(2010,11,29,23,0,0,0),'America/Halifax':new Date(2011,2,13,6,0,0,0),'America/Goose_Bay':new Date(2011,2,13,2,1,0,0),'America/Miquelon':new Date(2011,2,13,5,0,0,0),'America/Godthab':new Date(2011,2,27,1,0,0,0),'Europe/Moscow':t,'Asia/Yekaterinburg':t,'Asia/Omsk':t,'Asia/Krasnoyarsk':t,'Asia/Irkutsk':t,'Asia/Yakutsk':t,'Asia/Vladivostok':t,'Asia/Kamchatka':t,'Europe/Minsk':t,'Australia/Perth':new Date(2008,10,1,1,0,0,0)};return n[e]};return{determine:a,date_is_dst:o,dst_start_for:f}}();t.TimeZone=function(e){'use strict';var n={'America/Denver':['America/Denver','America/Mazatlan'],'America/Chicago':['America/Chicago','America/Mexico_City'],'America/Santiago':['America/Santiago','America/Asuncion','America/Campo_Grande'],'America/Montevideo':['America/Montevideo','America/Sao_Paulo'],'Asia/Beirut':['Asia/Beirut','Europe/Helsinki','Europe/Istanbul','Asia/Damascus','Asia/Jerusalem','Asia/Gaza'],'Pacific/Auckland':['Pacific/Auckland','Pacific/Fiji'],'America/Los_Angeles':['America/Los_Angeles','America/Santa_Isabel'],'America/New_York':['America/Havana','America/New_York'],'America/Halifax':['America/Goose_Bay','America/Halifax'],'America/Godthab':['America/Miquelon','America/Godthab'],'Asia/Dubai':['Europe/Moscow'],'Asia/Dhaka':['Asia/Yekaterinburg'],'Asia/Jakarta':['Asia/Omsk'],'Asia/Shanghai':['Asia/Krasnoyarsk','Australia/Perth'],'Asia/Tokyo':['Asia/Irkutsk'],'Australia/Brisbane':['Asia/Yakutsk'],'Pacific/Noumea':['Asia/Vladivostok'],'Pacific/Tarawa':['Asia/Kamchatka'],'Africa/Johannesburg':['Asia/Gaza','Africa/Cairo'],'Asia/Baghdad':['Europe/Minsk']},r=e,i=function(){var e=n[r],i=e.length,s=0,o=e[0];for(;s<i;s+=1){o=e[s];if(t.date_is_dst(t.dst_start_for(o))){r=o;return}}},s=function(){return typeof n[r]!='undefined'};return s()&&i(),{name:function(){return r}}},t.olson={},t.olson.timezones={'-720,0':'Etc/GMT+12','-660,0':'Pacific/Pago_Pago','-600,1':'America/Adak','-600,0':'Pacific/Honolulu','-570,0':'Pacific/Marquesas','-540,0':'Pacific/Gambier','-540,1':'America/Anchorage','-480,1':'America/Los_Angeles','-480,0':'Pacific/Pitcairn','-420,0':'America/Phoenix','-420,1':'America/Denver','-360,0':'America/Guatemala','-360,1':'America/Chicago','-360,1,s':'Pacific/Easter','-300,0':'America/Bogota','-300,1':'America/New_York','-270,0':'America/Caracas','-240,1':'America/Halifax','-240,0':'America/Santo_Domingo','-240,1,s':'America/Santiago','-210,1':'America/St_Johns','-180,1':'America/Godthab','-180,0':'America/Argentina/Buenos_Aires','-180,1,s':'America/Montevideo','-120,0':'Etc/GMT+2','-120,1':'Etc/GMT+2','-60,1':'Atlantic/Azores','-60,0':'Atlantic/Cape_Verde','0,0':'Etc/UTC','0,1':'Europe/London','60,1':'Europe/Berlin','60,0':'Africa/Lagos','60,1,s':'Africa/Windhoek','120,1':'Asia/Beirut','120,0':'Africa/Johannesburg','180,0':'Asia/Baghdad','180,1':'Europe/Moscow','210,1':'Asia/Tehran','240,0':'Asia/Dubai','240,1':'Asia/Baku','270,0':'Asia/Kabul','300,1':'Asia/Yekaterinburg','300,0':'Asia/Karachi','330,0':'Asia/Kolkata','345,0':'Asia/Kathmandu','360,0':'Asia/Dhaka','360,1':'Asia/Omsk','390,0':'Asia/Rangoon','420,1':'Asia/Krasnoyarsk','420,0':'Asia/Jakarta','480,0':'Asia/Shanghai','480,1':'Asia/Irkutsk','525,0':'Australia/Eucla','525,1,s':'Australia/Eucla','540,1':'Asia/Yakutsk','540,0':'Asia/Tokyo','570,0':'Australia/Darwin','570,1,s':'Australia/Adelaide','600,0':'Australia/Brisbane','600,1':'Asia/Vladivostok','600,1,s':'Australia/Sydney','630,1,s':'Australia/Lord_Howe','660,1':'Asia/Kamchatka','660,0':'Pacific/Noumea','690,0':'Pacific/Norfolk','720,1,s':'Pacific/Auckland','720,0':'Pacific/Tarawa','765,1,s':'Pacific/Chatham','780,0':'Pacific/Tongatapu','780,1,s':'Pacific/Apia','840,0':'Pacific/Kiritimati'},typeof exports!='undefined'?exports.jstz=t:e.jstz=t})(this);</script><script>var timezone=jstz.determine();console.log('Your Timezone is:' + timezone.name());document.getElementById('timezone').innerHTML = timezone.name();</script>";
  #endif
  
  #else
    const char WM_HTTP_SCRIPT_NTP_MSG[]     PROGMEM   = "";
    const char WM_HTTP_SCRIPT_NTP_HIDDEN[]  PROGMEM   = "";
    const char WM_HTTP_SCRIPT_NTP[]         PROGMEM   = "";
  #endif
  
  // KH, update from v1.0.10
  const char WM_HTTP_HEAD_END[] PROGMEM = "</head><body><div class='container'><div style='text-align:left;display:inline-block;min-width:260px;'>";
  
  const char WM_FLDSET_START[]  PROGMEM = "<fieldset>";
  const char WM_FLDSET_END[]    PROGMEM = "</fieldset>";
  //////
  
  const char WM_HTTP_PORTAL_OPTIONS[] PROGMEM = "<form action='/wifi' method='get'><button class='btn'>Configuration</button></form><br/><form action='/i' method='get'><button class='btn'>Information</button></form><br/><form action='/close' method='get'><button class='btn'>Exit Portal</button></form><br/>";
  const char WM_HTTP_ITEM[] PROGMEM = "<div><a href='#p' onclick='c(this)'>{v}</a>&nbsp;<span class='q {i}'>{r}%</span></div>";
  const char JSON_ITEM[] PROGMEM    = "{\"SSID\":\"{v}\", \"Encryption\":{i}, \"Quality\":\"{r}\"}";
  
  // KH, update from v1.1.0
  //const char WM_HTTP_FORM_START[] PROGMEM = "<form method='get' action='wifisave'><fieldset><div><label>SSID</label><input id='s' name='s' length=32 placeholder='SSID'><div></div></div><div><label>Password</label><input id='p' name='p' length=64 placeholder='password'><div></div></div><div><label>SSID1</label><input id='s1' name='s1' length=32 placeholder='SSID1'><div></div></div><div><label>Password</label><input id='p1' name='p1' length=64 placeholder='password1'><div></div></div></fieldset>";
  //const char WM_HTTP_FORM_START[] PROGMEM = "<form method='get' action='wifisave'><fieldset><div><label>SSID</label><input id='s' name='s' length=32 placeholder='SSID'><div></div></div><div><label>Password</label><input id='p' name='p' length=64 placeholder='password'><div></div></div><div hidden><label>SSID1</label><input id='s1' name='s1' length=32 placeholder='SSID1'><div></div></div><div hidden><label>Password</label><input id='p1' name='p1' length=64 placeholder='password1'><div></div></div></fieldset>";
  //////
  
  // KH, add from v1.0.10
  const char WM_HTTP_FORM_LABEL_BEFORE[] PROGMEM = "<div><label for='{i}'>{p}</label><input id='{i}' name='{n}' length={l} placeholder='{p}' value='{v}' {c}><div></div></div>";
  const char WM_HTTP_FORM_LABEL_AFTER[] PROGMEM = "<div><input id='{i}' name='{n}' length={l} placeholder='{p}' value='{v}' {c}><label for='{i}'>{p}</label><div></div></div>";
  //////
  
  const char WM_HTTP_FORM_LABEL[] PROGMEM = "<label for='{i}'>{p}</label>";
  const char WM_HTTP_FORM_PARAM[] PROGMEM = "<input id='{i}' name='{n}' length={l} placeholder='{p}' value='{v}' {c}>";
  
  const char WM_HTTP_FORM_END[] PROGMEM = "<button class='btn' type='submit'>Save</button></form>";
  
  // KH, update from v1.1.0
  const char WM_HTTP_SAVED[] PROGMEM = "<div class='msg'><b>Credentials Saved</b><br>Try connecting ESP to the {x}/{x1} network. Wait around 10 seconds then check <a href='/'>if it's OK.</a> <p/>The {v} AP will run on the same WiFi channel of the {x}/{x1} AP. You may have to manually reconnect to the {v} AP.</div>";
  //////
  
  const char WM_HTTP_END[] PROGMEM = "</div></body></html>";
  
  //KH, from v1.1.0
  const char WM_HTTP_HEAD_CL[]         PROGMEM = "Content-Length";
  const char WM_HTTP_HEAD_CT[]         PROGMEM = "text/html";
  const char WM_HTTP_HEAD_CT2[]        PROGMEM = "text/plain";
  
  //KH Add repeatedly used const
  const char WM_HTTP_CACHE_CONTROL[]   PROGMEM = "Cache-Control";
  const char WM_HTTP_NO_STORE[]        PROGMEM = "no-cache, no-store, must-revalidate";
  const char WM_HTTP_PRAGMA[]          PROGMEM = "Pragma";
  const char WM_HTTP_NO_CACHE[]        PROGMEM = "no-cache";
  const char WM_HTTP_EXPIRES[]         PROGMEM = "Expires";
  const char WM_HTTP_CORS[]            PROGMEM = "Access-Control-Allow-Origin";
  const char WM_HTTP_CORS_ALLOW_ALL[]  PROGMEM = "*";
  
  #if USE_AVAILABLE_PAGES
  const char WM_HTTP_AVAILABLE_PAGES[] PROGMEM = "<h3>Available Pages</h3><table class='table'><thead><tr><th>Page</th><th>Function</th></tr></thead><tbody><tr><td><a href='/'>/</a></td><td>Menu page.</td></tr><tr><td><a href='/wifi'>/wifi</a></td><td>Show WiFi scan results and enter WiFi configuration.</td></tr><tr><td><a href='/wifisave'>/wifisave</a></td><td>Save WiFi configuration information and configure device. Needs variables supplied.</td></tr><tr><td><a href='/close'>/close</a></td><td>Close the configuration server and configuration WiFi network.</td></tr><tr><td><a href='/i'>/i</a></td><td>This page.</td></tr><tr><td><a href='/r'>/r</a></td><td>Delete WiFi configuration and reboot. ESP device will not reconnect to a network until new WiFi configuration data is entered.</td></tr><tr><td><a href='/state'>/state</a></td><td>Current device state in JSON format. Interface for programmatic WiFi configuration.</td></tr><tr><td><a href='/scan'>/scan</a></td><td>Run a WiFi scan and return results in JSON format. Interface for programmatic WiFi configuration.</td></tr></table>";
  #else
  const char WM_HTTP_AVAILABLE_PAGES[] PROGMEM = "";
  #endif
  
  //KH
  #define WIFI_MANAGER_MAX_PARAMS 20
  
  /////////////////////////////////////////////////////////////////////////////
  // New in v1.4.0
  typedef struct
  {
    const char *_id;
    const char *_placeholder;
    char       *_value;
    int         _length;
    int         _labelPlacement;
  
  }  WMParam_Data;
  //////
  
  class ESP_WMParameter 
  {
    public:
      ESP_WMParameter(const char *custom);
      ESP_WMParameter(const char *id, const char *placeholder, const char *defaultValue, int length, 
                      const char *custom = "", int labelPlacement = WFM_LABEL_BEFORE);
      
      // New in v1.4.0              
      ESP_WMParameter(const WMParam_Data& WMParam_data);                   
      //////
                     
      ~ESP_WMParameter();
      
      // New in v1.4.0
      void setWMParam_Data(const WMParam_Data& WMParam_data);
      void getWMParam_Data(WMParam_Data &WMParam_data);
      //////
  
      const char *getID();
      const char *getValue();
      const char *getPlaceholder();
      int         getValueLength();
      int         getLabelPlacement();
      const char *getCustomHTML();
      
    private:
    
      WMParam_Data _WMParam_data;
      
      const char *_customHTML;
  
      void init(const char *id, const char *placeholder, const char *defaultValue, int length, const char *custom, int labelPlacement);
  
      friend class ESP_WiFiManager;
  };
  
  #define USE_DYNAMIC_PARAMS				true
  #define DEFAULT_PORTAL_TIMEOUT  	60000L
  
  // From v1.0.10 to permit disable/enable StaticIP configuration in Config Portal from sketch. Valid only if DHCP is used.
  // You have to explicitly specify false to disable the feature.
  #ifndef USE_STATIC_IP_CONFIG_IN_CP
    #define USE_STATIC_IP_CONFIG_IN_CP          true
  #endif
  
  class ESP_WiFiManager
  {
    public:
  
      ESP_WiFiManager(const char *iHostname = "");
  
      ~ESP_WiFiManager();
  
      // Update feature from v1.0.11. Can use with STA staticIP now
      bool          autoConnect();
      bool          autoConnect(char const *apName, char const *apPassword = NULL);
      //////
  
      //if you want to start the config portal
      bool          startConfigPortal();
      bool          startConfigPortal(char const *apName, char const *apPassword = NULL);
  
      // get the AP name of the config portal, so it can be used in the callback
      String        getConfigPortalSSID();
      // get the AP password of the config portal, so it can be used in the callback
      String        getConfigPortalPW();
  
      void          resetSettings();
  
      //sets timeout before webserver loop ends and exits even if there has been no setup.
      //usefully for devices that failed to connect at some point and got stuck in a webserver loop
      //in seconds setConfigPortalTimeout is a new name for setTimeout
      void          setConfigPortalTimeout(unsigned long seconds);
      void          setTimeout(unsigned long seconds);
  
      //sets timeout for which to attempt connecting, usefull if you get a lot of failed connects
      void          setConnectTimeout(unsigned long seconds);
  
  
      void          setDebugOutput(bool debug);
      //defaults to not showing anything under 8% signal quality if called
      void          setMinimumSignalQuality(int quality = 8);
      
      // KH, new from v1.0.10 to enable dynamic/random channel
      int           setConfigPortalChannel(int channel = 1);
      //////
      
      //sets a custom ip /gateway /subnet configuration
      void          setAPStaticIPConfig(const IPAddress& ip, const IPAddress& gw, const IPAddress& sn);
      
      // New in v1.4.0
      void          setAPStaticIPConfig(const WiFi_AP_IPConfig&  WM_AP_IPconfig);
      void          getAPStaticIPConfig(WiFi_AP_IPConfig  &WM_AP_IPconfig);
      //////
      
      //sets config for a static IP
      void          setSTAStaticIPConfig(const IPAddress& ip, const IPAddress& gw, const IPAddress& sn);
      
      // New in v1.4.0
      void          setSTAStaticIPConfig(const WiFi_STA_IPConfig&  WM_STA_IPconfig);
      void          getSTAStaticIPConfig(WiFi_STA_IPConfig  &WM_STA_IPconfig);
      //////
      
  #if USE_CONFIGURABLE_DNS
      void          setSTAStaticIPConfig(const IPAddress& ip, const IPAddress& gw, const IPAddress& sn,
                                         const IPAddress& dns_address_1, const IPAddress& dns_address_2);
  #endif
  
      //called when AP mode and config portal is started
      void          setAPCallback(void(*func)(ESP_WiFiManager*));
      //called when settings have been changed and connection was successful
      void          setSaveConfigCallback(void(*func)());
  
  #if USE_DYNAMIC_PARAMS
      //adds a custom parameter
      bool 				addParameter(ESP_WMParameter *p);
  #else
      //adds a custom parameter
      void 				addParameter(ESP_WMParameter *p);
  #endif
  
      //if this is set, it will exit after config, even if connection is unsucessful.
      void          setBreakAfterConfig(bool shouldBreak);
      //if this is set, try WPS setup when starting (this will delay config portal for up to 2 mins)
      //TODO
      //if this is set, customise style
      void          setCustomHeadElement(const char* element);
      //if this is true, remove duplicated Access Points - defaut true
      void          setRemoveDuplicateAPs(bool removeDuplicates);
      //Scan for WiFiNetworks in range and sort by signal strength
      //space for indices array allocated on the heap and should be freed when no longer required
      int           scanWifiNetworks(int **indicesptr);
  
      // return SSID of router in STA mode got from config portal. NULL if no user's input //KH
      String				getSSID() 
      {
        return _ssid;
      }
  
      // return password of router in STA mode got from config portal. NULL if no user's input //KH
      String				getPW() 
      {
        return _pass;
      }
      
      // New from v1.1.0
      // return SSID of router in STA mode got from config portal. NULL if no user's input //KH
      String				getSSID1() 
      {
        return _ssid1;
      }
  
      // return password of router in STA mode got from config portal. NULL if no user's input //KH
      String				getPW1() 
      {
        return _pass1;
      }
      
      #define MAX_WIFI_CREDENTIALS        2
      
      String				getSSID(uint8_t index) 
      {
        if (index == 0)
          return _ssid;
        else if (index == 1)
          return _ssid1;
        else     
          return String("");
      }
      
      String				getPW(uint8_t index) 
      {
        if (index == 0)
          return _pass;
        else if (index == 1)
          return _pass1;
        else     
          return String("");
      }
      //////
      
      // New from v1.1.1, for configure CORS Header, default to WM_HTTP_CORS_ALLOW_ALL = "*"
  #if USING_CORS_FEATURE
      void setCORSHeader(const char* CORSHeaders)
      {     
        _CORS_Header = CORSHeaders;
  
        LOGWARN1(F("Set CORS Header to : "), _CORS_Header);
      }
      
      const char* getCORSHeader()
      {
        return _CORS_Header;
      }
  #endif   
      
      //returns the list of Parameters
      ESP_WMParameter** getParameters();
      // returns the Parameters Count
      int           getParametersCount();
  
      const char*   getStatus(int status);
  
  #ifdef ESP32
      String getStoredWiFiSSID();
      String getStoredWiFiPass();
  #endif
  
      String WiFi_SSID()
      {
  #ifdef ESP8266
        return WiFi.SSID();
  #else
        return getStoredWiFiSSID();
  #endif
      }
  
      String WiFi_Pass()
      {
  #ifdef ESP8266
        return WiFi.psk();
  #else
        return getStoredWiFiPass();
  #endif
      }
  
      void setHostname()
      {
        if (RFC952_hostname[0] != 0)
        {
  #if ESP8266      
          WiFi.hostname(RFC952_hostname);
  #else
  
    // Check cores/esp32/esp_arduino_version.h and cores/esp32/core_version.h
    //#if ( ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) )  //(ESP_ARDUINO_VERSION_MAJOR >= 2)
    #if ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) )
        WiFi.setHostname(RFC952_hostname);
    #else     
        // Still have bug in ESP32_S2 for old core. If using WiFi.setHostname() => WiFi.localIP() always = 255.255.255.255
        if ( String(ARDUINO_BOARD) != "ESP32S2_DEV" )
        {
          // See https://github.com/espressif/arduino-esp32/issues/2537
          WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
          WiFi.setHostname(RFC952_hostname);
        } 
     #endif    
  #endif        
        }
      }
  
  #if USE_ESP_WIFIMANAGER_NTP
      
      String getTimezoneName() 
      {  
        return _timezoneName;
      }
  
      void setTimezoneName(const String& inTimezoneName) 
      {  
        _timezoneName = inTimezoneName;
      }
      //////
      
      //See: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
      // EST5EDT,M3.2.0,M11.1.0 (for America/New_York)
      // EST5EDT is the name of the time zone
      // EST is the abbreviation used when DST is off
      // 6 hours is the time difference from GMT
      // EDT is the abbreviation used when DST is on
      // ,M3 is the third month
      // .2 is the second occurrence of the day in the month
      // .0 is Sunday
      // ,M11 is the eleventh month
      // .1 is the first occurrence of the day in the month
      // .0 is Sunday   
      
      const char * getTZ(const char * timezoneName)
      {               
        //const char TZ_NAME[][TIMEZONE_MAX_LEN]
        for (uint16_t index = 0; index < sizeof(TZ_NAME) / TIMEZONE_MAX_LEN; index++)
        {
          if ( !strncmp(timezoneName, (TZ_NAME[index]), strlen((TZ_NAME[index])) ) )
          {
            yield();
            return (ESP_TZ_NAME[index]);            
          }    
        }
        
        return "";      
      }
      
      
      const char * getTZ(const String& timezoneName)
      {
        return getTZ(timezoneName.c_str());      
      }
      
      
  #endif    
  
    private:
      std::unique_ptr<DNSServer>        dnsServer;
  
      //KH, for ESP32
  #ifdef ESP8266
      std::unique_ptr<ESP8266WebServer> server;
  #else		//ESP32
      std::unique_ptr<WebServer>        server;
  #endif
  
  #define RFC952_HOSTNAME_MAXLEN      24
      char RFC952_hostname[RFC952_HOSTNAME_MAXLEN + 1];
  
      char* getRFC952_hostname(const char* iHostname);
  
      void          setupConfigPortal();
      void          startWPS();
      //const char*   getStatus(int status);
  
      const char*   _apName = "no-net";
      const char*   _apPassword = NULL;
      
      String        _ssid   = "";
      String        _pass   = "";
      
      // New from v1.1.0
      String        _ssid1  = "";
      String        _pass1  = "";
      //////
      
  #if USE_ESP_WIFIMANAGER_NTP
      // Timezone info
      String        _timezoneName         = "";
  #endif
  
      unsigned long _configPortalTimeout  = 0;
  
      unsigned long _connectTimeout       = 0;
      unsigned long _configPortalStart    = 0;
  
      int           numberOfNetworks;
      int           *networkIndices;
      
      // KH, new from v1.0.10 to enable dynamic/random channel
      // default to channel 1
      #define MIN_WIFI_CHANNEL      1
      #define MAX_WIFI_CHANNEL      11    // Channel 12,13 is flaky, because of bad number 13 ;-)
  
      int _WiFiAPChannel = 1;
      //////
  
      // New in v1.4.0
      WiFi_AP_IPConfig  _WiFi_AP_IPconfig;
      
      WiFi_STA_IPConfig _WiFi_STA_IPconfig = { IPAddress(0, 0, 0, 0), IPAddress(192, 168, 2, 1), IPAddress(255, 255, 255, 0),
                                               IPAddress(192, 168, 2, 1), IPAddress(8, 8, 8, 8) };
      //////
      
      int           _paramsCount            = 0;
      int           _minimumQuality         = -1;
      bool          _removeDuplicateAPs     = true;
      bool          _shouldBreakAfterConfig = false;
      bool          _tryWPS                 = false;
  
      const char*   _customHeadElement      = "";
  
      int           status                  = WL_IDLE_STATUS;
      
      // New from v1.1.0, for configure CORS Header, default to WM_HTTP_CORS_ALLOW_ALL = "*"
  #if USING_CORS_FEATURE
      const char*     _CORS_Header          = WM_HTTP_CORS_ALLOW_ALL;   //"*";
  #endif   
      //////
      
      // New v1.0.8
      void          setWifiStaticIP();
      
      // New v1.1.0
      int           reconnectWifi();
      //////
      
      // New v1.0.11
      int           connectWifi(const String& ssid = "", const String& pass = "");
      //////
      
      uint8_t       waitForConnectResult();
  
      void          handleRoot();
      void          handleWifi();
      void          handleWifiSave();
      void          handleServerClose();
      void          handleInfo();
      void          handleState();
      void          handleScan();
      void          handleReset();
      void          handleNotFound();
      bool          captivePortal();
      
      void          reportStatus(String &page);
  
      // DNS server
      const byte    DNS_PORT = 53;
  
      //helpers
      int           getRSSIasQuality(int RSSI);
      bool          isIp(const String& str);
      String        toStringIp(const IPAddress& ip);
  
      bool          connect;
      bool          stopConfigPortal = false;
      
      bool          _debug = false;     //true;
  
      void(*_apcallback)  (ESP_WiFiManager*)  = NULL;
      void(*_savecallback)()              = NULL;
  
  #if USE_DYNAMIC_PARAMS
      int                    _max_params;
      ESP_WMParameter** _params;
  #else
      ESP_WMParameter* _params[WIFI_MANAGER_MAX_PARAMS];
  #endif
  
      template <typename Generic>
      void          DEBUG_WM(Generic text);
  
      template <class T>
      auto optionalIPFromString(T *obj, const char *s) -> decltype(obj->fromString(s)) {
        return  obj->fromString(s);
      }
      auto optionalIPFromString(...) -> bool {
        LOGINFO("NO fromString METHOD ON IPAddress, you need ESP8266 core 2.1.0 or newer for Custom IP configuration to work.");
        return false;
      }
  };
  
  #endif    // ESP_WiFiManager_h
  
  