/* Data Logger * * Collect and store data from a DS18B20 probe thermometer * Range is -127ºF - 128ºF, accuracy ± 1ºF * * Nathaniel Baird * bairdn@oregonstate.edu * * 05/18/2023 - 06/02/2023 */ /* CITATIONS * * LCD panel tutorial: https://learn.sparkfun.com/tutorials/basic-character-lcd-hookup-guide * * POST and GET request tutorial: https://randomnerdtutorials.com/esp32-http-get-post-arduino/ * * WPA2 Enterprise "magic": JeroenBeemster (https://github.com/JeroenBeemster/ESP32-WPA2-enterprise) * WiFi Scan: expressif ESP32 documentation (WiFi scan example) */ #include // For LCD screen #include // For thermometer #include // For internet connection #include "esp_wpa2.h" // For OSU secured WiFi (eduroam) #include // For sending data to server #include "secrets.h" // My username & password for WiFi #include // For extracting server response data // General utility info #define DATA_LENGTH 144 // Define length of data storage (for 10 min avgs.) #define SEC *1000 // Make it easier to see how many seconds millis() is showing // Pin info #define THERMOMETER 13 // Set the pin for the thermometer's reading #define SWITCH 25 // Set pin for output switch // LCD screen info #define RS_PIN 22 #define EN_PIN 23 #define DB0_PIN 0 #define DB1_PIN 2 #define DB2_PIN 4 #define DB3_PIN 15 #define DB4_PIN 16 #define DB5_PIN 17 #define DB6_PIN 18 #define DB7_PIN 19 // Internet info #define ssid "eduroam" // Login credentials in secrets.h #define SERVER "https://web.engr.oregonstate.edu/~bairdn/data_logger/input.php" #define ROOT_CA_CERT \ "-----BEGIN CERTIFICATE-----\n" \ "MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\n" \ "iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n" \ "cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n" \ "BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\n" \ "MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\n" \ "BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\n" \ "aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\n" \ "dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n" \ "AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\n" \ "3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\n" \ "tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\n" \ "Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\n" \ "VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\n" \ "79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\n" \ "c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\n" \ "Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\n" \ "c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\n" \ "UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\n" \ "Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\n" \ "BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\n" \ "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\n" \ "Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\n" \ "VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\n" \ "ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\n" \ "8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\n" \ "iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\n" \ "Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\n" \ "XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\n" \ "qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\n" \ "VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\n" \ "L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\n" \ "jjxDah2nGN59PRbxYvnKkKj9\n" \ "-----END CERTIFICATE-----" // Valid until 1/18/2038 // Initialize API objects LiquidCrystal lcd(RS_PIN, EN_PIN, DB0_PIN, DB1_PIN, DB2_PIN, DB3_PIN, DB4_PIN, DB5_PIN, DB6_PIN, DB7_PIN); // Initialize LCD screen DS18B20 ds(THERMOMETER); // Initialize thermometer // Set global var to print WiFi connection status on LCD bool wiFiConnected = false; // Attempt to connect to WiFi using credentials in secrets.h // Returns true if successful or false if it timed out bool connectToWiFi(void) { int connectAttempts = 0; // WPA2 enterprise magic starts here // Credit: JeroenBeemster: https://github.com/JeroenBeemster/ESP32-WPA2-enterprise WiFi.disconnect(true); // Credit: expressif ESP32 WiFi scan example // Scan for networks first -- not part of WPA2 Enterprise magic, but necessary for some reason? if(digitalRead(SWITCH) == HIGH) Serial.println("Scanning for WiFi networks..."); // WiFi.scanNetworks will return the number of networks found int n = WiFi.scanNetworks(); // Print diagnostic info if desired if(digitalRead(SWITCH) == HIGH) { Serial.println("scan done"); Serial.printf("%d networks found\n", n); for (int i = 0; i < n; ++i) { // Print SSID and RSSI for each network found Serial.print(i + 1); Serial.print(": "); Serial.print(WiFi.SSID(i)); Serial.print(" ("); Serial.print(WiFi.RSSI(i)); Serial.print(")"); Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?" ":"*"); delay(10); } Serial.println(""); } // Back to WPA2 enterprise magic (from JeroenBeemster) if(digitalRead(SWITCH) == HIGH) Serial.printf("Setting WiFi configuration SSID %s...\n", ssid); esp_wifi_sta_wpa2_ent_set_identity((uint8_t *)EAP_ID, strlen(EAP_ID)); esp_wifi_sta_wpa2_ent_set_username((uint8_t *)EAP_USERNAME, strlen(EAP_USERNAME)); esp_wifi_sta_wpa2_ent_set_password((uint8_t *)EAP_PASSWORD, strlen(EAP_PASSWORD)); esp_wifi_sta_wpa2_ent_enable(); // WPA2 enterprise magic ends here // Connect to network w/ credentials from secrets.h WiFi.begin(ssid); if(digitalRead(SWITCH) == HIGH) Serial.print("Connecting to WiFi"); while(WiFi.status() != WL_CONNECTED) { connectAttempts++; if(digitalRead(SWITCH) == HIGH) Serial.print("."); // Time out connection after trying for 10 seconds if(connectAttempts > 20) { wiFiConnected = false; if(digitalRead(SWITCH) == HIGH) Serial.println("ERROR: Could not connect to WiFi. Timed out after 10 seconds."); return false; } delay(500); } // Update global var for lcd readout wiFiConnected = true; // Print diagnostic info if desired if(digitalRead(SWITCH) == HIGH) { Serial.println(""); Serial.print("Connected to WiFi network with IP Address: "); Serial.println(WiFi.localIP()); Serial.print("MAC address: "); Serial.println(WiFi.macAddress()); } return true; } // POST data to server specified in #defines void postData(String data) { //Check WiFi connection status if(WiFi.status() == WL_CONNECTED){ wiFiConnected = true; // Create object for WiFi API WiFiClientSecure client; // Allow secure connection using root certificate authority certificate client.setCACert(ROOT_CA_CERT); // Create object for HTTP API HTTPClient http; // Open connection to server over WiFi http.begin(client, SERVER); // Set header for how data is being sent http.addHeader("Content-Type", "application/x-www-form-urlencoded"); // Set data to send with HTTP POST String httpRequestData = data; // Send HTTP POST request int httpResponseCode = http.POST(httpRequestData); // Free resources http.end(); // Alert the user if abnormal response code if(httpResponseCode != 200 && digitalRead(SWITCH) == HIGH) { Serial.print("HTTP Response code: "); Serial.println(httpResponseCode); } } else { wiFiConnected = false; // Alert the user if WiFi had a problem if(digitalRead(SWITCH) == HIGH) Serial.println("WiFi Disconnected"); } } // Get the temperature reading from the sensor int8_t getTemp(void) { // Read temperature as Fahrenheit -- takes ~250 ms float t = ds.getTempF(); // Return early if failed to read if(isnan(t)) { return INT8_MIN; // Use for error since -128º is outside the sensor range anyway } return (int8_t) round(t); } // Update LCD to show current temperature & how long the program has been running void updateLCD(int8_t temperature) { lcd.clear(); // Tell user if WiFi is connected if(wiFiConnected) { lcd.print("WiFi connected"); } else { lcd.print("WiFi disconnected"); } // Print out the current temperature in ºF lcd.setCursor(0, 1); lcd.printf("%d%cF", (int) temperature, 223); // 223 = char code for 'º' on this LCD screen // Print out how long the device has been running unsigned long time = millis() / (1 SEC); unsigned short seconds = time % 60; unsigned short minutes = (time / 60) % 60; unsigned short hours = ((time / 60) / 60) % 24; unsigned long days = ((time / 60) / 60) / 24; // Print time right-aligned -- millis() overflows approx. every 54 days, so never need >2 digits for days lcd.setCursor(5, 1); lcd.printf("%2lu:%02hu:%02hu:%02hu", days, hours, minutes, seconds); } // Class to control access to data storage // Max 65535 observations in interval (10 min avg) class TempData { public: TempData() { // Initialize using -128º for undefined (outside sensor range anyway) location = DATA_LENGTH - 1; for(int i = 0; i < DATA_LENGTH; i++) { dataArr[i] = INT8_MIN; } sumForAvg = 0; obsForAvg = 0; minimum = INT8_MAX; maximum = INT8_MIN; serverMin = INT8_MAX; serverMax = INT8_MIN; average = 0; // Tell user about next step lcd.clear(); lcd.print("Checking for"); lcd.setCursor(0, 1); lcd.print("uploaded data..."); // Check the server for saved data; restore that data if it's there getData(); } // Update everything w/ new reading -- combines max/min check, adding for 10-min avg, adding to total average void processData(int8_t data) { addToInterval(data); // Add data to 10 min avg checkMaxMin(data); // Update max/min to include new reading updateLTAvg(data); // Update lifetime average with new reading } // Print everything from 10-min avg list, starting with oldest value void print(void) { // Print max, min, avg Serial.printf("\nLifetime maximum temperature: %dºF\n", maximum); Serial.printf("Lifetime minimum temperature: %dºF\n", minimum); Serial.printf("Lifetime average temperature: %.1fºF\n", average); // Print 10-min avgs Serial.println("10-minute averages from past 24 hours:"); Serial.println(dataArrToString()); Serial.println(); } // Calculate the average for the current interval, add it to list of averages, & reset interval void avgInterval(void) { // Calculate average int16_t avg = round((float) sumForAvg / obsForAvg * 10); // round from float for accuracy // Reset vars for next interval calculation sumForAvg = 0; obsForAvg = 0; // Store data // Move to next spot ++location %= DATA_LENGTH; // Same as location = (location + 1) % DATA_LENGTH -- prevents overflow without if statement // Overwrite whatever was there (now >24hrs old) dataArr[location] = avg; // Update long-term storage as well } // Send the maximum & minimum recorded temperatures to the server if it has different values stored void sendMaxMin() { if(minimum != serverMin || maximum != serverMax) { postData("minimum=" + String(minimum) + "&maximum=" + String(maximum)); serverMin = minimum; serverMax = maximum; } } // Send the 10-minute average array and lifetime average to the server void sendToServer() { postData("averages=" + dataArrToString() + "&fullAverage=" + String(average, 1) + "&observations=" + String(obsForAvg)); } private: // Use for storing 10-min avgs int16_t dataArr[DATA_LENGTH]; // Using int8_t for only 1 byte of number: save space uint8_t location; // ^ // Use for calculating 10-min avgs long sumForAvg; unsigned short obsForAvg; // Use for storing lifetime values int8_t minimum, maximum; float average; unsigned long observations; // Use to accurately update average // Use for checking what's been sent to server int8_t serverMin, serverMax; // Add newNum to the interval that's being averaged void addToInterval(int8_t newNum) { sumForAvg += newNum; obsForAvg++; } // Update max/min if value is largest/smallest seen so far void checkMaxMin(int8_t value) { if(value > maximum) maximum = value; if(value < minimum) minimum = value; } // Update the lifetime average of the collected data void updateLTAvg(int8_t newNum) { observations++; if(observations == 0) observations = ULONG_MAX; // If it overflowed somehow, just keep it @ highest value -- it'll still be pretty accurate // Calculate average: (old average * old # of observations) = weighted for adding new number & re-averageing average = (average * (observations - 1) + newNum) / observations; } // Create a string out of the 10-minute average array String dataArrToString() { // Find which array node is oldest (one after most recent overwrite) for 10-min avgs long first = (location + 1 < DATA_LENGTH) ? location + 1 : 0; String output; // Loop thru array to print, starting w/ oldest value & looping around if past end of array: incrementer is same as location = (location + 1) % DATA_LENGTH -- prevents overflow without if statement for(int i = first; i != location; ++i %= DATA_LENGTH) { // Print corresponding reading if it's been set if(dataArr[i] != INT8_MIN) // output += String(dataArr[i]) + ','; // snprintf(nextLine, "%.1f, ", (float) dataArr[i] / 10), 5); output += String(dataArr[i] / 10) + '.' + String(dataArr[i] % 10) + ','; } // Add most recent reading if it's been set w/ new line (had to break out of for() before printing this one to prevent infinite loop) if(dataArr[location] != INT8_MIN) { output += String(dataArr[location] / 10) + '.' + String(dataArr[location] % 10); // output += String(dataArr[location]); } return output; } // Get the data stored on the server & update all properties to match void getData(void) { if(digitalRead(SWITCH) == HIGH) Serial.println("Checking server for saved data..."); //Check WiFi connection status if(WiFi.status()== WL_CONNECTED){ // Create object for WiFi API WiFiClientSecure client; // Allow secure connection using root certificate authority certificate client.setCACert(ROOT_CA_CERT); // Create object for HTTP API HTTPClient http; // Open connection to server over WiFi http.begin(client, SERVER); // Send HTTP GET request int httpResponseCode = http.GET(); // Record server response String responseData = http.getString(); // Parse the data if it was given if(responseData != "") { JSONVar myObject = JSON.parse(responseData); if(JSON.typeof(myObject != "undefined")) { // Parse the response to update all properties minimum = int(myObject["minimum"]); serverMin = minimum; maximum = int(myObject["maximum"]); serverMax = maximum; average = double(myObject["fullAverage"]); obsForAvg = (unsigned long) myObject["observations"]; // Restore 10-minute averages int i; int high = myObject["averages"].length(); for(i = 0; i < high; i++) { dataArr[i % DATA_LENGTH] = double(myObject["averages"][i]) * 10; } location = (i - 1) % DATA_LENGTH; // Print diagnostic info if desired if(digitalRead(SWITCH) == HIGH) { Serial.print("Restored uploaded data:"); print(); } } else { // Print diagnostic info if desired if(digitalRead(SWITCH) == HIGH) Serial.print("No data found."); // Tell server to disregard old data if there was an error postData("current=-&averages=-"); } } else { // Print diagnostic info if desired if(digitalRead(SWITCH) == HIGH) Serial.print("No data found."); // Tell server to disregard any old data if it didn't send any saved data postData("current=-&averages=-"); } // Free resources http.end(); // Alert user if abnormal response code if(httpResponseCode != 200 && digitalRead(SWITCH) == HIGH) { Serial.print("Abnormal HTTP Response code: "); Serial.println(httpResponseCode); } } else { // Alert user if WiFi is disconnected & they want diagnostic info if(digitalRead(SWITCH) == HIGH) Serial.println("WiFi Disconnected"); } } }; // Arduino IDE function for code to run on startup void setup() { // Set up serial port Serial.begin(115200); // Prep status for switch pinMode(SWITCH, OUTPUT); // Prep screen lcd.begin(16, 2); // Prep thermometer if(!ds.selectNext() && digitalRead(SWITCH) == HIGH) Serial.println("Thermometer not found!"); // Print diagnostic info if desired if(digitalRead(SWITCH) == HIGH) Serial.printf("Thermometers detected: %d\n", ds.getNumberOfDevices()); // Show connection info on LCD lcd.clear(); lcd.print("Connecting to "); lcd.setCursor(0, 1); lcd.printf("%s...", ssid); // Attempt to connect to WiFi connectToWiFi(); // Print diagnostic info if desired if(digitalRead(SWITCH) == HIGH) Serial.println("Beginning normal operation."); } // Arduino IDE function for code to run indefinitely void loop() { static unsigned long lastRead = 0, lastAvg = 0, lastUpload = 0; // Only initializes first time static TempData timeArr; // Each second, print the sensor's reading & save reading for 10 minute average calculation // Subtract to protect against millis() overflow if(millis() - lastRead >= 1 SEC) { // Update timing for most recent print -- FIRST so each reading is 1 sec apart, not 1 sec + time to execute this lastRead = millis(); // Get reading from sensor int8_t reading = getTemp(); // process reading if valid if(reading != INT8_MIN) { // Use reading for data (only need 1 data pt/sec -- NOT 100,000 data pts/sec (600,000,000 data pts/10 min)) timeArr.processData(reading); // Output current temperature updateLCD(reading); // Print current reading over Serial if switch shows granular data is desired if(digitalRead(SWITCH) == HIGH) Serial.printf("Current temperature: %dºF\n", reading); } else { // Print error message if switch shows granular data is desired if(digitalRead(SWITCH) == HIGH) Serial.println("ERROR: Failed to read temperature"); } // Update server w/ info every 30 seconds // Nested to allow access to 'int8_t reading' if(millis() - lastUpload >= 30 SEC) { lastUpload = millis(); postData("current=" + String(reading)); timeArr.sendMaxMin(); } } // Every 10 minutes, calculate and store the average temperature from the last 10 minutes // Subtract to protect against millis() overflow if(millis() - lastAvg >= 600 SEC) { // Update timing for most recent average -- FIRST so each average is 10 min apart, not 10 min + time to execute this lastAvg = millis(); // Avg this interval & add to avg list timeArr.avgInterval(); timeArr.sendToServer(); } // Check if input from USB; print summary if command was sent if(Serial.available()) { String input = Serial.readStringUntil('\n'); if(input.equalsIgnoreCase("print")) { timeArr.print(); } } }