Log Temperature And Display It Graphically On A Website
The Raspberry Pi is more than just a small, cheap computer. It has a 40-pin GPIO connector that gives you direct access to the General Purpose Inputs and Outputs along with some 3.3 V, 5 V, and ground pins.
This makes it very easy to connect sensors, switches, LEDs, and relays to the Pi so that your code can interact with the real world. In this project, I connected a temperature sensor to the Pi's GPIO pins and then wrote a Python script to periodically read data from the temperature sensor. The temperature data is averaged over time to smooth out tiny fluctuations in the temperature measurement, and then written to a .csv file every hour. Once the data is recorded into the .csv file, we can create a html website to display the .csv data graphically. If you don't already have your Pi set up as a web server, check out this project to see if that is something you want to do first. If you don't want to display the information graphically on a website, you can skip those steps and just add a print statement to your Python script to display the temperature readings in the terminal.
The temperature sensor I used is a TMP36 Temperature Sensor. According to the datasheet this sensor outputs a voltage that is proportional to the measured temperature.
The output of the temperature sensor can't be connected directly to the Pi's GPIO pins because the GPIO pins can only read digital signals. Digital signals are considered to be on or off based on the voltage present on the pin. Since the temperature sensor outputs a varying voltage between 0 V - 2 V, we'll need another circuit to measure that voltage and convert it into a digital signal that can be sent to the Pi's IO pins. That circuit is an Analog to Digital Converter or ADC.
So it's not as simple as just hooking a temperature sensor directly to your Pi. That's okay. Don't quit yet. The Analog to Digital Conversion (ADC) does add an additional step, and therefore an additional layer of complexity to the project. But don't worry. We aren't the first ones to connect analog sensors to a Raspberry Pi. There are plenty of manufacturers that make small circuit boards called development boards that make this pretty easy. This project will use an ADS1115 16-bit ADC development board to translate the temperature sensor output voltages to a communication protocol that the Pi can read.
The ADS1115 board has 4 analog channels that read the voltage present on the pin, convert it into a digital value, and then send that value to the Pi using the I2C communication protocol. The ADC board connects to the Pi with 4 wires: Power, Ground, SDA, and SCL. The SDA and SCL wires are uses for the I2C communication back to the Pi. The Pi has 3.3 V power pins in the GPIO header along with grounds, so we can use those to power the ADC board. Reference the following table for the connections between the ADC board and the Pi's 40 pin header.
| Circuit | Raspberry Pi Pin | ADC Pin |
|---|---|---|
| 3.3 V Power | 1 | 1 |
| SDA | 3 | 4 |
| SCL | 5 | 3 |
| Ground | 9 | 2 |
For this project, I used a ribbon cable, a header splitter, and expansion board so that I could easily connect and disconnect the ADC and temperature sensor. The header splitter makes it possible to still use the Pi's available GPIO for other projects, like my indoor garden. The expansions board makes it easier to distribute power and ground to the 4 sensors that could potentially be connected to the ADC. I used pieces from this prototype board kit and jumper wire kit to build these boards and make the connections between the ADC board, the header splitter, and the temperature sensors. I even had enough room on the expansion board to solder one of the temperature sensors directly to the board. Up to 3 additional sensors can be added to this ADC board if I want to measure the temperatures of different locations. I added some screw terminals to connect more sensors later in case I want to measure indoor vs. outdoor temperature for example. The cartoon below roughly shows what the whole setup looks like.
If you are just tinkering and want to avoid building all these boards, you can certainly just wire the ADC directly to the Pi with some jumper wires. The basic circuit is shown below. Any way you decide to achieve these electrical connections should work, no need to be fancy if you are just tinkering.
The addr pin is grounded to set the address for the ADC board on the I2C bus. According to the ADS 1115 datasheet, the address is set by connecting the addr pin to ground, power, SDA, or SCL as shown in the table below. In our case, the address will be 0b1001000 or 0x48.
Once you have all the wiring squared away, it's time to jump into the Python code that will collect temperature readings, store them to a file, and then later recall the file for display on an html website.
If you don't already have Python up and running on your Pi, go ahead and do that first. The resources at Python.org should be more than enough to help you get started with Python. I also talk about Python a bit over on my CAN bus page so check that out for more on Python and what you can do with it once it's installed on your Pi.
Before we get into coding, we'll need to install the Adafruit ADS1115 Python library using pip. Current versions of Python come with pip already installed, but in case you don't have it, check out this page to get it installed. To install the ADS1115 library, open a terminal window and enter the following command.
sudo pip3 install adafruit-circuitpython-ads1x15
Now that the Adafruit library is installed, we can create a new Python file and start writing our code. I like to use the Thonny editor that comes with the Raspberry Pi OS because it allows you to run your code and instantly see if/how it's working. As we build our code, we'll add some print statements so that we can confirm the program is working as expected. So go ahead and create a new file in a directory of your choice and give it some name like temp_reader.py. Right click on your new Python file and select Open With->Thonny to open the editor.
We'll start by importing all the libraries we'll need. We need the Adafruit ADS1115 library, along with the libraries required to communicate on the I2C bus, and the datetime libraries to periodically sample the temperature.
import board
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
from time import time, strftime, sleep
from datetime import datetime
from datetime import timedelta
Next we'll set up the I2C bus to read the analog values in from the ADS1115 board's A0 pin. The pin is selected in the AnalogIn() function, represented by P0. To read pin A1, we'd replace the P0 with P1, for example.
i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
channel0 = AnalogIn(ads, ADS.P0)
We can now test our code and wiring to see if we are able to read raw voltage values from the ADS1115. Add a print statement to display the voltage reading in the console.
print(channel0.voltage)
Click the Run button to run your code and look for a voltage value between 0 and 2 printed in the small window in the bottom of the Thonny editor window.
Hopefully your code worked, but if you see any errors in the console window, use the errors to diagnose your code. If you code did execute properly, let's take the next step and convert that raw voltage value into a temperature. The formula for converting the voltage to temperature is derived based on the datasheet, which shows a voltage of 1.0 V corresponding to a temperature of 50°C. The response is linear, so we can estimate a formula of T = (V * 100) - 50 to convert the voltage to temperature. So lets update our code to print voltage and temperature. We'll add a new variable 'temp' to hold the calculated temperature value, and add that to the print statement so we can compare the voltage to the temperature.
temp = (channel0.voltage * 100 - 50)
print(channel0.voltage, temp)
Here's what the output should look like now. You can compare the calculated temperature and voltage to the chart in the datasheet to confirm your calculation worked correctly.
Now that we have a working temperature sensor that we can read into a Python script, the next step is to periodically sample the temperature, record it to a file, and then serve that file graphically on a website.
Your program currently runs once and displays the instantaneous temperature reading at the time the program ran. If you click run a few more times, you'll notice the temperature value jumps around a bit. This is due to accuracy issues with the sensor and ADC itself. To get a better estimate of the actual temperature, we'll want to take a bunch of samples and then use the average of those samples as our final value.
We'll start by adding a forever loop so that when we click run, our program will continuously loop until we click the stop button. Each time the loop executes, the temperature will be read and stored to an accumulating total, and a counter 'n' will increment so we can later divide the accumulated total by n to get our average. Here's what the code looks like, starting after the reading of the temperature sensor voltage.
while True:
timestamp = datetime.now()
temp = (channel0.voltage * 100 - 50)
temp_accum = temp_accum + temp
n = n + 1
if(timestamp.second == 0) and flag == 0:
flag = 1
temp_avg = temp_accum / n
temp_accum = 0
n = 0
print(temp_avg)
if(timestamp.second == 1) and flag == 1:
flag = 0
There's a lot of new things going on here, so let's break it down. You can see the first line above is the start of our forever loop. The program will first go through the import statements, initiate the I2C bus, set up a channel to read in the analog temperature sensor values, and then start repeating the code below the while True: line. The first step in our forever loop is to get the current time. The Pi has a real time clock, which we can access in our Python code by using the datetime.now() function. This will record a timestamp that we can use later to determine how long it has been in real time since the loop last executed.
The next step is to record our temperature sensor's voltage reading to an accumulating total, temp_accum, and incrementing a counter, n, to keep track of how many times we added the current temperature reading to our running total. As our forever loop is running and collecting temperature values, we need a way to periodically report the averaged temperature data on a regular interval. In this example, we'll use a relatively short interval so we can observe our code working.
Take a look at the two if statements, these will manage the reporting of the average temperature value calculated over the determined time interval. In this example, the first if statement executes when the seconds value of the timestamp is equal to zero. This means that every minute, the temperature data will be averaged by dividing the temp_accum by n and reported via the print() statement to the console. The temp_accum and n are reset after the average is calculated to get ready to record the next average. The flag variable is used to prevent the if statement from continuously executing for the entire time the seconds value is equal to zero. Since the program loops much faster than once per second, the print statement would execute several times during the first second of each minute. The flag variable is used to 'turn off' the reporting until the seconds value of the time stamp is no longer zero. Then, then flag variable is flipped back to reset the first if statement for the next minute's report. You can play around with the timestamp values used in the if statements to get different time intervals based on your application.
At this time, you can go ahead and run your code. The temperature value should be displayed in the console every minute. You'll notice that the timing of the report is not every minute as counted from the time the program began executing, but rather at the top of every minute as counted by the system clock.
Now that we're periodically recording the temperature, the next step is to save that value into a .csv file so we can read it later.
We'll start with the easy part. Create a new .csv file in the same directory as your Python file. If you don't already have Libre Office installed, go ahead and install it. Open the .csv file with Libre Office Calc and add some headers for your data in the top row of cells. I chose "Time" and "Temperature" but you can put whatever you like in here. These header names will be referenced by our chart.js call to read the .csv data into a html table. If you don't plan to post the data graphically, you don't really need headers at all. Save the .csv file (make sure you keep the .csv extension and don't convert to .ods) and close it.
Back in our Python script, we'll need to add an import statement to bring the csv module into our code.
import csv
Now we can create, open, read, and write to .csv files from our Python code. Add the following code just below the print statement, so that every time the temperature is printed to the console, it will also be recorded into the .csv file.
with open('temp_simple_log.csv', 'a+', newline='') as csvfile:
tempwriter = csv.writer(csvfile, delimiter=',')
tempwriter.writerow([strftime("%H:%M"), str(round(temp_avg, 1))])
The with open line will open the file named in the first argument of the open function. The 'a+' argument means that we'll be appending data to the existing data inside the file. This is important because otherwise, we'd just overwrite our last recorded temperature and time values. The next line creates a csv writer variable tempwriter that will allow us to write to the file. The next line is what actually writes a new row to the .csv file. You can see we are writing two columns. The first column is the time of the recording formatted as a string with hours and minutes. The second column will round the temp_avg value to one decimal place and then write it to the .csv file.
Go ahead and run your Python code and let it record a handful of temperature values. You should see the temperature print to the console at the top of every minute as before. Stop the program, and open the .csv file to see if your temperature values were recorded. You should see something like this in the .csv file.
If you open your .csv file with Libre Office and see a bunch of gibberish, don't panic. Just check the character set selection on the text import dialog. It should be set to Unicode (UTF-8).
Now that we have a .csv file with our time and temperature data, we can display that information graphically on an html website. We are going to use a free javascript library, chart.js to do the heavy lifting for us. Before getting into how to implement chart.js, you'll need to know how to get your Pi set up as a html server and create a new html file. If you can put the .csv file in the same directory as your html file, that will make things a bit simpler later. The html file itself is very simple. Open your html file and add the basic html stuff below to get a blank page. Save the html file and navigate to your new page to make sure it's working.
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Temperature Plots</title>
</head>
<body>
<h2>Temperature Plots</h2>
<!--javascript stuff goes here-->
</body>
</html>
You should see a blank page with the "Temperature Plots" header. Once you have that up and running, it's time to add the javascript code to read in the .csv file and render a graphical plot. All of the javascript code will go between your header and the body closing tag.
We'll start by referencing the char.js libraries with a script tag like this:
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Next, we'll write the script that will read in the .csv file and configure the chart.
<script>
d3.csv('temp_log.csv').then(makeChart);
function makeChart(temp){
var timeLabels = temp.map(function(d) {return d.Time});
var tempData = temp.map(function(d) {return d.Temperature});
var chart = new Chart('temperature', {
type: 'line',
data: {
labels: timeLabels,
datasets: [ {data: tempData, label: 'Temperature'}]
},
options: {
legend: { display: true},
scales: { xAxes: [{
scaleLabel: {
display: true,
labelString: 'Time'
}
}],
yAxes: [{
scaleLabel: {
display: true,
labelString: 'Temperature \u00B0C'
},
ticks: {
suggestedMin: 0,
suggestedMax: 30
}
}]
}
}
});
}
</script>
Going through this block of code from top to bottom, you see the first thing we do is read in the .csv file. This is where having the .csv file in the same directory makes things simple. You only need to use the file name in the first argument of the d3.csv function. In this example, the .csv file name is temp_log.csv.
The next step is to define the makeChart() function. Inside our function, we create some variables for our x-axis and y-axis values. The return argument in the temp.map() function needs to match the headers in your .csv file. In this example, I named my headers 'Time' and 'Temperature' so I made the return arguments d.Time and d.Temperature.