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 is an Arduino sketch template that is meant to power a sensor    //
// cocktail / nexus for the following project:                           //
//   * https://grimore.org/iot/creating_a_sensor_cocktail                //
// Note that this sketch as it is, does not carry too much value because //
// it is just a variant of the WiFi preboot environment to be found at:  //
//   * https://grimore.org/arduino/wifipreboot                           //
// with the changes being local to the sensors application that contains //
// stuff like GPIO to sensor pin mapps that are wortheless on their own. //
// If you are looking for the template that this sketch is based on then //
// check out the wifipreboot template mentioned previously.              //
///////////////////////////////////////////////////////////////////////////
//  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
 
// The MQTT broker to connect to.
#define MQTT_HOST "iot.internal"
// The MQTT broker username.
#define MQTT_USERNAME ""
// The MQTT broker password.
#define MQTT_PASSWORD ""
// The MQTT broker port.
#define MQTT_PORT 1883
// The MQTT topic
#define MQTT_TOPIC ""
#define MQTT_TOPIC_OUTBOX "messages"
// Maximal size of an MQTT payload
#define MQTT_PAYLOAD_MAX_LENGTH MQTT_MAX_PACKET_SIZE
 
// 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("sensors") //String("esp-" + String(GET_CHIP_ID(), HEX))
#define CONFIGURATION_FILE_NAME "/config.json"
#define CONFIGURATION_MAX_LENGTH 1024
 
///////////////////////////////////////////////////////////////////////////
//  includes                                                             //
///////////////////////////////////////////////////////////////////////////
#include <DNSServer.h>
#if defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include "esp_mac.h"
#include <WebServer.h>
#include <ESPmDNS.h>
#include <mutex>
#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>
 
// Sensors application.
#include <PubSubClient.h>
// DHT
#include <DHT22.h>
#include <DFRobot_Geiger.h>
#include <GP2YDustSensor.h>
 
// Sensors application.
#define DECIBELMETER_PIN_ANALOG 36
#define LIGHT_PIN_ANALOG 35
#define UV_PIN_ANALOG 39
//#define RAIN_PIN_ANALOG 32
#define DHT_PIN_ANALOG 26
// digital
#define DUST_LED_PIN 18
#define DUST_PIN_ANALOG 33
#define CO2_PWM_PIN 22
#define GEIGER_PIN_ANALOG 21
#define WIND_PIN_ANALOG 34
 
///////////////////////////////////////////////////////////////////////////
//  function definitions                                                 //
///////////////////////////////////////////////////////////////////////////
byte* getHardwareAddress(void);
char* getHardwareAddress(char colon);
String generateTemporarySSID(void);
char* randomStringHex(int length);
void arduinoOtaTickCallback(void);
void blinkDigitsDahTickCallback(void);
void blinkDigitsDitTickCallback(void);
void blinkDigitsBlinkTickCallback(void);
void clientWifiTickCallback(void);
void serverWifiTickCallback(void);
void handleServerWifi(void);
void handleClientWifi(void);
const char* wl_status_to_string(wl_status_t status);
 
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 rebootTickCallback(void);
 
// Sensors application
void mqttTickCallback(void);
String getMqttTopic(void);
String getMqttId(void);
bool mqttConnect(void);
void mqttCallback(char *topic, byte *payload, unsigned int length);
float mapValueToRange(float value, float xMin, float xMax, float yMin, float yMax);
void sensorsTickCallback(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
 
TickTwo arduinoOtaTick(arduinoOtaTickCallback, 1000);
TickTwo rebootTick(rebootTickCallback, 1000);
TickTwo clientWifiTick(clientWifiTickCallback, 25);
TickTwo serverWifiTick(serverWifiTickCallback, 250);
TickTwo blinkDigitsDahTick(blinkDigitsDahTickCallback, BLINK_DAH_LENGTH);
TickTwo blinkDigitsDitTick(blinkDigitsDitTickCallback, BLINK_DIT_LENGTH);
TickTwo blinkDigitsBlinkTick(blinkDigitsBlinkTickCallback, 25);
TickTwo sensorsTick(sensorsTickCallback, 1000);
 
enum bootMode : int {
  BOOT_MODE_NONE = 0,
  BOOT_MODE_CLIENT,
  BOOT_MODE_SERVER
};
char* authenticationCookie = NULL;
bool otaStarted;
bool otaInProgress;
bool networkConnected;
int clientConnectionTries;
bool rebootPending;
int temporarySSIDLength;
int temporarySSIDIndex;
int* temporarySSIDNumbers;
int blinkLedState;
 
// Sensors application
WiFiClient espClient;
PubSubClient mqttClient(espClient);
TickTwo mqttTick(mqttTickCallback, 250);
String mqttTopicCache;
String mqttIdCache;
DHT22 dht22(DHT_PIN_ANALOG);
DFRobot_Geiger geiger(GEIGER_PIN_ANALOG);
GP2YDustSensor dustSensor(GP2YDustSensorType::GP2Y1014AU0F, DUST_LED_PIN, DUST_PIN_ANALOG);
 
///////////////////////////////////////////////////////////////////////////
//  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
    configuration["boot"] = BOOT_MODE_SERVER;
    if(!setConfiguration(CONFIGURATION_FILE_NAME, configuration)) {
  #ifdef DEBUG
      Serial.printf("Failed to write configuration.\n");
  #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
      clientWifiTick.start();
      break;
    case BOOT_MODE_SERVER:
    case BOOT_MODE_NONE:
#ifdef DEBUG
      Serial.printf("Server AP starting...\n");
#endif
      // start soft AP
      rebootTick.start();
      serverWifiTick.start();
      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
    }
  });
 
  // start timers / threads
  arduinoOtaTick.start();
  rebootTick.start();
 
  // Sensors application
  pinMode(DECIBELMETER_PIN_ANALOG, INPUT);
  pinMode(LIGHT_PIN_ANALOG, INPUT);
  pinMode(CO2_PWM_PIN, INPUT);
  pinMode(WIND_PIN_ANALOG, INPUT_PULLUP);
  geiger.start();
  dustSensor.begin();
 
  // start the sensors ticker
  mqttTick.start();
  sensorsTick.start();
}
 
void loop() {
  arduinoOtaTick.update();
  rebootTick.update();
  clientWifiTick.update();
  serverWifiTick.update();
  blinkDigitsDitTick.update();
  blinkDigitsDahTick.update();
  blinkDigitsBlinkTick.update();
 
  // Sensors application
  mqttTick.update();
  sensorsTick.update();
}
///////////////////////////////////////////////////////////////////////////
//  end Arduino                                                          //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//  Sensors-MQTT                                                         //
///////////////////////////////////////////////////////////////////////////
// https://grimore.org/fuss/mathematics/algebra#linearly_map_a_value_in_a_range_into_another_range
float mapValueToRange(float value, float xMin, float xMax, float yMin, float yMax) {
  return yMin + (
           (
             yMax - yMin
           )
           *
           (
             value - xMin
           )
           /
           (
             xMax - xMin
           )
         );
}
 
void sensorsTickCallback(void) {
  // pause operations when OTA is in progress or MQTT not available
  if(otaInProgress || !mqttClient.connected()) {
    return;
  }
 
  DynamicJsonDocument msg(MQTT_PAYLOAD_MAX_LENGTH);
 
  // visually signal data acquisition
  digitalWrite(LED_BUILTIN, 1);
 
  // measure noise level (dBA)
  float noise = mapValueToRange(analogRead(DECIBELMETER_PIN_ANALOG), 0, 4095.0, 0, 3.3) * 50.0;
#ifdef DEBUG
  Serial.println("Noise (dBA): " + String(noise));
#endif
  msg["noise"] = noise;
 
  // LM393 light sensor
  int light = analogRead(LIGHT_PIN_ANALOG);
  msg["light"] = light;
#ifdef DEBUG
  Serial.println("Light level: " + String(light));
#endif
 
  // UV sensor
  float ultraviolet = (analogRead(UV_PIN_ANALOG) / 4095.0 * 5.0) / 0.1;
  msg["ultraviolet"] = ultraviolet;
#ifdef DEBUG
  Serial.println("UV index: " + String(light));
#endif
 
  // add multiple tmperature readings to array
#ifdef DEBUG
  Serial.printf("Temperature: ");
#endif
  float t = dht22.getTemperature();
  if(!isnan(t)) {
    msg["temperature"] = t;
#ifdef DEBUG
    Serial.printf("%.1f", t);
#endif
  }
#ifdef DEBUG
  Serial.printf("\n");
#endif
#ifdef DEBUG
  Serial.printf("Humidity: ");
#endif
  float h = dht22.getHumidity();
  if(!isnan(h)) {
    msg["humidity"] = h;
#ifdef DEBUG
    Serial.printf("%.1f", h);
#endif
  }
#ifdef DEBUG
  Serial.printf("\n");
#endif
 
  // dust sensor in ug/m3
  JsonObject dust = msg.createNestedObject("dust");
#ifdef DEBUG
  Serial.printf("Getting dust density: ");
#endif
  float dustDensity = dustSensor.getDustDensity();
#ifdef DEBUG
  Serial.printf("%.1f\n", dustDensity);
#endif
  dust["density"] = dustDensity;
#ifdef DEBUG
  Serial.printf("Getting dust average: ");
#endif
  float dustAverage = dustSensor.getRunningAverage();
#ifdef DEBUG
  Serial.printf("%.1f\n", dustAverage);
#endif
  dust["average"] = dustAverage;
 
  // CO2 sensor
  // max wait for two maximal pulses
  float th = pulseIn(CO2_PWM_PIN, HIGH, 2 * 1004000);
  float tl = pulseIn(CO2_PWM_PIN, LOW, 2 * 1004000);
  // CO2_{ppm} = 2000 * (TH - 2ms) / (TH + TL - 4ms)
  float gas_co2 = 5000.0 * (th - 2.0 * 1000.0) / (th + tl - 4.0 * 1000.0);
  msg["CO2"] = (int)gas_co2;
#ifdef DEBUG
  Serial.printf("CO2: %d\n", gas_co2);
#endif
 
  // geiger
  JsonObject radiation = msg.createNestedObject("radiation");
  float cpm = geiger.getCPM();
  float svh = geiger.getnSvh();
  float suh = geiger.getuSvh();
  radiation["CPM"] = cpm;
  radiation["SVH"] = svh;
  radiation["SUH"] = suh;
#ifdef DEBUG
  Serial.printf("CPM: %.1f, nSv/h: %f, uSv/h: %.1f\n", cpm, svh, suh);
#endif
 
#ifdef DEBUG
  Serial.printf("Measuring wind: ");
#endif
  // without calibration (anemometer):
  // measured peak using wind generator at ~0.250V
  // ESP goes from 0 to 4095 corresponding to 5V input
  // => max value is (4095.0 * 0.250)/5.0 = 205
  // so scale into 0 to 100 for percentage of max wind
  int wind = (int) mapValueToRange(analogRead(WIND_PIN_ANALOG), 0, 208, 0, 100);
#ifdef DEBUG
  Serial.printf("%d\n", wind);
#endif
  msg["wind"] = wind;
 
  // generate payload
  char buff[MQTT_PAYLOAD_MAX_LENGTH];
  size_t payloadLength = serializeJson(msg, buff);
#ifdef DEBUG
  Serial.println(String(buff));
#endif
  mqttClient.publish((getMqttTopic() + "/" + MQTT_TOPIC_OUTBOX).c_str(), buff, payloadLength);
 
  // visually signal data acquisition end
  digitalWrite(LED_BUILTIN, 0);
}
 
String getMqttTopic(void) {
  // store the MQTT topic in a global varible to prevent redundant CPU usage
  if(mqttTopicCache == NULL || mqttTopicCache.length() == 0) {
    mqttTopicCache = String(MQTT_TOPIC);
    if(mqttTopicCache == NULL || mqttTopicCache.length() == 0) {
      DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
      if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) {
    #ifdef DEBUG
        Serial.println("Unable to retrieve configuration.");
    #endif
        return mqttTopicCache;
      }
 
      if(configuration.containsKey("name")) {
        mqttTopicCache = configuration["name"].as<String>();
      }
    }
  }
 
  return mqttTopicCache;
}
 
void mqttCallback(char *topic, byte *payload, unsigned int length) {
}
 
String getMqttId(void) {
  // store the MQTT id in a global varible to prevent redundant CPU usage
  if(mqttIdCache == NULL || mqttIdCache.length() == 0) {
    mqttIdCache = String(HOSTNAME());
    if(mqttIdCache == NULL || mqttIdCache.length() == 0) {
      DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
      if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) {
    #ifdef DEBUG
        Serial.println("Unable to retrieve configuration.");
    #endif
        return mqttIdCache;
      }
 
      if(configuration.containsKey("name")) {
        mqttIdCache = configuration["name"].as<String>();
      }
    }
  }
 
  return mqttIdCache;
}
 
bool mqttConnect(void) {
#ifdef DEBUG
  Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST));
#endif
  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
 
  if (mqttClient.connect(getMqttId().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) {
    //mqttClient.setCallback(mqttCallback);
#ifdef DEBUG
    Serial.print("Subscribing to MQTT topic " + getMqttTopic() + ": ");
#endif
    if (!mqttClient.subscribe(getMqttTopic().c_str())) {
#ifdef DEBUG
      Serial.println("fail");
#endif
      return false;
    }
#ifdef DEBUG
    Serial.println("success");
#endif
 
    // Announce success at connecting and subscribing to MQTT broker.
    DynamicJsonDocument msg(MQTT_PAYLOAD_MAX_LENGTH);
    msg["client"] = getMqttId().c_str();
    msg["topic"] = getMqttTopic().c_str();
    msg["action"] = "hello";
 
    char buff[MQTT_PAYLOAD_MAX_LENGTH];
    size_t payloadLength = serializeJson(msg, buff);
    mqttClient.publish((getMqttTopic() + "/" + MQTT_TOPIC_OUTBOX).c_str(), buff, payloadLength);
 
    return true;
  }
 
#ifdef DEBUG
  Serial.println("Connection to MQTT broker failed with MQTT client state: " + String(mqttClient.state()));
#endif
  return false;
}
 
///////////////////////////////////////////////////////////////////////////
//  Arduino loop                                                         //
///////////////////////////////////////////////////////////////////////////
void mqttTickCallback(void) {
  if(!networkConnected) {
    return;
  }
 
  // Process MQTT client loop.
  if (mqttClient.connected()) {
    mqttClient.loop();
    return;
  }
 
  if (!mqttConnect()) {
#ifdef DEBUG
    Serial.printf("Unable to connect to MQTT\n");
#endif
  }
}
 
///////////////////////////////////////////////////////////////////////////
//  OTA updates                                                          //
///////////////////////////////////////////////////////////////////////////
void arduinoOtaTickCallback(void) {
  ArduinoOTA.handle();
 
  if(!networkConnected) {
    return;
  }
 
  if(!otaStarted) {
    ArduinoOTA.begin();
    otaStarted = true;
  }
}
 
///////////////////////////////////////////////////////////////////////////
//  system-wide reboot                                                   //
///////////////////////////////////////////////////////////////////////////
void rebootTickCallback(void) {
  // if not reboot hasbeen scheduled then just return
  if(!rebootPending) {
    return;
  }
 
#ifdef DEBUG
  Serial.printf("Stopping filesystem...\n");
#endif
#ifdef DEBUG
  LittleFS.end();
#endif
#ifdef DEBUG
  Serial.printf("Rebooting...\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;
  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)
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
#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 serverWifiTickCallback(void) {
  if(rebootPending || otaInProgress) {
    return;
  }
 
  unsigned long callbackTickTime = serverWifiTick.counter() * (serverWifiTick.interval() / 1000);
  if(callbackTickTime >= WIFI_SERVER_TIMEOUT) {
#ifdef DEBUG
    Serial.println("Server timeout, rebooting...\n");
#endif
 
    DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
    configuration["boot"] = BOOT_MODE_CLIENT;
 
    if(!setConfiguration(CONFIGURATION_FILE_NAME, configuration)) {
#ifdef DEBUG
      Serial.printf("Failed to write configuration.\n");
#endif
    }
 
    rebootPending = true;
    return;
  }
 
#ifdef DEBUG
/*
  if(callbackTickTime % 1000 == 0 ) {
    Serial.printf("Time till reboot %.0fs\n", (float)(WIFI_SERVER_TIMEOUT - callbackTickTime)/1000.0);
  }
*/
#endif
 
  // create the boot SSID
  String temporarySSID = generateTemporarySSID();
  if(WiFi.softAPSSID().equals(temporarySSID)) {
    // run WiFi server loops
    dnsServer.processNextRequest();
    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';
      }
      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.on("/style.css", handleRootCssRequest);
  // captive portal proprietary junk redirected to webserver root
  // connectivitycheck.gstatic.com/generate_204
  // www.googe.com/gen_204
  server.on("/generate_204", handleRootHttpRequest);
  server.on("/gen_204", handleRootHttpRequest);
  server.on("/fwlink", handleRootHttpRequest);
  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
 
  DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
  if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) != -1) {
    if (!MDNS.begin(configuration["name"].as<const char*>())) {
#ifdef DEBUG
      Serial.println("Error setting up MDNS responder.");
#endif
    }
  }
 
  WiFi.softAPConfig(softAPAddress, softAPAddress, softAPNetmask);
  WiFi.softAP(temporarySSID, String(), 1, false, 1);
  dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
  dnsServer.start(53, "*", softAPAddress);
 
#ifdef DEBUG
  Serial.println("Starting HTTP server.");
#endif
 
  server.begin();
}
 
///////////////////////////////////////////////////////////////////////////
//  connect to WiFi                                                      //
///////////////////////////////////////////////////////////////////////////
void clientWifiTickCallback(void) {
  if(rebootPending || otaInProgress) {
    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 mevery 10s...\n");
#endif
    clientWifiTick.interval(WIFI_RETRY_TIMEOUT);
    clientWifiTick.resume();
  }
 
  // 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
    clientConnectionTries = 0;
    networkConnected = true;
    return;
  }
 
#ifdef DEBUG
  Serial.printf("Client WiFi not connected: %d\n", wl_status_to_string(wifiStatus));
#endif
 
  networkConnected = false;
 
  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(++clientConnectionTries > 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);
}
 
///////////////////////////////////////////////////////////////////////////
//  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);
}
 

Index