ESP8266 temperature logging

Record Temperature and Pressure with ESP8266 and BerryIMU

In this guide, we will be using the ESP8266 and BerryIMU to record temperature and pressure and display the output onto a web page.

Google charts is used to display the data in an easy to read format.

ESP8266 Record temperature
ESP8266 Record temperature

Adafruit Feather Hazzuh! and BerryIMU
Adafruit Feather Hazzuh! and BerryIMU

Components used are;

Summary

  • NTP is used to get the correct time of day.
  • The free available RAM is calculated to work out how much data we can store,
  • Google charts is used to display chart data.
  • 4 web pages are created:
    /” – home page which is used to display the gauges.
    /chart.html” – Is used to display chart.
    /table.html” – Is used to display the data in a table.
    /data.json“- Is used by the home page to update the the gauge values.

Hook Up

The below diagram shows how to connect a BerryIMU to an ESP8266 microcontroller, in this case it is the  Adafruit Feather Huzzah .

Adafruit Huzzah IMU
Adafruit Huzzah and BerryIMU

 

Prepare Arduino IDE

The Arduino IDE needs to be updated with the board packages for the ESP8266.  This is very easy to do and both Sparkfun & Adafruit have detailed guides on how to do this;
Sparkfun Arduino IDE and ESP8266
Adafruit Arduino IDE and ESP8266

BerryIMU Raspberry Pi Gyroscope Accelerometer

The Code

The code can be found here. Download the entire Git repo. The code for this guide can be found under the directory;
ESP8266-BerryIMU/BerryIMU_ESP8266_Graph_Temperature/

 

Wireless SSID

The first thing to do is update the code with your wireless network settings.

const char* ssid = "******";
const char* password = "***************";

 

Setup()

Here we will only cover the important items within setup().

I2C is initialised.

Wire.begin(4,5);

The first value is the SDA pin and the second specifies the SCL pin. On the Adafruit Feather Huzzah we use pins 4 and 5.
Any pin on the ESP8266 can be used for I2C.

Wireless is then enabled  and then we try and connect to the wireless network. The IP address is then printed to the serial console.

  WiFi.begin(ssid, password);

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);

  //Print IP to console
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  delay(3);

Then the web services is started.

server.begin();

Calibration data from the BMP180 on BerryIMU is then read.
Each BMP180 is different, at time of manufacture, calibration data is written to EEPROM on the BMP180.

readCalBMP180();
 Time

Setup time using NTP, the correct time is needed for logging.. We will not be covering NTP in this guide

  //Setup time using NTP
  Serial.print("Resolving NTP Server IP ");
  WiFi.hostByName(timerServerDNSName, timeServer);
  Serial.println(timeServer.toString());

  Serial.print("Starting UDP... ");
  Udp.begin(localPort);
  Serial.print("local port: ");
  Serial.println(Udp.localPort());

  Serial.println("Waiting for NTP sync");
  setSyncProvider(getNtpTime);

NTP time is returned as UTC. To set it to the correct timezone, modify the below line with timezone offset.

const int timeZone = 11; ; // Sydney, Australia
RAM setup

During setup we need to calculate how much RAM can be used to store data. This is done in allocateRam().

Once the ESP8266 is up an running, the remaining RAM is divided up into;

  • RAM for TCP/IP Stack. If using the refresh function on the main page which has gauges, then there needs to be enough RAM left to accommodate the TCP sessions for the refresh.
  • RAM for the array which stores temperature and pressure data.
  • RAM for the ESP8266 to run.

We will allocate as much RAM as we can to track historical data. This means, we need to work out how much RAM we will need for the TCP/IP stack and we also need enough for the ESP8266 to run.
Once we get this value, we can allocate the rest of the RAM to the array used to store the data.

Working out RAM for TCP/IP Stack
Each TCP session expires every 2mins. (Default MSL is 2mins for Windows https:technet.microsoft.com/en-us/library/cc938217.aspx)

196KB RAM per refresh.

To calculate how much RAM is needed for the TCP/IP stack;
number-of-refreshes-in-2-minutes x RAM per REFRESH = RAM required for TCP/IP stack.

We then need to reserve some spare RAM for the ESP8266 to run. 10,000 KB should be enough.

Final formula:
(number-of-refreshes-in-2-minutes x RAM per REFRESH) + SPARE RAM = ALLOCATED_RAM

E.g.
For a refresh of every 1 second
(120x196KB) + 10,000 = 33,520

For a refresh of every 3 seconds
(40x196KB) + 10,000 = 17,840

The less frequent the refresh will result in a smaller value needed to be reserved. And this also means more RAM can be used to store historical data for a longer period.

If refresh isn’t used, then leave ALLOCATED_RAM to 10,000

Array to Store data

Once we know how much RAM to allocated for TCP/IP and ESP8266 to run, we can use the rest of the RAM to store the data.

  //Get freem RAM in bytes
  uint32_t free=system_get_free_heap_size() - ALLOCATED_RAM;

Divide the free RAM by the size of the variables used to store the data. This will allow us to work out the maximum number of records we can store.

  
numberOfRows = free / (sizeof(float)*2+ sizeof(unsigned long)); // 2 x float for temp and pressure.  one x long for time. 

Now re-declare the arrays with the number of rows.

  
  tempCdata = new float [numberOfRows];
  pressData = new float [numberOfRows];
  timeStamp = new unsigned long [numberOfRows];
Reading Temperature

The temperature and pressure is read within the main loop.

First, we need to let the BMP180 know that we want to read the temperature.

status = startTemperature();

The function startTemperature() informs the BMP180 that we want to read the temperature by sending BMP180_COMMAND_TEMPERATURE (0x2E) to the register BMP180_REG_CONTROL (0xF4). It returns the time to wait before a value can be read.

The below code reads the temperature value once the delay value in ‘status’ has expired. The temperature returned is in Celsius, this is also converted to Fahrenheit and stored in ‘F’.

  if (status != 0){
    delay(status);
    getTemperature(T);
    Serial.print("temperature: ");
    Serial.print(T,2);
    Serial.print(" deg C, ");
    F = (9.0/5.0)*T+32.0;
    Serial.print(F,2);
    Serial.print(" deg F          ");
  }
Reading Pressure

We now need to inform the BMP180 that we want to read the pressure. When information the BMP180 that we want to read pressure, we also need to specify the oversampling to use. Below, we specify 3.

status = startPressure(3);

The startPressure() function sends BMP180_COMMAND_PRESSURE3 (0xF4) to the register BMP180_REG_CONTROL (0xF4).It returns the time to wait before a value can be read.

The below code reads the pressure value once the delay value in ‘status’ has expired.

  //Read pressure
  status = startPressure(3);
  if (status != 0){
    // Wait for the measurement to complete:
    delay(status);
    status = getPressure(P,T);
    if (status != 0){
      // Print out the measurement:
      Serial.print("absolute pressure: ");
      Serial.print(P,2);
      Serial.print(" mb, ");
      Serial.print(P*0.0295333727,2);
      Serial.print(" inHg   ");

    }
   }
Recording Data

The portion of code below adds the current recorded temperature and pressure to the array.
It first checks to see if the poll period is expired.
It then checks to make sure each value is a number.

The variable count is used to work out if the array is full or not.  When the ESP8266 is first started, the array is empty and we just keep adding values to the next available row. Once the array is full, we cycle the data and then add the latest values to the end.

The current time is also captured.

// Check if poll period has expired
  if (millis()>=timeKeeper){
    timeKeeper = millis() + (POLLPERIOD * 1000);   // Update timeKeeper with the latest time + poll period (multiply by 1,000 as millis() is milliseconds)
    unsigned long currentTime = now();
    // Update each row in the array until it is full
    if(!(isnan(currentTime)) || !(isnan(T))|| !(isnan(P))){  // Make sure that all values are a number before updating
      if (count < numberOfRows){
          tempCdata[count] = T;   // Temperature
          pressData[count] = P;   // Pressure
          timeStamp[count] = currentTime;  //Current time
          count++;
        }
      else{
        for (int i = 0; i<(count) ; i++){          // Cycle the array. Move everything forward by one and add the new values to the end
          tempCdata[ i] = tempCdata[ i+1];
          pressData[ i] = pressData[ i+1];
          timeStamp[ i] = timeStamp[ i+1];
        }
        tempCdata[numberOfRows] = T;
        pressData[numberOfRows] = P;
        timeStamp[numberOfRows] = currentTime;
       }
    }
  }
Web Server and Web Pages

The web server functionally we using is very basic and has no error checking. In saying that, it is all we need and it performs the job well.

Our ESP8266 web server will have four pages;

  1.  “/”  Root or the home page. This is where the gauges are displayed.
  2. /data.json” is only used to provide data for the gauge refresh.
  3. /chart.html” shows the historical data within a chart.
  4. /table.html” is where all the data is shown in a table

 

The code below will check to see if a client is connected, it then reads the requested web page path and stores it in sPath.

  WiFiClient client = server.available();

  /////////////////////////////////////
  // Read the first line of the request
  /////////////////////////////////////
  String sRequest = client.readStringUntil('\r');
  client.flush();


  
  // get path; end of path is either space or ?
  // Syntax is e.g. GET /?show=1234 HTTP/1.1
  String sPath="",sParam="", sCmd="";
  String sGetstart="GET ";
  int iStart,iEndSpace,iEndQuest;
  iStart = sRequest.indexOf(sGetstart);
  if (iStart >= 0)
  {
    iStart+=+sGetstart.length();
    iEndSpace = sRequest.indexOf(" ",iStart);
    iEndQuest = sRequest.indexOf("?",iStart);
    
    // are there parameters?
    if(iEndSpace > 0)
    {
      if(iEndQuest > 0)
      {
        // there are parameters
        sPath  = sRequest.substring(iStart,iEndQuest);
        sParam = sRequest.substring(iEndQuest,iEndSpace);
      }
      else
      {
        // NO parameters
        sPath  = sRequest.substring(iStart,iEndSpace);
      }
    }
  }

 

The gauges, chart and table use Google Charts to display the data. This is a great option as it takes the load away from the ESP8266.

When a web browser requests a page from the ESP8266, the ESP8266 will respond with a HTML page which will have the temperature and pressure data, as well as code which will force the browser to use google charts to display this data.

Creating HTML pages with the ESP8266

Creating HTML pages for the ESP8266 web server is easy, however the pages have to be built from scratch. This means you have to include html headers and GET response codes.

The code snippet below is the code which creates the page for the chart.   Here is an example of what the final page looks like.

The first thing we do is declare a string(htmlContent) which is used to store the html content.

And then we just keep adding html code to the string and send it to the web browser with client.print(htmlContent);

It is important that you don’t make the string to large before sending to the web browser, otherwise the ESP8266 will crash. You can see that in the code snippet below that halfway through the html page, I send the string and then starting building the string  again.

 

 else if (sPath=="/chart.html"){ 
    //Standard html header stuff here.
    htmlContent = ("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"); 
    htmlContent += ("<html><head><title>chart</title>\n");
  
    //load google charts and create a button
    htmlContent += ("<script type=\"text/javascript\" src=\"https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization','version':'1','packages':['corechart']}]}\"></script>\n");
    htmlContent += ("<button id=\"change-chart\"></button>");
    htmlContent += ("<script type=\"text/javascript\"> google.setOnLoadCallback(drawChart);\n");
    htmlContent += ("var button = document.getElementById('change-chart');");
  
    //Create a function to draw the chart and then add the data into a table
    htmlContent += ("function drawChart() {var data = google.visualization.arrayToDataTable([\n");
    htmlContent += ("['Local Time', 'Temperature C', 'Temperature F', 'Pressure'],\n");
    //Send what we have to the client (web browser)
    client.print(htmlContent);
  
    //Here we loop through the temp and pressure data to place it into the html source code
    for (int i = 0; i< count ; i++){
      htmlContent = ("[new Date(" + String(timeStamp) +  "000)," + String(tempCdata) +  "," + String((9.0/5.0)*tempCdata+32.0) + "," + String(pressData) + "],\n");
      client.print(htmlContent);
    }
    htmlContent = ("]);\n");

    //Continue to build the rest of the web page.  Here we create three function that the buttons uses to dsiplay the chart data.
    htmlContent += ("function drawChartCelsius() {var tempCview = new google.visualization.DataView(data);\n    tempCview.setColumns([0,1]);\n    chart.draw(tempCview, optionsCelsius);\n    button.innerText = 'Change to Fahrenheit';\n    button.onclick = drawChartFahrenheit;}\n");
    htmlContent += ("function drawChartFahrenheit() {var tempFview = new google.visualization.DataView(data);\n    tempFview.setColumns([0,2]);\n    chart.draw(tempFview, optionsFahrenheit);\n    button.innerText = 'Change to Pressure';\n    button.onclick = drawChartPressure;}\n");
    htmlContent += ("function drawChartPressure() {var tempPressureView = new google.visualization.DataView(data);\n    tempPressureView.setColumns([0,3]);\n    chart.draw(tempPressureView, optionsPressure);\n    button.innerText = 'Change to Celsius';\n    button.onclick = drawChartCelsius;}\n");

    //specify date format and then update x labels with this time format
    htmlContent += ("var formatter = new google.visualization.DateFormat({ formatType: 'short',timeZone: 0});\n  formatter.format(data, 0);\n");
    htmlContent += ("// Set X-Axis Labels\nvar xTicks = [];\n");
    htmlContent += ("for (var i = 0; i < data.getNumberOfRows(); i++) {\n");
    htmlContent += ("   xTicks.push({\n    v: data.getValue(i, 0),\n    f: data.getFormattedValue(i, 0) });\n}\n");

    //Here are three chart options used for each chart.  E.g. colour, chart title, etc..
    htmlContent += ("var optionsPressure = {'height': 320,chartArea:{top:20, height:\"60%\"},hAxis:{gridlines:{color:'transparent'},ticks:xTicks,slantedText: true,slantedTextAngle :70,textStyle:{fontSize: 11} },vAxis:{format:\"##,### mb\"},series:{1:{curveType:'function'},0:{color:'orange'}},legend:{position: 'none'},title:'Pressure in Millibars' };\n");
    htmlContent += ("var optionsCelsius = {'height': 320,chartArea:{top:20,  height:\"60%\"},hAxis:{gridlines:{color:'transparent'},ticks:xTicks,slantedText: true,slantedTextAngle :70,textStyle:{fontSize: 11} },vAxis:{format:\"##.## " + String((char)176) + "C\"},series:{1:{curveType:'function'},0:{color:'red'}},legend:{position: 'none'},title:'Temperature in Celsius' };\n");
    htmlContent += ("var optionsFahrenheit = {'height': 320,chartArea:{top:20, height:\"60%\"},hAxis:{gridlines:{color:'transparent'},ticks:xTicks,slantedText: true,slantedTextAngle :70,textStyle:{fontSize: 11} },vAxis:{format:\"##.## " + String((char)176) + "F\"},series:{0:{curveType: 'function'},0:{color:'Blue'}},legend:{position: 'none'},title: 'Temperature in Fahrenheit'};\n");
    client.print(htmlContent);    
  
    //Draw chart 
    htmlContent = ("var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));drawChartCelsius();}\n");
    htmlContent += ("</script>\n");

    //Page heading
    htmlContent += ("<font color=\"#000000\"><body><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">\n<h1>Temperature and Pressure Chart</h1><a href=\"/\">back</a><BR><BR>\n");
    htmlContent += ("<div id=\"curve_chart\" style=\"width: 800px; height: 300px\"></div><BR><BR>Number of readings=" + String(count) + "<BR>Max allowed readings=" + String(numberOfRows) + "<BR>");

    //Display the data and time for first and last reading
    htmlContent += ("<BR><BR>First reading at : ");
    timeAndDate(timeStamp[0],htmlContent);    
    htmlContent += ("<BR>Most recent reading : ");
    timeAndDate(timeStamp[count-1],htmlContent);     
    htmlContent += ("<BR></body></html>\n");
    client.print(htmlContent);
  }//End chart

Here are some helpful tips for building the page;

  • Use the developer tools (F12) in your browser to troubleshoot.
  • “\n” is not needed, however it makes the source for the html file more readable.
  • Quotes used within html have to be escaped out with backslash “\”.
  • Variables have to be converted to strings using String()
  • Dont make the the string htmlContent too large as it will crash the ESP.

Leave a Reply