Voice- and SMS-Enabled Light Sensor Using Raspberry Pi and Twilio

Hacker School is a three-month self-directed environment for becoming a better programmer. I’m in the current batch.

Several of the current Hacker Schoolers expressed a desire to learn more about hardware hacking. @leocadiotine and I decided to spend an evening building a Raspberry Pi sensor interface project to show how easy it can be to get into hardware.

@sashalaundy gave a short talk this week on RESTful API design. She used the Twilio API for phone and SMS as an example — Leo and I decided to give it a try for this project.

Overview

Leo and I wanted to build a project that improved our surroundings in some small way. The Hacker School space in NYC has two restrooms: one attached to the main work area, and one downstairs. It’s a short walk, but we thought it would nice to know if the bathroom is occupied before taking the time to walk.

Our project makes it possible to check the bathroom status by phone or text message.

Bathroom occupancy status is determined using a light sensor attached to a Raspberry Pi. If the lights are on in the bathroom, we assume that the bathroom is occupied.

We created a web application hosted on Heroku which accepts periodic bathroom state updates from the Raspberry Pi and handles incoming requests from Twilio. When a user calls or texts the Twilio phone number, Twilio sends a request to the web app, which responds with an appropriate message to be spoken or texted to the user.

In addition to the voice and text interface, @gelstudios created a nice web interface using Bootstrap.

The project certainly isn’t doing anything that hasn’t been done before, but for an evening project it made a nice demo. I hope this post will serve as a nice guide for anyone looking to get started with a web-interfaced Raspberry Pi sensor project.

Server

Twilio

Twilio is a web-based service for sending and receiving phone calls and SMS text messages. It provides an easy-to-use API accessible via HTTP and a convenient Python package. A free trial of the service is available, though it does insert a nag notice into outgoing messages. We used the Twilio Python Quickstart Tutorials as our introduction.

Heroku

Heroku is a service which provides a complete, integrated stack for hosting web applications with a range of choices in language, framework, web server and data store. We created the server application for the project in Python using the Flask microframework. The Heroku Dev Center article Getting Started with Python on Heroku is a good walkthrough for setting up Flask on Heroku.

Server Code

Full source for the web application can be found at github.com/qqrs/twilio-light-sensor-server/blob/master/run.py. Interesting sections are included below.

The sensor state is not persisted to database — if the Heroku app instance spins down, the sensor state will be unknown when the next request causes it to spin up again. For purposes of a demo, the server will be kept active by frequent updates from the remote sensor — it should only spin down when there are no remote sensors sending data.

The /twilio/voice and /twilio/text routes handle requests from Twilio. When a user calls or sends an SMS message to the phone number assigned to our account, Twilio is configured so that it will make an HTTP POST request to these routes. When the server receives the request from Twilio, it generates an appropriate message indicating the status of the bathroom. The message is returned to Twilio in the HTTP response and is sent to the user as either audio (by text-to-speech) or as an SMS message.

The /update route accepts sensor state updates from the remote sensor via HTTP POST. Each request includes sensor_id and sensor_val parameters to identify the sensor and report the current value.

ServerGithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from flask import Flask, request, render_template, redirect
import time
import twilio.twiml

app = Flask(__name__)

sensor_states = {}
allowed_sensor_ids = [u'upstairs-wc', u'downstairs-wc', u'sidestairs-wc']
allowed_sensor_vals = [u'0', u'1']

@app.route("/twilio/voice", methods=['POST'])
def twilio_voice():
    """Respond to incoming Twilio voice phone requests."""
    resp = twilio.twiml.Response()
    resp.say(get_sensor_state_msg('upstairs-wc'))
    return str(resp)

@app.route("/twilio/text", methods=['POST'])
def twilio_text():
    """Respond to incoming Twilio SMS text message requests."""
    resp = twilio.twiml.Response()
    resp.sms(get_sensor_state_msg('upstairs-wc'))
    return str(resp)

@app.route("/update", methods=['POST'])
def update_state():
    """Update state following request from remote sensor."""
    if 'sensor_id' not in request.form or 'sensor_val' not in request.form:
        return ""

    sensor_id = request.form['sensor_id']
    if sensor_id not in allowed_sensor_ids:
        return ""

    sensor_val = request.form['sensor_val']
    if sensor_val not in allowed_sensor_vals:
        return ""

    sensor_time = int(time.time())
    global sensor_states
    sensor_states[sensor_id] = {'status': sensor_val, 'updated': sensor_time}
    return  ""

@app.route("/", methods=['GET'])
def web_state():
    global sensor_states
    now = int(time.time())
    return render_template('index.html', sensors=sensor_states, time=now)

def get_sensor_state_msg(sensor_id):
    global sensor_states
    sensor = sensor_states[sensor_id]
    state = sensor.get('status')

    if state == '0':
        return 'The bathroom is vacant.'
    elif state == '1':
        return 'The bathroom is occupied.'
    else:
        return 'The bathroom is undefined.'

Remote Sensor

Raspberry Pi

The Raspberry Pi is a single-board computer with an ARM-core processor, SD card slot, HDMI and composite video, USB, and optional Ethernet. While similar ARM dev boards and single-board computers have existed for years the Raspberry Pi was able to hit an enticing price point and combines several attractive qualities in one package, making it a popular choice for hobby electronics projects.

  1. Price point: $35 with Ethernet, $25 without (plus cost of shipping, tax, power supply, SD card). This is down into Arduino territory — low enough to build a project without worrying about tearing it apart right away to recover the Raspberry Pi.

  2. Integrated Peripherals: It has HDMI video (1080p with hardware decoding), audio, Ethernet, USB (with support for Wi-Fi). It also has an expansion header with general purpose input/output (GPIO) pins for interfacing to digital signals (LEDs, pushbuttons) and a UART, a SPI bus, and an I²C bus (memory devices, sensors). The combination of video and networking with low-level interfaces in one low-cost device opens up many possibilities.

  3. Linux OS: The 700 MHz ARM11-core processor is fast enough to run a full Linux distribution. The Raspberry Pi Foundation provides images of Debian and Arch Linux tailored for the hardware. Many other distributions have been contributed by users. Having a full Linux OS allows the use of higher-level languages like Python and interactive debugging on the device.

A 5V USB power supply and an SD card with an operating system installed are required to begin using the Raspberry Pi. A good guide to getting started can be found at the elinux.org wiki. Preloaded SD cards can be purchased from several vendors or an operating system image can be loaded onto a blank SD card.

Note that it is important to have a stable power supply. Some USB 5V supplies may be inadequate. I experienced SD card corruption several times until I bought a good supply and set over_voltage=2 in the config.txt file as described here. Any good supply with at leat a 1.0 amp current rating should be acceptable.

Light Sensor

The light sensor is a 10k CdS photocell, interfaced to a Raspberry Pi with an analog-to-digital converter (ADC) daughterboard. The light sensor is connected to an input of the ADC in a voltage divider configuration with a 10k resistor. With illumination from overhead lighting, the resistance of the photocell drops to about 1.5k.

ADC Daughterboard

An analog-to-digital converter (ADC) is required to read the signal from the light sensor. The Raspberry Pi does not have an integrated ADC like the Arduino and many microcontroller dev boards. However, it is straightforward to interface an external ADC via the SPI or I²C buses. Adafruit provides a good guide to using the Microchip MCP3008: Analog Inputs for Raspberry Pi Using the MCP3008.

I had soldered up an an MCP3008 on protoboard for another project so I reused it for this project.

A number of assembled expansion boards which provide an ADC are available from third-party vendors.

Raspberry Pi Remote Sensor Monitoring Script

Adafruit provides sample code in Python to read from the ADC via the SPI bus.

Full source for the remote sensor monitoring script can be found at github.com/qqrs/twilio-light-sensor-remote/blob/master/twilio_light_sensor.py. Interesting sections are included below.

The monitoring script consists of a polling loop which reads the sensor at a defined interval. It calls readadc(), a function provided by Adafruit which bit-bangs the SPI communication in software — some versions of the Raspberry Pi did not include hardware SPI. Sensor state is determined by comparing the ADC value to a fixed threshold — if greater than the threshold, the light is assumed to be on. A moving average is used to prevent a noisy read from being interpreted as a change in state. The sensor state is reported to the server via an HTTP POST request by update_server_state() whenever the sensor state changes, or at least every 60 seconds when it has not changed.

Remote sensor polling loopGithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
SENSOR_ACTIVE_THRESHOLD = 700		# threshold in ADC counts
SENSOR_READ_INTERVAL = 1		# interval in seconds
MAX_SERVER_UPDATE_INTERVAL = 60		# interval in seconds
FILTER_SAMPLES = 5			# samples to average

sensor_hist = list()     # this keeps track of the last potentiometer value
last_state = None
last_update_time = 0

while True:
        # read the analog pin
        sensor_counts = readadc(potentiometer_adc, SPICLK, SPIMOSI, SPIMISO, SPICS)
	if len(sensor_hist) > FILTER_SAMPLES:
		del sensor_hist[0]
        sensor_hist.append(sensor_counts)

	sensor_avg = sum(sensor_hist)/len(sensor_hist)
	sensor_state = sensor_avg > SENSOR_ACTIVE_THRESHOLD

        if DEBUG:
                print ("sensor_counts:", sensor_counts,
			" sensor_avg:", sensor_avg,
			" sensor_state: ", sensor_state)


	if (sensor_state != last_state
			or int(time.time()) - last_update_time > MAX_SERVER_UPDATE_INTERVAL):
		if update_server_state(sensor_state):
			last_update_time = int(time.time())	# update was successful
			last_state = sensor_state

        # hang out and do nothing for a half second
        time.sleep(SENSOR_READ_INTERVAL)
Remote sensor HTTP POST to serverGithub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import urllib

SENSOR_ID = "sensor_name_here"
SERVER_UPDATE_URL = "https://app-name-here.herokuapp.com/update"
def update_server_state(state):
	global SENSOR_ID
	global SERVER_UPDATE_URL

	sensor_val = "1" if state else "0"

	params = {}
	params['sensor_id'] = SENSOR_ID
	params['sensor_val'] = sensor_val
	params = urllib.urlencode(params)

	try:
		f = urllib.urlopen(SERVER_UPDATE_URL, params)
	except IOError as e:
		if DEBUG:
			print "I/O error %s: %s" % (e.errno, e.strerror)
			print "Connection error on HTTP POST to %s" % SERVER_UPDATE_URL
		return False

	return f.getcode() == 200

To run the script, Python packages must be installed. See the Adafruit article here on installing python-dev and rpi.gpio. The script can be run at the terminal: python twilio_light_sensor.py.

To run the script automatically at boot, it can be added to the end of /etc/rc.local, immediately before the exit 0 line: python /home/pi/twilio_light_sensor/twilio_light_sensor.py & (modifying the path to script if necessary).

Note that the trailing ampersand & is required — this forks a subshell and allows the rest of the init sequence to complete. The script can be stopped by switching to another tty with Ctrl-Alt-F2, logging in, finding the process ID with ps aux | grep twilio, and killing the process with kill <pid>.

Success

With the server running on Heroku and the light sensor attached to the Raspberry Pi, Twilio responds to text messages with the bathroom status: The bathroom is vacant. or The bathroom is occupied.. Same for voice calls — it speaks the bathroom status. Success!