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…
-
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.
-
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
}
}
