├── credentials.h ├── README.md ├── Licence.txt ├── ESP32_SHT30_SPIFFS_DataLogger_01.ino └── ESP32_DHT22_SPIFFS_DataLogger_01.ino /credentials.h: -------------------------------------------------------------------------------- 1 | // Change to your WiFi credentials 2 | const char* ssid = "your ssid"; 3 | const char* password = "your password"; 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32_Data_Logging_Webserver 2 | An ESP32/ESP8266 and SHT30 that together form a data logger that is accessed via a webserver and data is graphed via google Charts. 3 | 4 | Put both files (ESP32_SHT30_SPIFFS_DataLogger_01.ino and credentials.h) in your sketch folder and modifiy the Wi-Fi access requirements (password and SSID) to match yours via the file tab in the IDE. 5 | 6 | Monitor the Serial Port for the assigned IP address and connect to the server with http://IP/ e.g. http://192.168.0.5/ 7 | 8 | 9 | -------------------------------------------------------------------------------- /Licence.txt: -------------------------------------------------------------------------------- 1 | This software, the ideas and concepts is Copyright (c) David Bird 2014 and beyond. 2 | 3 | All rights to this software are reserved. 4 | 5 | It is prohibited to redistribute or reproduce of any part or all of the software contents in any form other than the following: 6 | 7 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 8 | 9 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 10 | 11 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 12 | 13 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 14 | 15 | 5. You MUST include all of this copyright and permission notice ('as annotated') and this shall be included in all copies or substantial portions of the software and where the software use is visible to an end-user. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. 18 | 19 | FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | 21 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /ESP32_SHT30_SPIFFS_DataLogger_01.ino: -------------------------------------------------------------------------------- 1 | /* ESP8266 plus WEMOS SHT30-D Sensor with a Temperature and Humidity Web Server 2 | Automous display of sensor results on a line-chart, gauge view and the ability to export the data via copy/paste for direct input to MS-Excel 3 | The 'MIT License (MIT) Copyright (c) 2016 by David Bird'. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 6 | following conditions: 7 | The above copyright ('as annotated') notice and this permission notice shall be included in all copies or substantial portions of the Software and where the 8 | software use is visible to an end-user. 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 11 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | See more at http://www.dsbird.org.uk 13 | */ 14 | 15 | #ifdef ESP8266 16 | #include 17 | #include 18 | #define TZone 0 // or set you your requirements e.g. -5 for EST 19 | //#include 20 | #include 21 | #include 22 | WiFiUDP ntpUDP; //** NTP client class 23 | NTPClient timeClient(ntpUDP); 24 | #include 25 | #else 26 | #include 27 | #include //https://github.com/Pedroalbuquerque/ESP32WebServer download and place in your Libraries folder 28 | #include 29 | #include "FS.h" 30 | #include "SPIFFS.h" 31 | #endif 32 | #include 33 | #include 34 | 35 | #include // https://github.com/wemos/WEMOS_SHT3x_Arduino_Library 36 | 37 | #include "credentials.h" 38 | 39 | SHT3X sht30(0x45); // SHT30 object to enable readings (I2C address = 0x45) 40 | String version = "v1.0"; // Version of this program 41 | String site_width = "1023"; 42 | WiFiClient client; 43 | 44 | #ifdef ESP8266 45 | ESP8266WebServer server(80); // Start server on port 80 (default for a web-browser, change to your requirements, e.g. 8080 if your Router uses port 80 46 | // To access server from the outside of a WiFi network e.g. ESP8266WebServer server(8266); and then add a rule on your Router that forwards a 47 | // connection request to http://your_network_ip_address:8266 to port 8266 and view your ESP server from anywhere. 48 | // Example http://g6ejd.uk.to:8266 will be directed to http://192.168.0.40:8266 or whatever IP address your router gives to this server 49 | #else 50 | ESP32WebServer server(80); // Start server on port 80 (default for a web-browser, change to your requirements, e.g. 8080 if your Router uses port 80 51 | #endif 52 | 53 | int log_time_unit = 15; // default is 1-minute between readings, 10=15secs 40=1min 200=5mins 400=10mins 2400=1hr 54 | int time_reference = 60; // Time reference for calculating /log-time (nearly in secs) to convert to minutes 55 | int const table_size = 72; // 80 is about the maximum for the available memory and Google Charts, based on 3 samples/hour * 24 * 1 day = 72 displayed, but not stored! 56 | int index_ptr, timer_cnt, log_interval, log_count, max_temp, min_temp; 57 | String webpage,time_now,log_time,lastcall,time_str, DataFile = "datalog.txt"; 58 | bool AScale, auto_smooth, AUpdate, log_delete_approved; 59 | float temp, humi; 60 | 61 | typedef signed short sint16; 62 | typedef struct { 63 | int lcnt; // Sequential log count 64 | String ltime; // Time record of when reading was taken 65 | sint16 temp; // Temperature values, short unsigned 16-bit integer to reduce memory requirement, saved as x10 more to preserve 0.1 resolution 66 | sint16 humi; // Humidity values, short unsigned 16-bit integer to reduce memory requirement, saved as x10 more to preserve 0.1 resolution 67 | } record_type; 68 | 69 | record_type sensor_data[table_size+1]; // Define the data array 70 | 71 | void setup() { 72 | Serial.begin(115200); 73 | StartSPIFFS(); 74 | //SPIFFS.remove("/"+DataFile); // In case file in ESP32 version gets corrupted, it happens a lot!! 75 | StartWiFi(ssid,password); 76 | StartTime(); 77 | Serial.println(F("WiFi connected..")); 78 | server.begin(); Serial.println(F("Webserver started...")); // Start the webserver 79 | Serial.println("Use this URL to connect: http://"+WiFi.localIP().toString()+"/");// Print the IP address 80 | //---------------------------------------------------------------------- 81 | server.on("/", display_temp_and_humidity); // The client connected with no arguments e.g. http:192.160.0.40/ 82 | server.on("/TH", display_temp_and_humidity); 83 | server.on("/TD", display_temp_and_dewpoint); 84 | server.on("/DV", display_dial); 85 | server.on("/AS", auto_scale); 86 | server.on("/AU", auto_update); 87 | server.on("/Setup", systemSetup); 88 | server.on("/Help", help); 89 | server.on("/LgTU", logtime_up); 90 | server.on("/LgTD", logtime_down); 91 | server.on("/LogV", LOG_view); 92 | server.on("/LogE", LOG_erase); 93 | server.on("/LogS", LOG_stats); 94 | server.begin(); 95 | Serial.println(F("Webserver started...")); // Start the webserver 96 | 97 | index_ptr = 0; // The array pointer that varies from 0 to table_size 98 | log_count = 0; // Keeps a count of readings taken 99 | AScale = false; // Google charts can AScale axis, this switches the function on/off 100 | max_temp = 30; // Maximum displayed temperature as default 101 | min_temp = -10; // Minimum displayed temperature as default 102 | auto_smooth = false; // If true, transitions of more than 10% between readings are smoothed out, so a reading followed by another that is 10% higher or lower is averaged 103 | AUpdate = true; // Used to prevent a command from continually auto-updating, for example increase temp-scale would increase every 30-secs if not prevented from doing so. 104 | lastcall = "temp_humi"; // To determine what requested the AScale change 105 | log_interval = log_time_unit*10; // inter-log time interval, default is 5-minutes between readings, 10=15secs 40=1min 200=5mins 400=10mins 2400=1hr 106 | timer_cnt = log_interval + 1; // To trigger first table update, essential 107 | update_log_time(); // Update the log_time 108 | log_delete_approved = false; // Used to prevent accidental deletion of card contents, requires two approvals 109 | reset_array(); // Clear storage array before use 110 | prefill_array(); // Load old data from FS back into readings array 111 | //Serial.println(system_get_free_heap_size()); // diagnostic print to check for available RAM 112 | } 113 | 114 | void loop() { 115 | server.handleClient(); 116 | sht30.get(); // Update temp and humi 117 | temp = sht30.cTemp*10; 118 | humi = sht30.humidity*10; 119 | time_t now = time(nullptr); 120 | time_now = String(ctime(&now)).substring(0,24); // Remove unwanted characters 121 | if (time_now != "Thu Jan 01 00:00:00 1970" and timer_cnt >= log_interval) { // If time is not yet set, returns 'Thu Jan 01 00:00:00 1970') so wait. 122 | timer_cnt = 0; // log_interval values are 10=15secs 40=1min 200=5mins 400=10mins 2400=1hr 123 | log_count += 1; // Increase logging event count 124 | sensor_data[index_ptr].lcnt = log_count; // Record current log number, time, temp and humidity readings 125 | sensor_data[index_ptr].temp = temp; 126 | sensor_data[index_ptr].humi = humi; 127 | sensor_data[index_ptr].ltime = calcDateTime(time(&now)); // time stamp of reading 'dd/mm/yy hh:mm:ss' 128 | #ifdef ESP8266 129 | File datafile = SPIFFS.open(DataFile, "a+"); 130 | #else 131 | File datafile = SPIFFS.open("/"+DataFile, FILE_APPEND); 132 | #endif 133 | time_t now = time(nullptr); 134 | if (datafile == true) { // if the file is available, write to it 135 | datafile.println(((log_count<10)?"0":"")+String(log_count)+char(9)+String(temp/10,2)+char(9)+String(humi/10,2)+char(9)+calcDateTime(time(&now))+"."); // TAB delimited 136 | Serial.println(((log_count<10)?"0":"")+String(log_count)+" New Record Added"); 137 | } 138 | datafile.close(); 139 | index_ptr += 1; // Increment data record pointer 140 | if (index_ptr > table_size) { // if number of readings exceeds max_readings (e.g. 100) shift all data to the left and effectively scroll the display left 141 | index_ptr = table_size; 142 | for (int i = 0; i < table_size; i++) { // If data table is full, scroll all readings to the left in graphical terms, then add new reading to the end 143 | sensor_data[i].lcnt = sensor_data[i+1].lcnt; 144 | sensor_data[i].temp = sensor_data[i+1].temp; 145 | sensor_data[i].humi = sensor_data[i+1].humi; 146 | sensor_data[i].ltime = sensor_data[i+1].ltime; 147 | } 148 | sensor_data[table_size].lcnt = log_count; 149 | sensor_data[table_size].temp = temp; 150 | sensor_data[table_size].humi = humi; 151 | sensor_data[table_size].ltime = calcDateTime(time(&now)); 152 | } 153 | } 154 | timer_cnt += 1; // Readings set by value of log_interval each 40 = 1min 155 | delay(500); // Delay before next check for a client, adjust for 1-sec repeat interval. Temperature readings take some time to complete. 156 | //Serial.println(millis()); // Used to measure inter-sample timing 157 | } 158 | 159 | void prefill_array(){ // After power-down or restart and if the FS has readings, load them back in 160 | #ifdef ESP8266 161 | File datafile = SPIFFS.open(DataFile, "r"); 162 | #else 163 | File datafile = SPIFFS.open("/"+DataFile, FILE_READ); 164 | #endif 165 | while (datafile.available()) { // if the file is available, read from it 166 | int read_ahead = datafile.parseInt(); // Sometimes at the end of file, NULL data is returned, this tests for that 167 | if (read_ahead != 0) { // Probably wasn't null data to use it, but first data element could have been zero and there is never a record 0! 168 | sensor_data[index_ptr].lcnt = read_ahead ; 169 | sensor_data[index_ptr].temp = datafile.parseFloat()*10; 170 | sensor_data[index_ptr].humi = datafile.parseFloat()*10; 171 | sensor_data[index_ptr].ltime = datafile.readStringUntil('.'); 172 | sensor_data[index_ptr].ltime.trim(); 173 | index_ptr += 1; 174 | log_count += 1; 175 | } 176 | if (index_ptr > table_size) { 177 | for (int i = 0; i < table_size; i++) { 178 | sensor_data[i].lcnt = sensor_data[i+1].lcnt; 179 | sensor_data[i].temp = sensor_data[i+1].temp; 180 | sensor_data[i].humi = sensor_data[i+1].humi; 181 | sensor_data[i].ltime = sensor_data[i+1].ltime; 182 | sensor_data[i].ltime.trim(); 183 | } 184 | index_ptr = table_size; 185 | } 186 | } 187 | datafile.close(); 188 | // Diagnostic print to check if data is being recovered from SPIFFS correctly 189 | for (int i = 0; i <= index_ptr; i++) { 190 | Serial.println(((i<10)?"0":"")+String(sensor_data[i].lcnt)+" "+String(sensor_data[i].temp)+" "+String(sensor_data[i].humi)+" "+String(sensor_data[i].ltime)); 191 | } 192 | datafile.close(); 193 | if (auto_smooth) { // During restarts there can be a discontinuity in readings, giving a spike in the graph, this smooths that out, off by default though 194 | // At this point the array holds data from the FS, but sometimes during outage and resume, reading discontinuity occurs, so try to correct those. 195 | float last_temp,last_humi; 196 | for (int i = 1; i < table_size; i++) { 197 | last_temp = sensor_data[i].temp; 198 | last_humi = sensor_data[i].humi; 199 | // Correct next reading if it is more than 10% different from last values 200 | if ((sensor_data[i+1].temp > (last_temp * 1.1)) || (sensor_data[i+1].temp < (last_temp / 1.1))) sensor_data[i+1].temp = (sensor_data[i+1].temp+last_temp)/2; // +/-1% different then use last value 201 | if ((sensor_data[i+1].humi > (last_humi * 1.1)) || (sensor_data[i+1].humi < (last_humi / 1.1))) sensor_data[i+1].humi = (sensor_data[i+1].humi+last_humi)/2; 202 | } 203 | } 204 | Serial.println("Restored data from SPIFFS"); 205 | } 206 | 207 | void display_temp_and_humidity() { // Processes a clients request for a graph of the data 208 | // See google charts api for more details. To load the APIs, include the following script in the header of your web page. 209 | // 210 | // To autoload APIs manually, you need to specify the list of APIs to load in the initial "); 221 | webpage += F(""); 254 | webpage += F("
"); 255 | append_page_footer(); 256 | server.send(200, "text/html", webpage); 257 | webpage = ""; 258 | lastcall = "temp_humi"; 259 | } 260 | 261 | void display_temp_and_dewpoint() { // Processes a clients request for a graph of the data 262 | float dew_point; 263 | // See google charts api for more details. To load the APIs, include the following script in the header of your web page. 264 | // 265 | // To autoload APIs manually, you need to specify the list of APIs to load in the initial "); 276 | webpage += F(""); 303 | webpage += F("
"); 304 | append_page_footer(); 305 | server.send(200, "text/html", webpage); 306 | webpage = ""; 307 | lastcall = "temp_dewp"; 308 | } 309 | 310 | void display_dial (){ // Processes a clients request for a dial-view of the data 311 | log_delete_approved = false; // PRevent accidental SD-Card deletion 312 | webpage = ""; // don't delete this command, it ensures the server works reliably! 313 | append_page_header(); 314 | webpage += F(""); 315 | webpage += F(""); 334 | webpage += F("
"); 335 | webpage += F("
"); 336 | append_page_footer(); 337 | server.send(200, "text/html", webpage); 338 | webpage = ""; 339 | lastcall = "dial"; 340 | } 341 | 342 | float Calc_DewPoint(float temp, float humi) { 343 | return 243.04*(log(humi/100.00)+((17.625*temp)/(243.04+temp)))/(17.625-log(humi/100.00)-((17.625*temp)/(243.04+temp))); 344 | } 345 | 346 | void reset_array() { 347 | for (int i = 0; i <= table_size; i++) { 348 | sensor_data[i].lcnt = 0; 349 | sensor_data[i].temp = 0; 350 | sensor_data[i].humi = 0; 351 | sensor_data[i].ltime = ""; 352 | } 353 | } 354 | 355 | // After the data has been displayed, select and copy it, then open Excel and Paste-Special and choose Text, then select, then insert graph to view 356 | void LOG_view() { 357 | #ifdef ESP8266 358 | File datafile = SPIFFS.open(DataFile, "r"); // Now read data from SPIFFS 359 | #else 360 | File datafile = SPIFFS.open("/"+DataFile, FILE_READ); // Now read data from FS 361 | #endif 362 | if (datafile) { 363 | if (datafile.available()) { // If data is available and present 364 | String dataType = "application/octet-stream"; 365 | if (server.streamFile(datafile, dataType) != datafile.size()) {Serial.print(F("Sent less data than expected!")); } 366 | } 367 | } 368 | datafile.close(); // close the file: 369 | webpage = ""; 370 | } 371 | 372 | void LOG_erase() { // Erase the datalog file 373 | webpage = ""; // don't delete this command, it ensures the server works reliably! 374 | append_page_header(); 375 | if (AUpdate) webpage += ""; // 30-sec refresh time and test is needed to stop auto updates repeating some commands 376 | if (log_delete_approved) { 377 | #ifdef ESP8266 378 | if (SPIFFS.exists(DataFile)) { 379 | SPIFFS.remove(DataFile); 380 | Serial.println(F("File deleted successfully")); 381 | } 382 | #else 383 | if (SPIFFS.exists("/"+DataFile)) { 384 | SPIFFS.remove("/"+DataFile); 385 | Serial.println(F("File deleted successfully")); 386 | } 387 | #endif 388 | webpage += "

Log file '"+DataFile+"' has been erased

"; 389 | log_count = 0; 390 | index_ptr = 0; 391 | timer_cnt = 2000; // To trigger first table update, essential 392 | log_delete_approved = false; // Re-enable FS deletion 393 | } 394 | else { 395 | log_delete_approved = true; 396 | webpage += "

Log file erasing is now enabled, repeat this option to erase the log. Graph or Dial Views disable erasing again

"; 397 | } 398 | append_page_footer(); 399 | server.send(200, "text/html", webpage); 400 | webpage = ""; 401 | } 402 | 403 | void LOG_stats(){ // Display file size of the datalog file 404 | webpage = ""; // don't delete this command, it ensures the server works reliably! 405 | append_page_header(); 406 | #ifdef ESP8266 407 | File datafile = SPIFFS.open(DataFile,"r"); // Now read data from SPIFFS 408 | #else 409 | File datafile = SPIFFS.open("/"+DataFile,FILE_READ); // Now read data from FS 410 | #endif 411 | webpage += "

Data Log file size = "+String(datafile.size())+"-Bytes

"; 412 | webpage += "

Number of readings = "+String(log_count)+"

"; 413 | datafile.close(); 414 | append_page_footer(); 415 | server.send(200, "text/html", webpage); 416 | webpage = ""; 417 | } 418 | 419 | void auto_scale () { // Google Charts can auto-scale graph axis, this turns it on/off 420 | if (AScale) AScale = false; else AScale = true; 421 | if (lastcall == "temp_humi") display_temp_and_humidity(); 422 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 423 | if (lastcall == "dial") display_dial(); 424 | } 425 | 426 | void auto_update () { // Google Charts can auto-scale graph axis, this turns it on/off 427 | if (AUpdate) AUpdate = false; else AUpdate = true; 428 | if (lastcall == "temp_humi") display_temp_and_humidity(); 429 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 430 | if (lastcall == "dial") display_dial(); 431 | } 432 | 433 | void logtime_down () { // Timer_cnt delay values 1=15secs 4=1min 20=5mins 40=10mins 240=1hr, increase the values with this function 434 | log_interval -= log_time_unit; 435 | if (log_interval < log_time_unit) log_interval = log_time_unit; 436 | update_log_time(); 437 | if (lastcall == "temp_humi") display_temp_and_humidity(); 438 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 439 | if (lastcall == "dial") display_dial(); 440 | } 441 | 442 | void logtime_up () { // Timer_cnt delay values 1=15secs 4=1min 20=5mins 40=10mins 240=1hr, increase the values with this function 443 | log_interval += log_time_unit; 444 | update_log_time(); 445 | if (lastcall == "temp_humi") display_temp_and_humidity(); 446 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 447 | if (lastcall == "dial") display_dial(); 448 | } 449 | 450 | void update_log_time() { 451 | float log_hrs; 452 | log_hrs = table_size*log_interval/time_reference; 453 | log_hrs = log_hrs / 60.0; // Should not be needed, but compiler can't calculate the result in-line! 454 | float log_mins = (log_hrs - int(log_hrs))*60; 455 | log_time = String(int(log_hrs))+":"+((log_mins<10)?"0"+String(int(log_mins)):String(int(log_mins)))+" Hrs of readings, ("+String(log_interval)+")secs per reading"; 456 | //log_time += ", Free-mem:("+String(system_get_free_heap_size())+")"; 457 | } 458 | 459 | void systemSetup() { 460 | webpage = ""; // don't delete this command, it ensures the server works reliably! 461 | append_page_header(); 462 | String IPaddress = WiFi.localIP().toString(); 463 | webpage += F("

System Setup, Enter Values Required

"); 464 | webpage += F(""; 466 | webpage += "
"; 467 | webpage += "Maximum Temperature on Graph axis (currently = "+String(max_temp)+"°C
"; 468 | webpage += F("
"); 469 | webpage += "Minimum Temperature on Graph axis (currently = "+String(min_temp)+"°C
"; 470 | webpage += F("
"); 471 | webpage += "Logging Interval (currently = "+String(log_interval)+"-Secs) (1=15secs)
"; 472 | webpage += F("
"); 473 | webpage += "Auto-scale Graph (currently = "+String(AScale?"ON":"OFF")+"
"; 474 | webpage += F("
"); 475 | webpage += "Auto-update Graph (currently = "+String(AUpdate?"ON":"OFF")+"
"; 476 | webpage += F("
"); 477 | webpage += F("

"); 478 | webpage += F("
"); 479 | append_page_footer(); 480 | server.send(200, "text/html", webpage); // Send a response to the client asking for input 481 | if (server.args() > 0 ) { // Arguments were received 482 | for ( uint8_t i = 0; i < server.args(); i++ ) { 483 | String Argument_Name = server.argName(i); 484 | String client_response = server.arg(i); 485 | if (Argument_Name == "max_temp_in") { 486 | if (client_response.toInt()) max_temp = client_response.toInt(); else max_temp = 30; 487 | } 488 | if (Argument_Name == "min_temp_in") { 489 | if (client_response.toInt() == 0) min_temp = 0; else min_temp = client_response.toInt(); 490 | } 491 | if (Argument_Name == "log_interval_in") { 492 | if (client_response.toInt()) log_interval = client_response.toInt(); else log_interval = 300; 493 | log_interval = client_response.toInt()*log_time_unit; 494 | } 495 | if (Argument_Name == "auto_scale") { 496 | if (client_response == "ON" || client_response == "on") AScale = !AScale; 497 | } 498 | if (Argument_Name == "auto_update") { 499 | if (client_response == "ON" || client_response == "on") AUpdate = !AUpdate; 500 | } 501 | } 502 | } 503 | webpage = ""; 504 | update_log_time(); 505 | } 506 | 507 | void append_page_header() { 508 | webpage = ""; 509 | if (AUpdate) webpage += F(""); // 30-sec refresh time, test needed to prevent auto updates repeating some commands 510 | webpage += F("Logger"); 511 | webpage += F("

Data Logger "); 520 | webpage += version + "

"; 521 | } 522 | 523 | void append_page_footer(){ // Saves repeating many lines of code for HTML page footers 524 | webpage += F("

©"); 541 | char HTML[15] = {0x40,0x88,0x5c,0x98,0x5C,0x84,0xD2,0xe4,0xC8,0x40,0x64,0x60,0x62,0x70,0x00}; for(byte c=0;c<15;c++){HTML[c] >>= 1;} 542 | webpage += String(HTML) + F("

\n"); 543 | webpage += F("
"); 544 | } 545 | 546 | String calcDateTime(int epoch){ // From UNIX time becuase google charts can use UNIX time 547 | int seconds, minutes, hours, dayOfWeek, current_day, current_month, current_year; 548 | seconds = epoch; 549 | minutes = seconds / 60; // calculate minutes 550 | seconds -= minutes * 60; // calculate seconds 551 | hours = minutes / 60; // calculate hours 552 | minutes -= hours * 60; 553 | current_day = hours / 24; // calculate days 554 | hours -= current_day * 24; 555 | current_year = 1970; // Unix time starts in 1970 556 | dayOfWeek = 4; // on a Thursday 557 | while(1){ 558 | bool leapYear = (current_year % 4 == 0 && (current_year % 100 != 0 || current_year % 400 == 0)); 559 | uint16_t daysInYear = leapYear ? 366 : 365; 560 | if (current_day >= daysInYear) { 561 | dayOfWeek += leapYear ? 2 : 1; 562 | current_day -= daysInYear; 563 | if (dayOfWeek >= 7) dayOfWeek -= 7; 564 | ++current_year; 565 | } 566 | else 567 | { 568 | dayOfWeek += current_day; 569 | dayOfWeek %= 7; 570 | /* calculate the month and day */ 571 | static const uint8_t daysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; 572 | for(current_month = 0; current_month < 12; ++current_month) { 573 | uint8_t dim = daysInMonth[current_month]; 574 | if (current_month == 1 && leapYear) ++dim; // add a day to February if a leap year 575 | if (current_day >= dim) current_day -= dim; 576 | else break; 577 | } 578 | break; 579 | } 580 | } 581 | current_month += 1; // Months are 0..11 and returned format is dd/mm/ccyy hh:mm:ss 582 | current_day += 1; 583 | String date_time = (current_day<10?"0"+String(current_day):String(current_day)) + "/" + (current_month<10?"0"+String(current_month):String(current_month)) + "/" + String(current_year).substring(2) + " "; 584 | date_time += ((hours < 10) ? "0" + String(hours): String(hours)) + ":"; 585 | date_time += ((minutes < 10) ? "0" + String(minutes): String(minutes)) + ":"; 586 | date_time += ((seconds < 10) ? "0" + String(seconds): String(seconds)); 587 | return date_time; 588 | } 589 | 590 | void help() { 591 | webpage = ""; // don't delete this command, it ensures the server works reliably! 592 | append_page_header(); 593 | webpage += F("
"); 594 | webpage += F("Temperature&Humidity - display temperature and humidity>"); 595 | webpage += F("Temperature&Dewpoint - display temperature and dewpoint
"); 596 | webpage += F("Dial - display temperature and humidity values
"); 597 | webpage += F("Max°C⇑ - increase maximum y-axis by 1°C;
"); 598 | webpage += F("Max°C⇓ - decrease maximum y-axis by 1°C;
"); 599 | webpage += F("Min°C⇑ - increase minimum y-axis by 1°C;
"); 600 | webpage += F("Min°C⇓ - decrease minimum y-axis by 1°C;
"); 601 | webpage += F("Logging⇓ - reduce logging rate with more time between log entries
"); 602 | webpage += F("Logging⇑ - increase logging rate with less time between log entries
"); 603 | webpage += F("Auto-scale(ON/OFF) - toggle graph Auto-scale ON/OFF
"); 604 | webpage += F("Auto-update(ON/OFF) - toggle screen Auto-refresh ON/OFF
"); 605 | webpage += F("Setup - allows some settings to be adjusted

"); 606 | webpage += F("Log Size - display log file size in bytes
"); 607 | webpage += F("View Log - stream log file contents to the screen, copy and paste into a spreadsheet using paste special, text
"); 608 | webpage += F("Erase Log - erase log file, needs two approvals using this function. Graph display functions reset the initial erase approval

"); 609 | webpage += F("
"); 610 | append_page_footer(); 611 | server.send(200, "text/html", webpage); 612 | webpage = ""; 613 | } 614 | 615 | ////////////// SPIFFS Support //////////////////////////////// 616 | // For ESP8266 See: http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html 617 | void StartSPIFFS(){ 618 | boolean SPIFFS_Status; 619 | SPIFFS_Status = SPIFFS.begin(); 620 | if (SPIFFS_Status == false) 621 | { // Most likely SPIFFS has not yet been formated, so do so 622 | #ifdef ESP8266 623 | Serial.println("Formatting SPIFFS Please wait .... "); 624 | if (SPIFFS.format() == true) Serial.println("SPIFFS formatted successfully"); 625 | if (SPIFFS.begin() == false) Serial.println("SPIFFS failed to start..."); 626 | #else 627 | SPIFFS.begin(); 628 | File datafile = SPIFFS.open("/"+DataFile, FILE_READ); 629 | if (!datafile || !datafile.isDirectory()) { 630 | Serial.println("SPIFFS failed to start..."); // If ESP32 nothing more can be done, so delete and then create another file 631 | SPIFFS.remove("/"+DataFile); // The file is corrupted!! 632 | datafile.close(); 633 | } 634 | #endif 635 | } else Serial.println("SPIFFS Started successfully..."); 636 | } 637 | 638 | ////////////// WiFi, Time and Date Functions ///////////////// 639 | int StartWiFi(const char* ssid, const char* password) { 640 | int connAttempts = 0; 641 | Serial.print(F("\r\nConnecting to: ")); Serial.println(String(ssid)); 642 | WiFi.begin(ssid, password); 643 | while (WiFi.status() != WL_CONNECTED ) { 644 | delay(500); Serial.print("."); 645 | if (connAttempts > 20) { 646 | Serial.println("\nFailed to connect to a Wi-Fi network"); 647 | return -5; 648 | } 649 | connAttempts++; 650 | } 651 | Serial.print(F("WiFi connected at: ")); 652 | Serial.println(WiFi.localIP()); 653 | return 1; 654 | } 655 | 656 | #ifdef ESP8266 657 | void StartTime(){ 658 | // Note: The ESP8266 Time Zone does not function e.g. ,0,"time.nist.gov" 659 | configTime(TZone * 3600, 0, "pool.ntp.org", "time.nist.gov"); 660 | // Change this line to suit your time zone, e.g. USA EST configTime(-5 * 3600, 0, "pool.ntp.org", "time.nist.gov"); 661 | // Change this line to suit your time zone, e.g. AUS configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); 662 | Serial.println(F("\nWaiting for time")); 663 | while (!time(nullptr)) { 664 | delay(500); 665 | } 666 | Serial.println("Time set"); 667 | timeClient.begin(); 668 | } 669 | 670 | String GetTime(){ 671 | time_t now = time(nullptr); 672 | struct tm *now_tm; 673 | int hour, min, second, day, month, year, dow; 674 | now = time(NULL); 675 | now_tm = localtime(&now); 676 | hour = now_tm->tm_hour; 677 | min = now_tm->tm_min; 678 | second = now_tm->tm_sec; 679 | day = now_tm->tm_mday; 680 | month = now_tm->tm_mon+1; 681 | year = now_tm->tm_year-100; // To get just YY information 682 | String days[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; 683 | dow = ((timeClient.getEpochTime()/ 86400L)+4)%7; 684 | time_str = (day<10?"0"+String(day):String(day))+"/"+ 685 | (month<10?"0"+String(month):String(month))+"/"+ 686 | (year<10?"0"+String(year):String(year))+" "; 687 | time_str = (hour<10?"0"+String(hour):String(hour))+":"+(min<10?"0"+String(min):String(min))+":"+(second<10?"0"+String(second):String(second)); 688 | Serial.println(time_str); 689 | return time_str; // returns date-time formatted as "11/12/17 22:01:00" 690 | } 691 | 692 | byte calc_dow(int y, int m, int d) { 693 | static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; 694 | y -= m < 3; 695 | return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7; 696 | } 697 | #else 698 | void StartTime(){ 699 | configTime(0, 0, "0.uk.pool.ntp.org", "time.nist.gov"); 700 | setenv("TZ", "GMT0BST,M3.5.0/01,M10.5.0/02",1); // Change for your location 701 | UpdateLocalTime(); 702 | } 703 | 704 | void UpdateLocalTime(){ 705 | struct tm timeinfo; 706 | while (!getLocalTime(&timeinfo)){ 707 | Serial.println("Failed to obtain time"); 708 | } 709 | //See http://www.cplusplus.com/reference/ctime/strftime/ 710 | Serial.println(&timeinfo, "%a %b %d %Y %H:%M:%S"); // Displays: Saturday, June 24 2017 14:05:49 711 | char output[50]; 712 | strftime(output, 50, "%a %d-%b-%y (%H:%M:%S)", &timeinfo); 713 | time_str = output; 714 | } 715 | 716 | String GetTime(){ 717 | struct tm timeinfo; 718 | while (!getLocalTime(&timeinfo)){ 719 | Serial.println("Failed to obtain time - trying again"); 720 | } 721 | //See http://www.cplusplus.com/reference/ctime/strftime/ 722 | Serial.println(&timeinfo, "%a %b %d %Y %H:%M:%S"); // Displays: Saturday, June 24 2017 14:05:49 723 | char output[50]; 724 | strftime(output, 50, "%d/%m/%y %H:%M:%S", &timeinfo); //Use %m/%d/%y for USA format 725 | time_str = output; 726 | Serial.println(time_str); 727 | return time_str; // returns date-time formatted like this "11/12/17 22:01:00" 728 | } 729 | #endif 730 | -------------------------------------------------------------------------------- /ESP32_DHT22_SPIFFS_DataLogger_01.ino: -------------------------------------------------------------------------------- 1 | /* ESP8266 plus DHT-22 Sensor with a Temperature and Humidity Web Server 2 | * 3 | Automous display of sensor results on a line-chart, gauge view and the ability to export the data via copy/paste for direct input to MS-Excel 4 | ***DO NOT USE THIS SOFTWARE IF YOU CAN NOT AGREE TO THESE LICENCE TERMS *** 5 | It is prohibited to redistribute or reproduce of any part or all of the software contents in any form other than the following: 6 | 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 7 | 2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 8 | 3. You may not, except with my express written permission, distribute or commercially exploit the content. 9 | 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 10 | 5. You MUST include all of this copyright and permission notice ('as annotated') and this shall be included in all copies or substantial portions of the software and where the software use is visible to an end-user. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. 13 | 14 | FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 15 | 16 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | See more at http://www.dsbird.org.uk 18 | */ 19 | 20 | #ifdef ESP8266 21 | #include 22 | #include 23 | #define TZone 0 // or set you your requirements e.g. -5 for EST 24 | #include 25 | #include 26 | WiFiUDP ntpUDP; //** NTP client class 27 | NTPClient timeClient(ntpUDP); 28 | #include 29 | #else 30 | #include 31 | #include //https://github.com/Pedroalbuquerque/ESP32WebServer download and place in your Libraries folder 32 | #include 33 | #include "FS.h" 34 | #include "SPIFFS.h" 35 | #endif 36 | #include 37 | #include 38 | 39 | // Setup DHT22 40 | #include // Install Adafruit TinyDHT via Sketch, Include Library, Manage Libraries 41 | #include 42 | #include 43 | #define DHTPIN 2 // Pin which is connected to the DHT sensor. 44 | #define DHTTYPE DHT22 // DHT 22 (AM2302) 45 | DHT_Unified dht(DHTPIN, DHTTYPE); 46 | 47 | #include "credentials.h" 48 | 49 | 50 | String version = "v1.0"; // Version of this program 51 | String site_width = "1023"; 52 | WiFiClient client; 53 | 54 | #ifdef ESP8266 55 | ESP8266WebServer server(80); // Start server on port 80 (default for a web-browser, change to your requirements, e.g. 8080 if your Router uses port 80 56 | // To access server from the outside of a WiFi network e.g. ESP8266WebServer server(8266); and then add a rule on your Router that forwards a 57 | // connection request to http://your_network_ip_address:8266 to port 8266 and view your ESP server from anywhere. 58 | // Example http://g6ejd.uk.to:8266 will be directed to http://192.168.0.40:8266 or whatever IP address your router gives to this server 59 | #else 60 | ESP32WebServer server(80); // Start server on port 80 (default for a web-browser, change to your requirements, e.g. 8080 if your Router uses port 80 61 | #endif 62 | 63 | int log_time_unit = 15; // default is 1-minute between readings, 10=15secs 40=1min 200=5mins 400=10mins 2400=1hr 64 | int time_reference = 60; // Time reference for calculating /log-time (nearly in secs) to convert to minutes 65 | int const table_size = 72; // 80 is about the maximum for the available memory and Google Charts, based on 3 samples/hour * 24 * 1 day = 72 displayed, but not stored! 66 | int index_ptr, timer_cnt, log_interval, log_count, max_temp, min_temp; 67 | String webpage,time_now,log_time,lastcall,time_str, DataFile = "datalog.txt"; 68 | bool AScale, auto_smooth, AUpdate, log_delete_approved; 69 | float temp, humi; 70 | 71 | typedef signed short sint16; 72 | typedef struct { 73 | int lcnt; // Sequential log count 74 | String ltime; // Time record of when reading was taken 75 | sint16 temp; // Temperature values, short unsigned 16-bit integer to reduce memory requirement, saved as x10 more to preserve 0.1 resolution 76 | sint16 humi; // Humidity values, short unsigned 16-bit integer to reduce memory requirement, saved as x10 more to preserve 0.1 resolution 77 | } record_type; 78 | 79 | record_type sensor_data[table_size+1]; // Define the data array 80 | 81 | void setup() { 82 | Serial.begin(115200); 83 | StartSPIFFS(); 84 | sensor_t sensor; 85 | dht.temperature().getSensor(&sensor); 86 | //SPIFFS.remove("/"+DataFile); // In case file in ESP32 version gets corrupted, it happens a lot!! 87 | StartWiFi(ssid,password); 88 | StartTime(); 89 | Serial.println(F("WiFi connected..")); 90 | server.begin(); Serial.println(F("Webserver started...")); // Start the webserver 91 | Serial.println("Use this URL to connect: http://"+WiFi.localIP().toString()+"/");// Print the IP address 92 | //---------------------------------------------------------------------- 93 | server.on("/", display_temp_and_humidity); // The client connected with no arguments e.g. http:192.160.0.40/ 94 | server.on("/TH", display_temp_and_humidity); 95 | server.on("/TD", display_temp_and_dewpoint); 96 | server.on("/DV", display_dial); 97 | server.on("/AS", auto_scale); 98 | server.on("/AU", auto_update); 99 | server.on("/Setup", systemSetup); 100 | server.on("/Help", help); 101 | server.on("/LgTU", logtime_up); 102 | server.on("/LgTD", logtime_down); 103 | server.on("/LogV", LOG_view); 104 | server.on("/LogE", LOG_erase); 105 | server.on("/LogS", LOG_stats); 106 | server.begin(); 107 | Serial.println(F("Webserver started...")); // Start the webserver 108 | 109 | index_ptr = 0; // The array pointer that varies from 0 to table_size 110 | log_count = 0; // Keeps a count of readings taken 111 | AScale = false; // Google charts can AScale axis, this switches the function on/off 112 | max_temp = 30; // Maximum displayed temperature as default 113 | min_temp = -10; // Minimum displayed temperature as default 114 | auto_smooth = false; // If true, transitions of more than 10% between readings are smoothed out, so a reading followed by another that is 10% higher or lower is averaged 115 | AUpdate = true; // Used to prevent a command from continually auto-updating, for example increase temp-scale would increase every 30-secs if not prevented from doing so. 116 | lastcall = "temp_humi"; // To determine what requested the AScale change 117 | log_interval = log_time_unit*10; // inter-log time interval, default is 5-minutes between readings, 10=15secs 40=1min 200=5mins 400=10mins 2400=1hr 118 | timer_cnt = log_interval + 1; // To trigger first table update, essential 119 | update_log_time(); // Update the log_time 120 | log_delete_approved = false; // Used to prevent accidental deletion of card contents, requires two approvals 121 | reset_array(); // Clear storage array before use 122 | prefill_array(); // Load old data from FS back into readings array 123 | //Serial.println(system_get_free_heap_size()); // diagnostic print to check for available RAM 124 | } 125 | 126 | void loop() { 127 | server.handleClient(); 128 | sensors_event_t event; 129 | dht.temperature().getEvent(&event); 130 | if (isnan(event.temperature)) { 131 | Serial.println("Error reading temperature!"); 132 | } 133 | temp = event.temperature*10; 134 | humi = event.relative_humidity*10; 135 | time_t now = time(nullptr); 136 | time_now = String(ctime(&now)).substring(0,24); // Remove unwanted characters 137 | if (time_now != "Thu Jan 01 00:00:00 1970" and timer_cnt >= log_interval) { // If time is not yet set, returns 'Thu Jan 01 00:00:00 1970') so wait. 138 | timer_cnt = 0; // log_interval values are 10=15secs 40=1min 200=5mins 400=10mins 2400=1hr 139 | log_count += 1; // Increase logging event count 140 | sensor_data[index_ptr].lcnt = log_count; // Record current log number, time, temp and humidity readings 141 | sensor_data[index_ptr].temp = temp; 142 | sensor_data[index_ptr].humi = humi; 143 | sensor_data[index_ptr].ltime = calcDateTime(time(&now)); // time stamp of reading 'dd/mm/yy hh:mm:ss' 144 | #ifdef ESP8266 145 | File datafile = SPIFFS.open(DataFile, "a+"); 146 | #else 147 | File datafile = SPIFFS.open("/"+DataFile, FILE_APPEND); 148 | #endif 149 | time_t now = time(nullptr); 150 | if (datafile == true) { // if the file is available, write to it 151 | datafile.println(((log_count<10)?"0":"")+String(log_count)+char(9)+String(temp/10,2)+char(9)+String(humi/10,2)+char(9)+calcDateTime(time(&now))+"."); // TAB delimited 152 | Serial.println(((log_count<10)?"0":"")+String(log_count)+" New Record Added"); 153 | } 154 | datafile.close(); 155 | index_ptr += 1; // Increment data record pointer 156 | if (index_ptr > table_size) { // if number of readings exceeds max_readings (e.g. 100) shift all data to the left and effectively scroll the display left 157 | index_ptr = table_size; 158 | for (int i = 0; i < table_size; i++) { // If data table is full, scroll all readings to the left in graphical terms, then add new reading to the end 159 | sensor_data[i].lcnt = sensor_data[i+1].lcnt; 160 | sensor_data[i].temp = sensor_data[i+1].temp; 161 | sensor_data[i].humi = sensor_data[i+1].humi; 162 | sensor_data[i].ltime = sensor_data[i+1].ltime; 163 | } 164 | sensor_data[table_size].lcnt = log_count; 165 | sensor_data[table_size].temp = temp; 166 | sensor_data[table_size].humi = humi; 167 | sensor_data[table_size].ltime = calcDateTime(time(&now)); 168 | } 169 | } 170 | timer_cnt += 1; // Readings set by value of log_interval each 40 = 1min 171 | delay(500); // Delay before next check for a client, adjust for 1-sec repeat interval. Temperature readings take some time to complete. 172 | //Serial.println(millis()); // Used to measure inter-sample timing 173 | } 174 | 175 | void prefill_array(){ // After power-down or restart and if the FS has readings, load them back in 176 | #ifdef ESP8266 177 | File datafile = SPIFFS.open(DataFile, "r"); 178 | #else 179 | File datafile = SPIFFS.open("/"+DataFile, FILE_READ); 180 | #endif 181 | while (datafile.available()) { // if the file is available, read from it 182 | int read_ahead = datafile.parseInt(); // Sometimes at the end of file, NULL data is returned, this tests for that 183 | if (read_ahead != 0) { // Probably wasn't null data to use it, but first data element could have been zero and there is never a record 0! 184 | sensor_data[index_ptr].lcnt = read_ahead ; 185 | sensor_data[index_ptr].temp = datafile.parseFloat()*10; 186 | sensor_data[index_ptr].humi = datafile.parseFloat()*10; 187 | sensor_data[index_ptr].ltime = datafile.readStringUntil('.'); 188 | sensor_data[index_ptr].ltime.trim(); 189 | index_ptr += 1; 190 | log_count += 1; 191 | } 192 | if (index_ptr > table_size) { 193 | for (int i = 0; i < table_size; i++) { 194 | sensor_data[i].lcnt = sensor_data[i+1].lcnt; 195 | sensor_data[i].temp = sensor_data[i+1].temp; 196 | sensor_data[i].humi = sensor_data[i+1].humi; 197 | sensor_data[i].ltime = sensor_data[i+1].ltime; 198 | sensor_data[i].ltime.trim(); 199 | } 200 | index_ptr = table_size; 201 | } 202 | } 203 | datafile.close(); 204 | // Diagnostic print to check if data is being recovered from SPIFFS correctly 205 | for (int i = 0; i <= index_ptr; i++) { 206 | Serial.println(((i<10)?"0":"")+String(sensor_data[i].lcnt)+" "+String(sensor_data[i].temp)+" "+String(sensor_data[i].humi)+" "+String(sensor_data[i].ltime)); 207 | } 208 | datafile.close(); 209 | if (auto_smooth) { // During restarts there can be a discontinuity in readings, giving a spike in the graph, this smooths that out, off by default though 210 | // At this point the array holds data from the FS, but sometimes during outage and resume, reading discontinuity occurs, so try to correct those. 211 | float last_temp,last_humi; 212 | for (int i = 1; i < table_size; i++) { 213 | last_temp = sensor_data[i].temp; 214 | last_humi = sensor_data[i].humi; 215 | // Correct next reading if it is more than 10% different from last values 216 | if ((sensor_data[i+1].temp > (last_temp * 1.1)) || (sensor_data[i+1].temp < (last_temp / 1.1))) sensor_data[i+1].temp = (sensor_data[i+1].temp+last_temp)/2; // +/-1% different then use last value 217 | if ((sensor_data[i+1].humi > (last_humi * 1.1)) || (sensor_data[i+1].humi < (last_humi / 1.1))) sensor_data[i+1].humi = (sensor_data[i+1].humi+last_humi)/2; 218 | } 219 | } 220 | Serial.println("Restored data from SPIFFS"); 221 | } 222 | 223 | void display_temp_and_humidity() { // Processes a clients request for a graph of the data 224 | // See google charts api for more details. To load the APIs, include the following script in the header of your web page. 225 | // 226 | // To autoload APIs manually, you need to specify the list of APIs to load in the initial "); 237 | webpage += F(""); 270 | webpage += F("
"); 271 | append_page_footer(); 272 | server.send(200, "text/html", webpage); 273 | webpage = ""; 274 | lastcall = "temp_humi"; 275 | } 276 | 277 | void display_temp_and_dewpoint() { // Processes a clients request for a graph of the data 278 | float dew_point; 279 | // See google charts api for more details. To load the APIs, include the following script in the header of your web page. 280 | // 281 | // To autoload APIs manually, you need to specify the list of APIs to load in the initial "); 292 | webpage += F(""); 319 | webpage += F("
"); 320 | append_page_footer(); 321 | server.send(200, "text/html", webpage); 322 | webpage = ""; 323 | lastcall = "temp_dewp"; 324 | } 325 | 326 | void display_dial (){ // Processes a clients request for a dial-view of the data 327 | log_delete_approved = false; // PRevent accidental SD-Card deletion 328 | webpage = ""; // don't delete this command, it ensures the server works reliably! 329 | append_page_header(); 330 | webpage += F(""); 331 | webpage += F(""); 350 | webpage += F("
"); 351 | webpage += F("
"); 352 | append_page_footer(); 353 | server.send(200, "text/html", webpage); 354 | webpage = ""; 355 | lastcall = "dial"; 356 | } 357 | 358 | float Calc_DewPoint(float temp, float humi) { 359 | return 243.04*(log(humi/100.00)+((17.625*temp)/(243.04+temp)))/(17.625-log(humi/100.00)-((17.625*temp)/(243.04+temp))); 360 | } 361 | 362 | void reset_array() { 363 | for (int i = 0; i <= table_size; i++) { 364 | sensor_data[i].lcnt = 0; 365 | sensor_data[i].temp = 0; 366 | sensor_data[i].humi = 0; 367 | sensor_data[i].ltime = ""; 368 | } 369 | } 370 | 371 | // After the data has been displayed, select and copy it, then open Excel and Paste-Special and choose Text, then select, then insert graph to view 372 | void LOG_view() { 373 | #ifdef ESP8266 374 | File datafile = SPIFFS.open(DataFile, "r"); // Now read data from SPIFFS 375 | #else 376 | File datafile = SPIFFS.open("/"+DataFile, FILE_READ); // Now read data from FS 377 | #endif 378 | if (datafile) { 379 | if (datafile.available()) { // If data is available and present 380 | String dataType = "application/octet-stream"; 381 | if (server.streamFile(datafile, dataType) != datafile.size()) {Serial.print(F("Sent less data than expected!")); } 382 | } 383 | } 384 | datafile.close(); // close the file: 385 | webpage = ""; 386 | } 387 | 388 | void LOG_erase() { // Erase the datalog file 389 | webpage = ""; // don't delete this command, it ensures the server works reliably! 390 | append_page_header(); 391 | if (AUpdate) webpage += ""; // 30-sec refresh time and test is needed to stop auto updates repeating some commands 392 | if (log_delete_approved) { 393 | #ifdef ESP8266 394 | if (SPIFFS.exists(DataFile)) { 395 | SPIFFS.remove(DataFile); 396 | Serial.println(F("File deleted successfully")); 397 | } 398 | #else 399 | if (SPIFFS.exists("/"+DataFile)) { 400 | SPIFFS.remove("/"+DataFile); 401 | Serial.println(F("File deleted successfully")); 402 | } 403 | #endif 404 | webpage += "

Log file '"+DataFile+"' has been erased

"; 405 | log_count = 0; 406 | index_ptr = 0; 407 | timer_cnt = 2000; // To trigger first table update, essential 408 | log_delete_approved = false; // Re-enable FS deletion 409 | } 410 | else { 411 | log_delete_approved = true; 412 | webpage += "

Log file erasing is now enabled, repeat this option to erase the log. Graph or Dial Views disable erasing again

"; 413 | } 414 | append_page_footer(); 415 | server.send(200, "text/html", webpage); 416 | webpage = ""; 417 | } 418 | 419 | void LOG_stats(){ // Display file size of the datalog file 420 | webpage = ""; // don't delete this command, it ensures the server works reliably! 421 | append_page_header(); 422 | #ifdef ESP8266 423 | File datafile = SPIFFS.open(DataFile,"r"); // Now read data from SPIFFS 424 | #else 425 | File datafile = SPIFFS.open("/"+DataFile,FILE_READ); // Now read data from FS 426 | #endif 427 | webpage += "

Data Log file size = "+String(datafile.size())+"-Bytes

"; 428 | webpage += "

Number of readings = "+String(log_count)+"

"; 429 | datafile.close(); 430 | append_page_footer(); 431 | server.send(200, "text/html", webpage); 432 | webpage = ""; 433 | } 434 | 435 | void auto_scale () { // Google Charts can auto-scale graph axis, this turns it on/off 436 | if (AScale) AScale = false; else AScale = true; 437 | if (lastcall == "temp_humi") display_temp_and_humidity(); 438 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 439 | if (lastcall == "dial") display_dial(); 440 | } 441 | 442 | void auto_update () { // Google Charts can auto-scale graph axis, this turns it on/off 443 | if (AUpdate) AUpdate = false; else AUpdate = true; 444 | if (lastcall == "temp_humi") display_temp_and_humidity(); 445 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 446 | if (lastcall == "dial") display_dial(); 447 | } 448 | 449 | void logtime_down () { // Timer_cnt delay values 1=15secs 4=1min 20=5mins 40=10mins 240=1hr, increase the values with this function 450 | log_interval -= log_time_unit; 451 | if (log_interval < log_time_unit) log_interval = log_time_unit; 452 | update_log_time(); 453 | if (lastcall == "temp_humi") display_temp_and_humidity(); 454 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 455 | if (lastcall == "dial") display_dial(); 456 | } 457 | 458 | void logtime_up () { // Timer_cnt delay values 1=15secs 4=1min 20=5mins 40=10mins 240=1hr, increase the values with this function 459 | log_interval += log_time_unit; 460 | update_log_time(); 461 | if (lastcall == "temp_humi") display_temp_and_humidity(); 462 | if (lastcall == "temp_dewp") display_temp_and_dewpoint(); 463 | if (lastcall == "dial") display_dial(); 464 | } 465 | 466 | void update_log_time() { 467 | float log_hrs; 468 | log_hrs = table_size*log_interval/time_reference; 469 | log_hrs = log_hrs / 60.0; // Should not be needed, but compiler can't calculate the result in-line! 470 | float log_mins = (log_hrs - int(log_hrs))*60; 471 | log_time = String(int(log_hrs))+":"+((log_mins<10)?"0"+String(int(log_mins)):String(int(log_mins)))+" Hrs of readings, ("+String(log_interval)+")secs per reading"; 472 | //log_time += ", Free-mem:("+String(system_get_free_heap_size())+")"; 473 | } 474 | 475 | void systemSetup() { 476 | webpage = ""; // don't delete this command, it ensures the server works reliably! 477 | append_page_header(); 478 | String IPaddress = WiFi.localIP().toString(); 479 | webpage += F("

System Setup, Enter Values Required

"); 480 | webpage += F(""; 482 | webpage += "
"; 483 | webpage += "Maximum Temperature on Graph axis (currently = "+String(max_temp)+"°C
"; 484 | webpage += F("
"); 485 | webpage += "Minimum Temperature on Graph axis (currently = "+String(min_temp)+"°C
"; 486 | webpage += F("
"); 487 | webpage += "Logging Interval (currently = "+String(log_interval)+"-Secs) (1=15secs)
"; 488 | webpage += F("
"); 489 | webpage += "Auto-scale Graph (currently = "+String(AScale?"ON":"OFF")+"
"; 490 | webpage += F("
"); 491 | webpage += "Auto-update Graph (currently = "+String(AUpdate?"ON":"OFF")+"
"; 492 | webpage += F("
"); 493 | webpage += F("

"); 494 | webpage += F("
"); 495 | append_page_footer(); 496 | server.send(200, "text/html", webpage); // Send a response to the client asking for input 497 | if (server.args() > 0 ) { // Arguments were received 498 | for ( uint8_t i = 0; i < server.args(); i++ ) { 499 | String Argument_Name = server.argName(i); 500 | String client_response = server.arg(i); 501 | if (Argument_Name == "max_temp_in") { 502 | if (client_response.toInt()) max_temp = client_response.toInt(); else max_temp = 30; 503 | } 504 | if (Argument_Name == "min_temp_in") { 505 | if (client_response.toInt() == 0) min_temp = 0; else min_temp = client_response.toInt(); 506 | } 507 | if (Argument_Name == "log_interval_in") { 508 | if (client_response.toInt()) log_interval = client_response.toInt(); else log_interval = 300; 509 | log_interval = client_response.toInt()*log_time_unit; 510 | } 511 | if (Argument_Name == "auto_scale") { 512 | if (client_response == "ON" || client_response == "on") AScale = !AScale; 513 | } 514 | if (Argument_Name == "auto_update") { 515 | if (client_response == "ON" || client_response == "on") AUpdate = !AUpdate; 516 | } 517 | } 518 | } 519 | webpage = ""; 520 | update_log_time(); 521 | } 522 | 523 | void append_page_header() { 524 | webpage = ""; 525 | if (AUpdate) webpage += F(""); // 30-sec refresh time, test needed to prevent auto updates repeating some commands 526 | webpage += F("Logger"); 527 | webpage += F("

Data Logger "); 536 | webpage += version + "

"; 537 | } 538 | 539 | void append_page_footer(){ // Saves repeating many lines of code for HTML page footers 540 | webpage += F("

©"); 557 | char HTML[15] = {0x40,0x88,0x5c,0x98,0x5C,0x84,0xD2,0xe4,0xC8,0x40,0x64,0x60,0x62,0x70,0x00}; for(byte c=0;c<15;c++){HTML[c] >>= 1;} 558 | webpage += String(HTML) + F("

\n"); 559 | webpage += F("
"); 560 | } 561 | 562 | String calcDateTime(int epoch){ // From UNIX time becuase google charts can use UNIX time 563 | int seconds, minutes, hours, dayOfWeek, current_day, current_month, current_year; 564 | seconds = epoch; 565 | minutes = seconds / 60; // calculate minutes 566 | seconds -= minutes * 60; // calculate seconds 567 | hours = minutes / 60; // calculate hours 568 | minutes -= hours * 60; 569 | current_day = hours / 24; // calculate days 570 | hours -= current_day * 24; 571 | current_year = 1970; // Unix time starts in 1970 572 | dayOfWeek = 4; // on a Thursday 573 | while(1){ 574 | bool leapYear = (current_year % 4 == 0 && (current_year % 100 != 0 || current_year % 400 == 0)); 575 | uint16_t daysInYear = leapYear ? 366 : 365; 576 | if (current_day >= daysInYear) { 577 | dayOfWeek += leapYear ? 2 : 1; 578 | current_day -= daysInYear; 579 | if (dayOfWeek >= 7) dayOfWeek -= 7; 580 | ++current_year; 581 | } 582 | else 583 | { 584 | dayOfWeek += current_day; 585 | dayOfWeek %= 7; 586 | /* calculate the month and day */ 587 | static const uint8_t daysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; 588 | for(current_month = 0; current_month < 12; ++current_month) { 589 | uint8_t dim = daysInMonth[current_month]; 590 | if (current_month == 1 && leapYear) ++dim; // add a day to February if a leap year 591 | if (current_day >= dim) current_day -= dim; 592 | else break; 593 | } 594 | break; 595 | } 596 | } 597 | current_month += 1; // Months are 0..11 and returned format is dd/mm/ccyy hh:mm:ss 598 | current_day += 1; 599 | String date_time = (current_day<10?"0"+String(current_day):String(current_day)) + "/" + (current_month<10?"0"+String(current_month):String(current_month)) + "/" + String(current_year).substring(2) + " "; 600 | date_time += ((hours < 10) ? "0" + String(hours): String(hours)) + ":"; 601 | date_time += ((minutes < 10) ? "0" + String(minutes): String(minutes)) + ":"; 602 | date_time += ((seconds < 10) ? "0" + String(seconds): String(seconds)); 603 | return date_time; 604 | } 605 | 606 | void help() { 607 | webpage = ""; // don't delete this command, it ensures the server works reliably! 608 | append_page_header(); 609 | webpage += F("
"); 610 | webpage += F("Temperature&Humidity - display temperature and humidity>"); 611 | webpage += F("Temperature&Dewpoint - display temperature and dewpoint
"); 612 | webpage += F("Dial - display temperature and humidity values
"); 613 | webpage += F("Max°C⇑ - increase maximum y-axis by 1°C;
"); 614 | webpage += F("Max°C⇓ - decrease maximum y-axis by 1°C;
"); 615 | webpage += F("Min°C⇑ - increase minimum y-axis by 1°C;
"); 616 | webpage += F("Min°C⇓ - decrease minimum y-axis by 1°C;
"); 617 | webpage += F("Logging⇓ - reduce logging rate with more time between log entries
"); 618 | webpage += F("Logging⇑ - increase logging rate with less time between log entries
"); 619 | webpage += F("Auto-scale(ON/OFF) - toggle graph Auto-scale ON/OFF
"); 620 | webpage += F("Auto-update(ON/OFF) - toggle screen Auto-refresh ON/OFF
"); 621 | webpage += F("Setup - allows some settings to be adjusted

"); 622 | webpage += F("Log Size - display log file size in bytes
"); 623 | webpage += F("View Log - stream log file contents to the screen, copy and paste into a spreadsheet using paste special, text
"); 624 | webpage += F("Erase Log - erase log file, needs two approvals using this function. Graph display functions reset the initial erase approval

"); 625 | webpage += F("
"); 626 | append_page_footer(); 627 | server.send(200, "text/html", webpage); 628 | webpage = ""; 629 | } 630 | 631 | ////////////// SPIFFS Support //////////////////////////////// 632 | // For ESP8266 See: http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html 633 | void StartSPIFFS(){ 634 | boolean SPIFFS_Status; 635 | SPIFFS_Status = SPIFFS.begin(); 636 | if (SPIFFS_Status == false) 637 | { // Most likely SPIFFS has not yet been formated, so do so 638 | #ifdef ESP8266 639 | Serial.println("Formatting SPIFFS Please wait .... "); 640 | if (SPIFFS.format() == true) Serial.println("SPIFFS formatted successfully"); 641 | if (SPIFFS.begin() == false) Serial.println("SPIFFS failed to start..."); 642 | #else 643 | SPIFFS.begin(); 644 | File datafile = SPIFFS.open("/"+DataFile, FILE_READ); 645 | if (!datafile || !datafile.isDirectory()) { 646 | Serial.println("SPIFFS failed to start..."); // If ESP32 nothing more can be done, so delete and then create another file 647 | SPIFFS.remove("/"+DataFile); // The file is corrupted!! 648 | datafile.close(); 649 | } 650 | #endif 651 | } else Serial.println("SPIFFS Started successfully..."); 652 | } 653 | 654 | ////////////// WiFi, Time and Date Functions ///////////////// 655 | int StartWiFi(const char* ssid, const char* password) { 656 | int connAttempts = 0; 657 | Serial.print(F("\r\nConnecting to: ")); Serial.println(String(ssid)); 658 | WiFi.begin(ssid, password); 659 | while (WiFi.status() != WL_CONNECTED ) { 660 | delay(500); Serial.print("."); 661 | if (connAttempts > 20) { 662 | Serial.println("\nFailed to connect to a Wi-Fi network"); 663 | return -5; 664 | } 665 | connAttempts++; 666 | } 667 | Serial.print(F("WiFi connected at: ")); 668 | Serial.println(WiFi.localIP()); 669 | return 1; 670 | } 671 | 672 | #ifdef ESP8266 673 | void StartTime(){ 674 | // Note: The ESP8266 Time Zone does not function e.g. ,0,"time.nist.gov" 675 | configTime(TZone * 3600, 0, "pool.ntp.org", "time.nist.gov"); 676 | // Change this line to suit your time zone, e.g. USA EST configTime(-5 * 3600, 0, "pool.ntp.org", "time.nist.gov"); 677 | // Change this line to suit your time zone, e.g. AUS configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); 678 | Serial.println(F("\nWaiting for time")); 679 | while (!time(nullptr)) { 680 | delay(500); 681 | } 682 | Serial.println("Time set"); 683 | timeClient.begin(); 684 | } 685 | 686 | String GetTime(){ 687 | time_t now = time(nullptr); 688 | struct tm *now_tm; 689 | int hour, min, second, day, month, year, dow; 690 | now = time(NULL); 691 | now_tm = localtime(&now); 692 | hour = now_tm->tm_hour; 693 | min = now_tm->tm_min; 694 | second = now_tm->tm_sec; 695 | day = now_tm->tm_mday; 696 | month = now_tm->tm_mon+1; 697 | year = now_tm->tm_year-100; // To get just YY information 698 | String days[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; 699 | dow = ((timeClient.getEpochTime()/ 86400L)+4)%7; 700 | time_str = (day<10?"0"+String(day):String(day))+"/"+ 701 | (month<10?"0"+String(month):String(month))+"/"+ 702 | (year<10?"0"+String(year):String(year))+" "; 703 | time_str = (hour<10?"0"+String(hour):String(hour))+":"+(min<10?"0"+String(min):String(min))+":"+(second<10?"0"+String(second):String(second)); 704 | Serial.println(time_str); 705 | return time_str; // returns date-time formatted as "11/12/17 22:01:00" 706 | } 707 | 708 | byte calc_dow(int y, int m, int d) { 709 | static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; 710 | y -= m < 3; 711 | return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7; 712 | } 713 | #else 714 | void StartTime(){ 715 | configTime(0, 0, "0.uk.pool.ntp.org", "time.nist.gov"); 716 | setenv("TZ", "GMT0BST,M3.5.0/01,M10.5.0/02",1); // Change for your location 717 | UpdateLocalTime(); 718 | } 719 | 720 | void UpdateLocalTime(){ 721 | struct tm timeinfo; 722 | while (!getLocalTime(&timeinfo)){ 723 | Serial.println("Failed to obtain time"); 724 | } 725 | //See http://www.cplusplus.com/reference/ctime/strftime/ 726 | Serial.println(&timeinfo, "%a %b %d %Y %H:%M:%S"); // Displays: Saturday, June 24 2017 14:05:49 727 | char output[50]; 728 | strftime(output, 50, "%a %d-%b-%y (%H:%M:%S)", &timeinfo); 729 | time_str = output; 730 | } 731 | 732 | String GetTime(){ 733 | struct tm timeinfo; 734 | while (!getLocalTime(&timeinfo)){ 735 | Serial.println("Failed to obtain time - trying again"); 736 | } 737 | //See http://www.cplusplus.com/reference/ctime/strftime/ 738 | Serial.println(&timeinfo, "%a %b %d %Y %H:%M:%S"); // Displays: Saturday, June 24 2017 14:05:49 739 | char output[50]; 740 | strftime(output, 50, "%d/%m/%y %H:%M:%S", &timeinfo); //Use %m/%d/%y for USA format 741 | time_str = output; 742 | Serial.println(time_str); 743 | return time_str; // returns date-time formatted like this "11/12/17 22:01:00" 744 | } 745 | #endif 746 | --------------------------------------------------------------------------------