/////////////////////////////////////////////////////////////////////////// // 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); }
For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.