Note

This is the synchronous version of the WiFi preboot environment.

Code

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2024 - License: GNU MIT        //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
// This template is a resilient implementation of a pre-WiFi connection  //
// environment that allows the user to configure the Ssid and password   //
// for the WiFi network via a built-in web-server that is automatically  //
// started by the template when no WiFi network has been configured.     //
///////////////////////////////////////////////////////////////////////////
// Purpose ////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// One of the problems is that given the cost, ESP devices are bought in //
// bulk, programmed and then sprawled out allover a site but typically   //
// Arduino templates have little accountibility or resillience built-in  //
// that would make the templates resist ESP resets and still be able to  //
// connect to the WiFi network. Similarly, in case the site is mobile,   //
// and in case the WiFi network changes, then all the ESPs will just     //
// have to be reprogrammed manually by the user which is a daunting task //
// relative to the amount of ESP devices in use. This template addresses //
// that issue by creating a robust mechanism where the ESP device will   //
// reboot in a preboot AP mode in case the WiFi network cannot be found  //
// or connected to in order to allow the user to reconfigure the ESP.    //
///////////////////////////////////////////////////////////////////////////
// Example Usage //////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
//   * configure the template parameters as need be within the           //
//     "configurable parameters" section of this template, the password  //
//     defined as a "master password" will grant access to configurng    //
//     the template and will also be used as the OTA password            //
//   * add any user-code to the connectedLoop() function that will be    //
//     called by the Arduino loop with a delay of 1 millisecond (the     //
//     user must include a delay in order to not throttle the CPU)       //
//   * when booting, the template will generate an Ssid based on the ESP //
//     CPU identifier consisting of up to two digits and will blink the  //
//     built-in ESP LED in sequence in order to give away the AP         //
//   * connect to the numeric AP started by the ESP and configure the    //
//     network Ssid and password                                         //
//   * the template will now connect to the WiFi network using the       //
//     provided Ssid and password; iff. the WiFi disconnects from the    //
//     WiFi network for more than the amount of milliseconds given by:   //
//                                                                       //
//     WIFI_RETRY_TIMEOUT * WIFI_CONNECT_TRIES                           //
//                                                                       //
//     then the template will restart again in AP mode, blink the LED    //
//     of the numeric Ssid and wait to be configured for a WiFi network  //
///////////////////////////////////////////////////////////////////////////
// Libraries //////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// The libraries used are minimal and the kind of libraries that have a  //
// wide-range of applications, in particular if the ESP device is WiFi   //
// enabled. Here is a complete list of libraries used by the template:   //
//   * ArduinoJson (very popular JSON library)                           //
///////////////////////////////////////////////////////////////////////////
// Credits ////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// The template is loosely inspired by the many captive portal solutions //
// out there but with some minimalism in mind and additionally exposing  //
// various configurable parameters such as the HTML webpage. Other close //
// similarities consist in the Tasmota firmware that accomplishes more   //
// or less the same switch between connected to a WiFi network and AP    //
// mode that allows the user to configure the WiFi network.              //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//  configurable parameters                                              //
///////////////////////////////////////////////////////////////////////////
 
// comment out to enable debugging
//#define DEBUG 1
 
// set the master password for OTA updates and access to the soft AP
#define PREBOOT_MASTER_PASSWORD ""
 
// the name and length of the cookie to use for authentication
#define PREBOOT_COOKIE_NAME "ArduinoPrebootCookie"
#define PREBOOT_COOKIE_MAX_LENGTH 256
 
// timeout to establish STA connection in milliseconds
#define WIFI_RETRY_TIMEOUT 1000 * 10
 
// retries as multiples of WIFI_RETRY_TIMEOUT milliseconds
#define WIFI_CONNECT_TRIES 60
 
// how much time to wait for a client to reconfigure before switching to client mode again
#define WIFI_SERVER_TIMEOUT 1000 * 60 * 3
 
// the time between blinking a single digit
#define BLINK_DIT_LENGTH 250
 
// the time between blinking the whole number
#define BLINK_DAH_LENGTH 2500
 
///////////////////////////////////////////////////////////////////////////
//  includes                                                             //
///////////////////////////////////////////////////////////////////////////
#include <DNSServer.h>
#if defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include "esp_mac.h"
#include <WebServer.h>
#include <ESPmDNS.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ESP8266WebServer.h>
#endif
 
#include <FS.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
// Arduino OTA
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
 
// Platform specific defines.
#if defined(ARDUINO_ARCH_ESP8266)
#define GET_CHIP_ID() (ESP.getChipId())
#elif defined(ARDUINO_ARCH_ESP32)
#define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32))
#endif
 
#define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX))
#define CONFIGURATION_FILE_NAME "/config.json"
#define CONFIGURATION_MAX_LENGTH 1024
 
///////////////////////////////////////////////////////////////////////////
//  function definitions                                                 //
///////////////////////////////////////////////////////////////////////////
byte* getHardwareAddress(void);
char* getHardwareAddress(char colon);
String generateTemporarySSID(void);
void blinkDigits(int* digits, int count, void (*callback)(), int dit = 250, int dah = 2500);
void clientWifi(void);
void serverWifi(void);
void blinkDigitsIdle(void);
 
bool setConfiguration(const char* configurationFile, JsonDocument& configuration);
int getConfiguration(const char* configurationFile, JsonDocument& configuration);
 
void handleRootHttpRequest(void);
void handleRootCssRequest(void);
void handleSetupHttpRequest(void);
void handleRootHttpGet(void);
void handleSetupHttpGet(void);
void handleRootHttpPost(void);
void handleSetupHttpPost(void);
void handleHttpNotFound(void);
 
void arduinoLoop(void);
 
///////////////////////////////////////////////////////////////////////////
//  variable declarations                                                //
///////////////////////////////////////////////////////////////////////////
IPAddress softAPAddress(8, 8, 8, 8);
IPAddress softAPNetmask(255, 255, 255, 0);
 
DNSServer dnsServer;
#if defined(ARDUINO_ARCH_ESP8266)
ESP8266WebServer server(80);
#elif defined(ARDUINO_ARCH_ESP32)
WebServer server(80);
#endif
 
int connectionTries;
bool rebootPending;
 
enum bootMode : int {
  BOOT_MODE_NONE = 0, 
  BOOT_MODE_CLIENT, 
  BOOT_MODE_SERVER
};
 
unsigned long lastWifiExecuteTime;
char* authenticationCookie = NULL;
bool otaStarted;
bool otaInProgress;
bool networkConnected;
int serverTicks;
 
///////////////////////////////////////////////////////////////////////////
//  HTML & CSS templates                                                 //
///////////////////////////////////////////////////////////////////////////
const char* GENERIC_CSS_TEMPLATE = R"html(
* {
  box-sizing: border-box;
}
body {
  background-color: #3498db;
  font-family: "Arial", sans-serif;
  padding: 50px;
}
.container {
  margin: 20px auto;
  padding: 10px;
  width: 300px;
  height: 100%;
  background-color: #fff;
  border-radius: 5px;
  margin-left: auto;
  margin-right: auto;
}
h1 {
  width: 70%;
  color: #777;
  font-size: 32px;
  margin: 28px auto;
  text-align: center;
}
form {
  text-align: center;
}
input {
  padding: 12px 0;
  margin-bottom: 10px;
  border-radius: 3px;
  border: 2px solid transparent;
  text-align: center;
  width: 90%;
  font-size: 16px;
  transition: border 0.2s, background-color 0.2s;
}
form .field {
  background-color: #ecf0f1;
}
form .field:focus {
  border: 2px solid #3498db;
}
form .btn {
  background-color: #3498db;
  color: #fff;
  line-height: 25px;
  cursor: pointer;
}
form .btn:hover,
form .btn:active {
  background-color: #1f78b4;
  border: 2px solid #1f78b4;
}
.pass-link {
  text-align: center;
}
.pass-link a:link,
.pass-link a:visited {
  font-size: 12px;
  color: #777;
}
table {
  border: 1px solid #dededf;
  border-collapse: collapse;
  border-spacing: 1px;
  margin-left: auto;
  margin-right: auto;
  width: 80%;
}
td {
  border: 1px solid #dededf;
  background-color: #ffffff;
  color: #000000;
  padding: 1em;
}
)html";
 
const char* HTML_SETUP_TEMPLATE = R"html(
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>setup</title>
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <div class="container">
      <h1>setup</h1>
      <table>
        <tr>
          <td>AP</td>
          <td>%AP%</td>
        </tr>
        <tr>
          <td>MAC</td>
          <td>%MAC%</td>
        </tr>
      </table>
      <br>
      <form method="POST" action="/setup">
        <label for="name">Name</label>
        <input id="name" type="text" name="name" value="%NAME%" class="field">
        <label for="Ssid">SSID</label>
        <input id="Ssid" type="text" name="Ssid" class="field">
        <label for="password">Password</label>
        <input id="password" type="password" name="password" class="field">
        <input type="submit" value="login" class="btn">
      </form>
    </div>
  </body>
</html>
)html";
 
const char* HTML_AUTH_TEMPLATE = R"html(
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Preboot Access</title>
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <div class="container">
      <h1>admin</h1>
      <form method="POST">
        <input id="password" type="password" name="password" class="field" placeholder="password">
        <input type="submit" value="login" class="btn">
      </form>
    </div>
  </body>
</html>
)html";
 
///////////////////////////////////////////////////////////////////////////
//  begin Arduino                                                        //
///////////////////////////////////////////////////////////////////////////
void setup() {
#ifdef DEBUG
  Serial.begin(115200);
  // wait for serial
  while (!Serial) {
    delay(100);
  }
 
  Serial.println();
#else
  Serial.end();
#endif
 
#ifdef DEBUG
  Serial.println("Mounting filesystem...");
#endif
#if defined(ARDUINO_ARCH_ESP8266)
  if (!LittleFS.begin()) {
#ifdef DEBUG
    Serial.println("LittleFS mount failed, formatting and rebooting...");
#endif
    LittleFS.format();
    delay(1000);
    ESP.restart();
#elif defined(ARDUINO_ARCH_ESP32)
  if (!LittleFS.begin(true)) {
#endif
#ifdef DEBUG
    Serial.println("LittleFS mount & format failed...");
#endif
    return;
  }
 
#ifdef DEBUG
  Serial.printf("Checking if WiFi server must be started...\n");
#endif
  // check if Ssid is set and start soft AP or STA mode
  DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
  if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) {
#ifdef DEBUG
    Serial.println("Unable to retrieve configuration.");
#endif
    delay(60000);
    ESP.restart();
    return;
  }
 
  switch(configuration["boot"].as<int>()) {
    case BOOT_MODE_CLIENT:
#ifdef DEBUG
      Serial.printf("Client connecting to WiFi...\n");
#endif
      clientWifi();
      arduinoLoop();
      delay(1);
      break;
    case BOOT_MODE_SERVER:
    case BOOT_MODE_NONE:
#ifdef DEBUG
      Serial.printf("Server AP starting...\n");
#endif
      // start soft AP
      serverWifi();
      delay(250);
      break;
  }
 
  // setup OTA
  ArduinoOTA.setHostname(configuration["name"].as<const char*>());
  // allow flashing with the master password
  ArduinoOTA.setPassword(PREBOOT_MASTER_PASSWORD);
  ArduinoOTA.onStart([]() {
    // mark OTA as started
    otaInProgress = true;
 
    // stop LittleFS as per the documentation
    LittleFS.end();
 
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else {  // U_FS
      type = "filesystem";
    }
 
    // NOTE: if updating FS this would be the place to unmount FS using FS.end()
#ifdef DEBUG
    Serial.println("Start updating " + type);
#endif
  });
  ArduinoOTA.onEnd([]() {
    otaInProgress = false;
#ifdef DEBUG
    Serial.println("\nEnd");
#endif
    // restart the device
#ifdef DEBUG
    Serial.printf("Restarting ESP.\n");
#endif
    delay(1000);
    ESP.restart();
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
#ifdef DEBUG
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
#endif
  });
  ArduinoOTA.onError([](ota_error_t error) {
#ifdef DEBUG
    Serial.printf("Error[%u]: ", error);
#endif
    if (error == OTA_AUTH_ERROR) {
#ifdef DEBUG
      Serial.println("Auth Failed");
#endif
    } else if (error == OTA_BEGIN_ERROR) {
#ifdef DEBUG
      Serial.println("Begin Failed");
#endif
    } else if (error == OTA_CONNECT_ERROR) {
#ifdef DEBUG
      Serial.println("Connect Failed");
#endif
    } else if (error == OTA_RECEIVE_ERROR) {
#ifdef DEBUG
      Serial.println("Receive Failed");
#endif
    } else if (error == OTA_END_ERROR) {
#ifdef DEBUG
      Serial.println("End Failed");
#endif
    }
  });
}
 
void loop() {
  // check if a reboot has been scheduled.
  if(rebootPending) {
#ifdef DEBUG
    Serial.printf("Reboot pending, restarting in 1s...\n");
#endif
    delay(1000);
    ESP.restart();
  }
 
  DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
  configuration["boot"] = BOOT_MODE_CLIENT;
  switch(configuration["boot"].as<int>()) {
    case BOOT_MODE_CLIENT:
      break;
    case BOOT_MODE_SERVER:
      unsigned long serverTime = ++serverTicks * 250;
      if(serverTime >= WIFI_SERVER_TIMEOUT) {
    #ifdef DEBUG
        Serial.println("Server timeout, rebooting...\n");
    #endif
        DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
        configuration["boot"] = BOOT_MODE_CLIENT;
 
        rebootPending = true;
        return;
      }
 
      #ifdef DEBUG
      /*
        if(callbackTickTime % 1000 == 0 ) {
          Serial.printf("Time till reboot %.0fs\n", (float)(WIFI_SERVER_TIMEOUT - callbackTickTime)/1000.0);
        }
      */
      #endif
      serverWifi();
      delay(250);
      break;
  }
 
}
 
///////////////////////////////////////////////////////////////////////////
//  end Arduino                                                          //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//  user code goes here, connectedLoop invoked from Arduino loop()       //
///////////////////////////////////////////////////////////////////////////
void arduinoLoop(void) {
  // USER CODE, USER CODE, USER CODE, USER CODE, USER CODE, USER CODE, ...
  Serial.printf("User code...\n");
  ArduinoOTA.handle();
  delay(1000);
}
 
///////////////////////////////////////////////////////////////////////////
//  HTTP route handling                                                  //
///////////////////////////////////////////////////////////////////////////
void handleRootHttpPost(void) {
  String password;
  for(int i = 0; i < server.args(); ++i) {
    if(server.argName(i) == "password") {
      password = server.arg(i);
      continue;
    }
  }
 
  if(!password.equals(PREBOOT_MASTER_PASSWORD)) {
    server.sendHeader("Location", "/");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(302);
    return;
  }
 
#ifdef DEBUG
  Serial.println("Authentication succeeded, setting cookie and redirecting.");
#endif
 
  // clear old authentication cookie
  if(authenticationCookie != NULL) {
    free(authenticationCookie);
    authenticationCookie = NULL;
  }
 
  authenticationCookie = randomStringHex(8);
  char* buff = (char*) malloc(PREBOOT_COOKIE_MAX_LENGTH * sizeof(char));
  snprintf(buff, PREBOOT_COOKIE_MAX_LENGTH, "%s=%s; Max-Age=600; SameSite=Strict", PREBOOT_COOKIE_NAME, authenticationCookie);
#ifdef DEBUG
  Serial.printf("Preboot cookie set to: %s\n", buff);
#endif
  server.sendHeader("Set-Cookie", buff);
  server.sendHeader("Location", "/setup");
  server.sendHeader("Cache-Control", "no-cache");
  server.send(302);
  free(buff);
}
 
void handleSetupHttpPost(void) {
  String espName, staSsid, password;
  for(int i = 0; i < server.args(); ++i) {
    if(server.argName(i) == "name") {
      espName = server.arg(i);
      continue;
    }
 
    if(server.argName(i) == "Ssid") {
      staSsid = server.arg(i);
      continue;
    }
 
    if(server.argName(i) == "password") {
      password = server.arg(i);
      continue;
    }
  }
 
  if(espName == NULL || staSsid == NULL || password == NULL) {
      server.sendHeader("Location", "/");
      server.sendHeader("Cache-Control", "no-cache");
      server.send(302);
      return;
  }
 
#ifdef DEBUG
  Serial.printf("Ssid %s and password %s received from web application.\n", staSsid, password);
#endif
  DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
  configuration["name"] = espName;
  configuration["Ssid"] = staSsid;
  configuration["password"] = password;
  configuration["boot"] = BOOT_MODE_CLIENT;
  if(!setConfiguration(CONFIGURATION_FILE_NAME, configuration)) {
#ifdef DEBUG
    Serial.printf("Failed to write configuration.\n");
#endif
    server.sendHeader("Location", "/setup");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(307);
    return;
  }
 
  server.send(200, "text/plain", "Parameters applied. Scheduling reboot...");
 
#ifdef DEBUG
  Serial.printf("Configuration applied...\n");
#endif
 
  rebootPending = true;
}
 
void handleRootHttpGet(void) {  
  // send login form
#ifdef DEBUG
  Serial.printf("Sending authentication webpage.\n");
#endif
  String processTemplate = String(HTML_AUTH_TEMPLATE);
  server.send(200, "text/html", processTemplate);
}
 
void handleSetupHttpGet(void) {  
  DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
  if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) {
#ifdef DEBUG
    Serial.println("Unable to retrieve configuration.");
#endif
    server.sendHeader("Location", "/setup");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(307);
  }
 
  String espName = HOSTNAME();
  if(configuration.containsKey("name")) {
    espName = configuration["name"].as<const char*>();
  }
  // send default boot webpage
#ifdef DEBUG
  Serial.printf("Sending configuration form webpage.\n");
#endif
  String processTemplate = String(HTML_SETUP_TEMPLATE);
  processTemplate.replace("%AP%", generateTemporarySSID());
  processTemplate.replace("%MAC%", getHardwareAddress(':'));
  processTemplate.replace("%NAME%", espName);
  server.send(200, "text/html", processTemplate);
}
 
void handleRootHttpRequest(void) {
  switch(server.method()) {
    case HTTP_GET:
      handleRootHttpGet();
      break;
    case HTTP_POST:
      handleRootHttpPost();
      break;
  }
}
 
void handleRootCssRequest(void) {
  if(server.method() != HTTP_GET) {
    handleHttpNotFound();
    return;
  }
 
#ifdef DEBUG
  Serial.println("Sending stylesheet...");
#endif  
  String rootCss = String(GENERIC_CSS_TEMPLATE);
  server.send(200, "text/css", rootCss);
}
 
void handleSetupHttpRequest(void) {
#ifdef DEBUG
  Serial.println("HTTP setup request received.");
#endif
  if(!server.hasHeader("Cookie")) {
#ifdef DEBUG
    Serial.println("No cookie header found.");
#endif
    server.sendHeader("Location", "/");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(302);
    return;
  }
 
  String cookie = server.header("Cookie");
  if(authenticationCookie == NULL || cookie.indexOf(authenticationCookie) == -1) {
#ifdef DEBUG
    Serial.println("Authentication failed.");
#endif
    server.sendHeader("Location", "/");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(302);
    return;
  }
 
  switch(server.method()) {
    case HTTP_GET:
#ifdef DEBUG
      Serial.printf("HTTP GET request received for setup.\n");
#endif
      handleSetupHttpGet();
      break;
    case HTTP_POST:
#ifdef DEBUG
      Serial.printf("HTTP POST request received for setup.\n");
#endif
      handleSetupHttpPost();
      break;
  }
}
 
void handleHttpNotFound(void) {
  server.sendHeader("Cache-Control", "no-cache");
  server.send(404);
}
 
///////////////////////////////////////////////////////////////////////////
//  set the current configuration                                        //
///////////////////////////////////////////////////////////////////////////
bool setConfiguration(const char* configurationFile, JsonDocument& configuration) {
#if defined(ARDUINO_ARCH_ESP8266)
  File file = LittleFS.open(configurationFile, "w");
#elif defined(ARDUINO_ARCH_ESP32)
  File file = LittleFS.open(configurationFile, FILE_WRITE);
#endif
  if(!file) {
#ifdef DEBUG
    Serial.println("Failed to open file for writing.");
#endif
    return false;
  }
  size_t bytesWritten = serializeJson(configuration, file);
  file.close();
#ifdef DEBUG
  Serial.printf("Written bytes %d vs. document bytes %d\n", bytesWritten, measureJson(configuration));
#endif
  return bytesWritten == measureJson(configuration);
}
 
///////////////////////////////////////////////////////////////////////////
//  get the current configuration                                        //
///////////////////////////////////////////////////////////////////////////
int getConfiguration(const char* configurationFile, JsonDocument& configuration) {
#if defined(ARDUINO_ARCH_ESP8266)
  File file = LittleFS.open(configurationFile, "r");
#elif defined(ARDUINO_ARCH_ESP32)
  File file = LittleFS.open(configurationFile);
#endif
 
  if (!file) {
#ifdef DEBUG
    Serial.println("Failed to open file for reading.");
#endif
    return false;
  }
 
  DeserializationError error = deserializeJson(configuration, file);
  file.close();
  if(error) {
#ifdef DEBUG
    Serial.printf("Deserialization failed with error %s\n", error.c_str());
#endif
    return -1;
  }
 
  return measureJson(configuration);
}
 
///////////////////////////////////////////////////////////////////////////
//  generate random string                                               //
///////////////////////////////////////////////////////////////////////////
char* randomStringHex(int length) {
  const char alphabet[] = "0123456789abcdef";
  char* payload = (char*) malloc(length * sizeof(char));
  int i;
  for (i=0; i<length; ++i) {
    payload[i] = alphabet[random(16)];
  }
  payload[i] = '\0';
  return payload;
}
 
 
///////////////////////////////////////////////////////////////////////////
//  get wireless status                                                  //
///////////////////////////////////////////////////////////////////////////
const char* wl_status_to_string(wl_status_t status) {
  switch (status) {
    case WL_NO_SHIELD: return "WL_NO_SHIELD";
    case WL_IDLE_STATUS: return "WL_IDLE_STATUS";
    case WL_NO_SSID_AVAIL: return "WL_NO_SSID_AVAIL";
    case WL_SCAN_COMPLETED: return "WL_SCAN_COMPLETED";
    case WL_CONNECTED: return "WL_CONNECTED";
    case WL_CONNECT_FAILED: return "WL_CONNECT_FAILED";
    case WL_CONNECTION_LOST: return "WL_CONNECTION_LOST";
    case WL_DISCONNECTED: return "WL_DISCONNECTED";
#if defined(ARDUINO_ARCH_ESP32)
    case WL_STOPPED: return "WL_STOPPED";
#endif
  }
 
  return "UNKNOWN";
}
 
///////////////////////////////////////////////////////////////////////////
//  get WiFi MAC address                                                 //
///////////////////////////////////////////////////////////////////////////
byte* getHardwareAddress(void) {
  // get mac address
  byte* mac = (byte *)malloc(6 * sizeof(byte));
#if defined(ARDUINO_ARCH_ESP8266)
  WiFi.macAddress(mac);
#elif defined(ARDUINO_ARCH_ESP32)
  Network.macAddress(mac);
#endif
  return mac;
}
 
///////////////////////////////////////////////////////////////////////////
//  convert MAC address to string                                        //
///////////////////////////////////////////////////////////////////////////
char* getHardwareAddress(char colon) {
  byte* mac = getHardwareAddress();
  char* buff = (char *)malloc(18 * sizeof(char));
  sprintf(buff, "%02x%c%02x%c%02x%c%02x%c%02x%c%02x", 
    mac[0], 
    colon,
    mac[1],
    colon,
    mac[2],
    colon, 
    mac[3],
    colon, 
    mac[4],
    colon, 
    mac[5]
  );
 
  free(mac);
  return buff;
}
 
 
///////////////////////////////////////////////////////////////////////////
//  get WiFi soft AP                                                     //
///////////////////////////////////////////////////////////////////////////
String generateTemporarySSID(void) {
  byte* mac = getHardwareAddress();
  String ssid = String(mac[0] ^ mac[1] ^ mac[2] ^ mac[3] ^ mac[4] ^ mac[5], DEC);
  free(mac);
  return ssid;
}
 
///////////////////////////////////////////////////////////////////////////
//  serve WiFi AP                                                        //
///////////////////////////////////////////////////////////////////////////
void serverWifi(void) {
  if(rebootPending) {
    return;
  }
 
  // create the boot Ssid
  String temporarySsid = generateTemporarySSID();
  if(WiFi.softAPSSID().equals(temporarySsid)) {
    // run WiFi server loops
    server.handleClient();
#ifdef DEBUG
    Serial.printf("ESP not configured, blinking soft AP SSID %s\n", temporarySsid.c_str());
#endif
    int lengthSsid = temporarySsid.length();
    int buff[lengthSsid];
    for(int i = 0; i < lengthSsid; ++i) {
        buff[i] = temporarySsid[i] - '0';
    }
    blinkDigits(buff, lengthSsid, blinkDigitsIdle);
    return;
  }
 
#ifdef DEBUG
  Serial.println("Starting HTTP server for Wifi server.");
#endif
  // handle HTTP REST requests
  server.on("/", handleRootHttpRequest);
  server.on("/setup", handleSetupHttpRequest);
  server.onNotFound(handleHttpNotFound);
 
#ifdef DEBUG
  Serial.println("Ensure HTTP headers are collected by the HTTP server.");
#endif
#if defined(ARDUINO_ARCH_ESP8266)
  server.collectHeaders("Cookie");
#elif defined(ARDUINO_ARCH_ESP32)
  const char* collectHeaders[] = { "Cookie" };
  size_t headerkeyssize = sizeof(collectHeaders) / sizeof(char *);
  server.collectHeaders(collectHeaders, headerkeyssize);
#endif
 
  // the soft AP (or WiFi) must be started before the HTTP server or it will result in a crash on ESP32
#ifdef DEBUG
  Serial.println("Starting temporary AP.");
#endif
  WiFi.softAP(temporarySsid, String(), 1, false, 1);
 
#ifdef DEBUG
  Serial.println("Starting HTTP server.");
#endif
  server.begin();
}
 
///////////////////////////////////////////////////////////////////////////
//  connect to WiFi                                                      //
///////////////////////////////////////////////////////////////////////////
void clientWifi(void) {
  if(rebootPending || otaInProgress) {
    return;
  }
 
  // only execute the check every WIFI_RETRY_TIMEOUT milliseconds
  const unsigned long currentTime = millis();
  if(currentTime - lastWifiExecuteTime < WIFI_RETRY_TIMEOUT) {
    return;
  }
 
  // if WiFi is already connected or a reboot is pending just bail out
  wl_status_t wifiStatus = WiFi.status();
  if(wifiStatus == WL_CONNECTED) {
#ifdef DEBUG
    Serial.println("--MARK--");
#endif
    if(!otaStarted) {
      ArduinoOTA.begin();
      otaStarted = true;
    }
    lastWifiExecuteTime = millis();
    return;
  }
 
#ifdef DEBUG
  Serial.printf("Client WiFi not connected: %d\n", wl_status_to_string(wifiStatus));
#endif
 
  DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
  if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) {
#ifdef DEBUG
    Serial.println("Unable to retrieve configuration.");
#endif
    return;
  }
 
  // too many retries so reboot to soft AP
  if(++connectionTries > WIFI_CONNECT_TRIES) {
    configuration["boot"] = BOOT_MODE_SERVER;
    if(!setConfiguration(CONFIGURATION_FILE_NAME, configuration)) {
#ifdef DEBUG
      Serial.printf("Failed to write configuration.\n");
#endif
    }
 
#ifdef DEBUG
    Serial.printf("Restarting in 1 second...\n");
#endif
 
    rebootPending = true;
    return;
  }
 
#ifdef DEBUG
  Serial.printf("Attempting to establish WiFi STA connecton [%d/%d]\n", (WIFI_CONNECT_TRIES - clientConnectionTries) + 1, WIFI_CONNECT_TRIES);
#endif
#if defined(ARDUINO_ARCH_ESP8266)
  WiFi.hostname(configuration["name"].as<String>());
#elif defined(ARDUINO_ARCH_ESP32)
  WiFi.setHostname(configuration["name"].as<const char*>());
#endif
  if (!MDNS.begin(configuration["name"].as<const char*>())) {
#ifdef DEBUG
    Serial.println("Error setting up MDNS responder.");
#endif
  }
  String Ssid = configuration["Ssid"].as<String>();
  String password = configuration["password"].as<String>();
#ifdef DEBUG
  Serial.printf("Trying connection to %s with %s...\n", Ssid, password);
#endif
  //WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
  WiFi.begin(Ssid, password);
 
  lastWifiExecuteTime = millis();
}
 
///////////////////////////////////////////////////////////////////////////
//  blink a string of numbers                                            //
///////////////////////////////////////////////////////////////////////////
void blinkDigits(int* digits, int count, void (*callback)(), int dit, int dah) {
  pinMode(LED_BUILTIN, OUTPUT);
  for(int i = 0; i < count; ++i) {
    do {
      digitalWrite(LED_BUILTIN, HIGH);
      delay(dit);
      callback();
      digitalWrite(LED_BUILTIN, LOW);
      delay(dit);
      callback();
    } while(--digits[i] > 0);
    delay(dah);
    callback();
  }
}
 
///////////////////////////////////////////////////////////////////////////
//  blinkDigits callback                                                 //
///////////////////////////////////////////////////////////////////////////
void blinkDigitsIdle() {
  server.handleClient();
}