My Pi Description

My Experiences With the Raspberry Pi -- Tracking My Learning -- My Pi Projects

Tuesday, June 25, 2013

A Raspberry Pi Thermometer - My Software To Display Temperature

Let's back up for a moment. Two posts ago, I indicated that the Raspbian and Occidentals distributions have support for the 1-Wire interface and the DS18B20 temperature probes. We see this implemented in the kernel modules w1-gpio and w1-therm. I said that the only support I knew of was to read the temperature, and that you could not write to the device to change the resolution or set temperature limits. There is one other limitation I forgot to mention: You can only use GPIO 4 to interface the device to the Pi - no other choice is possible.
I wrote a python script using my two temperature sensors. It takes repeated measurements but is not greatly practical as it does not store the measurements. I'm going to add that capability next. I guess I'll start by writing the results to a file. I'm going to investigate RRDTool which is a way to put the results into a database. The program includes graphing capabilities, which sounds fascinating.
I kind of overloaded my script with a lot of stuff, because, after all, I'm doing this to learn. I don't really have a pressing need to make temperature measurements. I do this for fun. Luckily, I don't have to program to please a client, or employer, or to comply with any deadline. The Pi, along with python programming, is just an adult toy.
Functionally, the script asks the user how frequently to make measurements and which of the two probes to use. The program outputs each measurement to the terminal screen along with the date and time and the sensor choice. It also outputs the temperature, along with the date and time, to my 16 character by 2 line LCD display. The user terminates the script by issuing a keyboard interrupt (CTRL-C) or by pressing the button on the breadboard. The script then tells you when you started the run and the duration of the run. Here is a sample from a very short run:
Screen Shot of Terminal Output After a Short Run of the Script
Readout on the LCD Display
Without further ado, here is the script:
I would like to talk about what I find interesting about the script. I'll take it from the top and work down.
Lines 17 - 23: We need to import quite a few modules. I'll come back and talk about the commented out module in line 19 later. Line 23 is the module I wrote that has the functions to control the 16X2 LCD display. You can see this code on my blog entry "16 X 2 LCD Base Code" of March 23, 2013.
Lines 25 - 36: Here's a bit of extravagance. The only lines here that are necessary are lines 34 and 35. All the other lines in the function are superfluous. This function looks to see if the two modules w1-gpio and w1-therm are loaded in the kernel. If not, it loads them. When you load the modules, run the script. and then terminate the script, the modules stay loaded in the kernel. If you run the script again, it's OK if the script says to load them, if they are already loaded. My checking to see if they are loaded is unnecessary. But, I'm not doing this for a living, so a few extra lines is no big deal.
When I saw lines 34 and 35 in every other python program dealing with the DS18B20, I was very curious about that Linux command "modprobe". What did it do? I found this site.It has everything I needed to know: how to load modules, remove modules, and see what modules are present in the kernel. I wanted to use this information in my script, thus all those extra lines.
The loadmodules function demonstrates two methods to run a Linux command in Python. One uses the Popen method in the subprocess module (line 29), and the other uses the system method in the os module (lines 34 and 35). The second method is useful for commands that do not return anything you are interested in knowing about. It would be the one to use if you wanted to run "cd" to change directory. If you wanted to list the contents of the directory using "ls", the Popen would be the one to use. To see if the w1-gpio and wi-therm modules are loaded we issue the "lsmod" command, which lists the loaded modules like "ls" does for directories and files. The output is redirected to the stdio using the "stdio = subprocess.PIPE" optional argument. In the same way, we redirect any error to the stdio.
There appears to be an error in either line 31 or lines 34/35. But they are correct. If the two modules are loaded, the output of "lsmod" shows the module names with the underscore. When we load the modules, we use the dash in the names. Strange.
Lines 38 - 67: This function is rather long because it captures the occurrences of the w1-slave file failing to open. I talked about this in previous posts. If that glitch never occured, only six lines would be necessary (38, 47, 48, 63, 64, and 65). We use the Popen method to read the two line w1-slave file in line 47 (see the last post to see examples of these two lines). The output is redirected to the stdio and we read the contents in line 48. We do not have to bother to close the file. Lines 47 and 48 are a good alternative to using the open command, followed by the read command, and the close command. Note in line 48, we also read any error, such as ".....file not found" and can easily deal with the error. Using the open command, if the file is not found, then an exception is thrown and we have to handle that exception. Not a big deal, but the Popen approach seems much more efficient.
If an error occurs, my approach to recover is to unload the modules using the modprobe -r command and then reload them after a short delay. I wait 10 seconds at the end to give plenty of time for the modules to load. I tried to recover from the error by simply rereading the file (reissuing the Popen command), but that did not work. Once the error occurred, it kept occurring until the script was terminated. Unloading and reloading the modules does seem to work. I only do the unload and reload module process three times. If there is still an error, the script stops, by the statement raise(IOError). We'll talk about this statement when we discuss the main program. For my own information, I keep track of the occurrences of w1-slave failing to open in the variable glitches.
Lines 69 - 83: The print2display function simply takes the current time and the temperature and outputs the former to line 1 and the latter to line two of the LCD display. I found that those 10ms. delays were required to keep the display from going berserk after a time.
One of the fun aspects of this project was playing with the methods in the datetime module. The current time is determined by the now method in the datetime.datetime module and passed to the print2display function. The strftime method is a way to format the time in a variety of ways. Look here at section 8.17. in the Python documentation.
Lines 85 - 104: This is another example of throwing something in for the fun of it. Before the program terminates I wanted to print the duration of the run. Why do this? Simply because I wanted to figure out how to do it. I had read about the method timedelta in the datetime module in the Python documentation but could not understand it. That was because I thought timedelta took two date/times and reported the difference - just what I wanted. I was wrong, timedelta applies a time difference to a date/time, and reports a new date/time. I did make use of timedelta in the commented out line 92. More on this later.
At the beginning of the main part of the program I save the current time in the variable starttime. To use starttime in my function, I declared it a global variable. I could have easily passed the value of starttime to the function totaltime. I have a feeling that would have been the preferred method. To find the elapsed time, I simply subtract the starttime from the time at the end of the run and make a string of the result.
Now is a good time to discuss the commented out line 92 and the commented out line 19. I left those lines in the script for the purpose of discussing testing. You should test as much of your scripts as possible to assure they will work under various circumstances, and, for this script, will work with long duration runs. The string in line 97, the value of elapsedtime can look very different if the run time is over or under a day. Let's say the run time was 23 hours, 59 minutes, and 59.99 seconds. The string value of elapsedtime will be '23:59:59.990000'. If the run duration is one minute longer, elapsedtime looks like this: '1 day, 0:00:59.990000'. Quite a bit different. For testing this, I didn't want to make a run of over one day, so that's is where I used line 92. Using timedelta, I just added 24 hours to elapsedtime to get the string with the 1 day in it. Worked like a charm. You can see how I parsed the string value of elapsedtime to print the time in days, if needed, then hours, minutes, and seconds.
Lines 106 - 116: There is a momentary switch on the breadboard (that square job with the number 4 printed on it). It's purpose, here, is to provide a second way to exit the script. Checking the switch position is a matter of looking at the GPIO pin connected to the switch. If it is a logic high, the switch is not pressed (pin is pulled up by a resistor to 3.3V). Pressing the switch grounds the pin. If the function sees the pin low, it waits 200ms. and looks again. If the pin is still a logic low, it assumes the switch is really being pressed. The final action is the statement raise(KeyboardInterrupt). I'll reserve comment about this until later.
The Main Program:
Lines 131 - 137: One of the elements I really wanted to incorporate into my script was error handling, and error creating. These seven lines are a small manifestation of error handling which is implemented with the try and except statements. When a script encounters an error that is not "handled" within the script, it just stops and you receive an error message. The try and except statements are what you use to "handle" error messages. If an error occurs while processing the try block (the indented code below the try: statement), execution moves from the try block to the except block. Once the code in the except block is executed, execution moves to the code beyond the except block. For example, when asked to input a value to measurement_interval you input a letter rather than a number. Without the error handling, python will stop the script because measurement_interval expects a float, not a string literal. Python throws an exception, and prints: "ValueError: could not convert string to float" Since we don't want the program to stop because of a simple keystroke error, we trap the error by executing the except block. That block has the one word command of "pass", which means do nothing. The except block must have something in it, so "pass" suffices. So, if you press a letter instead of a number, absolutely nothing happens and you go back to the beginning of the while loop. Once you enter any number, including zero, you exit the loop with the break command with a good value for measurement_interval.
Line 125: These are the sub-directories representing my two temperature sensors. The first one is the breadboard sensor while the second one is on the end of the cable. Since this is all a learning experience, and I was reading about dictionaries, I was going to make this line: device_id = {breadboard: "28-00000400d39d", cable: "28-000004986cbb"}. But, I didn't get around to it. Tuples are pretty efficient though.
Lines 152 - 187: Here is the main loop of the script. I want to discuss the error handling first. The entire while loop is a try block so if any error occurs while executing within the loop, including execution within one of the functions, execution will jump out of the try block (thus out of the while loop) to one of the except blocks following the try block. The first two except statements look for specific errors. The first except statement looks for a keyboard interrupt, as you get by typing CTRL-C. This is one of the two normal ways I have provided to end the script. If, indeed CTRL-C was pressed, the code within that first except block (line 178) is processed and execution passes to line 189, bypassing the next two except blocks.
When executing the read_temp_raw function, if after three attempts we fail to open the w1-slave file, we get to the statement: raise(IOError) (line 67). The raise statement actually creates an error, in this case, IOError. If that indeed occurs, we jump out of the read_temp_raw function, out of the while loop in the main program, and land in the except(IOError) block. After executing the two print statements, we jump to line 189.
If some other, unanticipated, error occurs, execution passes out of the try block, bypasses the first and second except blocks, and lands in the third except block. Any error except a keyboard interrupt, or an IOError will be processed by that bare except statement. The print statement says we have an unexpected error and prints out what that error is. Once this is done, we move to line 189.
Within the while loop
Now let's look within the while loop. If the read_temp_raw function can open w1-slave, it returns the two line contents of w1-slave as a two element list. We check to see if the first line ends in 'YES'. If it does not, which seems to be the case about half the time, we wait a short time (the value of the variable short_wait) and go back to the beginning of the while loop to try again with a new read of w1-slave. If we do find a 'YES' we get the contents of the end of the second line following 't='. If 't=' is not found (rare if ever), we do like we do if 'YES' is not found: simply wait a short time and return to the top of the while loop and try again. If we do find 't=' we read the five digit number following it. That number is divided by 1000 to get the temperature in Centigrade and then converted to Fahrenheit.
After the temperature is found the current time is determined. The temperature and time along with the probe in use is printed to the screen and sent to the print2display function. It was rather a fun endeavor to come up with that print statement. I talked about the string formatting method, strftime, previously. Finding the unicode value ((hex B0) for the degree character was a challange. Finally, the time to wait between measurements is set to the value of measurement_interval inputted by the user.
Previously, I had the code to check the switch in the while loop in the main part of the program. Also, the wait between measurements was implemented, simply, with a sleep statement with the value of measurement_interval. This meant that if measurement_interval was a long time, in minutes or hours, I would have to press the switch at exactly the right time to catch the code outside of the sleep time, or press the switch for a long, long time. With the code in lines 170 - 174, I check the switch every second so the longest time to press the switch before the script stops is one second (actually 1.2 seconds to account for the wait statement in the check_switch function). It is necessary to check the switch in line 170 before getting into the small while loop in case the user selected zero seconds for measurement_interval.
I have just discovered a glitch in this script. If the user selects a negative number or a non-whole number for measurement_interval, that while loop will never get to exactly 0. I'll have to fix that. I should have tested for that before
Finally, how does the switch provide an exit from the script? It does so by the raise(KeyboardInterrupt) statement in line 116. If the switch is pressed, the effect is the same as if the user typed CTRL-C.

Monday, June 24, 2013

A Raspberry Pi Thermometer - Interpreting the Data

The W1-gpio and W1-therm modules create the following directory structure: /sys/bus/w1/devices followed by a directory tree for each 1- Wire device on the bus. These directory names are ids of the devices in hex. The DS18B20's ids start with 28-, so the two directories of my devices have the names 28-00000400d39d and 28-000004986cbb. Each of these directories contain a file called w1_slave. When queried, the DS18B20 returns 9 bytes of data which is placed into the w1_slave file. We get our temperature by reading this file.
w1_slave is not a file like a text or python file. Successive reads can contain different data, but the modified date/time of the file does not change. It's modify date/time reflects the date/time that the W1-gpio and W1-therm modules were loaded into the kernel.
Let's take a look at the contents of the w1_slave file. The following consists of two successive readings of that file using the cat command:
Notice the top line, everything before the "crc=". This is the output from the device in hex. The first two bytes represents the temperature in LSB followed by the MSB. Here, the temperature is 0166h. Convert that to decimal and multiply that number by 62.5 and you get 22375 which you see on the second line after the "t=". Divide this number by 1000 to get the temperature, 22.375 in degrees centigrade. The last byte returned is the CRC (cyclic redundancy check) which is calculated by the device based on the other eight bytes. Following the "crc=" you have the CRC calculated by the software. If that matches the CRC returned by the device, the software says "YES", otherwise "NO".
Note the second reading of w1_slave. Here the file says "NO". Note that the CRC returned by the device is 2Dh as before, but the software calculates ECh. Also note that the seventh byte returned by the device is 2Ah not 0Ah. The sixth, seventh, and eighth bytes are called "reserved" in the DS18B20's documentation. No further details are given except that they can not be overwritten. The sixth byte is always FFh and the eighth byte is always 10h. It does not say what the seventh byte should be. But, I have noted that, usually, if there is a "NO" at the end of the first line, the seventh byte is 2Ah, if "YES" it is always 0Ah. I say "usually" because, once in a while, we get a really lousy return when reading w1_slave. It looks like this:
I indicated in my previous post that this reading of w1_slave "almost works", and I'm not talking about whether there is a "YES" or a "NO" in the file, but something worse. I can make 10,000, or 20,000, successive measurements by reading w1_slave, and the next attempt will fail. Python says the file does not exist, but when you look, the file is still there. A very frustrating problem. I "think" I have solved that problem by unloading and reloading the W1-gpio and W1-therm modules, within my programs, when this failure is detected.
Enough of this. The next post will reveal the python program I wrote to use these temperature sensors.

Friday, June 14, 2013

A Raspberry Pi Thermometer - Software Introduction

The best place to start with the DS18B20 1-Wire temperature sensor is Simon Monk's "Adafruit's Raspberry Pi Lesson 11. DS18B20 Temperature Sensing".
If you have a recent Raspbian or Occidentals distribution, support for the the 1-Wire interface and the DS18B20 temperature sensor is built in. If you don't use a distro that has this support, you would probably have to write your own. Because of the timing requirements, I don't think you could write it in Python, but would have to write it in C/C++. As it is, using the Raspberry Pi's GPIO and the 1-Wire/DS18B20 software modules almost works. I have a couple of python scripts that make temperature measurements about two seconds apart and report the result. After a while, maybe one hour, eight hours, or 24 hours, the program just quits. I spent many hours trying to solve that problem. This project would be better implemented on a microcontroller like the ATMega on the Arduino and the Gertboard.
If you want to know more about the DS18B20 and 1 Wire interfaces you need the "DS18B20 Programmable Resolution 1-Wire Digital Thermometer" data sheet from Maxim Integrated. To get the datasheet from this blog, I could not make the link work properly. It always redirected my to the wrong URL so just do a Google search on DS18B20. Look for URL: datasheets.maximintegrated.com/en/ds/DS18B20.pd. It probably will be the first entry. FYI: The datasheet for the device from Dallas Semiconductor is actually the same datasheet.
From the datasheet, you will find that the device has programmable resolution, the ability to set low and high temperature limits, and can report whenever the temperature is outside of those limits. Quite a lot of capability for a device about the size of a pea. You may also connect as many temperature sensors, and other 1-Wire devices, as you like onto that one wire. So, how does the computer decide what device to communicate with? Each device has a unique 64 bit code programmed into non-volatile memory. No matter when or where the device was made, or what type of 1-Wire device you have, it's code will not be repeated on any other device. It's like Mac addresses for computer devices.
The 1-Wire software modules are not automatically loaded upon Raspberry Pi start-up. You use the Linux command "modprobe" to load them. The modules are "w1_gpio" and "w_therm". Loading these two modules also load the "wire" module, which, in turn, loads the "cn" module. As far as I can find out, the only thing you can do with these software modules is to read the temperatures from the devices. There is no documentation that I have been able to find (I posed the question on the Raspberry Pi and Adafruit forums) that tells how you can change the resolution, or set lower and upper temperature limits, and report temperatures out of range.
I think I have given enough background, so rather than me going on and on about how you use the 1-Wire modules, I suggest you look at Simon Monk's lesson. I'll have some examples of use of the temperature sensors in my following blogs.

Saturday, June 8, 2013

A Raspberry Pi Thermometer - The Hardware

This is my first post in quite a while. I have, however, been busy with my Pi - just havn't been writing about it. There will be upcoming posts about my Gertboard and my camera remote control/motion detector project. The project uses the ATmega microcontroller on the Gertboard, so I got to use the Arduino IDE (Integrated Development Environment) and program in C rather than Python. I'll have a lot to say about the Gertboard, and not all I'll say is flattering. This post, however, is about a project that does not use the Gertboard, just the Pi and the breadboard that you see in the header of this blog, with the addition of temperature sensors.
I purchased two temperature sensors from Adafruit, both are DS18B20 1 wire devices. The only difference between them is one looks like a TO-92 package transistor and the other is packaged into metal, waterproof, cylindrical case (6 mm diameter, 30 mm long) with a 36" integrated, shielded, cable. Adafruit also sells a higher temperature version. Actually, what makes it a higher temperature device is that it has a PTFC cable that allows it to to be used up to the 125°C rating of the sensor. The sensor is the same. The less expensive version, I purchased, has a PVC cable. Adafruit suggests not to expose the PVC cable to over 100°C.
Both versions (we'll consider the two waterproof versions as the same), are the same electronically, and will measure between -55°C to 125°C (-67°F to +257°F). They measure with 9 to 12 bit resolution. The more bits, the longer it takes between measurements. The default is 12 bit resolution and it can take a measurement about every 750ms. The remarkable thing is they only have three leads (the waterproof model has the cable shield brought out along with the three leads). One lead is ground, another is power which can be from 3.0Vdc to 5.5Vdc. The third pin is for data, used to write to and read from the device. The data line is "open collector" (when not driven to a logic low, acts like it is not connected to the circuit), therefore it is necessary to include a "pull-up" resistor between the data pin and the source of power that the device connects to (in the case of RaspberryPi, 3.3V). The open collector configuration allows you to connect the data pin of more than one device to the same Pi GPIO pin (of course, only one device can talk to the Pi at any one time). Adafruit, ships the devices with a 4.7K resistor. In fact, it is possible to ignore the power pin and power the device from the data pin (via the pull-up resistor. This mode complicates the timing and thus the programming, so I don't think I will try that feature.
My project is installed on my breadboard that has a 16 character by two line LCD display and a push button switch installed. Please see my previous blogs where I use these devices. My projects with the temperature sensors, detailed in subsequent blogs include:
1.    Making a temperature reading every two seconds and reporting the readings, along with the date and time, to the terminal
2.    Doing the same thing but also displaying temperature, date, and time to the LCD display
3.    Doing all the same but making a reading every minute
4.    Doing all the same but logging the results to a file.