Sunday, November 10, 2013

Tumble Dryer Remote Display - Part III

Welcome back to the third of my four part series on the tumble dryer remote display, a small project of mine, based on the Raspberry Pi. While in part one I tried to give you the idea what this is all about, the last posting was on the hardware of the interface. This time I will cover the software I created to read and interpret the data from the interface and build some sort of display from that. The final part will give some kind of outlook on where I (or maybe you, too) could be going from here by describing ideas and possible options that could improve the current version of the tumble dryer remote display. But first things first.

Shortly after getting my first Raspberry Pi, I decided to go with Adafruit's Raspberry Pi Educational Linux Distro for my electronic projects. I started using Occidentalis mostly because the changes I would have had to make for using it for what I had in mind were already done in this distro. While it wouldn't make much sense to use it "in the wild", it is perfect to just get started. (If you are new to the Raspberry Pi you might find Adafruit's Learning System section on it helpful, or at least interesting.) So an up-to-date version of Occidentalis v0.2 is what I used for this project.

One of the basic ideas for the software I wanted to build to do the actual remote display job was born when I was playing around with a DS18S20 temperature sensor and the 1-Wire bus shortly after getting the Raspberry Pi. The nice thing about using said bus is that the related (driver-)package presents the relevant data using what is called the the 1-Wire File System (OWFS): reading sensor data is as easy as reading from a file. So I created a very raw shell script that reads the file for the appropriate sensor, gets the temperature from it, and compile a simple HTML file with the data. Using the Apache web server of the running Linux distro and a simple cron job that triggered the shell script on a regular basis, I could check the temperature from any browser on my network.

A word of warning: since I was writing the file directly to the /var/www directory (instead of a job-related subdirectory) I had to update the permissions there - if I remember correctly. Since the whole "solution" was only meant to run on a protected network at home I neither was nor am concerned about the security issues here. But it is far from being even close to "best practice"! Please keep that in mind in case you are thinking about creating a similar project.

For the tumble dryer remote display, the basic approach for me was:

  • let cron demon trigger a piece of software
  • that collects and interprets the data,
  • then creates a HTML file from that
  • which can be accessed through the web server.

I decided to use Python to write the script needed, mostly out of curiosity because I am new to it and there is nothing like a specific problem to solve to start learning a new programming language. (It is easy to tell that I am neither an experienced programmer nor a Python Pro by having a look at the code - either at the end of this posting or by downloading the file.)

For the test runs of the first prototype I used some lines of code I had found in the thread Grumpy Mike's instructions for PCF8591 at the Raspberry Pi Forum. Parts of that code can still be found at the core of my script's function that deals with the I²C-bus device. It really helped me to understand what I was actually trying to do there. Apart from that, the step to create some basic data logging wasn't that big any more. The data collected was very important to answer the question which values represented an LED that was actually on, compared to LEDs that were off at the same moment.

Working out what I wanted to achieve with the program and thinking of ways to do so, I decided that for the time being I could live with detecting only few things of the setup "automatically". As I am planning to use the interface hardware in combination with different Raspberry Pis, a function to detect the revision number of the board is quite a nice thing to have. (It is actually important as the ID of the I²C-bus depends on it.) Apart from that one, the other values needed are hard coded, but I tried to keep the program manageable by assigning them to global variables (in the top section of the code). The most important ones are:

  • the I²C-bus address of the A/D converter and
  • the 1-Wire bus address of the temperature sensor. 

The current version of the script is made up from the functions taking care of:
  • the revision number of the Raspberry Pi board [getBoardRevision()],
  • the data from the temperature sensor [readTemperature()] (a kind of bonus),
  • the data from the A/D converter [readDisplays()] (basically what it all is about),
  • the converting of collected data into HTML code [buildHtmlFile()] (for the remote display).
The program uses the functions above to prepare and finally write the HTML code for the remote display to a file (/var/www/trocknerdisplay.html). On top of that, there is (still) a very basic function (doLogging()) to do some logging for testing and debugging.

example output for the remote display pageI am using the system's cron demon to run the program once per minute; the HTML file created will make the browser reload it once per minute, too. That way the remote display page is pretty much up-to-date most of the time. (I added the information when the HTML file has been updated at the bottom of the page which allows to see if the job itself did really run or if there is something wrong with it.)

Even though the text in the picture is in German, I am sure that it gives you the idea what the actual output looks like. (It is a screenshot made on an iPhone.) I have to admit that the layout might need some work. But for the moment it is - adequate.

Now that I have told you how the code works (or at least how I have found it to work for me so far) I should tell you about the tiny specialties I implemented. I cannot tell you about "best practice" here as I am not an professional or even an experienced programmer. All I can say is that I found those specialties quite helpful.

One thing you might have spotted is that I am using "special" values throughout the script, like 9999 if the Pi's board revision number could not be identified, or -99 if the communication with the interface failed. While I tried to write the code to handle those failures as gracefully as possible, the "special" values are interpreted too, and the appropriate hint is used to replace the status information in the output file (for the dryer as well as for the temperature).

The other thing I would like to point at is that I actually create the value that represents the status of the dryer's control panel LEDs, instead of using a one-time result. Having a look at a log file I found that the values of each of the A/D converters channels would hardly ever be exactly the same in a series of readings, even if the status of the corresponding LED had not changed. Further more, it could be the case that the LED status either just changed while reading or that it changed shortly before or after reading the channel's value. The idea here is to check all channels multiple times (25 times in the current code) and using the average value as the basis for interpretation.

Now that you know about the code I am using (at the moment), and about the hardware interface I created, you can see what the tumble dryer remote display is and how it works. In the final part of this series I will tell you about the changes I already have in mind, about some ideas I have what else could be done with the current setup, and which options I can think of that might be interesting or helpful if implemented.



For those who prefer having a look at the code here instead of downloading it, well, here you go. (The syntax highlighting is far from perfect, but it should work for most of the lines below.)

#!/usr/bin/env python

"""file name: dryer_remote.py

    read status leds from tumble dryer via ldrs/pcf8591
    over i2c and temperature via ds18s20 over 1-wire.

    sensor box and script designed to work on/with
    raspi (model b), board revision will be detected,
    the bus id will be adjusted accordingly. (this
    feature hasn't been tested yet.).
    id of 1-wire slave ds18s20 not checked in this
    version, so it has to be adjusted in the global
    init section below.

    parts of the script (reading from an i2c device)
    taken from raspi forum at http://is.gd/WW8c8q
    and from raspberry pi spy at http://is.gd/sdP911
    (reading the baord's revision number).

    file created by LordGU, 01.11.2013
    last modified by LordGU, 06.11.2013
    
    """


# import needed libraries
import smbus
import time

# global init 
readValues = [-1,-1,-1,-1,-1,-1,-1]
w1DeviceId = "10-000802542123"
i2cAddress = 0x48
boardRevDefault = "0000"

# reading board revision number
#
def getBoardRevision(revisionFallback):
    # Extract board revision from cpuinfo file
    thisBoardRevision = revisionFallback
    try:
        with open('/proc/cpuinfo','r') as cpuinfofile:
            for line in cpuinfofile:
                if line[0:8] == 'Revision':
                    length=len(line)
                    thisBoardRevision = line[11:length-1]
        cpuinfofile.close()
    except:
        thisBoardRevision = "9999"
    
    return thisBoardRevision


# temperature stuff goes here
#
def readTemperature(tempSensorId):
    thisTemperature = 0
    try:    # adjust path for id of used sensor if needed
        with open("/sys/bus/w1/devices/" + tempSensorId +
                  "/w1_slave","r") as w1tFile: 
            fileText = w1tFile.read()
            thisTemperature = float(fileText.split("t=")[1]) / 1000 
        w1tFile.close()
    except:
        thisTemperature = -99

    return thisTemperature


# i2c stuff goes here
#
def readDisplays(checkBus,checkAddress):
    # init
    displayValues = [0, 0, 0, 0]
    n_samples = 25
    thisBusId = 0
    runChecks = False

    # set bus id
    if (checkBus == "9999"):
        thisBusId = 99
        runChecks = False
    elif (checkBus == "0002") or (checkBus == "0003"):
        thisBusId = 0
        runChecks = True
    else:
        thisBusId = 1
        runChecks = True
    
    bus = smbus.SMBus(thisBusId)

    try:    # check if a device is available at "checkAddress"
        bus.write_quick(checkAddress)
    except:
        runChecks = False
    
    if runChecks:
        
        for b in range(0,n_samples):
            # repeat for all for input ports of chip
            for a in range(1,5):
                # select input of chip at "checkAddress" and read from port
                bus.write_byte(checkAddress, (0x40 | a) )
                displayValues[a-1] = (displayValues[a-1] + 
                                    bus.read_byte(checkAddress))
            time.sleep(0.05)
        
        for c in range(0,4):
            displayValues[c] = displayValues[c] / n_samples
    else:
        displayValues = [-99, -99, -99, -99]

    return displayValues
    
 
# html stuff goes here
#
def buildHtmlFile(computedValues):
    #init
    htmlDocument = []
    ledOnOff = 100
    pollError = -99

    htmlDocument.append( """<!DOCTYPE HTML PUBLIC
        "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
        <html>"""
        )
    htmlDocument.append( """<head>
                <title>RasPi Trockner-Monitor</title>
                <meta http-equiv="Content-Type" content="text/html;
                  charset=utf-8">
                <meta http-equiv="refresh" content="60">
            </head>"""
            )
    htmlDocument.append( """<body>
                <h1>Trockner-Monitor</h1>
                <h2>Trockner-Status</h2>
                <p>"""
                )

    if ((computedValues[2] == pollError) and (computedValues[3] == pollError)
        and (computedValues[4] == pollError)):
        htmlDocument.append("Fehler bei der Abfrage")
    elif ((computedValues[2] < ledOnOff) and (computedValues[3] < ledOnOff) and
        (computedValues[4] < ledOnOff)):
        htmlDocument.append("nicht in Betrieb")
    elif ((computedValues[2] > ledOnOff) and (computedValues[3] > ledOnOff) and
        (computedValues[4] < ledOnOff)):
        htmlDocument.append("Betriebsbereit")
    elif ((computedValues[2] > ledOnOff) and (computedValues[3] < ledOnOff) and
        (computedValues[4] < ledOnOff)):
        htmlDocument.append("Trocknen")
    elif ((computedValues[2] < ledOnOff) and (computedValues[3] > ledOnOff) and
        (computedValues[4] < ledOnOff)):
        htmlDocument.append("Abkühlen")
    elif ((computedValues[2] < ledOnOff) and (computedValues[3] < ledOnOff) and
        (computedValues[4] > ledOnOff)):
        htmlDocument.append("Ende")
    else:
        htmlDocument.append("Störung")
    
    htmlDocument.append( """</p>
                <h2>Temperaturwert</h2>
                <p>"""
                )
    
    if (computedValues[6] == pollError):
        htmlDocument.append("Fehler bei der Abfrage")
    else:
        htmlDocument.append((str(computedValues[6]) + " °C"))

    htmlDocument.append( """</p>
                <hr>
                <p>Letzte Aktualisierung: """
                )
    htmlDocument.append((computedValues[0] + ", " + computedValues[1] + 
                       " bis " + computedValues[7] + " Uhr"))
    htmlDocument.append( """</p>
            </body>
        </html>"""
        )
    return htmlDocument


# debug logging is done here - if activated
# with adjustments: could be used to do error logging,
#
def doLogging(logValues):
    try:
        with  open("/var/log/tumbledryer.log","a") as logFile:
            logFile.write(str(logValues))
            logFile.write("\n")
        logFile.close()
    except:
        pass    # something could be really wrong then

# _main_
#
readValues[0] = time.strftime("%d.%m.%Y")
readValues[1] = time.strftime("%H:%M:%S")
readValues[2:5] = readDisplays(getBoardRevision(boardRevDefault),i2cAddress)
readValues[6] = readTemperature(w1DeviceId)
readValues[7] = time.strftime("%H:%M:%S")

try: # write the HTML file
    with open("/var/www/trocknerdisplay.html", "w") as htmlFile:
        htmlOutput = buildHtmlFile(readValues)
        for line in range(0,len(htmlOutput)):
            htmlFile.write(htmlOutput[line])
    htmlFile.close()
except:
    pass    # maybe some error logging would make sense here

# only for testing and debugging
#
# doLogging(readValues)
#print(readValues)

# EOF

No comments:

Post a Comment