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 ""
// 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 ""
// Maximal size of an MQTT payload
#define MQTT_PAYLOAD_MAX_LENGTH 256
 
///////////////////////////////////////////////////////////////////////////
//  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>
#include <TinyGPSPlus.h>
#include <HardwareSerial.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
#define SERIAL_RX_PIN 23
#define SERIAL_TX_PIN 5
 
// Platform specific defines.
#if defined(ARDUINO_ARCH_ESP8266)
#define GET_CHIP_ID() (ESP.getChipId())
#elif defined(ARDUINO_ARCH_ESP32)
#define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32))
#endif
 
#define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX))
#define CONFIGURATION_FILE_NAME "/config.json"
#define CONFIGURATION_MAX_LENGTH 1024
 
///////////////////////////////////////////////////////////////////////////
//  function definitions                                                 //
///////////////////////////////////////////////////////////////////////////
byte* getHardwareAddress(void);
char* getHardwareAddress(char colon);
String generateTemporarySSID(void);
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);
 
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);
char *mqttSerialize(JsonDocument& doc, size_t maxLength);
float mapValueToRange(float value, float xMin, float xMax, float yMin, float yMax);
void sensorsTickCallback(void);
void gpsTickCallback(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);
TickTwo gpsTick(gpsTickCallback, 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);
// GPS
#if defined(ARDUINO_ARCH_ESP32)
SemaphoreHandle_t xGpsMutex = NULL;
#endif
TinyGPSPlus gps;
String tmpNmeaSentence;
String nmeaSentence;
HardwareSerial gpsSerial(1);
 
///////////////////////////////////////////////////////////////////////////
//  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                                                        //
///////////////////////////////////////////////////////////////////////////
#line 338 "C:\\Users\\cicero\\AppData\\Local\\Temp\\arduino_modified_sketch_141473\\arduinoSensorsCocktail.ino"
void setup();
#line 487 "C:\\Users\\cicero\\AppData\\Local\\Temp\\arduino_modified_sketch_141473\\arduinoSensorsCocktail.ino"
void loop();
#line 710 "C:\\Users\\cicero\\AppData\\Local\\Temp\\arduino_modified_sketch_141473\\arduinoSensorsCocktail.ino"
bool mqttConnect(void);
#line 1077 "C:\\Users\\cicero\\AppData\\Local\\Temp\\arduino_modified_sketch_141473\\arduinoSensorsCocktail.ino"
const char * wl_status_to_string(wl_status_t status);
#line 338 "C:\\Users\\cicero\\AppData\\Local\\Temp\\arduino_modified_sketch_141473\\arduinoSensorsCocktail.ino"
void setup() {
#ifdef DEBUG
  Serial.begin(115200);
  // wait for serial
  while (!Serial) {
    delay(100);
  }
 
  Serial.println();
#else
  Serial.end();
#endif
 
#ifdef DEBUG
  Serial.println("Mounting filesystem...");
#endif
#if defined(ARDUINO_ARCH_ESP8266)
  if (!LittleFS.begin()) {
#ifdef DEBUG
    Serial.println("LittleFS mount failed, formatting and rebooting...");
#endif
    LittleFS.format();
    delay(1000);
    ESP.restart();
#elif defined(ARDUINO_ARCH_ESP32)
  if (!LittleFS.begin(true)) {
#endif
#ifdef DEBUG
    Serial.println("LittleFS mount & format failed...");
#endif
    return;
  }
 
#ifdef DEBUG
  Serial.printf("Checking if WiFi server must be started...\n");
#endif
  // check if Ssid is set and start soft AP or STA mode
  DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH);
  if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) {
#ifdef DEBUG
    Serial.println("Unable to retrieve configuration.");
#endif
    delay(60000);
    ESP.restart();
    return;
  }
 
  switch(configuration["boot"].as<int>()) {
    case BOOT_MODE_CLIENT:
#ifdef DEBUG
      Serial.printf("Client connecting to WiFi...\n");
#endif
      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);
  geiger.start();
  dustSensor.begin();
  // GY-NEO6MV2 9600 bps
  gpsSerial.begin(9200, SERIAL_8N1, SERIAL_RX_PIN, SERIAL_TX_PIN);
#if defined(ARDUINO_ARCH_ESP32)
  xGpsMutex = xSemaphoreCreateMutex();
#endif
 
  // start the sensors ticker
  mqttTick.start();
  sensorsTick.start();
  gpsTick.start();
}
 
void loop() {
  arduinoOtaTick.update();
  rebootTick.update();
  clientWifiTick.update();
  serverWifiTick.update();
  blinkDigitsDitTick.update();
  blinkDigitsDahTick.update();
  blinkDigitsBlinkTick.update();
 
  // Sensors application
  mqttTick.update();
  sensorsTick.update();
  gpsTick.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 gpsTickCallback(void) {
  // pause operations when OTA is in progress or MQTT not available
  if(otaInProgress || !mqttClient.connected()) {
    return;
  }
 
  // sensors application
  while(gpsSerial.available() > 0) {
    tmpNmeaSentence += (char)gpsSerial.read();
    size_t size = tmpNmeaSentence.length();
#ifdef DEBUG
    //Serial.println("GPS NMEA: " + tmpNmeaSentence);
#endif 
    // check if the message exceeds the standard NMEA 
    // sentence length in case we're reading garbage
    if(size > 79) {
#ifdef DEBUG
      Serial.println("Standard NMEA sentence too long, receiving garbage from GPS.");
#endif
      tmpNmeaSentence = "";
      continue;
    }
 
    // continue reading until NMEA terminator is reached
    if(!tmpNmeaSentence.endsWith("\r\n")) {
      continue;
    }
 
    int i;
    for(i = 0; i < size; ++i) {
      if(gps.encode(tmpNmeaSentence[i])) {
#if defined(ARDUINO_ARCH_ESP32)
        xSemaphoreTake(xGpsMutex, portMAX_DELAY);
#endif
        nmeaSentence = String(tmpNmeaSentence);
#if defined(ARDUINO_ARCH_ESP32)
        xSemaphoreGive(xGpsMutex);
#endif
        tmpNmeaSentence = "";
      }
    }
  }
}
 
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;
 
  // pass along GPS NMEA string
#if defined(ARDUINO_ARCH_ESP32)
  xSemaphoreTake(xGpsMutex, portMAX_DELAY);
#endif
  msg["GPS"] = nmeaSentence;
#if defined(ARDUINO_ARCH_ESP32)
  xSemaphoreGive(xGpsMutex);
#endif
 
  // generate payload
  char* msgPayload = mqttSerialize(msg, MQTT_PAYLOAD_MAX_LENGTH);
#ifdef DEBUG
  Serial.println(String(msgPayload));
#endif
  mqttClient.publish(getMqttTopic().c_str(), msgPayload);
  free(msgPayload);
 
  // 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;
}
 
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;
}
 
char *mqttSerialize(JsonDocument& doc, size_t maxLength) {
  char* buff = (char*) malloc(maxLength * sizeof(char));
  size_t bytesWritten = serializeJson(doc, buff, maxLength);
#ifdef DEBUG
  Serial.printf("Written bytes %d vs. document bytes %d\n", bytesWritten, measureJson(doc));
#endif
  if(bytesWritten != measureJson(doc)) {
    free(buff);
    return NULL;
  }
  return buff;
}
 
bool mqttConnect(void) {
#ifdef DEBUG
  Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST));
#endif
  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
 
  DynamicJsonDocument msg(MQTT_PAYLOAD_MAX_LENGTH);
  if (mqttClient.connect(getMqttId().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) {
#ifdef DEBUG
    Serial.println("Established connection with MQTT broker using client ID: " + getMqttId());
#endif
    msg["action"] = "connected";
    char *payload = mqttSerialize(msg, MQTT_PAYLOAD_MAX_LENGTH);
    mqttClient.publish(getMqttTopic().c_str(), payload);
    free(payload);
#ifdef DEBUG
    Serial.println("Attempting to subscribe to MQTT topic: " + getMqttTopic());
#endif
    if (!mqttClient.subscribe(getMqttTopic().c_str())) {
#ifdef DEBUG
      Serial.println("Failed to subscribe to MQTT topic: " + getMqttTopic());
#endif
      return false;
    }
#ifdef DEBUG
    Serial.println("Subscribed to MQTT topic: " + getMqttTopic());
#endif
    msg["action"] = "subscribed";
    payload = mqttSerialize(msg, MQTT_PAYLOAD_MAX_LENGTH);
    mqttClient.publish(getMqttTopic().c_str(), payload);
    free(payload);
    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;
 
    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) {
#ifdef DEBUG
    Serial.println("Unable to retrieve configuration.");
#endif
    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


iot/creating_a_sensor_cocktail/code.txt ยท Last modified: 2025/03/31 14:27 by office

Wizardry and Steamworks

© 2025 Wizardry and Steamworks

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.