Advertisement
marko221

Untitled

Feb 3rd, 2025
40
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 38.81 KB | Software | 0 0
  1. /*  ------------------------------------------- Project "KLIMERKO" ---------------------------------------------
  2.  *  Citizen Air Quality measuring device with cloud monitoring, built at https://descon.me for the whole world.
  3.  *  Programmed, built and maintained by Vanja Stanic // www.vanjastanic.com
  4.  *  ------------------------------------------------------------------------------------------------------------
  5.  *  This is a continued effort from https://descon.me/2018/winning-product/
  6.  *  Supported by ISOC (Internet Society, Belgrade Chapter) // https://isoc.rs
  7.  *  IoT Cloud Services and Communications SDK by AllThingsTalk // www.allthingstalk.com/
  8.  *  3D Case for the device designed and manufactured by Dusan Nikic // [email protected]
  9.  *  ------------------------------------------------------------------------------------------------------------
  10.  *  This sketch is downloaded from https://github.com/DesconBelgrade/Klimerko
  11.  *  Head over there to read instructions and more about the project.
  12.  *  Do not change anything in here unless you know what you're doing. Just upload this sketch to your device.
  13.  *  You'll configure your WiFi and Cloud credentials once the sketch is uploaded to the device by
  14.  *  pressing the FLASH button on the NodeMCU for 2 seconds and connecting to Klimerko using any WiFi-enabled device.
  15.  *  ------------------------------------------------------------------------------------------------------------
  16.  *  Textual Air Quality Scale is based on PM10 criteria defined by RS Government (http://www.amskv.sepa.gov.rs/kriterijumi.php)
  17.  *  Excellent (0-20), Good (21-40), Acceptable (41-50), Polluted (51-100), Very Polluted (Over 100)
  18.  */
  19.  
  20. #include "src/AdafruitBME280/Adafruit_Sensor.h"
  21. #include "src/AdafruitBME280/Adafruit_BME280.h"
  22. #include "src/pmsLibrary/PMS.h"
  23. #include "src/movingAvg/movingAvg.h"
  24. #include "src/WiFiManager/WiFiManager.h"
  25. #include "src/PubSubClient/PubSubClient.h"
  26. #include "src/ArduinoJson-v6.18.5.h"
  27. #include <SoftwareSerial.h>
  28. #include <Wire.h>
  29. #include <EEPROM.h>
  30.  
  31. #define BUTTON_PIN     0
  32. #define pmsTX          D5
  33. #define pmsRX          D6
  34.  
  35. // ------------------------- Device -----------------------------------------------------
  36. String         firmwareVersion         = "2.1.1";
  37. const char*    firmwareVersionPortal   = "<p>Firmware Version: 2.1.1</p>";
  38. char           klimerkoID[32];
  39.  
  40. // -------------------------- WiFi ------------------------------------------------------
  41. const int      wifiReconnectInterval   = 60;
  42. bool           wifiConnectionLost      = true;
  43. unsigned long  wifiReconnectLastAttempt;
  44.  
  45. // ------------------- WiFi Configuration Portal ----------------------------------------
  46. char const     *wifiConfigPortalPassword = "ConfigMode"; // Password for WiFi Configuration Portal WiFi Network
  47. const int      wifiConfigTimeout         = 1800;         // Seconds before WiFi Configuration expires
  48. unsigned long  wifiConfigActiveSince;
  49.  
  50. // -------------------------- MQTT ------------------------------------------------------
  51. const char*    MQTT_SERVER             = "api.allthingstalk.io";
  52. const uint16_t MQTT_PORT               = 1883;
  53. const char*    MQTT_PASSWORD           = "arbitrary";
  54. uint16_t       MQTT_MAX_MESSAGE_SIZE   = 2048;
  55. char           deviceId[32], deviceToken[64];
  56.  
  57. const int      mqttReconnectInterval   = 30; // Seconds between retries
  58. bool           mqttConnectionLost      = true;
  59. unsigned long  mqttReconnectLastAttempt;
  60.  
  61. char*          PM1_ASSET               = "pm1";
  62. char*          PM2_5_ASSET             = "pm2-5";
  63. char*          PM10_ASSET              = "pm10";
  64. char*          AQ_ASSET                = "air-quality";
  65. char*          TEMPERATURE_ASSET       = "temperature";
  66. char*          TEMP_OFFSET_ASSET       = "temperature-offset";
  67. char*          HUMIDITY_ASSET          = "humidity";
  68. char*          PRESSURE_ASSET          = "pressure";
  69. char*          INTERVAL_ASSET          = "interval";
  70. char*          FIRMWARE_ASSET          = "firmware";
  71. char*          WIFI_SIGNAL_ASSET       = "wifi-signal";
  72.  
  73. // -------------------------- BUTTON ------------------------------------------------------
  74. const int      buttonLongPressTime     = 15000; // (milliseconds) Everything above this is considered a long press
  75. const int      buttonMediumPressTime   = 1000;  // (milliseconds) Everything above this and below long press time is considered a medium press
  76. const int      buttonShortPressTime    = 50;    // (milliseconds) Everything above this and below medium press time is considered a short press
  77. unsigned long  buttonPressedTime       = 0;
  78. unsigned long  buttonReleasedTime      = 0;
  79. bool           buttonPressed           = false;
  80. bool           buttonLongPressDetected = false;
  81. int            buttonLastState         = HIGH;
  82. int            buttonCurrentState;
  83.  
  84. // -------------------------- LED ---------------------------------------------------------
  85. bool           ledState                = false;
  86. bool           ledSuccessBlink         = false;
  87. const int      ledBlinkInterval        = 1000;  // (milliseconds) How often to blink LED when there's no connection
  88. unsigned long  ledLastUpdate;
  89.  
  90. // --------------------- SENSORS (GENERAL) ------------------------------------------------
  91. uint8_t        dataPublishInterval     = 15;    // [MINUTES] Default sensor data sending interval
  92. const uint8_t  sensorAverageSamples    = 10;    // Number of samples used to average values from sensors
  93. const int      sensorRetriesUntilConsideredOffline = 3;
  94. bool           dataPublishFailed       = false; // Keeps track if a payload has failed to send so we can retry
  95. unsigned long  sensorReadTime, dataPublishTime;
  96.  
  97. // -------------------------- PMS7003 -----------------------------------------------------
  98. const uint8_t  pmsWakeBefore           = 30;    // [SECONDS] Seconds PMS sensor should be active before reading it
  99. bool           pmsSensorOnline         = true;
  100. int            pmsSensorRetry          = 0;
  101. bool           pmsNoSleep              = false;
  102. bool           pmsWoken                = false;
  103. const char     *airQuality, *airQualityRaw;
  104. int            avgPM1, avgPM25, avgPM10;
  105.  
  106. // -------------------------- BME280 -----------------------------------------------------
  107. bool           bmeSensorOnline                = true;
  108. int            bmeSensorRetry                 = 0;
  109. float          bmeTemperatureOffset           = -4;   // Default temperature offset
  110. char           bmeTemperatureOffsetChar[8];           // Used for WiFi Configuration Portal and memory
  111. const char     bmeTemperatureOffsetDefault[8] = "-4"; // Used for WiFi Configuration Portal and memory
  112. const int      bmeTemperatureOffsetMax        = 25;
  113. const int      bmeTemperatureOffsetMin        = -25;
  114. float          avgTemperature, avgHumidity, avgPressure;
  115.  
  116. // -------------------------- MEMORY -----------------------------------------------------
  117. const uint16_t EEPROM_attStartAddress  = 0;
  118. const uint16_t EEPROMsize              = 256;
  119.  
  120. // -------------------------- OBJECTS -----------------------------------------------------
  121. WiFiManager wm;
  122. WiFiManagerParameter portalDeviceID("device_id", "AllThingsTalk Device ID", deviceId, 32);
  123. WiFiManagerParameter portalDeviceToken("device_token", "AllThingsTalk Device Token", deviceToken, 64);
  124. WiFiManagerParameter portalTemperatureOffset("temperature_offset", "Temperature Offset", bmeTemperatureOffsetChar, 8);
  125. WiFiManagerParameter portalDisplayFirmwareVersion(firmwareVersionPortal);
  126. WiFiManagerParameter portalDisplayCredits("Firmware Designed and Developed by Vanja Stanic");
  127. WiFiClient networkClient;
  128. PubSubClient mqtt(networkClient);
  129. SoftwareSerial pmsSerial(pmsTX, pmsRX);
  130. PMS pms(pmsSerial);
  131. PMS::DATA data;
  132. Adafruit_BME280 bme;
  133. movingAvg pm1(sensorAverageSamples);
  134. movingAvg pm25(sensorAverageSamples);
  135. movingAvg pm10(sensorAverageSamples);
  136. movingAvg temp(sensorAverageSamples);
  137. movingAvg hum(sensorAverageSamples);
  138. movingAvg pres(sensorAverageSamples);
  139.  
  140. void sensorLoop() { // Reads and publishes sensor data and wakes up pms sensor in predefined intervals
  141.   // Check if it's time to wake up PMS7003
  142.   if (millis() - sensorReadTime >= readIntervalMillis() - (pmsWakeBefore * 1000) && !pmsWoken && pmsSensorOnline) {
  143.     Serial.println("[PMS] Now waking up Air Quality Sensor");
  144.     pmsPower(true);
  145.   }
  146.  
  147.   // Read sensor data
  148.   if (millis() - sensorReadTime >= readIntervalMillis()) {
  149.     sensorReadTime = millis();
  150.     readSensorData();
  151.   }
  152.  
  153.   // Send average sensor data
  154.   if (millis() - dataPublishTime >= dataPublishInterval * 60000) {
  155.     if (!wifiConnectionLost) {
  156.       if (!mqttConnectionLost) {
  157.         dataPublishFailed = false;
  158.         dataPublishTime = millis();
  159.         publishSensorData();
  160.       } else {
  161.         if (!dataPublishFailed) {
  162.           Serial.println("[DATA] Can't send sensor data because Klimerko is not connected to AllThingsTalk");
  163.           dataPublishFailed = true;
  164.         }
  165.       }
  166.     } else {
  167.       if (!dataPublishFailed) {
  168.         Serial.println("[DATA] Can't send sensor data because Klimerko is not connected to WiFi");
  169.         dataPublishFailed = true;
  170.       }
  171.     }
  172.   }
  173. }
  174.  
  175. void readSensorData() {
  176.   Serial.println("------------------------------DATA------------------------------");
  177.   readPMS();
  178.   readBME();
  179.   Serial.println("----------------------------------------------------------------");
  180.   if (!pmsNoSleep && pmsSensorOnline) {
  181.     Serial.print("[PMS] Air Quality Sensor will sleep until ");
  182.     Serial.print(pmsWakeBefore);
  183.     Serial.println(" seconds before next reading.");
  184.     pmsPower(false);
  185.   }
  186. }
  187.  
  188. void publishSensorData() {
  189.   char JSONmessageBuffer[512];
  190.   DynamicJsonDocument doc(512);
  191.   if (pmsSensorOnline) {
  192.     JsonObject airQualityJson = doc.createNestedObject(AQ_ASSET);
  193.     airQualityJson["value"] = airQuality;
  194.     JsonObject pm1Json = doc.createNestedObject(PM1_ASSET);
  195.     pm1Json["value"] = avgPM1;
  196.     JsonObject pm25Json = doc.createNestedObject(PM2_5_ASSET);
  197.     pm25Json["value"] = avgPM25;
  198.     JsonObject pm10Json = doc.createNestedObject(PM10_ASSET);
  199.     pm10Json["value"] = avgPM10;
  200.   } else {
  201.     Serial.println("[DATA] Won't send Air Quality Sensor (PMS7003) data because it seems to be offline.");
  202.   }
  203.   if (bmeSensorOnline) {
  204.     JsonObject temperatureJson = doc.createNestedObject(TEMPERATURE_ASSET);
  205.     temperatureJson["value"] = avgTemperature;
  206.     JsonObject humidityJson = doc.createNestedObject(HUMIDITY_ASSET);
  207.     humidityJson["value"] = avgHumidity;
  208.     JsonObject pressureJson = doc.createNestedObject(PRESSURE_ASSET);
  209.     pressureJson["value"] = avgPressure;
  210.   } else {
  211.     Serial.println("[DATA] Won't send Temperature/Humidity/Pressure Sensor (BME280) data because it seems to be offline.");
  212.   }
  213.   JsonObject firmwareJson = doc.createNestedObject(FIRMWARE_ASSET);
  214.   firmwareJson["value"] = firmwareVersion;
  215.   JsonObject wifiJson = doc.createNestedObject(WIFI_SIGNAL_ASSET);
  216.   wifiJson["value"] = wifiSignal();
  217.   serializeJson(doc, JSONmessageBuffer);
  218.  
  219.   char topic[128];
  220.   snprintf(topic, sizeof topic, "%s%s%s", "device/", deviceId, "/state");
  221.   mqtt.publish(topic, JSONmessageBuffer, false);
  222.   Serial.print("[DATA] Published sensor data to AllThingsTalk: ");
  223.   Serial.println(JSONmessageBuffer);
  224. }
  225.  
  226. void readPMS() { // Function that reads data from the PMS7003
  227.   while (pmsSerial.available()) { pmsSerial.read(); }
  228.   pms.requestRead(); // Now get the real data
  229.  
  230.   if (pms.readUntil(data)) {
  231.     int PM1 = data.PM_AE_UG_1_0;
  232.     int PM2_5 = data.PM_AE_UG_2_5;
  233.     int PM10 = data.PM_AE_UG_10_0;
  234.  
  235.     avgPM1 = pm1.reading(PM1);
  236.     avgPM25 = pm25.reading(PM2_5);
  237.     avgPM10 = pm10.reading(PM10);
  238.  
  239.     // Assign a text value of how good the air is based on current value
  240.     // http://www.amskv.sepa.gov.rs/kriterijumi.php
  241.     if (PM10 <= 20) {
  242.       airQualityRaw = "Excellent";
  243.     } else if (PM10 >= 21 && PM10 <= 40) {
  244.       airQualityRaw = "Good";
  245.     } else if (PM10 >= 41 && PM10 <= 50) {
  246.       airQualityRaw = "Acceptable";
  247.     } else if (PM10 >= 51 && PM10 <= 100) {
  248.       airQualityRaw = "Polluted";
  249.     } else if (PM10 > 100) {
  250.       airQualityRaw = "Very Polluted";
  251.     }
  252.  
  253.     // Assign a text value of how good the air is based on average value
  254.     // http://www.amskv.sepa.gov.rs/kriterijumi.php
  255.     if (avgPM10 <= 20) {
  256.       airQuality = "Excellent";
  257.     } else if (avgPM10 >= 21 && avgPM10 <= 40) {
  258.       airQuality = "Good";
  259.     } else if (avgPM10 >= 41 && avgPM10 <= 50) {
  260.       airQuality = "Acceptable";
  261.     } else if (avgPM10 >= 51 && avgPM10 <= 100) {
  262.       airQuality = "Polluted";
  263.     } else if (avgPM10 > 100) {
  264.       airQuality = "Very Polluted";
  265.     }
  266.  
  267.     // Print via SERIAL
  268.     Serial.print("Air Quality is ");
  269.     Serial.print(airQualityRaw);
  270.     Serial.print(" (Average: ");
  271.     Serial.print(airQuality);
  272.     Serial.println(")");
  273.     Serial.print("PM 1:          ");
  274.     Serial.print(PM1);
  275.     Serial.print(" µg/m³ (Average: ");
  276.     Serial.print(avgPM1);
  277.     Serial.println(")");
  278.     Serial.print("PM 2.5:        ");
  279.     Serial.print(PM2_5);
  280.     Serial.print(" µg/m³ (Average: ");
  281.     Serial.print(avgPM25);
  282.     Serial.println(")");
  283.     Serial.print("PM 10:         ");
  284.     Serial.print(PM10);
  285.     Serial.print(" µg/m³ (Average: ");
  286.     Serial.print(avgPM10);
  287.     Serial.println(")");
  288.  
  289.     pmsSensorRetry = 0;
  290.     if (!pmsSensorOnline) {
  291.       pmsSensorOnline = true;
  292.       Serial.println("[PMS] Air Quality Sensor (PMS7003) seems to be back online!");
  293.     }
  294.   } else {
  295.     if (pmsSensorOnline) {
  296.       Serial.println("[PMS] Air Quality Sensor (PMS7003) returned no data on data request this time.");
  297.       pmsSensorRetry++;
  298.       if (pmsSensorRetry > sensorRetriesUntilConsideredOffline) {
  299.         pmsSensorOnline = false;
  300.         Serial.println("[PMS] Air Quality Sensor (PMS7003) seems to be offline!");
  301.         pm1.reset();
  302.         pm25.reset();
  303.         pm10.reset();
  304.         initPMS();
  305.       }
  306.     } else {
  307.       Serial.println("[PMS] Air Quality Sensor (PMS7003) is offline.");
  308.       initPMS();
  309.     }
  310.   }
  311. }
  312.  
  313. void readBME() { // Function for reading data from the BME280 Sensor
  314.   float temperatureRaw = bme.readTemperature();
  315.   float temperature    = temperatureRaw + bmeTemperatureOffset;
  316.   float humidityRaw    = bme.readHumidity();
  317.   float humidity       = humidityRaw * exp(243.12 * 17.62 * (temperatureRaw - temperature) / (243.12 + temperatureRaw) / (243.12 + temperature)); // Compensates the RH in accordance to temperature offset so the RH isn't wrong when the temp is offset
  318.   float pressure       = bme.readPressure() / 100.0F;
  319.  
  320.   if (temperatureRaw > -100 && temperatureRaw < 150 && humidity >= 0 && humidity <= 100) {
  321.     avgTemperature = temp.reading(temperature*100);
  322.     avgTemperature = avgTemperature/100;
  323.     avgHumidity    = hum.reading(humidity*100);
  324.     avgHumidity    = avgHumidity/100;
  325.     avgPressure    = pres.reading(pressure*100);
  326.     avgPressure    = avgPressure/100;
  327.  
  328.     Serial.print("Temperature:   ");
  329.     Serial.print(temperature);
  330.     Serial.print("°C (Average: ");
  331.     Serial.print(avgTemperature);
  332.     Serial.print(", Raw: ");
  333.     Serial.print(temperatureRaw);
  334.     Serial.print(", Offset: ");
  335.     Serial.print(bmeTemperatureOffset);
  336.     Serial.println(")");
  337.     Serial.print("Humidity:      ");
  338.     Serial.print(humidity);
  339.     Serial.print(" % (Average: ");
  340.     Serial.print(avgHumidity);
  341.     Serial.print(", Raw: ");
  342.     Serial.print(humidityRaw);
  343.     Serial.println(")");
  344.     Serial.print("Pressure:      ");
  345.     Serial.print(pressure);
  346.     Serial.print(" mbar (Average: ");
  347.     Serial.print(avgPressure);
  348.     Serial.println(")");
  349.  
  350.     bmeSensorRetry = 0;
  351.     if (!bmeSensorOnline) {
  352.       bmeSensorOnline = true;
  353.       Serial.println("[BME] Temperature/Humidity/Pressure Sensor (BME280) is back online!");
  354.     }
  355.   } else {
  356.     if (bmeSensorOnline) {
  357.       Serial.println("[BME] Temperature/Humidity/Pressure Sensor (BME280) returned no data this time.");
  358.       bmeSensorRetry++;
  359.       if (bmeSensorRetry > sensorRetriesUntilConsideredOffline) {
  360.         bmeSensorOnline = false;
  361.         Serial.println("[BME] Temperature/Humidity/Pressure Sensor (BME280) seems to be offline!");
  362.         temp.reset();
  363.         hum.reset();
  364.         pres.reset();
  365.         initBME();
  366.       }
  367.     } else {
  368.       Serial.println("[BME] Temperature/Humidity/Pressure Sensor (BME280) is offline.");
  369.       initBME();
  370.     }
  371.   }
  372. }
  373.  
  374. void pmsPower(bool state) { // Controls sleep state of PMS sensor
  375.   if (state) {
  376.     pms.wakeUp();
  377.     pms.passiveMode();
  378.     pmsWoken = true;
  379.   } else {
  380.     pmsSerial.flush();
  381.     unsigned long now = millis();
  382.     while(millis() < now + 100);
  383.     pmsWoken = false;
  384.     pms.sleep();
  385.   }
  386. }
  387.  
  388. void changeInterval(int interval) { // Changes sensor data reporting interval
  389.   if (interval > 5 && interval <= 60) {
  390.     dataPublishInterval = interval;
  391.     pmsNoSleep = false;
  392.     Serial.print("[DATA] Device reporting interval set to ");
  393.     Serial.print(interval);
  394.     Serial.println(" minutes");
  395.     Serial.print("[DATA] Sensor data will be read every ");
  396.     Serial.print(readIntervalSeconds());
  397.     Serial.println(" seconds for averaging.");
  398.     publishDiagnosticData();
  399.   } else if (interval <= 1) {
  400.     dataPublishInterval = 1;
  401.     pmsNoSleep = true;
  402.     pmsPower(true);
  403.     Serial.println("[DATA] Reporting interval set to 1 minute (minimum).");
  404.     Serial.print("[DATA] Sensor data will be read every ");
  405.     Serial.print(readIntervalSeconds());
  406.     Serial.println(" seconds for averaging.");
  407.     Serial.println("[DATA] This prevents sleeping of Air Quality Sensor and reduces its lifespan.");
  408.     publishDiagnosticData();
  409.   } else if (interval <= 5) {
  410.     dataPublishInterval = interval;
  411.     pmsNoSleep = true;
  412.     pmsPower(true);
  413.     Serial.print("[DATA] Device reporting interval set to ");
  414.     Serial.print(interval);
  415.     Serial.println(" minutes");
  416.     Serial.print("[DATA] Sensor data will be read every ");
  417.     Serial.print(readIntervalSeconds());
  418.     Serial.println(" seconds for averaging.");
  419.     Serial.println("[DATA] This prevents sleeping of Air Quality Sensor and reduces its lifespan.");
  420.     publishDiagnosticData();
  421.   } else if (interval >= 60) {
  422.     dataPublishInterval = 60;
  423.     pmsNoSleep = false;
  424.     Serial.print("[DATA] Device reporting interval set to ");
  425.     Serial.print(dataPublishInterval);
  426.     Serial.println(" minutes");
  427.     Serial.print("[DATA] Sensor data will be read every ");
  428.     Serial.print(readIntervalSeconds());
  429.     Serial.println(" seconds for averaging.");
  430.     publishDiagnosticData();
  431.   }
  432. }
  433.  
  434. void publishDiagnosticData() { // Publishes diagnostic data to AllThingsTalk
  435.   if (!wifiConnectionLost) {
  436.     if (!mqttConnectionLost) {
  437.       char JSONmessageBuffer[256];
  438.       DynamicJsonDocument doc(256);
  439.       JsonObject dataPublishIntervalJson = doc.createNestedObject(INTERVAL_ASSET);
  440.       dataPublishIntervalJson["value"] = dataPublishInterval;
  441.       JsonObject firmwareJson = doc.createNestedObject(FIRMWARE_ASSET);
  442.       firmwareJson["value"] = firmwareVersion;
  443.       JsonObject wifiJson = doc.createNestedObject(WIFI_SIGNAL_ASSET);
  444.       wifiJson["value"] = wifiSignal();
  445.       JsonObject tempOffsetJson = doc.createNestedObject(TEMP_OFFSET_ASSET);
  446.       tempOffsetJson["value"] = bmeTemperatureOffset;
  447.       serializeJson(doc, JSONmessageBuffer);
  448.    
  449.       char topic[256];
  450.       snprintf(topic, sizeof topic, "%s%s%s", "device/", deviceId, "/state");
  451.       mqtt.publish(topic, JSONmessageBuffer, false);
  452.       Serial.print("[DATA] Published diagnostic data to AllThingsTalk: ");
  453.       Serial.println(JSONmessageBuffer);
  454.     } else {
  455.       Serial.println("[DATA] Can't send diagnostic data because Klimerko is not connected to AllThingsTalk");
  456.     }
  457.   } else {
  458.     Serial.println("[DATA] Can't send diagnostic data because Klimerko is not connected to WiFi");
  459.   }
  460. }
  461.  
  462. unsigned long readIntervalMillis() {
  463.   unsigned long result = (dataPublishInterval * 60000) / sensorAverageSamples;
  464.   return result;
  465. }
  466.  
  467. int readIntervalSeconds() {
  468.   int result = (dataPublishInterval * 60) / sensorAverageSamples;
  469.   return result;
  470. }
  471.  
  472. void restoreData() { // Restores AllThingsTalk credentials from EEPROM as well as temperature offset data
  473.   char okCreds[2+1];
  474.   char okOffset[2+1];
  475.   EEPROM.begin(EEPROMsize);
  476.   EEPROM.get(EEPROM_attStartAddress, deviceId);
  477.   EEPROM.get(EEPROM_attStartAddress+sizeof(deviceId), deviceToken);
  478.   EEPROM.get(EEPROM_attStartAddress+sizeof(deviceId)+sizeof(deviceToken), okCreds);
  479.   EEPROM.get(EEPROM_attStartAddress+sizeof(deviceId)+sizeof(deviceToken)+sizeof(okCreds), bmeTemperatureOffsetChar);
  480.   EEPROM.get(EEPROM_attStartAddress+sizeof(deviceId)+sizeof(deviceToken)+sizeof(okCreds)+sizeof(bmeTemperatureOffsetChar), okOffset);
  481.   EEPROM.end();
  482.   if (String(okCreds) != String("OK")) {
  483.     deviceId[0] = 0;
  484.     deviceToken[0] = 0;
  485.     Serial.println("[MEMORY] AllThingsTalk Device ID: Nothing in Memory");
  486.   } else {
  487.     Serial.print("[MEMORY] AllThingsTalk Device ID: ");
  488.     Serial.println(deviceId);
  489. //    portalDeviceID.setValue(deviceId, sizeof(deviceId)); // Set WiFi Configuration Portal to show real value
  490. //    Serial.print("[MEMORY] AllThingsTalk Device Token: ");
  491. //    Serial.println(deviceToken);
  492. //    portalDeviceToken.setValue(deviceToken, sizeof(deviceToken)); // Set WiFi Configuration Portal to show real value
  493.   }
  494.   if (String(okOffset) != String("OK")) {
  495.     Serial.print("[MEMORY] Temperature Offset: Nothing in Memory. Using default: ");
  496.     Serial.println(bmeTemperatureOffset);
  497.     portalTemperatureOffset.setValue(bmeTemperatureOffsetChar, sizeof(bmeTemperatureOffsetChar));
  498.   } else {
  499.     bmeTemperatureOffset = atof(bmeTemperatureOffsetChar); // Store the char that was in memory as a double (lazy)
  500.     portalTemperatureOffset.setValue(bmeTemperatureOffsetChar, sizeof(bmeTemperatureOffsetChar)); // Update the value on WiFi Configuration Portal
  501.     Serial.print("[MEMORY] Temperature Offset: ");
  502.     Serial.print(bmeTemperatureOffsetChar);
  503.     Serial.print("°C (Float: ");
  504.     Serial.print(bmeTemperatureOffset);
  505.     Serial.println("°C)");
  506.   }
  507. }
  508.  
  509. void saveData() { // Saves new ATT credentials in memory and connects to AllThingsTalk
  510.   Serial.println("[MEMORY] Saving data in persistent memory...");
  511.   bool deviceIdCanBeSaved = false;
  512.   bool deviceTokenCanBeSaved = false;
  513.   bool tempOffsetCanBeSaved = false;
  514.  
  515.   if (sizeof(portalDeviceID.getValue()) >= sizeof(deviceId)) {
  516.     Serial.print("[MEMORY] Won't save Device ID '");
  517.     Serial.print(portalDeviceID.getValue());
  518.     Serial.println("' because it's too long");
  519.   } else if (String(portalDeviceID.getValue()) == "") {
  520.     Serial.println("[MEMORY] Won't save Device ID because it's empty.");
  521.   } else if (String(portalDeviceID.getValue()) == String(deviceId)) {
  522.     Serial.print("[MEMORY] Won't save Device ID '");
  523.     Serial.print(portalDeviceID.getValue());
  524.     Serial.println("' because it's the same as the current one.");
  525.   } else {
  526.     sprintf(deviceId, "%s", portalDeviceID.getValue());
  527.     Serial.print("[MEMORY] Saving Device ID: ");
  528.     Serial.println(deviceId);
  529.     deviceIdCanBeSaved = true;
  530.   }
  531.  
  532.   if (sizeof(portalDeviceToken.getValue()) >= sizeof(deviceToken)) {
  533.     Serial.print("[MEMORY] Won't save Device Token '");
  534.     Serial.print(portalDeviceToken.getValue());
  535.     Serial.println("' because it's too long");
  536.   } else if (String(portalDeviceToken.getValue()) == "") {
  537.     Serial.println("[MEMORY] Won't save Device Token because it's empty.");
  538.   } else if (String(portalDeviceToken.getValue()) == String(deviceToken)) {
  539.     Serial.print("[MEMORY] Won't save Device Token '");
  540.     Serial.print(portalDeviceToken.getValue());
  541.     Serial.println("' because it's the same as the current one.");
  542.   } else {
  543.     sprintf(deviceToken, "%s", portalDeviceToken.getValue());
  544.     Serial.print("[MEMORY] Saving Device Token: ");
  545.     Serial.println(deviceToken);
  546.     deviceTokenCanBeSaved = true;
  547.   }
  548.  
  549.   if (sizeof(portalTemperatureOffset.getValue()) >= sizeof(bmeTemperatureOffsetChar)) {
  550.     Serial.print("[MEMORY] Won't save Temperature Offset '");
  551.     Serial.print(portalTemperatureOffset.getValue());
  552.     Serial.println("' because it's too long.");
  553.   } else if (String(portalTemperatureOffset.getValue()) == String(bmeTemperatureOffsetChar)) {
  554.     Serial.println("[MEMORY] Won't save Temperature Offset because it's the same as current value.");
  555.   } else if (!isNumber(portalTemperatureOffset.getValue())) {
  556.     Serial.print("[MEMORY] Won't save Temperature Offset '");
  557.     Serial.print(portalTemperatureOffset.getValue());
  558.     Serial.println("' because it's not a number.");
  559.   } else if (atof(portalTemperatureOffset.getValue()) > bmeTemperatureOffsetMax) {
  560.     Serial.print("[MEMORY] Won't save Temperature Offset '");
  561.     Serial.print(portalTemperatureOffset.getValue());
  562.     Serial.print("' because it's above the maximum of ");
  563.     Serial.println(bmeTemperatureOffsetMax);
  564.   } else if (atof(portalTemperatureOffset.getValue()) < bmeTemperatureOffsetMin) {
  565.     Serial.print("[MEMORY] Won't save Temperature Offset '");
  566.     Serial.print(portalTemperatureOffset.getValue());
  567.     Serial.print("' because it's below the minimum of ");
  568.     Serial.println(bmeTemperatureOffsetMin);
  569.   } else if (String(portalTemperatureOffset.getValue()) == "") {
  570.     Serial.println("[MEMORY] Won't save Temperature Offset because it's empty.");
  571.   } else {
  572.     sprintf(bmeTemperatureOffsetChar, "%s", portalTemperatureOffset.getValue()); // Convert const char* to char array for saving in memory
  573.     portalTemperatureOffset.setValue(bmeTemperatureOffsetChar, sizeof(bmeTemperatureOffsetChar)); // Set WiFi Configuration Portal to show the real value of offset
  574.     bmeTemperatureOffset = atof(bmeTemperatureOffsetChar); // Convert the entered value to double (even though the variable is a float - I know, I know...)
  575.     Serial.print("[MEMORY] Saving Temperature Offset: ");
  576.     Serial.print(bmeTemperatureOffsetChar);
  577.     Serial.print("°C (Float: ");
  578.     Serial.print(bmeTemperatureOffset);
  579.     Serial.println("°C)");
  580.     // Reset average temperature and humidity values in case the offset was changed during device operation since already-existing averaging data would be wrong due to new temperature offset.
  581.     temp.reset();
  582.     hum.reset();
  583.     tempOffsetCanBeSaved = true;
  584.   }
  585.  
  586.   portalTemperatureOffset.setValue(bmeTemperatureOffsetChar, sizeof(bmeTemperatureOffsetChar)); // Set WiFi Configuration Portal to show real value (in case user entered it wrong and it was disregarded)
  587.  
  588.   if (deviceIdCanBeSaved || deviceTokenCanBeSaved || tempOffsetCanBeSaved) {
  589.     char ok[2+1] = "OK";
  590.     EEPROM.begin(EEPROMsize);
  591.     if (deviceIdCanBeSaved || deviceTokenCanBeSaved) {
  592.       if (deviceIdCanBeSaved) {
  593.         EEPROM.put(EEPROM_attStartAddress, deviceId);
  594.       }
  595.       if (deviceTokenCanBeSaved) {
  596.         EEPROM.put(EEPROM_attStartAddress+sizeof(deviceId), deviceToken);
  597.       }
  598.       EEPROM.put(EEPROM_attStartAddress+sizeof(deviceId)+sizeof(deviceToken), ok);
  599.     }
  600.     if (tempOffsetCanBeSaved) {
  601.       EEPROM.put(EEPROM_attStartAddress+sizeof(deviceId)+sizeof(deviceToken)+sizeof(ok), bmeTemperatureOffsetChar);
  602.       EEPROM.put(EEPROM_attStartAddress+sizeof(deviceId)+sizeof(deviceToken)+sizeof(ok)+sizeof(bmeTemperatureOffsetChar), ok);
  603.     }
  604.     if (EEPROM.commit()) {
  605.       Serial.println("[MEMORY] Data saved.");
  606.       EEPROM.end();
  607.     } else {
  608.       Serial.println("[MEMORY] Data couldn't be saved to memory.");
  609.       EEPROM.end();
  610.     }
  611.   }
  612. }
  613.  
  614. bool isNumber(const char* value) {
  615.   if (value[0] != '-' && value[0] != '+' && !isDigit(value[0])) {
  616.     return false;
  617.   }
  618.   if (value[1] != '\0') {
  619.     int i = 1;
  620.     do {
  621.       if (!isDigit(value[i]) && value[i] != '.') {
  622.         return false;
  623.       }
  624.       i++;
  625.     } while(value[i] != '\0');
  626.   }
  627.   return true;
  628. }
  629.  
  630. void connectAfterSavingData() {
  631.   connectMQTT();
  632. }
  633.  
  634. void factoryReset() { // Deletes WiFi and AllThingsTalk credentials and reboots Klimerko
  635.   for (int i=0;i<40;i++) {
  636.     digitalWrite(LED_BUILTIN, HIGH);
  637.     delay(50);
  638.     digitalWrite(LED_BUILTIN, LOW);
  639.     delay(50);
  640.   }
  641.   wm.resetSettings();
  642.   ESP.eraseConfig();
  643.   EEPROM.begin(EEPROMsize);
  644.   for (int i=EEPROM_attStartAddress; i <= sizeof(deviceId)+sizeof(deviceToken)+3+sizeof(bmeTemperatureOffsetChar)+3; i++) {
  645.     EEPROM.write(i, 0);
  646.   }
  647.   EEPROM.commit();
  648.   EEPROM.end();
  649.   Serial.println("[SYSTEM] Klimerko has been factory reset. All data has been erased. Rebooting in 5 seconds.");
  650.   delay(5000);
  651.   ESP.restart();
  652. }
  653.  
  654. void wifiConfigWebServerStarted() {
  655.   wm.server->on("/exit", wifiConfigStop); // If user presses Exit, turn off WiFi Configuration Portal.
  656. }
  657.  
  658. void wifiConfigStarted(WiFiManager *wm) { // Called once WiFi Configuration Portal is started
  659.   wifiConfigActiveSince = millis();
  660. }
  661.  
  662. void wifiConfigStart() { // Starts WiFi Configuration Portal
  663.   if (!wm.getConfigPortalActive()) {
  664.     Serial.println("[WIFICONFIG] Entering WiFi Configuration Mode...");
  665.     wm.startConfigPortal(klimerkoID, wifiConfigPortalPassword);
  666.     Serial.println("[WIFICONFIG] WiFi Configuration Mode Activated!");
  667.   } else {
  668.     Serial.println("[WIFICONFIG] WiFi Configuration Mode already active!");
  669.   }
  670.   Serial.print("[WIFICONFIG] Use your computer or smartphone to connect to WiFi network '");
  671.   Serial.print(klimerkoID);
  672.   Serial.print("' (password: '");
  673.   Serial.print(wifiConfigPortalPassword);
  674.   Serial.println("') to configure your Klimerko.");
  675. }
  676.  
  677. void wifiConfigStop() { // Stops WiFi Configuration Portal
  678.   if (wm.getConfigPortalActive()) {
  679.     wm.stopConfigPortal();
  680.     Serial.println("[WIFICONFIG] WiFi Configuration Portal has been stopped.");
  681.   } else {
  682.     Serial.println("[WIFICONFIG] Can't stop WiFi Configuration Portal because it's not running.");
  683.   }
  684. }
  685.  
  686. void wifiConfigLoop() { // Keep WiFi Configurartion mode portal in the loop if it's supposed to be active
  687.   if (wm.getConfigPortalActive()) {
  688.     wm.process();
  689.      if (millis() - wifiConfigActiveSince >= wifiConfigTimeout * 1000) {
  690.        Serial.println("[WIFICONFIG] WiFi Configuration Mode Expired.");
  691.        wifiConfigStop();
  692.      }
  693.   }
  694. }
  695.  
  696. void buttonLoop() { // Handles the FLASH button and all it's features
  697.   buttonCurrentState = digitalRead(BUTTON_PIN);
  698.   if (buttonLastState == HIGH && buttonCurrentState == LOW) {
  699.     buttonPressedTime = millis();
  700.     buttonPressed = true;
  701.     buttonLongPressDetected = false;
  702.     // Button is being pressed at the moment
  703.      wifiConfigStart();
  704.   } else if (buttonLastState == LOW && buttonCurrentState == HIGH) {
  705.     buttonReleasedTime = millis();
  706.     buttonPressed = false;
  707.     long buttonPressDuration = buttonReleasedTime - buttonPressedTime;
  708.     if (buttonPressDuration > buttonShortPressTime && buttonPressDuration < buttonMediumPressTime && buttonPressDuration < buttonLongPressTime) {
  709.       Serial.println("[BUTTON] Short Press Detected!");
  710.       wifiConfigStop();
  711.     } else if (buttonPressDuration > buttonShortPressTime && buttonPressDuration > buttonMediumPressTime && buttonPressDuration < buttonLongPressTime) {
  712.       Serial.println("[BUTTON] Long Press Detected!");
  713.      
  714.     }
  715.   }
  716.  
  717.   if (buttonPressed && !buttonLongPressDetected) {
  718.     if (millis() - buttonPressedTime > buttonLongPressTime) {
  719.       buttonLongPressDetected = true;
  720.       Serial.println("[BUTTON] Super Long Press Detected!");
  721.       factoryReset();
  722.     }
  723.   }
  724.   buttonLastState = buttonCurrentState;
  725. }
  726.  
  727. void ledLoop() { // Handles status LED
  728.   if (ledSuccessBlink) {
  729.     for (int i=0;i<6;i++) {
  730.       digitalWrite(LED_BUILTIN, LOW);
  731.       delay(100);
  732.       digitalWrite(LED_BUILTIN, HIGH);
  733.       delay(100);
  734.     }
  735.     ledSuccessBlink = false;
  736.   }
  737.  
  738.   if (wm.getConfigPortalActive()) {
  739.     ledState = true;
  740.   } else {
  741.     if (wifiConnectionLost || mqttConnectionLost) {
  742.       if (millis() - ledLastUpdate >= ledBlinkInterval) {
  743.         ledState = !ledState;
  744.         ledLastUpdate = millis();
  745.       }
  746.     } else {
  747.       ledState = false;
  748.     }
  749.   }
  750.  
  751.   if (ledState) {
  752.     digitalWrite(LED_BUILTIN, LOW);
  753.   } else {
  754.     digitalWrite(LED_BUILTIN, HIGH);
  755.   }
  756. }
  757.  
  758. String extractAssetNameFromTopic(String topic) {
  759.   const int devicePrefixLength = 38;
  760.   const int stateSuffixLength = 8;
  761.   return topic.substring(devicePrefixLength, topic.length()-stateSuffixLength);
  762. }
  763.  
  764. void mqttCallback(char* p_topic, byte* p_payload, unsigned int p_length) {
  765.   Serial.println("[MQTT] Message Received from AllThingsTalk");
  766.   String topic(p_topic);
  767.  
  768.   // Deserialize JSON
  769.   DynamicJsonDocument doc(256);
  770.   char json[256];
  771.   for (int i = 0; i < p_length; i++) {
  772.       json[i] = (char)p_payload[i];
  773.   }
  774.   auto error = deserializeJson(doc, json);
  775.   if (error) {
  776.       Serial.print("[MQTT] Parsing JSON failed. Code: ");
  777.       Serial.println(error.c_str());
  778.       return;
  779.   }
  780.  
  781.   String asset = extractAssetNameFromTopic(topic);
  782. //  Serial.print("[MQTT] Asset Name: ");
  783. //  Serial.println(asset);
  784.  
  785.   if (asset == INTERVAL_ASSET) {
  786.     int value = doc["value"];
  787.     changeInterval(value);
  788.   }
  789. }
  790.  
  791. String wifiSignal() {
  792.   if (!wifiConnectionLost) {
  793.     int signal = WiFi.RSSI();
  794.     String signalString;
  795.     if (signal < -87) {
  796.         signalString = "Horrible";
  797.     } else if (signal >= -87 && signal <= -80) {
  798.         signalString = "Bad";
  799.     } else if (signal > -80 && signal <= -70) {
  800.         signalString = "Decent";
  801.     } else if (signal > -70 && signal <= -55) {
  802.         signalString = "Good";
  803.     } else if (signal > -55) {
  804.         signalString = "Excellent";
  805.     } else {
  806.         signalString = "Error";
  807.     }
  808.     return signalString;
  809.   }
  810.   return "Error";
  811. }
  812.  
  813. void initPMS() {
  814.   pmsSerial.begin(9600);
  815.   pmsPower(true);
  816. }
  817.  
  818. void initBME() {
  819.   bme.begin(0x76);
  820. }
  821.  
  822. void generateID() {
  823.   snprintf(klimerkoID, sizeof(klimerkoID), "%s%i", "KLIMERKO-", ESP.getChipId());
  824.   Serial.print("[ID] Unique Klimerko ID: ");
  825.   Serial.println(klimerkoID);
  826. }
  827.  
  828. void mqttSubscribeTopics() {
  829.   char command_topic[256];
  830.   snprintf(command_topic, sizeof command_topic, "%s%s%s", "device/", deviceId, "/asset/+/command");
  831.   mqtt.subscribe(command_topic);
  832. }
  833.  
  834. bool connectMQTT() {
  835.   if (!wifiConnectionLost) {
  836.     Serial.print("[MQTT] Connecting to AllThingsTalk... ");
  837.     if (mqtt.connect(klimerkoID, deviceToken, MQTT_PASSWORD)) {
  838.       Serial.println("Connected!");
  839.       if (mqttConnectionLost) {
  840.         mqttConnectionLost = false;
  841.         ledSuccessBlink = true;
  842.       }
  843.       mqttSubscribeTopics();
  844.       publishDiagnosticData();
  845.       return true;
  846.     } else {
  847.       Serial.print("Failed! Reason: ");
  848.       Serial.println(mqtt.state());
  849.       mqttConnectionLost = true;
  850.       return false;
  851.     }
  852.   }
  853.   return false;
  854. }
  855.  
  856. void maintainMQTT() {
  857.   mqtt.loop();
  858.   if (mqtt.connected()) {
  859.     if (mqttConnectionLost) {
  860.       mqttConnectionLost = false;
  861.       publishDiagnosticData();
  862.     }
  863.   } else {
  864.     if (!mqttConnectionLost) {
  865.       if (wifiConnectionLost) {
  866.         Serial.println("[MQTT] Lost connection due to WiFi!");
  867.       } else {
  868.         Serial.print("[MQTT] Lost Connection. Reason: ");
  869.         Serial.println(mqtt.state());
  870.       }
  871.       mqttConnectionLost = true;
  872.     }
  873.     if (millis() - mqttReconnectLastAttempt >= mqttReconnectInterval * 1000 && !wifiConnectionLost) {
  874.       connectMQTT();
  875.       mqttReconnectLastAttempt = millis();
  876.     }
  877.   }
  878. }
  879.  
  880. bool initMQTT() {
  881.   mqtt.setBufferSize(MQTT_MAX_MESSAGE_SIZE);
  882.   mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  883.   mqtt.setKeepAlive(30);
  884.   mqtt.setCallback(mqttCallback);
  885.   return connectMQTT();
  886. }
  887.  
  888. bool connectWiFi() {
  889.   Serial.print("[WiFi] Connecting to WiFi... ");
  890.   if(!wm.autoConnect(klimerkoID, wifiConfigPortalPassword)) {
  891.     Serial.print("Failed! Reason: ");
  892.     Serial.println(WiFi.status());
  893.     wifiConnectionLost = true;
  894.     return false;
  895.   } else {
  896.     Serial.print("Connected! IP: ");
  897.     Serial.println(WiFi.localIP());
  898.     wifiConnectionLost = false;
  899.     ledSuccessBlink = true;
  900.     return true;
  901.   }
  902. }
  903.  
  904. void maintainWiFi() {
  905.   if (WiFi.status() == WL_CONNECTED) {
  906.     if (wifiConnectionLost) {
  907.       Serial.print("[WiFi] Connection Re-Established! IP: ");
  908.       Serial.println(WiFi.localIP());
  909.       wifiConnectionLost = false;
  910.       ledSuccessBlink = true;
  911.     }
  912.   } else {
  913.     if (!wifiConnectionLost) {
  914.       Serial.print("[WiFi] Connection Lost! Reason: ");
  915.       Serial.println(WiFi.status());
  916.       wifiConnectionLost = true;
  917.     }
  918.     // AutoReconnect handles this, this here exists as backup
  919.     if (millis() - wifiReconnectLastAttempt >= wifiReconnectInterval * 1000 && !wm.getConfigPortalActive()) {
  920.       connectWiFi();
  921.       wifiReconnectLastAttempt = millis();
  922.     }
  923.   }
  924. }
  925.  
  926. void initWiFi() {
  927.   wm.setDebugOutput(false);
  928.   wm.addParameter(&portalDeviceID);
  929.   wm.addParameter(&portalDeviceToken);
  930.   wm.addParameter(&portalTemperatureOffset);
  931.   wm.addParameter(&portalDisplayFirmwareVersion);
  932.   wm.addParameter(&portalDisplayCredits);
  933.   wm.setSaveParamsCallback(saveData);
  934.   wm.setSaveConfigCallback(connectAfterSavingData);
  935.   wm.setWebServerCallback(wifiConfigWebServerStarted);
  936.   wm.setAPCallback(wifiConfigStarted);
  937.   wm.setConfigPortalBlocking(false);
  938.   wm.setConfigPortalTimeout(wifiConfigTimeout);
  939.   wm.setConnectRetries(2);
  940.   wm.setConnectTimeout(5);
  941.   wm.setDarkMode(true);
  942.   wm.setTitle("Klimerko");
  943.   wm.setHostname(klimerkoID);
  944.   wm.setCountry("RS");
  945.   wm.setEnableConfigPortal(false);
  946.   wm.setParamsPage(false);
  947.   wm.setSaveConnect(true);
  948.   wm.setBreakAfterConfig(true);
  949.   wm.setWiFiAutoReconnect(true);
  950.   WiFi.mode(WIFI_STA);
  951.   Serial.print("[MEMORY] WiFi SSID: ");
  952.   if (wm.getWiFiIsSaved()) {
  953.     Serial.println((String)wm.getWiFiSSID());
  954.   } else {
  955.     Serial.println("Nothing in Memory");
  956.   }
  957.   connectWiFi();
  958. }
  959.  
  960. void initAvg() {
  961.   pm1.begin();
  962.   pm25.begin();
  963.   pm10.begin();
  964.   temp.begin();
  965.   hum.begin();
  966.   pres.begin();
  967. }
  968.  
  969. void initPins() {
  970.   pinMode(BUTTON_PIN, INPUT);
  971.   pinMode(LED_BUILTIN, OUTPUT);
  972.   digitalWrite(LED_BUILTIN, HIGH);
  973. }
  974.  
  975. void setup() {
  976.   Serial.begin(115200);
  977.   Serial.println("");
  978.   Serial.println(" ------------------------------ Project 'KLIMERKO' ------------------------------");
  979.   Serial.println("|                  https://github.com/DesconBelgrade/Klimerko                    |");
  980.   Serial.println("|                               www.klimerko.org                                 |");
  981.   Serial.print("|                           Firmware Version: ");
  982.   Serial.print(firmwareVersion);
  983.   Serial.println("                              |");
  984.   Serial.println("|    Hold NodeMCU FLASH button for 2 seconds to enter WiFi Configuration Mode.   |");
  985.   Serial.print("| Sensors are read every ");
  986.   Serial.print(readIntervalSeconds());
  987.   Serial.print(" seconds and averages are published every ");
  988.   Serial.print(dataPublishInterval);
  989.   Serial.println(" minutes. |");
  990.   Serial.println(" --------------------------------------------------------------------------------");
  991.   initAvg();
  992.   initPins();
  993.   initPMS();
  994.   initBME();
  995.   generateID();
  996.   restoreData();
  997.   initWiFi();
  998.   initMQTT();
  999.   Serial.println("");
  1000. }
  1001.  
  1002. void loop() {
  1003.   sensorLoop();
  1004.   maintainWiFi();
  1005.   maintainMQTT();
  1006.   wifiConfigLoop();
  1007.   buttonLoop();
  1008.   ledLoop();  
  1009. }
  1010.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement