Last year I decided to use a Raspberry Pi and a relay board to switch my Christmas lights on and off. This had two benefits; the first is that the kids thought it was super cool, especially my niece who turned on our lights from the other end of the country while watching it on Skype, and secondly it became straightforward to schedule them to switch on and off at multiple different times each day.
Anyway, it's now mid November and it's gotten too cold and dark for it to be fun outside after work so I've been looking for indoors projects to do, which inevitably led to the Pi. During the last year I've been playing with a bit more Python (a welcome break from the whole lotta Powershell we've been doing), and have recently touched on Cordova for mobile apps, so wanted to look at something which might tie in with that, so I decided to resurrect the Xmas Lights project for this year, but build it with a bit more care this time around.
I've been hacking away for the last couple of evenings, and have got the board into what I think is reasonable shape. It's a bit early in the year to have the Christmas lights on outside still, so this post just covers the Pi and relay board. I might make a YouTube video nearer Christmas when i plug it all in.
So, here's the board:
It's an "8 Channel 5V Active Low Relay Module Board" which sounds advanced but is actually the cheapest piece of crap I could find on eBay at the time and cost £4.28. That said, it's been fine, although it's not earthed or particularly well assembled, so I'm not sure I'd want to run 240v through it... The black box is a Pi A+ that I've had kicking around for a while since I bricked my original B and haven't soldered the headers onto my Zero
You might be able to see in the pictures, but the pin mapping is:
RelayBoard Pi-pin GPIO
GND <--------> 6 <--> Ground
IN1 <--------> 37 <--> 26
IN2 <--------> 35 <--> 19
IN3 <--------> 33 <--> 13
IN4 <--------> 31 <--> 6
IN5 <--------> 29 <--> 5
IN6 <--------> 40 <--> 21
IN7 <--------> 38 <--> 20
IN8 <--------> 36 <--> 16
VCC <--------> 2 <--> +5v
Like last year, the project is based on Python Flask. The folder structure and code are at the end of this post.
The main differences since last year in views.py are that it uses more HTTP verbs in the bindings, uses dictionary objects to manage the pins states and returns proper JSON documents to communicate state requests between the client and server. The other big difference is in lights.html that is now using bootstrap (so my site can look just like everyone else's...), much smarter AJAX to send state requests and a UI to render the current state based the server response from each request.
As the state is actually derived from querying the GPIO pins for their value, this is clearly not thread safe in any way so I'm not sure there's a huge amount of value in taking the UI further, but I might look into using web sockets or something and pushing updates to clients in future.
I'll have to look into the finer points of the weird pseudo JS that cordova uses, but if it's straightforward to translate my Javascript then I might also dip my toe in the waters of "native" mobile apps.
run.py
|-app
|-templates
| |-help.html
| |-lights.html
|-__init__.py
|-views.py
and here is the code:
run.py
| #!/usr/bin/env python |
| #!flask/bin/python |
| from app import app |
| app.run(debug=True, host='0.0.0.0', port=80) |
__init__.py
| from flask import Flask |
| import RPi.GPIO as GPIO |
| |
| app = Flask(__name__) |
| |
| from app import views |
| |
| |
| GPIO.setwarnings(False) |
| GPIO.setmode(GPIO.BCM) |
| pins = {"26":1, "19":1, "13":1, "6":1, "5":1, "21":1, "20":1, "16":1} |
| for p in pins: |
| GPIO.setup(int(p), GPIO.OUT) |
| GPIO.output(int(p), pins[p]) |
views.py
| from app import app |
| from flask import request |
| import RPi.GPIO as GPIO |
| from flask import render_template |
| import time |
| import json |
| |
| mapping = {"a":26, "b":19, "c":13, "d":6, "e":5, "f":21, "g":20, "h":16} |
| |
| def getState(): |
| relays = {"a":"x", "b":"x", "c":"x", "d":"x", "e":"x", "f":"x", "g":"x", "h":"x"} |
| for m in mapping: |
| if GPIO.input(mapping[m]) == 1: |
| relays[m] = "off" |
| else: |
| relays[m] = "on" |
| return relays |
| |
| def setState(newState): |
| for s in newState: |
| if s in mapping: |
| if newState[s] == "on": |
| GPIO.output(int(mapping[s]), 0) |
| time.sleep(0.2) |
| elif newState[s] == "off": |
| GPIO.output(int(mapping[s]), 1) |
| time.sleep(0.2) |
| elif newState[s] == "toggle": |
| if getState()[s] == 'on': |
| setState({s:"off"}) |
| else: |
| setState({s:"on"}) |
| time.sleep(0.2) |
| else: |
| #newState contains unknown relay |
| print "err" |
| |
| |
| #def setPins(pinVal): |
| # pins = [26, 19, 13, 6, 5] |
| # for p in pins: |
| # GPIO.output(int(p), pinVal) |
| # time.sleep(0.1) |
| |
| |
| @app.route('/lights', methods=['PUT']) |
| def lights_on(): |
| setState({"a":"on", "b":"on", "c":"on", "d":"on", "e":"on", "f":"on", "g":"on", "h":"on"}) |
| return json.dumps(getState()) |
| |
| @app.route('/lights', methods=['DELETE']) |
| def lights_off(): |
| setState({"a":"off", "b":"off", "c":"off", "d":"off", "e":"off", "f":"off", "g":"off", "h":"off"}) |
| return json.dumps(getState()) |
| |
| @app.route('/lights', methods=['POST']) |
| def lights_set(): |
| setState(request.form) |
| return json.dumps(getState()) |
| |
| @app.route('/', methods=['GET']) |
| def jsLights(): |
| return render_template('lights.html') |
| |
| @app.route('/status') |
| def status(): |
| return json.dumps(getState()) |
| |
| @app.route('/help') |
| def help(): |
| return render_template('help.html', relays=getState()) |
lights.html
| <html> |
| <head> |
| <title>Xmas Lights 2016</title> |
| <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> |
| <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /> |
| <style> |
| .row-buffer{ margin-bottom: 20px; } |
| </style> |
| <script> |
| |
| function doAlert(lightsStatus){ |
| var obj = jQuery.parseJSON(lightsStatus); |
| $("#lightsA").removeClass("btn-warning"); |
| $("#lightsB").removeClass("btn-warning"); |
| $("#lightsC").removeClass("btn-warning"); |
| $("#lightsD").removeClass("btn-warning"); |
| $("#lightsE").removeClass("btn-warning"); |
| $("#lightsF").removeClass("btn-warning"); |
| $("#lightsG").removeClass("btn-warning"); |
| $("#lightsH").removeClass("btn-warning"); |
| if(obj.a=="on"){ $("#lightsA").addClass("btn-warning") } |
| if(obj.b=="on"){ $("#lightsB").addClass("btn-warning") } |
| if(obj.c=="on"){ $("#lightsC").addClass("btn-warning") } |
| if(obj.d=="on"){ $("#lightsD").addClass("btn-warning") } |
| if(obj.e=="on"){ $("#lightsE").addClass("btn-warning") } |
| if(obj.f=="on"){ $("#lightsF").addClass("btn-warning") } |
| if(obj.g=="on"){ $("#lightsG").addClass("btn-warning") } |
| if(obj.h=="on"){ $("#lightsH").addClass("btn-warning") } |
| } |
| |
| $(document).ready(function() { |
| $("#lightsOn").click(function() { |
| $.ajax({url: "http://192.168.0.50/lights", type: "PUT"}).done(function(data) { |
| doAlert(data); |
| }); |
| }); |
| $("#lightsOff").click(function() { |
| $.ajax({url: "http://192.168.0.50/lights", type: "DELETE"}).done(function(data) { |
| doAlert(data); |
| }); |
| }); |
| $("#lightsA").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"a":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| $("#lightsB").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"b":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| $("#lightsC").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"c":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| $("#lightsD").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"d":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| $("#lightsE").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"e":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| $("#lightsF").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"f":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| $("#lightsG").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"g":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| $("#lightsH").click(function() { $.ajax({url: "http://192.168.0.50/lights", type: "POST", data: {"h":"toggle"} }).done(function(data) {doAlert(data)}) }); |
| |
| }); |
| |
| </script> |
| </head> |
| <body> |
| <h1>Xmas 2016 lights control</h1> |
| <div class="row row-buffer"> |
| <div class="col-sm-8"> |
| <button id="lightsOn" type="button" class="btn btn-primary btn-block">All lights on</button> |
| </div> |
| </div> |
| <div class="row row-buffer"> |
| <div class="col-sm-8"> |
| <button id="lightsOff" type="button" class="btn btn-primary btn-block">All lights off</button> |
| </div> |
| </div> |
| <div class="row"> |
| <div class="col-sm-1">A</div> |
| <div class="col-sm-1">B</div> |
| <div class="col-sm-1">C</div> |
| <div class="col-sm-1">D</div> |
| <div class="col-sm-1">E</div> |
| <div class="col-sm-1">F</div> |
| <div class="col-sm-1">G</div> |
| <div class="col-sm-1">H</div> |
| </div> |
| <div class="row"> |
| <div class="col-sm-1"><button id="lightsA" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| <div class="col-sm-1"><button id="lightsB" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| <div class="col-sm-1"><button id="lightsC" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| <div class="col-sm-1"><button id="lightsD" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| <div class="col-sm-1"><button id="lightsE" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| <div class="col-sm-1"><button id="lightsF" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| <div class="col-sm-1"><button id="lightsG" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| <div class="col-sm-1"><button id="lightsH" type="button" class="btn btn-lg btn-block"><span class="glyphicon glyphicon-off"></span></button></div> |
| </div> |
| </body> |
| </html> |
help.html
| <html> |
| <head> |
| <title>Xmas Lights 2016 - Help</title> |
| <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /> |
| </head> |
| <body> |
| <h1> |
| Xmas2016 lights controller - Help |
| </h1> |
| <div> |
| Usage: |
| <table class="table table-bordered"> |
| <tr> |
| <td>switch all on</td><td>PUT root/lights</td> |
| </tr> |
| <tr> |
| <td>switch all off</td><td>DELETE root/lights</td> |
| </tr> |
| <tr> |
| <td>control relays</td><td>POST {{relays}} root/lights</td> |
| </tr> |
| </table> |
| </div> |
| </body> |
| </html> |
Finally a picture I took while I was messing around with my camera - you can clearly see the opto-isolators are not even vaguely straight - not a quality of board to be handling voltages and currents that could burn my house down I think.
wiping my Pi for another project today, so a few extra notes
ReplyDeleteCRONTAB
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
#00 21 * * * curl --silent -X DELETE localhost/xmas15/api/v1.0/lights
#00 17 * * * curl --silent -X PUT localhost/xmas15/api/v1.0/lights
00 15 * * * curl --silent -H "Content-Type: application/x-www-form-urlencoded" --data 'all=on' http://localhost/lights
00 17 * * * curl --silent -H "Content-Type: application/x-www-form-urlencoded" --data 'all=off&b=on' http://localhost/lights
00 22 * * * curl --silent -H "Content-Type: application/x-www-form-urlencoded" --data 'all=off' http://localhost/lights
PYTHON SERVICE START
ReplyDeletepi@raspberrypi ~ $ cat /etc/rc.local
#!/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
nohup python /home/pi/xmas16/run.py &
nohup python /home/pi/xmas16/button.py &
exit 0