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.

4 comments:

  1. Hi :)

    nice blog, you're explaining everythin in a very detailed way, i like that.
    I have to questions, what is the standard Resolution choosen by the 1wire modules and coming with that, how often is it possible to read that temperature? In my experiments my script updated ~every second.
    Thanks for your answer

    ReplyDelete
    Replies
    1. You have a choice of 4 resolutions, 9 to 12 bits. The default is 12 bits. The maximum conversion time at 12 bits is 750ms. At 9 bits, it is 1/8 of that time. I would like to try changing the resolution but the 1-wire interface modules in Debian do not provide the means to write, only read. I would like to try writing my own interface to the temperature sensor, but that would be a chore. Don't think I would do it for the Pi, but for the Atmega microcontroller on my Gert Board.

      When I set the minimum time between measurements to 0, I get readings about 1 second apart. As you probably have seen, often when you read from the device, the CRC fails and you have to read again, and maybe read a couple of times before you get a valid reading.

      Since you have found this post of some interest, check out my extensions of the program where I graph the results and have added a graphical interface. Check out my recent posts at thepiandi.blogspot.com

      Delete
    2. Tanks for your fast response. That's exactly what I wanted to know.
      I'm a newbie at Python and Programming in general so I won't be able to write the interface on my own.. My next project is to read out data from a ADXL345 Sensor which ouputs values up to 100 times per second, I'm looking forward to that :)
      I like your blog(s), they all seem like a lot of work.
      You deserve more attention with all that, much luck :)

      Delete
    3. Hi
      Thanks a million for your comment. I wish the blog had more attention too. A lot of people land on it but comments are rare.

      I'm not familiar with the ADXL345, but I'm definitely going to check it out.

      Delete