Thursday, 5 March 2015

Raspberry Pi GPIO Sensors Part 2: Record

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.
---------------------------------------
This is the second of three posts working through a project looking at Measuring Recording and Exploring information via the GPIO pins on the Raspberry Pi. The first can be found here.

Please be aware that the python script in this current post differs significantly from the original posted (which was not a clever piece of code). Many thanks to reader 'salmon' for suggested fixes!

Record

To record this data we will use a Python program that uses our sensor like a trigger and writes an identifier for the sensor into our MySQL database. At the same time a time stamp will be added automatically. You may be wondering why we’re not recording the value of the sensor (0 or 1) and that’s a valid question. What we are going to use our sensor for is to determine when it has been triggered. For all intents and purposes, we can make the assumption that our sensor represents a device that can be in one of two states. We are going to be interested when it changes state, not when it is steady. In particular we are going to be interested in when it changes from low to high (0 to 1) and therefore represents a ‘rising’ signal.
for this project to work, our Python script has to run continuously and will only write to our database when triggered.

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 ‘events’ and the number of columns is ‘2’.
We will use two columns so that we can store an event name 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 ‘event’ and the type is ‘VARCHAR’ with a ‘Length/Values’ of 30.
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 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 ‘event’ name 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’.
The event names are simply descriptions of the type of events we want to capture, so we will use a variable type ‘VARCHAR’ which is for characters. We can also specify the maximum length of the information stored in the database to make things a little more efficient. In theory we could use the ‘CHAR’ type which is more efficient, but in this instance I prefer ‘VARCHAR’ which will allow the length of the recorded information to be flexible.

Record the events

The following Python code (which is based on the code that is part of the great blog series on FIRSIM) is a script which allows us to check the state of a sensor and write an entry to our database when an event occurs.
The full code can be found in the code samples bundled with this book (events.py).
First a quick shout-out and thanks to the reader ‘salmon’ who kindly suggested a significant change to the original code which changed it from a CPU killing ‘loop of doom’ to a far more well behaved script.
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Import the python libraries
import RPi.GPIO as GPIO
import logging
import MySQLdb as mdb
import time

logging.basicConfig(filename='/home/event_error.log',
  level=logging.DEBUG,
  format='%(asctime)s %(levelname)s %(name)s %(message)s')
logger=logging.getLogger(__name__)

# Function called when GPIO.RISING
def storeFunction(channel):
  print("Signal detected")
  
  con = mdb.connect('localhost', \
                    'pi_insert', \
                    'xxxxxxxxxx', \
                    'measurements');
  
  try:
    cur = con.cursor()
    cur.execute("""INSERT INTO events(event) VALUES(%s)""", ('catflap'))
    con.commit()

  except mdb.Error, e:
    logger.error(e)

  finally:
    if con:
      con.close()

print "Sensor event monitoring (CTRL-C to exit)"

# use the BCM GPIO numbering
GPIO.setmode(GPIO.BCM)

# Definie the BCM-PIN number
GPIO_PIR = 2

# Define pin as input with standard high signaal
GPIO.setup(GPIO_PIR, GPIO.IN, pull_up_down = GPIO.PUD_UP)

try:
  # Loop while true = true
  while True :

    # Wait for the trigger then call the function
    GPIO.wait_for_edge(GPIO_PIR, GPIO.RISING)
    storeFunction(2)
    time.sleep(1)

except KeyboardInterrupt:
  # Reset the GPIO settings
  GPIO.cleanup()
This script can be saved in our home directory (/home/pi) and can be run by typing;
sudo python events.py
The observant amongst you will notice that we need to run the program as the superuser (by invoking sudo before the command to runevents.py). This is because the GPIO library requires the accessing of the GPIO pins to be done by the superuser.
Once the command is run the pi will generate a warning to let us know that there is a pull up resister fitted to channel 2. That’s fine. From here if we move our magnet towards and away from the sensor we should see the Signal detected notification appear in the terminal per below.
pi@raspberrypi ~ $ sudo python event.py
Sensor event monitoring (CTRL-C to exit)
event.py:23: RuntimeWarning: A physical pull up resistor is fitted on this ch\
annel!
  GPIO.setup(GPIO_PIR, GPIO.IN, pull_up_down = GPIO.PUD_UP)
Signal detected
Signal detected
We should then be able to check our MySQL database and see an entry for each time that the sensor triggered.
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 measurements;
import RPi.GPIO as GPIO
import logging
import MySQLdb as mdb
import time
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.
Then the code sets up the logging module. We are going to use the basicConfig() function to set up the default handler so that any debug messages are written to the file /home/pi/event_error.log.
logging.basicConfig(filename='/home/event_error.log',
  level=logging.DEBUG,
  format='%(asctime)s %(levelname)s %(name)s %(message)s')
logger=logging.getLogger(__name__)
In the following function where we are getting our readings or writing to the database we write to the log file if there is an error.
Which brings us to our function storeFunction that will write information to our database when called later;
def storeFunction(channel):
  print("Signal detected")
  
  con = mdb.connect('localhost', \
                    'pi_insert', \
                    'xxxxxxxxxx', \
                    'measurements');
  
  try:
    cur = con.cursor()
    cur.execute("""INSERT INTO events(event) VALUES(%s)""", ('catflap'))
    con.commit()
    
  except mdb.Error, e:
    logger.error(e)
    
  finally:    
    if con:    
      con.close()
This is very much a rinse and repeat of the function found in the single temperature project. We configure our connection details, connect and write the name of this particular sensor (‘catflap’) into the database (Remember that when we store an event name into the database the timestamp dtg is added to the record automatically.). We then have some house-keeping code that will log any errors and then close the connection to the database.
The program can then issues the GPIO commands that start the interface to the sensor and get it configured (the code below has been condensed for clarity);
GPIO.setmode(GPIO.BCM)
GPIO_PIR = 2
GPIO.setup(GPIO_PIR, GPIO.IN, pull_up_down = GPIO.PUD_UP)
The GPIO Python module was developed and is maintained by Ben Croston. You can find a wealth of information on its usage at theofficial wiki site.
GPIO.setmode allows us to tell which convention of numbering the IO pins on the Raspberry Pi we will use when we say that we’re going to read a value off one of them. Observant Pi users will have noticed that the GPIO numbers don’t match the pin numbers. Believe it or not there is a good reason for this and in the words of the good folks from raspberrypi.org;
While there are good reasons for software engineers to use the BCM numbering system (the GPIO pins can do more than just simple input and output), most beginners find the human readable numbering system more useful. Counting down the pins is simple, and you don’t need a reference or have to remember which is which. Take your pick though; as long as you use the same scheme within a program then all will be well.
In our case we are using the BCM numbering (GPIO.setmode(GPIO.BCM)).
We then assign the GPIO channel as GPIO 2 to a variable (GPIO_PIR = 2).
Then we configure how we are going to read the GPIO channel with GPIO.setup. We specify the channel with GPIO_PIR, whether the channel will be an input or an output (GPIO.IN) and we configure the Broadcom SCO to use a pull up resistor in software (pull_up_down=GPIO.PUD_UP).
Then the code executes an ‘endless’ loop that asks itself “Is TrueTrue?”. While it is the program essentially pauses while it waits for a signal from the sensor via the GPIO channel.
try:
  # Loop while true = true
  while True :

    # Wait for the trigger then call the function
    GPIO.wait_for_edge(GPIO_PIR, GPIO.RISING)
    storeFunction(2)
    time.sleep(1)
Ultimately True stops being True when the program receives a ‘break’ which can occur with a ‘ctrl-c’ keypress.
In the mean time we’re using wait_for_edge to look for a specific type of event. We specify the channel (GPIO_PIR) we want to look for and the type of change in the channel which is a rising edge (GPIO.RISING) (which signifies a change in state (going from 0 to 1)). When this occurs the program progresses and the storeFunction function is called (which will record the event in the database). Lastly we let the program sleep for 1 second (time.sleep(1)) which reduces the occurrence of false events in the case of the cat flap swinging backwards and forwards when closing. This is the equivalent of ‘debouncing’ a switch.
Finally we use the keyboard interruption of the loop to do some housekeeping and reset our GPOI ports;
except KeyboardInterrupt:
  # Reset the GPIO settings
  GPIO.cleanup()
This results in any of the GPIO ports that have been used in the program being set back to input mode. This occurs because it is safer (for the equipment) to leave the ports as inputs which removes any extraneous voltages.

Start the code automatically at boot

While we can run our script easily from the command line, this is not going to be convenient when we deploy our cat flap activity logger. The alternative is to automatically start the script using rc.local in a similar way that we did with ‘tightvncserver’ in our initial set-up.
We will add the following command into rc.local;
python /home/pi/events.py
This command looks slightly different for the way that we have been running the script so far (sudo python events.py). This is because we do not need to use sudo (since rc.local runs as as the root user already, and we need to specify the full path to the script (/home/pi/events.py) as there is no environment set up in a home directory or similar (i.e. we’re not starting from /home/pi/).
To do this we will edit the rc.local file with the following command;
sudo nano /etc/rc.local
Add in our lines so that the file looks like the following;
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi

# Start tightvncserver
su - pi -c '/usr/bin/tightvncserver :1'

# Start the event monitoring script 
python /home/pi/events.py

exit 0
(We can also add our own comment into the file to let future readers know what’s going on)
That should be it. We should now be able to test that the service starts when the Pi boots by typing in;
sudo reboot
Then check our MySQL database to see the events increment as we wave our magnet in front of the Hall effect sensor.
Nice job! We’re measuring and recording a change in a magnetic field!

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 :-)).

9 comments:

  1. I don't get it. You set callback for pin 2 and then you are in loop again reading status of pin 2. It doesn't make sense. You are killing CPU witg this loop. In such case you can remove callback and use wait_for_edge function.

    ReplyDelete
    Replies
    1. You are dead right. I care nothing for the CPU. However, The only reason I'm doing this is because I don't know any better. I will read up about the wait_for_edge function and will have a play. It sounds like a really good idea :-). Thanks for the feedback.

      Delete
    2. Well, first up, yes, you're right, a simple look using 'top' confirms it... The CPU IS getting hammered. Actually averaging approx 97% usage. Not good.

      Delete
    3. The simplest solution is to add time.sleep(1) to the loop and import time before the loop.
      The best solution is to use wait_for_the_edge

      Delete
    4. I concur. I've rewritten the code to use wait_for_edge and will be editing the book and this post to reflect the changes. It doesn't thrash the cpu and the code is much simpler. It's been a great learning experience :-).

      Delete
    5. Awesome. All done. Code changed, Book edited and blog post updated. I used a time.sleep as well to add a little debounce after the edge detect trigger. Many thanks again.

      Delete
  2. hello,i need some help to get a data of my ultrasonic in realtime then print in web interface.i read another application like about temperature but i did not understand.

    ReplyDelete
    Replies
    1. Hi Yanick. Sorry, I have never experimented with ultrasonic sensors before and I don't really have the time to branch off and experiment I'm afraid. Best of luck

      Delete