Sensors and Network Communication with the Raspberry Pi

Sensors and Network Communication with the Raspberry Pi
Dan Jackson, 2015.
Overview
This is a tutorial about sensors and network communication with the Raspberry Pi.
You will need:
•
•
•
•
•
•
•
•
•
•
•
a Raspberry Pi model B+
a power supply (5V to micro-USB)
a micro-SD card with Raspbian
a configured network connection (or otherwise an HDMI cable and compatible display,
USB keyboard, USB mouse).
solderless breadboard
a photoresistor (or light-dependent resistor, or LDR)
a Maxim Integrated DS18B20 1-Wire bus thermistor
a HC-SR501 PIR motion sensor
a 1 μF capacitor
some resistors (330Ω and 4.7kΩ)
some ‘jumper’ wires
Digital peripherals – PIR
In the previous tutorial, we read a simple button as a digital input. We can connect more
complex peripherals to our Pi with digital signal outputs, which can be read directly as an
input through the GPIO pins.
A passive infrared (PIR) sensor is a device which detects certain wavelengths of infrared
light radiated by hot bodies. The actual detector is only a single element, but the covering
focusses the light from a range of directions, meaning that anyone moving in front of the
sensor is likely to cause a change the light level seen by the sensor. This PIR sensor can
sense even tiny changes, and triggers the output signal wire if movement is detected.
Connect the PIR sensor as shown below – red is VCC 5V (note, not the 3.3V pin we’ve been
using before), black is GND ground, yellow is the OUT output signal:
PIR module connection
At the terminal command line, type:
cd ~/python
nano pir.py
As a reminder, this changes directory (cd) to the python directory under your home
directory (represented by a ~, if you don’t have the directory, you can make it first with
mkdir ~/python), then runs a text editor called nano to edit a new Python source file.
Type the following in to your Python file:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
try:
GPIO.setup(8, GPIO.IN)
print "Waiting for PIR to settle"
while True:
while
print
while
print
GPIO.input(8) == GPIO.HIGH: pass
"Waiting for movement..."
GPIO.input(8) == GPIO.LOW: pass
"Movement detected!"
finally:
GPIO.cleanup()
As a reminder, the import says we’re using a module (another Python file) RPi.GPIO and
we’re going to refer to it in our code as GPIO. The GPIO mode is set (GPIO.setmode()) to
say we’re addressing pins by their physical numbers (not GPIO index number), and the
finally block is guaranteed to run the GPIO cleanup() function – to make sure the pins
are in a known, safe, state – no matter how the try block ends (even if you press Ctrl+C to
terminate the program).
The main body of the code loops forever, first waiting (the pass in the while loop means
“do nothing”) until the PIR output is low (“no movement”), then waiting until it goes high
(“movement”).
Press Ctrl+X to exit Nano, press ‘Y’ to save the changes, and Enter to confirm.
sudo python pir.py
Remember that we have to sudo this command as it’s directly controlling the Pi’s
hardware. Wait until the PIR settles, then make a movement in front of the sensor, your
program should output this. Press Ctrl+C to stop the running command.
If you have completed this very quickly, you might want to build on what we did in the last
tutorial and add an LED or buzzer output for you “alarm” when there’s movement (or take
a photo from a webcam!)
Communication bus – thermistor
Up until now, our digital inputs have only been used to transfer a single state value
(e.g. movement/non-movement, button up/down). In digital systems, more complex data
often needs to be transported between two or more devices, in either direction, and using
as few connections as possible. Such a system is called a communication bus, and there are
several popular types of communication bus for sending data to, or receiving data from,
peripheral devices (e.g. SPI, I2C). We are going to use a “1-wire” bus to discover and
communicate with a thermistor temperature sensor. Build the circuit as shown below.
1-Wire bus thermistor
At the terminal, we need to test the 1-wire (w1) interface, type:
sudo modprobe w1-gpio
sudo modprobe w1-therm
ls -l /sys/bus/w1/devices/28-*
The modprobe command ensures the specified driver modules are loaded. Linux drivers
typically expose access to hardware as virtual files and directories (even the GPIO pins
through /sys/class/gpio/gpiochip0/!). In this case the DS18B20 thermistor 1-Wire
“family code” is 28 (hex), the * is a wildcard (which matches any letters in the filename, so
the “28-*" will find any files with names starting with “28-”). The last command should list
a single entry if your thermistor was found successfully – and this listing is for a directory.
Let’s look inside by listing (ls) the files:
ls -l /sys/bus/w1/devices/28-*/
You can see a few virtual file entries for this device, we’ll look at the contents of one called
w1_slave using the cat command (from concatenate, as it can output the contents of any
number of files back-to-back):
cat /sys/bus/w1/devices/28-*/w1_slave
Reading this ‘file’ actually causes the sensor to be read over the bus, and the contents
represent the values read from the sensor. The first line should end with “YES” if the data
seems valid (its CRC, or Cyclic Redundancy Check matched the data). The last part of the
second line should start with “t=” and a number – this is the current temperature, as just
measured, in 1/1000ths of a degree Celsius.
Knowing this, we can now access the temperature sensor from Python (we won’t even need
the GPIO library). At the terminal command line, type:
cd ~/python
nano therm.py
Type the following in to your Python file:
from os import system
from glob import glob
from time import sleep
def getDevice(family):
system('modprobe w1-gpio')
system('modprobe w1-therm')
devices = glob('/sys/bus/w1/devices/' + family + '-*')
if not devices:
return None
return devices[0] + '/w1_slave'
def readTemperature(device):
while True:
print "reading"
with open(device) as f:
lines = f.read().splitlines()
if lines[0].strip()[-3:] == 'YES':
break
sleep(0.25)
idx = lines[1].find('t=')
if idx <= -1:
return None
return float(lines[1][idx+2:]) / 1000.0
device = getDevice('28')
print 'Device:', device
if device:
while True:
print(readTemperature(device))
sleep(1)
The getDevice() function is the equivalent to the earlier terminal commands – it uses
system() calls to scan for devices, and glob performs wildcard matching to find the
resulting device. The readTemperature() function simply opens the “file” we saw earlier
and, if the CRC is OK, returns the temperature.
Press Ctrl+X to exit Nano, press ‘Y’ to save the changes, and Enter to confirm.
sudo python therm.py
Your program should now list temperature values. Press Ctrl+C to stop the running
command.
Analogue sensing – photoresistor
So far, we have only dealt with digital inputs – they were either considered as being
logically 0 (voltage close to ground) or logically 1 (a voltage higher than ground). Now let’s
look at an analogue (or analog) sensor – a photoresistor (or light-dependent resistor, or
LDR). A photoresistor has a variable resistance which depends on the amount of light that it
receives. The resistance of a component affects the voltage that drops across it, so being
able to measuring the voltage will allow us to indirectly measure the light level.
Determining a numeric value for a voltage level is called analogue to digital conversion (or
ADC). Many embedded microcontrollers (e.g. ones powering Arduinos) have dedicated logic
to perform precise ADC, but the Raspberry Pi does not have a direct analogue inputs.
However, it is possible to get a rough measure of the resistance on the photoresistor with
just a couple of components by using an RC charging circuit (RC is resistor/capacitor).
Wire your breadboard as shown below. This circuit uses a capacitor – a capacitor stores
electric charge, and its capacitance is measured in Farads (F). The resistance in the circuit
(from the fixed resistor and the variable, light dependent resistor) will limit how quickly
the capacitor charges. As it reaches about two-thirds charged, our digital input will start to
read logical 1 rather than logical 0. This means that if we discharge the capacitor, then let it
charge again, the time it takes to charge depends on the amount of light on the LDR. Note
that this 1 μF capacitor is an electrolytic capacitor, which has a polarity – it’s important that
you wire the negative end (labelled with a white stripe) appropriately (in this case, to
ground).
RC charging circuit for measuring an LDR
At the terminal command line, type:
cd ~/python
nano ldr.py
Type the following in to your Python file (the comment lines beginning with # are
optional):
import RPi.GPIO as GPIO
from time import sleep, time
def rctime(pin):
# Ground the pin to empty the capacitor
GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, GPIO.LOW)
sleep(0.2)
# Set as an input and begin timing
GPIO.setup(pin, GPIO.IN)
startTime = time()
# Wait until the pin goes high
while GPIO.input(pin) == GPIO.LOW: pass
# Calculate how long it took (measure the variable resistance)
elapsed = int((time() - startTime) * 1000000)
return elapsed
GPIO.setmode(GPIO.BOARD)
try:
while True:
ldr = rctime(5)
print ldr
sleep(0.25)
finally:
GPIO.cleanup()
The function rctime() times how long an RC circuit (connected to the pin specified) takes
to charge by performing the following steps:
1.
2.
3.
4.
5.
6.
Sets the pin as a low (grounded) output for 0.2 seconds. This drains the capacitor.
Sets the pin as an input.
Records the current time (startTime).
While the pin remains logically low (0), do nothing (pass) – the capacitor is charging.
Once the pin is logically high (1), stop looping – the capacitor is around two-thirds
charged.
Using the current time(), calculate the elapsed time (in microseconds) and return
that.
The main body of the code (in the try block) loops forever, displaying the result of the
rctime() function then waiting (sleep()).
Press Ctrl+X to exit Nano, press ‘Y’ to save the changes, and Enter to confirm.
sudo python ldr.py
Make the LDR vary its resistance by covering the board with your hand, or shining a light
on it – note how the output values change. Press Ctrl+C to stop the running command.
Logging a sensor value over time
We have a measure of the light level over time. Let’s make a version of this program which
logs it to a file. First, let’s copy our program:
cp ldr.py ldr-log.py
nano ldr-log.py
We’ll be using the current date and time from the datetime module, add a line at the top of
your Python file:
import datetime
Now, edit the main try block of your code to read:
try:
with open('ldr.csv', 'a') as fp:
while True:
ldr = rctime(5)
now = datetime.datetime.now();
timestamp = now.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
output = timestamp + "," + str(ldr)
print output
fp.write(output + "\n")
sleep(0.25)
A log file is opened in append mode (to add to the existing file contents), and Python’s with
operator makes sure that the file is closed when the inner while loop exits. For each
reading, an output line is prepared, made up of the current time to the millisecond
(timestamp) and the sensor value – separated by a comma. Comma-separated value (CSV)
files are a very common way of sharing data between programs. Run your program for a
while, vary the light level, exit your program and open the resulting CSV file (ldr.csv) in
something you can make a graph with (such as a line chart in spreadsheet software).
Connectionless network data
A simple analogy for networking would be a postal service, where each network interface
on a computer (LAN, WiFi, etc.) has a street address (IP address), and applications can be
sending things from or too a different flat/apartment number within the building (a
network port). In this analogy, the simpler of the two most common network protocols (at
the transport layer of the Internet), UDP (User Datagram Protocol), is a little like sending a
postcard from one room/building (port/IP address) to another. Much like postcards, UDP
packets are limited in size, have no guarantees on whether they’ll arrive in the same order
as they’re sent and, in fact, no guarantee on whether they’ll arrive at all.
Receiving UDP
In computing, a network socket is an end-point for the network (sending or receiving data).
This is a small Python program (udp-receive.py) to create a UDP (datagram) socket tied
(bind) to port 6000, which receives (recvfrom) and prints any data that arrives:
import socket
hostPort = ('', 6000)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(hostPort)
print "Waiting for incoming data:", hostPort[0] + ":" +
str(hostPort[1])
while True:
data, sender = sock.recvfrom(65536)
print ">", data
finally:
sock.close()
Sending UDP
In your current terminal, start the receiver (python udp-receive.py). Start a new
terminal connection (a new terminal window, or a new SSH session). Now use the netcat
utility to send UDP packets:
netcat -u localhost 6000
This reads lines from the user and sends them to the current computer’s (localhost)
loopback connection (this is a dummy network interface that the computer uses to talk to
services on itself, it has the IP address 127.0.0.1). Type some lines of text, check that they
appear in your receiver in the other window. Press Ctrl+C when you’ve finished.
This is a small Python program (udp-send.py) to do the same job – it creates a UDP
(datagram) socket, and uses it to send packets to the specified interface (host) and port:
import socket
hostPort = ('localhost', 6000)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
line = raw_input("> ")
if not line: break
sock.sendto(bytes(line), hostPort)
finally:
sock.close()
Test the sender (python udp-send.py), type a few lines, check they appear in the
receiver in the other window. Press Ctrl+C when you’ve finished.
Logging sensed data remotely
In the terminal window that has your receiver running, end it with Ctrl+C. Edit udpreceive.py and change the while loop to be:
with open('udp-receive.log', 'a') as fp:
while True:
data, sender = sock.recvfrom(65536)
print ">", data
fp.write(data + "\n")
This will append any received packets to a log file. Restart the receiver (python udpreceive.py).
Now, let’s copy the LDR logger (cp ldr-log.py ldr-send.py), and edit ldr-send.py.
Add a new import line:
import socket
Edit the main part of your code so that it reads as follows: (the two deleted lines start with
fp, and the four new lines start with hostPort and sock)
GPIO.setmode(GPIO.BOARD)
hostPort = ('localhost', 6000)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
ldr = rctime(5)
now = datetime.datetime.now();
timestamp = now.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
output = timestamp + "," + str(ldr)
print output
sock.sendto(bytes(output), hostPort)
sleep(0.25)
finally:
sock.close()
GPIO.cleanup()
These changes make it transmit the sensor data over the network, and leaves the logging to
the receiver. Run the code with sudo python ldr-send.py. The receiver can now log
data from any number of computers and sensors on the network, or could be made to act
on the incoming data (e.g. to control an output).
Network connections
Because of the limitations of UDP packets, Transmission Control Protocol (TCP) is often used
instead. TCP is a reliable, connection-based protocol, which means you open a connection
from one interface/port to another and, once established, the data is received as it is
transmitted (security issues aside), or the connection fails – the protocol handles error
detection, retransmission of lost packets, re-arranging out-of-order packets, etc.
Receiving TCP
This is a Python program (tcp-receive.py) to create a TCP (stream) socket tied (bind)
to port 8000, which accepts a connection (accept) and prints any data that arrives while
the connection stays open:
import socket
def ReadLine(sock):
line = ""
while True:
nextChar = sock.recv(1)
if nextChar == "\n" or nextChar == "": return line
elif nextChar != "\r": line += nextChar
hostPort = ('', 8000)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(hostPort)
sock.listen(5)
while True:
print 'Waiting on: ' + hostPort[0] + ':' +
str(hostPort[1])
connection, clientAddress = sock.accept()
print 'Connection from:', clientAddress
while True:
line = ReadLine(connection)
if line == "": break
print ">", line
print 'Closing connection...'
connection.close()
finally:
sock.close()
Sending TCP
In your current terminal, start the receiver (python tcp-receive.py). Start a new
terminal connection (a new terminal window, or a new SSH session). Now use the netcat
utility to start a TCP session and send data down the connection.
netcat localhost 8000
Then type some lines of text, check that they appear in your receiver in the other window.
If you press return on an empty line, the receiver will disconnect the connection and
netcat exits.
This is a small Python program (tcp-send.py) to do the same job – it creates a TCP
(stream) socket, starts the connection (connect()) and uses it to send lines to the
specified interface (host) and port:
import socket
hostPort = ('localhost', 8000)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(hostPort)
while True:
line = raw_input("> ")
if not line:
break
sock.sendto(bytes(line + "\n"), hostPort)
finally:
sock.close()
Test the sender (python tcp-send.py), type a few lines, check they appear in the
receiver in the other window. Press Ctrl+C when you’ve finished.
HTTP
Make sure you have your TCP receiver running in another window (python tcpreceive.py).
Make an HTTP (web) request to your TCP receiver using the curl command:
curl http://localhost:8000/
Take a look at the receiver window – the HTTP request came through as a load of text lines.
The HTTP protocol is actually pretty simple:
1.
2.
3.
The first line indicates the type of method (GET), the address (/) and the protocol
version (HTTP/1.1)
The remaining lines are header names and values (with a colon : separator)
The end of the request headers was a blank line
4.
(If there was a “Content-length” header, there would be a request body of that size
following the header)
Copy your tcp-receive.py program as server.py:
cp tcp-receive.py server.py
nano server.py
Change your inner while loop (after “Connection from” and before “Closing connection”)
to read:
requestLine = ReadLine(connection)
while True:
headerLine = ReadLine(connection)
if headerLine == "": break
requestAddress = requestLine.split(" ", 2)[1]
print "REQUEST: ", requestAddress
responseBody = "Hello world! You asked for: " + requestAddress
response = "HTTP/1.0 200 -\r\nContent-length: " +
str(len(responseBody)) + "\r\n\r\n" + responseBody
connection.send(response)
Stop tcp-receive.py if it’s still running, and start server.py, then make an HTTP
request curl http://localhost:8000/, repeat but change the path at the end of the
URL (e.g. /abc at the end) – check the responses differ.
Now test this in a web browser. If in a graphical environment on your Pi, start a browser
and visit the address: http://localhost:8000/ or, if on a remote connection, visit the
address: http://YOURPINAME:8000/ where YOURPINAME is the name of your Pi, or its IP
address (see ifconfig) if the name doesn’t work. If it’s successful, you’ve made a web
server in just 30 lines of Python!
For this server to be useful, let’s transmit some sensor data. Open server.py and add the
import lines and copy the rctime() function from ldr.py, add the GPIO.cleanup() line
to the finally block, and the GPIO.setmode(GPIO.BOARD) before the try block. After
the responseBody line, add:
if requestAddress == '/ldr':
responseBody = "Light: " + str(rctime(5))
Re-run the server (sudo python server.py), and try the address
http://localhost:8000/ldr - refresh the page in your browser to update the light level
reading.
Advanced exercises
You can choose as few or as many of these to do as you like:
1.
Modify server.py to set the response body to be the contents of the log file made
earlier (ldr.csv) if the requestAddress == '/ldrlog'. Try: with
open("ldr.csv") as f: responseBody = f.read(). Test by visiting the address
http://localhost:8000/ldrlog.
2.
Modify server.py to set the response body to be the contents of a file index.html
(see above hint) if the requestAddress == '/'. To successfully serve HTML, you
must add the header "Content-type: text/html\r\n" (in the same place the
Content-length header is set). You can start off creating an index.html file
containing <h1>Hello HTML world!</h1>, but you should research the correct
format of an HTML document. Add an inner-frame tag to show the current LDR value (I
know, an IFRAME is not good, but it’s the quickest way without code!):
<iframe src="/ldr" id="ldr">
Add a button to refresh the LDR value:
<button onclick="document.getElementById('ldr').src =
document.getElementById('ldr').src;">Refresh</button>
3.
Modify server.py to, depending on the requestAddress, to return one of several
different sensor samples as a response body, and/or control different outputs
(LED/buzzer). If you have done step 2, you could make buttons for each action.
4.
If you’re good at HTML and Javascript, modify server.py to serve a JSON string
representing a sensor value as the response body (with a "Content-type:
application/json\r\n" header) and fetch that into your page dynamically (AJAX).
5.
Modify server.py to set the response to be the contents of the file photo.jpg (see
last week’s webcam photo taking tutorial) if the
requestAddress.startswith('/photo') (startswith rather than equals to
ignore any URL parameters to allow the browser-cache-avoiding hack below to work).
To successfully serve JPEG files, you must add the header "Content-type:
image/jpeg\r\n" (in the same place the Content-length header is set). You can
start off using the existing photo.jpg, but you should include the code from last week
to take a new photo on demand. You can improve the application by serving an HTML
page (if the requestAddress == '/camera', and with the header "Content-type:
text/html\r\n"), and that page includes an image tag for the photo that
automatically refreshes every 15 seconds (this is a pretty hacky way):
<img src="/photo?t=" onload='setTimeout(function() {src =
src.substring(0, (src.lastIndexOf("t=")+2))+(new
Date()).getTime()}, 15000)' />