arduino-sketches – Rev 39

Subversion Repositories:
Rev:
///////////////////////////////////////////////////////////////////////////
//  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 supplements the software side of the HAM radio antenna  //
// commuter project that can be found at the following addresss:         //
//   * https://grimore.org/ham_radio/                                    //
//       designing_a_remotely_controlled_motorized_antenna_commuter      //
//                                                                       //
// The template is used for remotely controlling an antenna switcher     //
// such that a single antenna can be used for two different radios. The  //
// template also implements the WiFi preboot environment from:           //
//   * https://grimore.org/arduino/wifipreboot                           //
// in order to ensure that the device can be easily connected to the     //
// WiFi network even after a disconnect.                                 //
//                                                                       //
// HAM antenna switch-specific variables are:                            //
//   * GPIO_BUTTON_A                                                     //
//   * GPIO_BUTTON_B                                                     //
//   * SERVO_GPIO_PIN                                                    //
//   * SERVO_ANGLE_A                                                     //
//   * SERVO_ANGLE_B                                                     //
// that correspond to the "A", respectively "B" buttons on the remote    //
// and "SERVO_GPIO_PIN" coresponds to the GPIO pin connected to the      //
// servo signal pin. Similarly, "SERVO_ANGLE_A"  and "SERVO_ANGLE_B" are //
// both the angles that correspond to the button "A", respectively the   //
// button "B" on the remote also corresponding to the "A", respectively  //
// "B" output ports of the antenna switch.                               //
//                                                                       //
// These variables must be set before uploading the sketch because they  //
// cannot be set later at runtime. Setting the angle of the servo is     //
// mostly a matter of trial and error and there is no magic that would   //
// yield the correct values for the servo.                               //
///////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////
//  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

// antenna commuter application
#define GPIO_BUTTON_A D5
#define GPIO_BUTTON_B D7
#define SERVO_GPIO_PIN D4
#define SERVO_ANGLE_A 0
#define SERVO_ANGLE_B 130

///////////////////////////////////////////////////////////////////////////
//  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>
#include <TickTwo.h>

// antenna commuter application
#include <Servo.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 arduinoOtaTickCallback(void);
void blinkDigitsDahTickCallback(void);
void blinkDigitsDitTickCallback(void);
void blinkDigitsBlinkTickCallback(void);
void clientWifiTickCallback(void);
void serverWifiTickCallback(void);
void handleServerWifi(void);
void handleClientWifi(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 rebootTickCallback(void);

// antenna commuter application
void buttonReadTickCallback(void);
void antennaSwitchTickCallback(void);
void antennaSwitchOffTickCallback(void);

///////////////////////////////////////////////////////////////////////////
//  variable declarations                                                //
///////////////////////////////////////////////////////////////////////////
#if defined(ARDUINO_ARCH_ESP8266)
ESP8266WebServer server(80);
#elif defined(ARDUINO_ARCH_ESP32)
WebServer server(80);
#endif

TickTwo arduinoOtaTick(arduinoOtaTickCallback, 1000);
TickTwo rebootTick(rebootTickCallback, 1000);
TickTwo clientWifiTick(clientWifiTickCallback, 250);
TickTwo serverWifiTick(serverWifiTickCallback, 250);
TickTwo blinkDigitsDahTick(blinkDigitsDahTickCallback, BLINK_DAH_LENGTH);
TickTwo blinkDigitsDitTick(blinkDigitsDitTickCallback, BLINK_DIT_LENGTH);
TickTwo blinkDigitsBlinkTick(blinkDigitsBlinkTickCallback, 25);

char* authenticationCookie = NULL;
bool otaStarted;
bool networkConnected;
int connectionTries;
bool rebootPending;
int temporarySsidLength;
int temporarySsidIndex;
int* temporarySsidNumbers;
int blinkLedState;

// antenna commuter application
TickTwo buttonReadTick(buttonReadTickCallback, 500);
TickTwo antennaSwitchTick(antennaSwitchTickCallback, 500);
TickTwo antennaSwitchOffTick(antennaSwitchOffTickCallback, 1000);

typedef enum {
  BUTTON_NONE,
  BUTTON_A,
  BUTTON_B
} antennaSwitch;
antennaSwitch antennaDirection = BUTTON_NONE;

// antenna commuter application
Servo antennaServo;

///////////////////////////////////////////////////////////////////////////
//  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" action="/setup">
         <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
    // start soft AP
    rebootTick.start();
    serverWifiTick.start();
    return;
  }
  
#ifdef DEBUG
    Serial.printf("No stored STA Ssid found, proceeding to soft AP...\n");
#endif
  clientWifiTick.start();

  // setup OTA
  ArduinoOTA.setHostname(configuration["name"].as<const char*>());
  // allow flashing with the master password
  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");
    }
  });

  // start timers / threads
  arduinoOtaTick.start();
  rebootTick.start();

  // antenna commuter application
  pinMode(GPIO_BUTTON_A, INPUT_PULLUP);
  pinMode(GPIO_BUTTON_B, INPUT_PULLUP);
  buttonReadTick.start();
  antennaSwitchTick.start();
}

void loop() {
  arduinoOtaTick.update();
  rebootTick.update();
  clientWifiTick.update();
  serverWifiTick.update();
  blinkDigitsDitTick.update();
  blinkDigitsDahTick.update();
  blinkDigitsBlinkTick.update();

  // antenna commuter application
  buttonReadTick.update();
  antennaSwitchTick.update();
  antennaSwitchOffTick.update();
}

///////////////////////////////////////////////////////////////////////////
//  end Arduino                                                          //
///////////////////////////////////////////////////////////////////////////


///////////////////////////////////////////////////////////////////////////
//  antenna commuter application                                         //
///////////////////////////////////////////////////////////////////////////
void buttonReadTickCallback(void) {
  // D5 -> A, D7 -> B
  int a = digitalRead(GPIO_BUTTON_A);
  int b = digitalRead(GPIO_BUTTON_B);

#ifdef DEBUG
  Serial.printf("Button A: %d, button B: %d\n", a, b);
#endif

  if(a == LOW) {
    antennaDirection = BUTTON_A;
    return;
  }

  if(b == LOW) {
    antennaDirection = BUTTON_B;
    return;
  }

  antennaDirection = BUTTON_NONE;
}

void antennaSwitchOffTickCallback(void) {
  antennaSwitchOffTick.pause();
  antennaServo.detach();
}

void antennaSwitchTickCallback(void) {
  if(antennaDirection == BUTTON_NONE) {
    return;
  }

  antennaServo.attach(SERVO_GPIO_PIN);
  switch(antennaDirection) {
    case BUTTON_A:
      antennaServo.write(SERVO_ANGLE_A);
      break;
    case BUTTON_B:
      antennaServo.write(SERVO_ANGLE_B);
      break;
  }
  antennaSwitchOffTick.interval(1000);
  antennaSwitchOffTick.resume();
 
  
  antennaDirection = BUTTON_NONE;
}

///////////////////////////////////////////////////////////////////////////
//  OTA updates                                                          //
///////////////////////////////////////////////////////////////////////////
void arduinoOtaTickCallback(void) {
  ArduinoOTA.handle();
  
  if(!networkConnected) {
    return;
  }
  
  if(!otaStarted) {
    ArduinoOTA.begin();
    otaStarted = true;
  }
}

///////////////////////////////////////////////////////////////////////////
//  system-wide reboot                                                   //
///////////////////////////////////////////////////////////////////////////
void rebootTickCallback(void) {
  // check if a reboot has been scheduled.
  if(!rebootPending) {
    return;
  }
#ifdef DEBUG
  Serial.printf("Reboot pending, restarting in 1s...\n");
#endif
  ESP.restart();
}

///////////////////////////////////////////////////////////////////////////
//  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 serverWifiTickCallback(void) {
  if(rebootPending) {
    return;
  }
  
  // create the boot Ssid
  String temporarySsid = computeTemporarySsid();
  if(WiFi.softAPSSID().equals(temporarySsid)) {
    // run WiFi server loops
    server.handleClient();
    if(blinkDigitsDahTick.state() == STOPPED) {
      temporarySsidLength = temporarySsid.length();
      temporarySsidNumbers = (int *) malloc(temporarySsidLength * sizeof(int));
      for(int i = 0; i < temporarySsidLength; ++i) {
        temporarySsidNumbers[i] = temporarySsid[i] - '0';
      }
#ifdef DEBUG
      //Serial.printf("Started blinking...\n");
#endif
      temporarySsidIndex = 0;
      blinkDigitsDahTick.start();
    }
    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 clientWifiTickCallback(void) {
  if(rebootPending) {
    return;
  }
  
  unsigned long callbackCount = clientWifiTick.counter();
#ifdef DEBUG
  //Serial.printf("Client tick %lu\n", callbackCount);
#endif
  if(callbackCount == 1) {
    #ifdef DEBUG
    Serial.printf("Rescheduling client WiFi to check every 10s...\n");
    #endif
    clientWifiTick.interval(WIFI_RETRY_TIMEOUT);
    clientWifiTick.resume();
  }
  
  // if WiFi is already connected or a reboot is pending just bail out
  if(WiFi.status() == WL_CONNECTED) {
#ifdef DEBUG
    Serial.printf("WiFi IP: %s\n", WiFi.localIP().toString().c_str());
#endif
    connectionTries = 0;
    networkConnected = true;
    return;
  }

  networkConnected = false;

  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 [%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>();
#ifdef DEBUG
  Serial.printf("Trying connection to %s with %s...\n", Ssid, password);
#endif
  WiFi.begin(Ssid, password);
}

///////////////////////////////////////////////////////////////////////////
//  blink the temporary Ssid                                             //
///////////////////////////////////////////////////////////////////////////
void blinkDigitsDahTickCallback(void) {
  // wait for the dits to complete
  if(blinkDigitsDitTick.state() != STOPPED) {
    return;
  }
  
  if(temporarySsidIndex >= temporarySsidLength) {
    blinkDigitsDahTick.stop();
    blinkDigitsDitTick.stop();
    blinkDigitsBlinkTick.stop();
    free(temporarySsidNumbers);
#ifdef DEBUG
    Serial.println();
    Serial.println("Dah-dit blink sequence completed.");
#endif
    return;
  }
  
#ifdef DEBUG
  Serial.printf("Starting to blink %d times: ", temporarySsidNumbers[temporarySsidIndex]);
#endif

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW); 
  blinkDigitsDitTick.start();
}

void blinkDigitsDitTickCallback(void) {
#ifdef DEBUG
  Serial.printf("Dit: %d/%d\n", blinkDigitsDitTick.counter(), temporarySsidNumbers[temporarySsidIndex]);
#endif
  if(blinkDigitsDitTick.counter() > temporarySsidNumbers[temporarySsidIndex]) {
    blinkDigitsDitTick.stop();
    ++temporarySsidIndex;
#ifdef DEBUG
    Serial.println("Dits completed...");
#endif
    return;
  }
  
  blinkDigitsDitTick.pause();
  blinkDigitsBlinkTick.start();
}

void blinkDigitsBlinkTickCallback(void) {
  if(blinkDigitsBlinkTick.counter() > 2) {
    blinkDigitsBlinkTick.stop();    
    blinkDigitsDitTick.resume();
    return;
  }
  blinkLedState = !blinkLedState;
  digitalWrite(LED_BUILTIN, blinkLedState);
}