Monday, 26 January 2015

Raspberry Pi Single Temperature Measurement, Recording and Display

The following post is a section of the book 'Raspberry Pi: Measure, Record, Explore'.  The entire book can be downloaded in pdf format for free from Leanpub or you can read it online here.
Since this post is a snapshot in time. I recommend that you download a copy of the book which is updated frequently to improve and expand the content.
---------------------------------------

Single Temperature Measurement

This project will measure temperature using a single DS18B20 sensor. It will use the waterproof version of the sensor since it has more potential practical applications. It also builds on the initial setup we covered in Setting up the Raspberry Pi.

Measure

Hardware required

There are actually quite a fee options to how you can put the circuit together. the following equipment is based on the examples described.

  • DS18B20 sensor (the waterproof version)
  • 10k Ohm resister
  • Jumper cables
  • Cobbler Board
  • Ribbon cable
  • Heat-shrink
  • Pin header cables

Connect


The DS18B20 sensor needs to be connected with the black wire to ground, the red wire to the 3V3 pin and the blue or yellow (some are blue and some are yellow) wire to the GPIO4 pin. A resistor between the value of 4.7k Ohms to 10k Ohms needs to be connected between the 3V3 and GPIO4 pins to act as a ‘pull-up’ resistor.
The Raspbian Operating System image that we are using only supports GPIO4 as a 1-Wire pin, so we need to ensure that this is the pin that we use for connecting our temperature sensor.
The following diagram is a simplified view of the connection.
Single DS18B20 Connection
To connect the sensor practically can be achieved in a number of ways. You could use a Pi Cobbler break out connector mounted on a bread board connected to the GPIO pins.
Single DS18B20 Connection via Bread Board
Or we could build a minimal configuration that will plug directly onto the appropriate GPIO pins.
Minimal Single DS18B20 Connection

Test

From the terminal as the ‘pi’ user run the command;
sudo modprobe w1-gpio
modprobe w1-gpio registers the new sensor connected to GPIO4 so that now the Raspberry Pi knows that there is a 1-Wire device connected to the GPIO connector (For more information on the modprobe command check out the Glossary).
modprobe is a Linux program used to add a loadable kernel module (LKM) to the Linux kernel or to remove a LKM from the kernel. It is commonly used to load drivers for automatically detected hardware.
Then run the command;
sudo modprobe w1-therm
modprobe w1-therm tells the Raspberry Pi to add the ability to measure temperature on the 1-Wire system.
Then we change into the /sys/bus/w1/devices directory and list the contents using the following commands;
cd /sys/bus/w1/devices
ls
(For more information on the cd command check out the Glossary here. Or to find out more about the lscommand go here)
This should list out the contents of the /sys/bus/w1/devices which should include a directory starting 28-. The portion of the name following the 28- is the unique serial number of the sensor.
We then change into that unique directory;
cd 28-xxxx (change xxxx to match the serial number of the directory)
We are then going to view the ‘w1_slave’ file with the cat command using;
cat w1_slave
The cat program takes the specified file (or files) and by default outputs the results to the screen (there are a multitude of different options for cat).
The output should look something like the following;
73 01 4b 46 7f ff 0d 10 41 : crc=41 YES
73 01 4b 46 7f ff 0d 10 41 t=23187
At the end of the first line we see a YES for a successful CRC check (CRC stands for Cyclic Redundancy Check, a good sign that things are going well). If we get a response like NO or FALSE or ERROR, it will be an indication that there is some kind of problem that needs addressing. Check the circuit connections and start troubleshooting.
At the end of the second line we can now find the current temperature. The t=23187 is an indication that the temperature is 23.187 degrees Celsius (we need to divide the reported value by 1000).
To convert from degrees Celsius to degrees Fahrenheit, multiply by 9, then divide by 5, then add 32.

Record

To record this data we will use a Python program that checks the sensor every minute and writes the temperature (with a time stamp) into our MySQL database.

Database preparation

First we will set up our database table that will store our data.
Using the phpMyAdmin web interface that we set up, log on using the administrator (root) account and select the ‘measurements’ database that we created as part of the initial set-up.
Create the MySQL Table
Enter in the name of the table and the number of columns that we are going to use for our measured values. In the screenshot above we can see that the name of the table is ‘temperature’ (how imaginative) and the number of columns is ‘2’.
We will use two columns so that we can store a temperature reading and the time it was recorded.
Once we click on ‘Go’ we are presented with a list of options to configure our table’s columns. Don’t be intimidated by the number of options that are presented, we are going to keep the process as simple as practical.
For the first column we can enter the name of the ‘Column’ as ‘dtg’ (short for date time group) the ‘Type’ as ‘TIMESTAMP’ and the ‘Default’ value as ‘CURRENT_TIMESTAMP’. For the second column we will enter the name ‘temperature’ and the ‘Type’ is ‘FLOAT’ (we won’t use a default value).
Configure the MySQL Table Columns
Scroll down a little and click on the ‘Save’ button and we’re done.
Save the MySQL Table Columns
WHY DID WE CHOOSE THOSE PARTICULAR SETTINGS FOR OUR TABLE?
Our ‘dtg’ column needs to store a value of time that includes the date and the time, so either of the types ‘TIMESTAMP’ or ‘DATETIME’ would be suitable. Either of them stores the time in the format ‘YYYY-MM-DD HH:MM:SS’. The advantage of selecting TIMESTAMP in this case is that we can select the default value to be the current time which means that when we write our data to the table we only need to write the temperature value and the ‘dtg’ will be entered automatically for us. The disadvantage of using ‘TIMESTAMP’ is that it has a more limited range than DATETIME. TIMESTAMP can only have a range between ‘1970-01-01 00:00:01’ to ‘2038-01-19 03:14:07’.
Our temperature readings are generated (by our sensor) as an integer value that needs to be divided by 1000 to show degrees Centigrade. We could therefore store the value as an integer. However when we were selecting the data or in later processing we would then need to do the math to convert it to the correct value. It could be argued (successfully) that this would be a more efficient solution in terms of the amount of space taken to support the data on the Pi. However, I have a preference for storing the values as they would be used later and as a result we need to use a numerical format that supports numbers with decimal places. There are a range of options for defining the ranges for decimal numbers, but FLOAT allows us to ignore the options (at the expense of efficiency) and rely on our recorded values being somewhere between -3.402823466E+38 and 3.402823466E+38 (if our temperature falls outside those extremes we are in trouble).

Record the temperature values

The following code (which is based on the code that is part of the great temperature sensing tutorial onAdafruit) is a Python script which allows us to check the temperature reading from the sensor approximately every 10 seconds and write it to our database.
The full code can be found in the code samples bundled with this book (s_temp.py).
#!/usr/bin/python
# -*- coding: utf-8 -*-

import os
import glob
import time
import MySQLdb as mdb

os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')

base_dir = '/sys/bus/w1/devices/'
device_folder = glob.glob(base_dir + '28*')[0]
device_file = device_folder + '/w1_slave'

def read_temp_raw():
    f = open(device_file, 'r')
    lines = f.readlines()
    f.close()
    return lines

def read_temp():
    lines = read_temp_raw()
    while lines[0].strip()[-3:] != 'YES':
        time.sleep(0.2)
        lines = read_temp_raw()
    equals_pos = lines[1].find('t=')
    if equals_pos != -1:
        temp_string = lines[1][equals_pos+2:]
        temp_c = float(temp_string) / 1000.0
        return temp_c

while True:

    try:
        pi_temp = read_temp()
        con = mdb.connect('localhost', 'pi_insert', 'xxxxxxxxxx', 'measurements');
        cur = con.cursor()
        cur.execute("""INSERT INTO temperature(temperature) VALUES(%s)""", (pi_temp))
        con.commit()

    except mdb.Error, e:
        con.rollback()
        print "Error %d: %s" % (e.args[0],e.args[1])
        sys.exit(1)

    finally:
        if con:
            con.close()

 time.sleep(10)
This script can be saved in our home directory (/home/pi) and needs to be run as root (sudo) as follows;
sudo python s_temp.py
While we won’t see much happening at the command line, if we use our web browser to go to the phpMyAdmin interface and select the ‘measurements’ database and then the ‘temperature’ table we will see a range of temperature measurements and their associated time of reading presented.
Save the MySQL Table Columns
CODE EXPLANATION
The script starts by importing the modules that it’s going to use for the process of reading and recording the temperature measurements;
import os
import glob
import time
import MySQLdb as mdb
Python code in one module gains access to the code in another module by the process of importing it. The import statement invokes the process and combines two operations; it searches for the named module, then it binds the results of that search to a name in the local scope.
The program then issues the modprobe commands that start the interface to the sensor;
os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')
Then we need to find the file (w1_slave) where the readings are being recorded in much the same way that we did it manually earlier;
base_dir = '/sys/bus/w1/devices/'
device_folder = glob.glob(base_dir + '28*')[0]
device_file = device_folder + '/w1_slave'
We then set the function for reading the temperature in a ‘raw’ form from the w1_slave file using theread_temp_raw function that fetches the two lines of messaging from the interface.
def read_temp_raw():
    f = open(device_file, 'r')
    lines = f.readlines()
    f.close()
    return lines
The read_temp function is then declared which checks for bad messages and keeps reading until it gets a message with ‘YES’ on end of the first line. Then the function returns the value of the temperature in degrees C.
def read_temp():
    lines = read_temp_raw()
    while lines[0].strip()[-3:] != 'YES':
        time.sleep(0.2)
        lines = read_temp_raw()
    equals_pos = lines[1].find('t=')
    if equals_pos != -1:
        temp_string = lines[1][equals_pos+2:]
        temp_c = float(temp_string) / 1000.0
        return temp_c
From here we enter into a while loop to continually read the temperature and insert the value into the MySQL database;
while True:

    try:
        pi_temp = read_temp()
        con = mdb.connect('localhost', 'pi_insert', 'xxxxxxxxxx', 'measurements');
        cur = con.cursor()
        cur.execute("""INSERT INTO temperature(temperature) VALUES(%s)""", (pi_temp))
        con.commit()

    except mdb.Error, e:
        con.rollback()
        print "Error %d: %s" % (e.args[0],e.args[1])
        sys.exit(1)

    finally:
        if con:
            con.close()

 time.sleep(10)
The while statement takes an expression and executes the loop body while the expression evaluates to (boolean) “true”. True executes the loop body indefinitely. Note that most languages usually have some way of breaking out of the loop early. In the case of Python it’s the break statement (not that it’s used here).

Explore

This section has a working solution for presenting temperature data but is a simple representation and is intended to provide a starting point for the display of data from a measurement process. Our data display techniques will become more advanced as we work out different things to measure. In the mean time, enjoy this simple effort.

The Code

The following code is a PHP file that we can place on our Raspberry Pi’s web server (in the /var/www directory) that will allow us to view all of the results that have been recorded in the temperature directory on a graph;
This is the same code that is used in the set-up description in the book and as such I won’t repeat the explanation of the code. The full code can be found in the code samples bundled with this book (s_temp.php).
<?php
$hostname = 'localhost';
$username = 'pi_select';
$password = 'xxxxxxxxxx';

try {
    $dbh = new PDO("mysql:host=$hostname;dbname=measurements", 
                               $username, $password);

    /*** The SQL SELECT statement ***/
    $sth = $dbh->prepare("
       SELECT  `dtg`, `temperature` FROM  `temperature`
    ");
    $sth->execute();

    /* Fetch all of the remaining rows in the result set */
    $result = $sth->fetchAll(PDO::FETCH_ASSOC);

    /*** close the database connection ***/
    $dbh = null;
    
}
catch(PDOException $e)
    {
        echo $e->getMessage();
    }

$json_data = json_encode($result); 
?>
<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */

body { font: 12px Arial;}

path {
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
}

.axis path,
.axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
}

</style>
<body>

<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v3.min.js"></script>

<script>

// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50},
    width = 800 - margin.left - margin.right,
    height = 270 - margin.top - margin.bottom;

// Parse the date / time
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;

// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);

// Define the axes
var xAxis = d3.svg.axis().scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis().scale(y)
    .orient("left").ticks(5);

// Define the line
var valueline = d3.svg.line()
    .x(function(d) { return x(d.dtg); })
    .y(function(d) { return y(d.temperature); });

// Adds the svg canvas
var svg = d3.select("body")
    .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
    .append("g")
        .attr("transform",
              "translate(" + margin.left + "," + margin.top + ")");

// Get the data
<?php echo "data=".$json_data.";" ?>
data.forEach(function(d) {
 d.dtg = parseDate(d.dtg);
 d.temperature = +d.temperature;
});

// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.dtg; }));
y.domain([0, d3.max(data, function(d) { return d.temperature; })]);

// Add the valueline path.
svg.append("path")
 .attr("d", valueline(data));

// Add the X Axis
svg.append("g")
 .attr("class", "x axis")
 .attr("transform", "translate(0," + height + ")")
 .call(xAxis);

// Add the Y Axis
svg.append("g")
 .attr("class", "y axis")
 .call(yAxis);

</script>
</body>
The graph that will look a little like this (except the data will be different of course).
Simple Line Graph of Temperature
This is a VERY simple graph (i.e, there is no title, labeling of axis or any real embellishment) and as such it has some limitations. For example it will automatically want to display ALL the recorded temperature data in the database. We might initially think that this would be a good thing to do, but in this case there are over 3000 recordings trying to be displayed on a graph that is less than 800 pixels wide. Not only are we not going to see as much detail as could be possible, but the web browser can only cope with a finite amount of data being crammed into it. Eventually it will break.
So as an addendum to the code above we shall look at making a change to our database query to make a slightly more rational selection of data.

Different MySQL Selection Options

Currently our SELECT statement looks like the following;
SELECT  `dtg`, `temperature` FROM  `temperature`
As described earlier, this query is telling the database to SELECT our date/time data (from the dtg column) and the temperature values (from the temperature column) FROM the table temperature. If there are 4 entries in the database, the query will return 4 readings. If there is 400,000 entries in the database, it will return 400,000 readings.
We can limit the number of returned rows to 800 with the query as follows;
SELECT `dtg`, `temperature` FROM  `temperature` LIMIT 0 , 800
This adds in the LIMIT 0,800 specifier which returns 800 rows starting at row ‘0’.

800 Temperature Readings
However, this is probably not satisfactory as in the case of the data we have recorded in our Python script, there is only a gap of 10 seconds or so between readings. This restricts us to just over 2 hours worth of recording and the values start when the recording started, so our returned values will never change.
We can improve on this by sorting the returned values and therefore take the most recent ones with the following query;
SELECT `dtg`, `temperature`
FROM `temperature`
ORDER BY `dtg` DESC
LIMIT 0,800
Here we order the returned rows by dtg descending. Which means the most recent date/time value is the one at row ‘0’ and we will capture the most recent two hours worth of readings every time we run the query.
800 Latest Temperature Readings
Of course in the case of the data that was in the database, this is a fairly ‘booring’ set with little variation.
We now have an efficient number of data points being returned to the web browser to graph, but we only see two hours worth of data. It could be argued that the fidelity of reading every 10 seconds is a little high, and if we were to return values for every minute that would be adequate. As an interesting exercise we can return that type of information using our current data set by using a slightly more sophisticated MySQL query;
SELECT
ROUND(AVG(`temperature`),1) AS temperature,
TIMESTAMP(LEFT(`dtg`,16)) AS dtg
FROM `temperature`
GROUP BY LEFT(`dtg`,16)
ORDER BY `dtg` DESC
LIMIT 0,800
Here we are telling MySQL to average our temperature readings (AVG('temperature')) then round the number to 1 decimal place (ROUND(AVG('temperature'),1)) and label the column as ‘temperature’ (AS temperature). At the same time we take the left-most 16 characters of our dtg value (LEFT(dtg,16)) and then convert them back into a TIMESTAMP value (TIMESTAMP(LEFT('dtg',16))). This quirk allows us to eliminate the seconds from our ‘dtg’ values and replace it with ‘00’. We then label the column as ‘dtg’ (AS dtg ).
Now comes the interesting part. We group all the rows that have the same left-most 16 characters (in other words any dtg value that has the same year, month, day, hours and minutes). This has no effect on the dtg values of course, but the temperature values for all the rows with identical dtg’s get mashed together. And in this case we have already told MySQL that we want to average those values out with our earlierROUND(AVG('temperature'),1) statement.
If we consider the resulting graph, it looks remarkably similar to our original one that included over 3000 points;
800 Temperature Readings Averaged at 1 Minute Intervals
The end result has been a ‘smoothing’ of sorts. I can’t claim that it is any better or worse, but it certainly represents the way the temperature varied and does so in a way that won’t break the browser.

The post above (and heaps of other stuff) is in the book 'Raspberry Pi: Measure, Record, Explore' that can be downloaded for free (or donate if you really want to :-)).

5 comments:

  1. About to try this; Thanks for the tutorial!

    ReplyDelete
  2. I seem to end up with a graph that's labeled correctly, but has no data.
    Temp probes are reading correctly but the data isn't being entered into the database.
    The only error I'm seeing is when s_temp.py is run manually, NameError: name "con" is not defined
    Anyone have any ideas?

    ReplyDelete
    Replies
    1. It would seem as if you have the web page working pretty well, but for whatever reason the data is not being written to your database from the python script. The error you report would be an indicator that this line "con = mdb.connect('localhost', 'pi_insert', 'xxxxxxxxxx', 'measurements');" isn't doing what it's supposed to do fore some reason. I would recommend doing a sanity check for a slight syntax error if possible or check the details for the username, password and database name (I haven't stated it explicitly in the post (apologies), but these should be your own details, and not necessarily those in the script (for example the password 'xxxxxxxxx' is probably not the password that you would choose)).

      Delete
  3. Thanks for the reply, I'm now able to see the recordings of 3 probes.
    How are the graphs viewed? The php command executes the file, but the curl command cannot find it (404 not found) even when the address is correct.
    Is there a seperate program to view the graphs as illustrated?

    ReplyDelete
    Replies
    1. Good progress! The 'Explore' section describes the php file that is the web page for the project. You should be able to browse to this file on your web browser and have it display. If you're getting a 404 then either your web server isn't accessible / working or the file isn't there. Have a check and see if there might be something weird going on there.

      Delete