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
 
// 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 10000
 
// retries as multiples of WIFI_RETRY_TIMEOUT milliseconds
#define WIFI_CONNECT_TRIES 30
 
// 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 <Arduino.h>
#if defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <WebServer.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 computeTemporarySsid(void);
void blinkDigits(int* digits, int count, void (*callback)(), int dit = 250, int dah = 2500);
void clientWifi(void);
void serverWifi(void);
void blinkDigitsIdle(void);
 
void setConfiguration(const char* configurationFile, DynamicJsonDocument configuration, int bufferSize);
DynamicJsonDocument getConfiguration(const char* configurationFile, int bufferSize);
 
void handleRootHttpRequest(void);
void handleSetupHttpRequest(void);
void handleRootHttpGet(void);
void handleSetupHttpGet(void);
void handleRootHttpPost(void);
void handleSetupHttpPost(void);
void handleHttpNotFound(void);
 
bool fsWriteFile(fs::FS &fs, const char *path, const char *payload);
bool fsReadFile(fs::FS &fs, const char *path, char *payload, size_t maxLength);
 
void arduinoLoop(void);
 
///////////////////////////////////////////////////////////////////////////
//  variable declarations                                                //
///////////////////////////////////////////////////////////////////////////
#if defined(ARDUINO_ARCH_ESP8266)
ESP8266WebServer server(80);
#elif defined(ARDUINO_ARCH_ESP32)
WebServer server(80);
#endif
 
int connectionTries;
bool rebootPending;
 
typedef enum {
  NONE,
  SERVER,
  CLIENT
} ExecuteState;
ExecuteState runtimeExecutionState;
 
unsigned long lastWifiExecuteTime;
char* authenticationCookie = NULL;
bool otaStarted;
 
///////////////////////////////////////////////////////////////////////////
//  HTML templates                                                       //
///////////////////////////////////////////////////////////////////////////
const char* HTML_BOOT_TEMPLATE = R"html(
<!DOCTYPE html>
<html lang="en">
   <head>
      <title>ESP Setup</title>
   </head>
   <body>
      <h1>ESP Setup</h1>
      <hr>
      AP: %AP%<br>
      MAC: %MAC%<br>
      <hr>
      <form method="POST">
         <label for="name">Name: </label>
         <input id="name" type="text" name="name" value="%NAME%">
         <br>
         <label for="Ssid">Ssid: </label>
         <input id="Ssid" type="text" name="Ssid">
         <br>
         <label for="password">Password: </label>
         <input id="password" type="password" name="password">
         <hr>
         <input type="submit" value="submit">
      </form>
   </body>
</html>
)html";
 
const char* HTML_AUTH_TEMPLATE = R"html(
<!DOCTYPE html>
<html lang="en">
   <head>
      <title>Preboot Access</title>
   </head>
   <body>
      <h1>Preboot Access</h1>
      <form method="POST">
         <label for="password">Master password: </label>
         <input id="password" type="password" name="password">
         <hr>
         <input type="submit" value="submit">
      </form>
   </body>
</html>
)html";
 
///////////////////////////////////////////////////////////////////////////
//  begin Arduino                                                        //
///////////////////////////////////////////////////////////////////////////
void setup() {
#ifdef DEBUG
  Serial.begin(115200);
  // wait for serial
  while (!Serial) {
    delay(100);
  }
 
  Serial.println();
#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
    Serial.println("LittleFS mount failed...");
    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 = getConfiguration(CONFIGURATION_FILE_NAME, CONFIGURATION_MAX_LENGTH);
  if(configuration.isNull() || !configuration.containsKey("Ssid")) {
#ifdef DEBUG
    Serial.printf("No stored STA Ssid found, proceeding to soft AP...\n");
#endif
    // set server mode
    runtimeExecutionState = SERVER;
    return;
  }
 
  runtimeExecutionState = CLIENT;
 
  // setup OTA
  ArduinoOTA.setHostname(configuration["name"].as<const char *>());
  ArduinoOTA.setPassword(PREBOOT_MASTER_PASSWORD);
  ArduinoOTA.onStart([]() {
    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()
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) {
      Serial.println("Auth Failed");
    } else if (error == OTA_BEGIN_ERROR) {
      Serial.println("Begin Failed");
    } else if (error == OTA_CONNECT_ERROR) {
      Serial.println("Connect Failed");
    } else if (error == OTA_RECEIVE_ERROR) {
      Serial.println("Receive Failed");
    } else if (error == OTA_END_ERROR) {
      Serial.println("End Failed");
    }
  });
}
 
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();
  }
 
  if(runtimeExecutionState == SERVER) {
    serverWifi();
    delay(250);
    return;
  }
 
  clientWifi();
  arduinoLoop();
  delay(1);
}
 
///////////////////////////////////////////////////////////////////////////
//  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;
  setConfiguration(CONFIGURATION_FILE_NAME, configuration, CONFIGURATION_MAX_LENGTH);
 
  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 = getConfiguration(CONFIGURATION_FILE_NAME, CONFIGURATION_MAX_LENGTH);
  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_BOOT_TEMPLATE);
  processTemplate.replace("%AP%", computeTemporarySsid());
  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 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("Location", "/");
  server.send(302);
}
 
///////////////////////////////////////////////////////////////////////////
//  LittleFS file operations                                               //
///////////////////////////////////////////////////////////////////////////
bool fsWriteFile(fs::FS &fs, const char *path, const char *payload) {
#if defined(ARDUINO_ARCH_ESP8266)
  File file = fs.open(path, "w");
#elif defined(ARDUINO_ARCH_ESP32)
  File file = fs.open(path, FILE_WRITE);
#endif
  if (!file) {
#ifdef DEBUG
    Serial.println("Failed to open file for writing.");
#endif
    return false;
  }
  bool success = file.println(payload);
  file.close();
 
  return success;
}
 
bool fsReadFile(fs::FS &fs, const char *path, char *payload, size_t maxLength) {
#if defined(ARDUINO_ARCH_ESP8266)
  File file = fs.open(path, "r");
#elif defined(ARDUINO_ARCH_ESP32)
  File file = fs.open(path);
#endif
  if (!file || file.isDirectory()) {
#ifdef DEBUG
    Serial.println("Failed to open file for reading.");
#endif
    return false;
  }
 
  int i = 0;
  while(file.available() && i < maxLength) {
    payload[i] = file.read();
    ++i;
  }
  file.close();
  payload[i] = '\0';
 
  return true;
}
 
///////////////////////////////////////////////////////////////////////////
//  set the current configuration                                        //
///////////////////////////////////////////////////////////////////////////
void setConfiguration(const char* configurationFile, DynamicJsonDocument configuration, int bufferSize) {
  char payload[bufferSize];
  serializeJson(configuration, payload, bufferSize);
  if(!fsWriteFile(LittleFS, configurationFile, payload)) {
#ifdef DEBUG
    Serial.printf("Unable to store configuration.\n");
#endif
  }
}
 
///////////////////////////////////////////////////////////////////////////
//  get the current configuration                                        //
///////////////////////////////////////////////////////////////////////////
DynamicJsonDocument getConfiguration(const char* configurationFile, int bufferSize) {
  DynamicJsonDocument configuration(bufferSize);
#ifdef DEBUG
  Serial.printf("Attempting to read configuration...\n");
#endif
  char* payload = (char *) malloc(bufferSize * sizeof(char));
  if (fsReadFile(LittleFS, configurationFile, payload, bufferSize)) {
#ifdef DEBUG
    Serial.printf("Found a valid configuration payload...\n");
#endif
    DeserializationError error = deserializeJson(configuration, payload);
    if(error) {
#ifdef DEBUG
      Serial.printf("Deserialization of configuration failed.\n");
#endif
    }
  }
#ifdef DEBUG
  Serial.printf("Configuration read complete.\n");
#endif
 
  free(payload);
  return 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 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 computeTemporarySsid(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;
  }
 
  String softAp = WiFi.softAPSSID();
#ifdef DEBUG
  Serial.printf("Checking whether the Wifi server is running %s\n", softAp);
#endif
 
  // create the boot Ssid
  String temporarySsid = computeTemporarySsid();
  if(softAp.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) {
    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
  if(WiFi.status() == WL_CONNECTED || rebootPending) {
#ifdef DEBUG
    Serial.printf("WiFi IP: %s\n", WiFi.localIP().toString().c_str());
#endif
    if(otaStarted) {
      ArduinoOTA.begin();
      otaStarted = true;
    }
    lastWifiExecuteTime = millis();
    return;
  }
 
  DynamicJsonDocument configuration = getConfiguration(CONFIGURATION_FILE_NAME, CONFIGURATION_MAX_LENGTH);
  // too many retries so reboot to soft AP
  if(++connectionTries > WIFI_CONNECT_TRIES) {
    // zap the Ssid in order to start softAP
    if(configuration.containsKey("Ssid")) {
      configuration.remove("Ssid");
    }
    if(configuration.containsKey("password")) {
      configuration.remove("password");
    }
 
    setConfiguration(CONFIGURATION_FILE_NAME, configuration, CONFIGURATION_MAX_LENGTH);
 
#ifdef DEBUG
    Serial.printf("Restarting in 1 second...\n");
#endif
 
    rebootPending = true;
    return;
  }
 
#ifdef DEBUG
  Serial.printf("Attempting to establish WiFi STA connecton with Ssid [%d/%d]\n", (WIFI_CONNECT_TRIES - connectionTries) + 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
  String Ssid = configuration["Ssid"].as<String>();
  String password = configuration["password"].as<String>();
  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();
}