Web based temperature logger with ESP8266,DS18B20,DS3231 and LittleFS

This is a demo project that was written a few years ago when we hosted the family (US) Thanksgiving gathering. With all the extra food we were using the garage as a refrigerator for storage. Hence the name Garagerator. This code was initially written to insure the temperature was food safe.

The code is a stand alone access point, so needs no connection to a LAN. On the board options for Flash Size set the file system size - I use 3MB. After uploading the code…

  1. Connect to the WiFi network Garagerator the password is cooltemp You may get complaints that there is no internet connection, take the option to continue anyway.

  2. Start a browser and use the URL www.garagerator.com

You should then see the root page with some system info and a button to download data. It has been tested with i-Phone, Android phone and PC running Windows or Linux.

See the obvious lines in the code if you want to change any of these.

Other web pages

URL/download got directly to the download page

URL/delete delete files – this requires a jumper from D7 to ground

URL/timeset set the DS3231 from the browser time, this can take few seconds so the displayed times might be slightly different

URL/config change the default sample rate

/* ***********************************************************************************************
 * 
 * This sketch is an ESP8266 test of a WiFi interface 
 * 
 * It reads and saves data from A Dallas DS18B20 temperature sensor
 *
 * A DNS server is started to handle the www.garagerator.com URL
 * 
 * A WiFi server is started to provide a user interface via a web browser
 * URL handlers are provided to
 *    Display the current time and temperature via HTML page
 *    Display files for download via HTML page
 *    Download files
 *    Delete files via HTML page (when pin D7 is grounded)
 *    Set time via HTML page  
 *   
 * For each timer interrupt
 *   1) Retrieve the date and time from the rtc
 *   2) Retrieve the temperature from the rtc
 *   3) Retrieve the temperature from the DS18B20
 *   4) Write the retrieved date/time and temperature
 *       sensor data to the flash file system for later retrieval 
 *
 *
 * NOTES:
 *    I am not a web programmer. The web interface is a WEB101 level. If you think you can
 *    do better you may be right.
 *    
 *    Delete file requires pin D7 be grounded. This is to require physical access for deletes.  
 *
 * WARNING:
 *   This is proof of concept code - not for prime time. 
 *     Error handling is sparse 
 *     "Best practices" are not necessarily followed 
*********************************************************************************************** */

// Include the libraries we need

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>

#include <DNSServer.h>
const byte DNS_PORT = 53;
IPAddress apIP(192, 168, 6, 1);
DNSServer dnsServer;

ESP8266WebServer server(80);

#include <LittleFS.h>

#include <user_interface.h>

//  DS18B20 Temperature Sensor
#include <OneWire.h>
#include <DallasTemperature.h>
OneWire  onewire(2);  // on pin 2 (a 4.7K resistor to vcc is necessary)
// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&onewire);

// Real time clock
#include <Wire.h> // must be included here so that Arduino library object file references work
// from "Rtc by Makuna" in library manager (the other gazillion DS3231 interfaces may not work)
#include <RtcDS3231.h>    
RtcDS3231<TwoWire> Rtc(Wire);

#include <Streaming.h>  // from "Streaming" in library manager

// names on board vs actual I/O pin
// D0   = 16;  // blue LED attached
// D1   = 5;   // I2C scl
// D2   = 4;   // I2C sda
// D3   = 0    // flash button
// D4   = 2;   // gpio txd1
// D5   = 14;  // SPI sclk
// D6   = 12;  // SPI miso
// D7   = 13;  // SPI mosi
// D8   = 15;  // SPI cs
// D9   = 3;   // rdx0
// D10  = 1;   // txd0 
// other names on board - don't use if there is
//  4 bit flash on board

const uint8_t interruptPin = D5;

const char* ap_ssid     = "Garagerator"; // AP ssid
const char* ap_password = "cooltemp"; // AP Password
const char* url = "www.garagerator.com"; // url for browser

const char fwVersion[] ="1.0";     // firmware version

// global variables

/* interval between readings
 *  input the interval in minutes (1-1440) so the interface 
 *  doesn't have to deal with houres and minutes - particularly
 *  when readings interval is an hour or less 
 */
uint16_t minuteInterval = 15; // number of minutes between readings
uint16_t minuteMatch;        // minute count to match
uint8_t minutePart;          // minute part of interval
uint8_t hourPart;            // hour part of interval

String logEntry = "";

volatile boolean timerInterupt = true;   // force initial processing 
// volatile because it is shared with the interrupt routine

String serialNumber;
FSInfo fs_info; 

// Interrupt service routine for external interrupt 
void IRAM_ATTR rtcISR()  // make sure it is in code ram
{
    // Keep this as short as possible. 
   timerInterupt = true;  // we are only interested in the high to low change
}

void setNextSampleTime(){  // set alarm for next sample time
   // set next timer interrupt
   RtcDateTime present;
   present = Rtc.GetDateTime(); //get the current date-time 
  // round next time up to a multiple of match interval
  minuteMatch = (((present.Hour()*60+present.Minute()) / minuteInterval) +1 )*minuteInterval;
  // split interval into minutes and hours
  minutePart = minuteMatch%60;
  hourPart = (minuteMatch/60)%24; // keep within a day            
  // set next data collection interrupt time 
  DS3231AlarmOne alarm1(
      0,           // day - don't care
      hourPart,
      minutePart,
      0,          // seconds
  DS3231AlarmOneControl_HoursMinutesSecondsMatch);  // interrupt at (h,m,s)
  Rtc.SetAlarmOne(alarm1);
}

time_t myTimeCallback() {
    return Rtc.GetDateTime().Epoch32Time();  // timestamp
}

String dtToString(const RtcDateTime& dt){
  char datestring[26];

  snprintf(datestring, 
      26,
      "%04u-%02u-%02u %02u:%02u:%02u",
      dt.Year(),
      dt.Month(),
      dt.Day(),
      dt.Hour(),
      dt.Minute(),
      dt.Second() );
   return String(datestring);
}


float gettemp() {
  // call sensors.requestTemperatures() to issue a global temperature 
  // request to all devices on the bus
  //  Serial.print("Requesting temperatures...");
  sensors.requestTemperatures(); // Send the command to get temperatures
  //  Serial.println("DONE");
  // After we got the temperatures, we can print them here.
  // We use the function ByIndex, and as an example get the temperature from the first sensor only.
  return sensors.getTempCByIndex(0); 
}

void handleRoot() {
  float celsius, fahrenheit;
  
  // get file system information
  LittleFS.info(fs_info);  
         
  // get temperature 
  celsius = gettemp(); 
//  Serial.println(celsius); 
  fahrenheit = celsius * 1.8 + 32;  
  
  // Get the current date-time and convert it to a String
  String timeString = dtToString(Rtc.GetDateTime());
  
    // build html page
    String htmlPage =
        String("<!DOCTYPE HTML>\n")
        + "<html>\n"  
        + "<head>\n"
        + "<meta name='viewport' content='width=device-width, initial-scale=1.0'>\n"         
        +    "<title>Garagerator</title>\n"
        +"<style>\n"
        +"a:link, a:visited {\n"
        +     "background-color: #044336;\n"
        +     "color: white;\n"
        +     "padding: 10px 20px;\n"
        +     "text-align: center;\n"
        +     "text-decoration: none;\n"
        +     "font-size: 50px;\n" 
        +     "display: inline-block;\n"
        +  "}\n"
        +"a:hover, a:active {\n"
        +      "background-color: red;\n"
        +  "}\n"
        +"</style>\n"
        +"</head>\n"
        + "<body>\n\n"+
         "<h1 style='text-align: center;'>Garagerator ESP8266 Server With DS18B20 Temperature Sensor</h1>\n"+
         "<p style='text-align: left;'><span style='font-size: x-large;'><strong>Time  </strong>" +
         timeString +
         "</span>"+
         "</p>\n"
          "<p style='text-align: left;'><span style='font-size: x-large;'><strong>Last log entry  </strong>" +
         logEntry +
         "</span>"+
         "</p>\n"
        "<p style='text-align: left;'><span style='font-size: x-large;'><strong>Software version  </strong>" +
         String(fwVersion) +
         "</span>"+
         "</p>\n"
         "<p style='text-align: left;'><span style='font-size: x-large;'><strong>File System Size  </strong>" +
         String(fs_info.totalBytes) + "     "
         "</span>"+
         " <span style='font-size: x-large;'><strong>File System Used  </strong>" +
         String(fs_info.usedBytes) +
         "</span>"+
         "</p>\n"
         "<p style='text-align: left;'><span style='font-size: x-large;'><strong>Update interval in minutes  </strong>" +
         String(minuteInterval) +
         "</span>"+
         "</p>\n"
         "<p style='text-align: left;'><span style='color: #0000ff;'><strong style='font-size: x-large;'>Temperature = " +
         String(celsius) +
         "<sup>o</sup>C, " +
         String(fahrenheit) +
         "<sup>o</sup>F</strong></span></p>\n" +
         "<br><br>\n"+
         "<a href=\"/download\">Download file</a>" +
         "<br><br>\n"+
        "</body>\n"+
        "</html>\n";
 //  Serial.println(page);       
   server.send(200, "text/html",htmlPage);
}

void handleLoadFile() {
  Serial.println("In load file");
      String htmlPage =
        String("<!DOCTYPE HTML>\n") 
        + "<html>\n"
        + "<head>\n"
        +" <meta name='viewport' content='width=device-width, initial-scale=1.0'>"        
        + "<style>\n"
        + "ul {\n"
        +     " list-style-type: none;\n"
        +     " margin: 0;\n"
        +     " padding: 0;\n"
        +  "}\n"
        + "li a {\n"
        +    "padding: 30px 16px\n"
        +    "height: 200px;"
         + "}\n"
        + "</style>\n"
        + "</head>\n"
        + "<body>\n\n"
        + "<h1 style='text-align: center;'>Garagerator ESP8266 Server With DS18B20 Temperature Sensor</h1>\n"
        + "<p> To download right click or long tap on the file name, then use the download option for your system/browser </p> \n"
        + "<ul>\n";

   // build file list
   {
    Dir dir = LittleFS.openDir("");
    while (dir.next()) {
      String fileName = dir.fileName();
      size_t fileSize = dir.fileSize();
      htmlPage = htmlPage 
       + "<li><a href='"
       + fileName
       +"'>"
       + fileName
       + "  "
       + fileSize
       + "</a></li><br>\n";
     }
    htmlPage = htmlPage + "</ul>\n</body>\n</html>\n";
  }
  server.send(200, "text/html", htmlPage);
}

void handleDeleteFile() {
    
//  Serial.println("In delete file ");
  if(!digitalRead(D7)){    // delete enabled
    if(server.args() > 0){
      for (int i = 0;i<server.args();i++){
        Serial.print("deleting ");
        Serial.println(server.arg(i));
        LittleFS.remove(server.arg(i));
      }
    }
  } else {
     Serial.println("delete disabled");
  }
      String htmlPage =
        String("<!DOCTYPE HTML>\n") 
        + "<html>\n"
        + "<head>\n"
        + "<meta name='viewport' content='width=device-width, initial-scale=1.0'>\n"        
        + "<style>\n"
        + "ul {\n"
        +     " list-style-type: none;\n"
        +     " margin: 0;\n"
        +     " padding: 0;\n"
        +  "}\n"
        + "li a {\n"
        +    "padding: 30px 16px\n"
        +    "height: 200px;"
         + "}\n"
        + "</style>\n"
        + "</head>\n"
        + "<body>\n\n"
        +"<h1 style='text-align: center;'>Garagerator ESP8266 Server With DS18B20 Temperature Sensor</h1>\n"
        + "<p> Check on the file(s) to delete then press Delete Files </p> \n"
        + "<form>\n";

   // build file list
   {
    int argNum=0;
    Dir dir = LittleFS.openDir("");
    while (dir.next()) {
      String fileName = dir.fileName();
      size_t fileSize = dir.fileSize();
      htmlPage = htmlPage 
       + "<input type='checkbox' id='"+"arg"+argNum+"' name='"+"arg"+argNum+"' value='"+fileName+"'>"
       + " <label for='"+"arg"+argNum+"'>" + fileName+" size "+fileSize + "</label>"
       + "<br><br>\n";
       argNum++;
     }
    htmlPage = htmlPage + "<br><br> <input type='submit' value='Delete Files'>\n"
      + "</form>\n</body>\n</html>\n";
  }
  server.send(200, "text/html", htmlPage);
}

void handleSetTime(){
  RtcDateTime timeToSet;
  Serial.print("In setTime ");
  Serial.println(server.args());
  if (server.hasArg("ts")){
    Serial.println(server.arg("ts"));
    uint64_t ts; 
    ts = (server.arg("ts")).toInt();
//   Serial.println(ts);
    //Update the rtc
    timeToSet.InitWithEpoch64Time(ts); 
    Serial.println(timeToSet);
    Rtc.SetDateTime(timeToSet);   // set rtc from input timestamp 
    setNextSampleTime();  // set alarm for next sample time
  }
   String htmlPage =
    String("<!DOCTYPE HTML>\n") 
    + "<html>\n"
    + "<head>\n"
    +" <meta name='viewport' content='width=device-width, initial-scale=1.0'>" 
    + "</head>\n"
    + "<body>\n\n"
    + "<h1 style='text-align: center;'>Garagerator ESP8266 Server With DS18B20 Temperature Sensor</h1>\n"   
    + "  <noscript>\n"
    + "       <p style='color: red; font-size: 30px;'> This page requires Javascript for the Browser to get the system time. </p>\n"
    + "  </noscript>\n"
    + "<p> Browser clock </p> \n"
    + "<p id='BrTime'></p>\n"
    + "<script>\n" 
    +   "document.getElementById('BrTime').innerHTML = new Date();\n"
    +   "function getTS() {\n"
    +     "var d = new Date();\n"
    +     "var offset = (-60000 * d.getTimezoneOffset());\n"
    +     "var tstamp =  ((d.getTime()+offset)/1000).toFixed(0) ;\n"
    +     "document.getElementById('time').value = tstamp;\n"
    +     "document.getElementById('frm1').submit();\n"
    +  "}\n"
    + "</script>\n"
    + "<p> Monitor clock </p> \n"
    + "<p>";
    
      // Get the current date-time and convert it to a String
  String timeString = dtToString(Rtc.GetDateTime());
  
    htmlPage = htmlPage + timeString
    + "<form id='frm1' >\n"
    +  "<input id='time' type='text' name='ts' value=''>\n"
    +  "<button type=button style='font-size:40px' onclick='getTS()'>Set clock</button>\n"
    +  "</form>\n" 
    + "</body>\n</html>\n";
    server.send(200, "text/html", String(htmlPage));  
     
  // get date-time just set and convert to a timestamp   
//  uint32_t rts = Rtc.GetDateTime().Epoch32Time();   
  //server.send(200, "text/plain", String(rts));               
}

void handleConfig() {
//  Serial.println("In config ");
  if (server.hasArg("rate")){
    minuteInterval = server.arg("rate").toInt();
    setNextSampleTime();  // set alarm for next sample time
  }
      String htmlPage =
        String("<!DOCTYPE HTML>\n") 
        + "<html>\n"
        + "<head>\n"
        + "<meta name='viewport' content='width=device-width, initial-scale=1.0'>"           
        + "</head>\n"
        + "<body>\n\n"
        + "<h1 style='text-align: center;'>Garagerator ESP8266 Server With DS18B20 Temperature Sensor</h1>\n"
        + "<form style='background-color: Lime'>\n"
        +    "Current update rate in minutes <span style='background-color:white'> "
        +    String(minuteInterval)
        +    "</span><br>\n" 
        +    "Set new rate in minutes (between 1 and 1440):\n"
        +    " <input type='number' name='rate' min='1' max='1440' required>\n"
        +    "<br><br>\n"
        +    "<input type='submit' value='Update rate command'>\n"
        +    "<br><br>\n"
        + "</form>\n" 
        +"</body>\n</html>\n";
   server.send(200, "text/html", htmlPage);
}

void handleNotFound() {
  char fileName[32];
  uint8_t nameLen;
  // assume an unhandled url is probably a file link
  if (server.uri().endsWith(".CSV")) {  // data file
     nameLen = server.uri().length(); 
     server.uri().substring(1).toCharArray(fileName,(sizeof(fileName)-1)); // remove leading / and convert to char string
     fileName[nameLen] = 0;      // null terminate - note we removed the leading / so this is one past the name
     File dataFile = LittleFS.open(fileName, "r");
     server.streamFile(dataFile,"text/plain");      
     dataFile.close(); 
  } else {
    // no clue what they sent
    String message = "File Not Found\n\n";
    message += "URI: ";
    message += server.uri();
    message += "\nMethod: ";
    message += (server.method() == HTTP_GET) ? "GET" : "POST";
    message += "\nArguments: ";
    message += server.args();
    message += "\n";
    for (uint8_t i = 0; i < server.args(); i++) {
      message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
    }
    server.send(404, "text/plain", message);
  }
}


void LogData(const RtcDateTime* logTime, const String logEntry) {
    /*
     *  Write data to file system     
     *  Determine file name from date/time
     *  If file exists add data to file
     *  Else
     *     Create file
     *     Add header line
     *     Add data
     *  
     * The data file names will be of the form datawk[week of year].csv
     * 
     * If you go to 2100 the leap year check needs to be updated.   
     * 
     */ 

    const String header = "date/time,RTC_T,DS1820_T,Serial_Number";
    const uint16_t daysSofar[12] = {0,31,59,90,120,151,181,212,243,273,304,334};
    uint16_t dowJan1; // day of week Jan 1 
    uint16_t dayOfYear;
    uint16_t weekOfYear;
    char cweekOfYear[6];     // character form of week
    char fileName[13];       // 8.3 name
//    int fileSize;
    File dataFile;
    
    dayOfYear = daysSofar[logTime->Month()-1] + logTime->Day();
    if (logTime->Month()>2 && (logTime->Year()/4 ==0)) {  // check for leap year - valid until 2100
        dayOfYear++;
    }
    weekOfYear = (dayOfYear+6)/7;     // round up for full week

    /* 
     *  
     * adjust for short week in Jan 
     * start from 2017 - the year after a leap year and Jan 1 was a Sunday
     * assume Sun = 0 Mon = 1 ... Sat = 6 this make it easy to do MOD 7
     * computation for the day of the week
     *  
     */
     
     dowJan1 = (((logTime->Year()-2017)     // 1 day for each year after 2017
               +((logTime->Year()-2017)/4)) // additional day for each leap year(fix in 2100)
               % 7);               // result mod 7 is day of week for year 0 origin
                 
    if (logTime->DayOfWeek() < dowJan1) {
        weekOfYear++;  // adjust for partial first week in Jan
    }
    
    sprintf(cweekOfYear,"%d",weekOfYear);
    strcpy(fileName,"DATA1W");
    strcat(fileName,cweekOfYear);
    strcat(fileName,".CSV");
//    Serial.println(fileName);
    
    boolean newFile = true;
    if (LittleFS.exists(fileName)) {   // check for data file
        newFile = false;     // file exists
//        Serial.print("file exists ");
//       Serial.println(fileName);
    }

    dataFile = LittleFS.open(fileName, "a");
//    Serial.println(dataFile);
    // if the file is available, write to it:
    if (dataFile) {
          if (newFile){   // if no data yet output header
            dataFile.println(header);
            Serial.println(header);
          }
        dataFile.println(logEntry);

        dataFile.close();
        // print to the serial port too:
//        Serial.println(logEntry);
    }
    // if the file isn't open, send error
    else {
        Serial.print("error opening ");
        Serial.println(fileName);
    }  
 }

void setup() {

  // pin for delete disable
  pinMode(D7,INPUT_PULLUP);
  
  Serial.begin(115200);

  LittleFS.setTimeCallback(myTimeCallback);
  LittleFS.begin();   // start file system
  
  serialNumber = String(ESP.getChipId());     // get chip ID
  
  // Start up the DallasTemperature library
  sensors.begin();
  sensors.setResolution(11);
  
  // set up for external interrupt 
  pinMode(interruptPin, INPUT_PULLUP);
  // we are only interested in the high to low change
  attachInterrupt(digitalPinToInterrupt(interruptPin), rtcISR, FALLING);

  // make sure interval is between 1 minute and 1 day
  minuteInterval = (minuteInterval == 0) ? 1:((minuteInterval > 1440) ? 1440:minuteInterval); 
  Serial.print("minuteInterval ");
  Serial.println(minuteInterval);

  // Set up real time clock/calendar
  Rtc.Begin();
  Rtc.SetIsRunning(true);
  Rtc.Enable32kHzPin(false);
  Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeAlarmOne); 

  // Set up WiFi network
  Serial.println();
  Serial.print("Creating Access Point ");
  Serial.println(ap_ssid);

  // set IP
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(apIP, apIP,IPAddress(255, 255, 255, 0));
  
  if (WiFi.softAP(ap_ssid, ap_password)) {
    Serial.println("AP started");
  } else{
    Serial.println("AP create failed");
    
  }
  Serial.print("Soft-AP IP address = ");
  Serial.println(WiFi.softAPIP());

  // dns 
  // modify TTL associated  with the domain name (in seconds)
  // default is 60 seconds
  dnsServer.setTTL(300);
  // set which return code will be used for all other domains (e.g. sending
  // ServerFailure instead of NonExistentDomain will reduce number of queries
  // sent by clients)
  // default is DNSReplyCode::NonExistentDomain
  dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure);

  // start DNS server for a specific domain name
  dnsServer.start(DNS_PORT, url, apIP);


  server.on("/", handleRoot);
  server.on("/download", handleLoadFile);
  server.on("/delete", handleDeleteFile);
  server.on("/timeset", handleSetTime);
  server.on("/config", handleConfig);
  
  server.onNotFound(handleNotFound);
  
  // Start the server
  server.begin();
  Serial.println("Server started");

}

void loop() {
  RtcDateTime present;

  dnsServer.processNextRequest();  
  server.handleClient(); 
   
  if (timerInterupt) {
      logEntry = "";
      // Get current date/time
      present = Rtc.GetDateTime(); //get the current date-time
      //Convert it to a String
      logEntry += dtToString(present);
      logEntry += ",";
  
     // Get board temperature
     logEntry += String(Rtc.GetTemperature().AsFloatDegC(),1); //read register and display the temperature
     logEntry += ",";
       
      // get DS18B20 Temperature reading and output it
      logEntry +=  String(gettemp());

      // Add chip ID
      logEntry += ",";
      logEntry += serialNumber;
  
   // log data here 
      LogData(&present,logEntry); 
   
      Serial.println(logEntry);
      // set next time interrupt
      // round next time up to a multiple of match interval
      minuteMatch = (((present.Hour()*60+present.Minute()) / minuteInterval) +1 )*minuteInterval;
  //            Serial.print(F("minuteMatch "));
  //            Serial.println(minuteMatch);
      // split interval into minutes and hours
      minutePart = minuteMatch%60;
      hourPart = (minuteMatch/60)%24; // keep within a day
      
      // This clears the interrupt flag in status register of the clock
      // The next timed interrupt will not be sent until this is cleared
      Rtc.LatchAlarmsTriggeredFlags();
      timerInterupt = false;  // done processing interrupt
 
      // try stream operator
 //     Serial<<"Next interrupt "<< ((hourPart<10)?"0":"")<<hourPart<<":"<<((minutePart<10)?"0":"")<<minutePart<<endl;

      setNextSampleTime();  // set alarm for next sample time
      }  
      
}     
1 Like