#include <WiFi.h> #include <PubSubClient.h> #include "home_secrets.h" /* home wifi passwords and MQTT info */ #include "time.h" /* Pull time from NTP server to timestamp readings */ #include <Wire.h> /* Needed for I2C interface for Battery Fuel Gauge */ #include <SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library.h> /* Library for MAX17044 Fuel Gauge */ // Base Code borrowed from Steve Murch's gas tank monitor // www.stevemurch.com //SFE_MAX1704X lipo; // Defaults to the MAX17043 //SFE_MAX1704X lipo(MAX1704X_MAX17043); // Create a MAX17043 SFE_MAX1704X lipo(MAX1704X_MAX17044); // Create a MAX17044 //SFE_MAX1704X lipo(MAX1704X_MAX17048); // Create a MAX17048 //SFE_MAX1704X lipo(MAX1704X_MAX17049); // Create a MAX17049 double voltage = 0; // Variable to keep track of LiPo voltage double soc = 0; // Variable to keep track of LiPo state-of-charge (SOC) // Using ADC7 Pin which is GPIO35 on ESP32-WROOM-32 board for 250Gal propane tank // Using ADC6 Pin which is GPIO34 on ESP32-WROOM-32 board for 500Gal propane tank const int GPIO_PIN_HALL_EFFECT_SENSOR_READ_INPUT = 35; // 250Gal Tank const int GPIO_PIN_HALL_EFFECT_SENSOR2_READ_INPUT = 34; // 500Gal Tank /* Manuall read tank gauge values in 5% increments to use for CASE and MAP statements below Gauge Reading % Full 250gal mounted sensor 500gal mounted sensor 05 1837 1680 10 1822 1661 15 1827 1665 20 1837 1677 25 1846 1695 30 1861 1715 35 1874 1735 40 1893 1758 45 1907 1786 50 1928 1812 55 1948 1840 60 1969 1870 65 1988 1897 70 2001 1922 75 2012 1940 80 2025 1955 85 2031 1966 90 2028 1967 95 2005 1936 Propane tanks are never filled above 80% becasue the liquid needs space to turn into a gas to be consumed, so any readings above 85% are truncated. Also note that the values for above 85% start declining so they aren't trustworthy anyway. Likewise, the values below 10% begin climbing again due to the location of the Hall Sensor so those aren't trustworthy either. Hence once the sensors hit 15%, the software sets a "low" switch variable that is passed during deep sleep through the RTC memory location. Once the tank reads above 15% again, then the low value is reset in memory again. NOTE - if the ESP32 looses all power or is hardware reset, the memory valus are lost. */ // Set Hall Sensor minimum values taken from testing the sensors manually const int Tank1_Low_Cutoff = 1827; // 15% reading of guage const int Tank2_Low_Cutoff = 1665; // 15% reading of guage const int Tank1_15_Percent = 1827; // 15% reading of guage const int Tank2_15_Percent = 1665; // 15% reading of guage int Tank1_Percentage = 0; // Set percentage full of 250gal tank to 0 int Tank2_Percentage = 0; // Set percentage full of 500gal tank to 0 const int mqtt_port = 1883; // Default MQTT port number const char* ntpServer = "pool.ntp.org"; // Server to pull time from const long gmtOffset_sec = -21600; // GMT offset in seconds for Central Time (6 hrs) const int daylightOffset_sec = 3600; // seconds offset for daylight savings #define uS_TO_S_FACTOR 1000000 /* Conversion factor for micro seconds to seconds */ #define TIME_TO_SLEEP 30 /* Seconds. Time ESP32 will go to sleep */ // Setup RTC memory variables to compensate for hall sensor readings climbing // after they drop below 10% of full. These memory values are used as an integer // value to determine if the tank has been filled to 15. They are set to a "1" // when they get below the tank-low-cutoff value and back to "0" once they hit 15%. RTC_DATA_ATTR int Tank1_Low = 0; RTC_DATA_ATTR int Tank2_Low = 0; RTC_DATA_ATTR int BootCount = 0; // For tracking boot count WiFiClient espClient; PubSubClient client(espClient); void setup() { // Set software serial baud to 115200; Serial.begin(115200); Serial.println("Waking up..."); // Define GPIO Pins are input pinMode(GPIO_PIN_HALL_EFFECT_SENSOR_READ_INPUT, INPUT); pinMode(GPIO_PIN_HALL_EFFECT_SENSOR2_READ_INPUT, INPUT); // Increase boot count ++BootCount; Serial.print("Boot cycle: "); Serial.println(BootCount); Wire.begin(); // Initialize I2C lipo.enableDebugging(); // Uncomment this line to enable helpful debug messages on Serial // Initialize Fuel Gauge // Set up the MAX17043 LiPo fuel gauge: if (lipo.begin() == false) // Connect to the MAX17043 using the default wire port { Serial.println(F("MAX17043 not detected. Please check wiring.")); } // Step1 - Get sensor values and average them int sum = 0; int sum2 = 0; int val = 0; int val2 = 0; int averageRead = 0; int averageRead2 = 0; Serial.print("Reading 100 values both sensors..."); for (int i = 1; i <= 100; i++) { // Read 250gal tank sensor int val = analogRead(GPIO_PIN_HALL_EFFECT_SENSOR_READ_INPUT); sum = sum + val; // Read 500gal tank sensor int val2 = analogRead(GPIO_PIN_HALL_EFFECT_SENSOR2_READ_INPUT); sum2 = sum2 + val2; delay(100); } // now get the average of reads averageRead = sum / 100; averageRead2 = sum2 / 100; Serial.print("Average digital representation -- "); Serial.print(averageRead); Serial.print(", "); Serial.println(averageRead2); // Step2 - Translate the digital readings into a percentage tank full to report // Once the readings drop below 10% the Hall sensor increases value which results // in false higher tank readings. So, use RTC memory to store previous cycle reads // to catch when the amount of porpane has dropped below 10% until it is filled again. // NOTE - if power is lost and deep sleep broken, RTC memory is cleared // Check to see if the previous cycle reading was at or below 10% by checking RTC memory if (Tank1_Low == 1 && averageRead > Tank1_15_Percent) { Tank1_Low = 0; } if (Tank1_Low == 0 && averageRead <= Tank1_Low_Cutoff) { Tank1_Low = 1; } if (Tank2_Low == 1 && averageRead2 > Tank2_15_Percent) { Tank2_Low = 0; } if (Tank2_Low == 0 && averageRead2 <= Tank2_Low_Cutoff) { Tank2_Low = 1; } // If tank is already below 10%, just setup a 7% value // and skip all the selection processing if (Tank1_Low == 1){ averageRead = Tank1_Low_Cutoff; Tank1_Percentage=10; } else { // Now use the case statement to map the read values to ranges already determined through testing // and assign a percentage. Since the readings aren't linear and there are only collected values // between 15% and 85% in 5% increments, we use the map function to get the asignments. switch (averageRead) { case 1828 ... 1837 : { Tank1_Percentage=map(averageRead,1827,1837,15,20); break; } case 1838 ... 1846 : { Tank1_Percentage=map(averageRead,1838,1846,20,25); break; } case 1847 ... 1861 : { Tank1_Percentage=map(averageRead,1847,1861,25,30); break; } case 1862 ... 1874 : { Tank1_Percentage=map(averageRead,1862,1874,30,35); break; } case 1875 ... 1893 : { Tank1_Percentage=map(averageRead,1875,1893,35,40); break; } case 1894 ... 1907 : { Tank1_Percentage=map(averageRead,1894,1907,40,45); break; } case 1908 ... 1928 : { Tank1_Percentage=map(averageRead,1908,1928,45,50); break; } case 1929 ... 1948 : { Tank1_Percentage=map(averageRead,1929,1948,50,55); break; } case 1949 ... 1969 : { Tank1_Percentage=map(averageRead,1949,1969,55,60); break; } case 1970 ... 1988 : { Tank1_Percentage=map(averageRead,1970,1988,60,65); break; } case 1989 ... 2001 : { Tank1_Percentage=map(averageRead,1989,2001,65,70); break; } case 2002 ... 2012 : { Tank1_Percentage=map(averageRead,2002,2012,70,75); break; } case 2013 ... 2025 : { Tank1_Percentage=map(averageRead,2013,2025,75,80); break; } case 2026 ... 2031 : { Tank1_Percentage=map(averageRead,2026,2031,80,85); break; } case 2032 ... 6000 : { Tank1_Percentage=90; break; } } } // If tank2 is already below 10%, just setup a 7% value // and skip all the selection processing if (Tank2_Low == 1){ averageRead2 = Tank2_Low_Cutoff; Tank2_Percentage=10; } else { // Now use the case statement to map the read values to ranges already determined through testing // and assign a percentage. Since the readings aren't linear and there are only collected values // between 15% and 85% in 5% increments, we use the map function to get the asignments. switch (averageRead2) { case 1665 ... 1677 : { Tank2_Percentage=map(averageRead2,1665,1677,15,20); break; } case 1678 ... 1695 : { Tank2_Percentage=map(averageRead2,1678,1695,20,25); break; } case 1696 ... 1715 : { Tank2_Percentage=map(averageRead2,1696,1715,25,30); break; } case 1716 ... 1735 : { Tank2_Percentage=map(averageRead2,1716,1735,30,35); break; } case 1736 ... 1758 : { Tank2_Percentage=map(averageRead2,1736,1758,35,40); break; } case 1759 ... 1786 : { Tank2_Percentage=map(averageRead2,1759,1786,40,45); break; } case 1787 ... 1812 : { Tank2_Percentage=map(averageRead2,1787,1812,45,50); break; } case 1813 ... 1840 : { Tank2_Percentage=map(averageRead2,1813,1840,50,55); break; } case 1841 ... 1870 : { Tank2_Percentage=map(averageRead2,1841,1870,55,60); break; } case 1871 ... 1897 : { Tank2_Percentage=map(averageRead2,1871,1897,60,65); break; } case 1898 ... 1922 : { Tank2_Percentage=map(averageRead2,1898,1922,65,70); break; } case 1923 ... 1940 : { Tank2_Percentage=map(averageRead2,1923,1940,70,75); break; } case 1941 ... 1955 : { Tank2_Percentage=map(averageRead2,1941,1955,75,80); break; } case 1956 ... 1966 : { Tank2_Percentage=map(averageRead2,1956,1966,80,85); break; } case 1967 ... 6000 : { Tank2_Percentage=90; } } } // Quick start restarts the MAX17043 in hopes of getting a more accurate // guess for the SOC. lipo.quickStart(); // We can set an interrupt to alert when the battery SoC gets too low. // We can alert at anywhere between 1% - 32%: //lipo.setThreshold(20); // Set alert threshold to 20%. // Get voltage and State of Charge (SOC) from Fuel Gauge // lipo.getVoltage() returns a voltage value (e.g. 3.93) voltage = lipo.getVoltage(); // lipo.getSOC() returns the estimated state of charge (e.g. 79%) soc = lipo.getSOC(); // print out battery voltage and SOC Serial.print("Voltage: "); Serial.print(voltage); Serial.println(" mV"); Serial.print("Percent: "); Serial.print(soc); Serial.println("%"); // Connect to WiFi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.println("Connecting to WiFi.."); } Serial.println("Connected to the Wi-Fi network"); // Now that WiFi is connected, Init and get the time configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); struct tm timeinfo; while (!getLocalTime(&timeinfo)){ Serial.println("Failed to obtain time"); delay(500); } Serial.println(&timeinfo, "%d-%B-%Y-%H:%M:%S"); char timeCheck[32]; strftime(timeCheck,32,"%d-%B-%Y-%H:%M:%S",&timeinfo); //Serial.print("Value of timeCheck -->"); //Serial.println(timeCheck); // Connect to MQTT service client.setServer(mqtt_broker, mqtt_port); while (!client.connected()) { String client_id = "esp32-client-"; client_id += String(WiFi.macAddress()); Serial.printf("The client %s connects to the public MQTT broker\n", client_id.c_str()); if (client.connect(client_id.c_str(), mqtt_username, mqtt_password)) { Serial.println("Public EMQX MQTT broker connected"); } else { Serial.print("failed with state "); Serial.println(client.state()); delay(2000); } } // Send 250gal tank data to MQTT service char msg_out[32]; sprintf(msg_out, "%d", Tank1_Percentage); client.publish(outputTopicPercentage, msg_out); sprintf(msg_out, "%d", averageRead); client.publish(outputTopicVoltage, msg_out); sprintf(msg_out, "%s", timeCheck); client.publish(outputTopicTime, msg_out); // Send 500gal tank data to MQTT service sprintf(msg_out, "%d", Tank2_Percentage); client.publish(outputTopic2Percentage, msg_out); sprintf(msg_out, "%d", averageRead2); client.publish(outputTopic2Voltage, msg_out); sprintf(msg_out, "%s", timeCheck); client.publish(outputTopic2Time, msg_out); // Send battery information to MQTT service // Since values are float, use dtostrf command to conver to string char floatBuffer[10]; // Convert double to string (dtostrf(value, width, decimal_places, buffer)) dtostrf(soc, 6, 2, floatBuffer); // Width: 6, Decimal places: 2 sprintf(msg_out, "%s%%", floatBuffer); client.publish(outputTopicBatteryPercentage, msg_out); Serial.print("BatterySOC MsgOut-->"); Serial.println(msg_out); // Convert double to string (dtostrf(value, width, decimal_places, buffer)) dtostrf(voltage, 6, 2, floatBuffer); // Width: 6, Decimal places: 2 sprintf(msg_out, "%sV", floatBuffer); client.publish(outputTopicBatteryVoltage, msg_out); Serial.print("BatteryVoltage MsgOut-->"); Serial.println(msg_out); client.loop(); Serial.println("Good night. Going to sleep."); // Close Wifi and MQTT service delay(1000); client.disconnect(); delay(1000); WiFi.disconnect(); // Go to sleep mode esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); esp_deep_sleep_start(); } void loop() { // loop isn't used since we are using deep seep mode }