Created
January 6, 2016 19:03
-
-
Save cnelson/750202a0dc2c5b26b202 to your computer and use it in GitHub Desktop.
Naughty or Nice Santa hat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- setup spi | |
spi.setup(1, spi.MASTER, spi.CPOL_HIGH, spi.CPHA_HIGH, spi.DATABITS_8, 0); | |
-- number of LEDS connected to the SPI device | |
NUM_LEDS = 42; | |
-- connect to iphone tether | |
wifi.setmode(wifi.STATION) | |
wifi.sta.config("SSID", "PASSWORD") | |
-- our server | |
THE_CLOWN = 'IP ADDRESS RUNNING hat.py' | |
THE_CLOWN_PORT = 8080 | |
-- handle button presses | |
gpio.mode(3, gpio.INPUT) | |
-- globals | |
FRAME = {} | |
MODE = 'dunno' | |
-- flatten our frame table so it's suitable for spi.send | |
-- {{1, 2, 3, 4}. {5, 6, 7, 8}} becomes {1, 2, 3, 4, 5, 6, 7, 8} | |
-- is there a better way to do this in LUA" | |
function lframe(frame) | |
result = {}; | |
z = 0; | |
for k, v in pairs(frame) do | |
for kk, vv in pairs(v) do | |
result[z] = vv; | |
z = z + 1; | |
end | |
end | |
return result; | |
end | |
-- all our pattern functions are iterators, each are expected to write _one_ frame | |
-- to the SPI device each time they are called | |
-- generate a green "barber pole" / "chase" pattern" | |
function nice() | |
local start = 0; | |
return function () | |
for k = 0, NUM_LEDS, 1 do | |
if start == 0 then | |
FRAME[k] = {255, 0, 0, 0}; | |
start = 1 | |
else | |
FRAME[k] = {255, 255, 0, 0}; | |
start = 0 | |
end | |
end | |
spi.send(1, {0, 0, 0, 0}); | |
spi.send(1, lframe(FRAME)); | |
return start | |
end | |
end | |
-- generate a red "barber pole" / "chase" pattern" | |
function naughty() | |
local start = 0; | |
return function () | |
for k = 0, NUM_LEDS, 1 do | |
if start == 0 then | |
FRAME[k] = {255, 0, 0, 0}; | |
start = 1 | |
else | |
FRAME[k] = {255, 0, 0, 255}; | |
start = 0 | |
end | |
end | |
spi.send(1, {0, 0, 0, 0}); | |
spi.send(1, lframe(FRAME)); | |
return start | |
end | |
end | |
-- immediately set entire strip red | |
-- then fill pixel by pixel with white | |
function white() | |
local i = 0; | |
-- fill white | |
for k = 0, NUM_LEDS, 1 do | |
FRAME[k] = {255, 255, 0, 0}; | |
end | |
-- fill one pixel red each iteration until we reach the end of the strip | |
return function () | |
i = i + 1; | |
if i == NUM_LEDS then | |
return nil | |
end | |
FRAME[i] = {255, 255, 255, 255} | |
spi.send(1, {0, 0, 0, 0}); | |
spi.send(1, lframe(FRAME)); | |
return i | |
end | |
end | |
-- immediately set entire strip white | |
-- then fill pixel by pixel with green | |
function green() | |
local i = 0; | |
for k= 0, NUM_LEDS, 1 do | |
FRAME[k] = {255, 255, 255, 255}; | |
end | |
return function () | |
i = i + 1; | |
if i == NUM_LEDS then | |
return nil | |
end | |
FRAME[i] = {255, 0, 0, 255} | |
spi.send(1, {0, 0, 0, 0}); | |
spi.send(1, lframe(FRAME)); | |
return i | |
end | |
end | |
-- immediately set entire strip green | |
-- then fill pixel by pixel with red | |
function red() | |
local i = 0; | |
for k = 0, NUM_LEDS, 1 do | |
FRAME[k] = {255, 0, 0, 255}; | |
end | |
return function () | |
i = i + 1; | |
if i == NUM_LEDS then | |
return nil | |
end | |
FRAME[i] = {255, 255, 0, 0} | |
spi.send(1, {0, 0, 0, 0}); | |
spi.send(1, lframe(FRAME)); | |
return i | |
end | |
end | |
-- exepcted to be called like: wifi.sta.getap(geoloc) | |
-- packs all discovered APs into a string and sends to the clown | |
-- the clown does geolocation based on observed APs, | |
-- then looks up last week's crime stats and returns a naughty/nice/dunno ranking | |
function geoloc(aps) | |
-- if we aren't connected to wifi, just return 'don't know' | |
if wifi.sta.status() ~= 5 then | |
print("Not connected to WIFI, unable to load crime stats") | |
MODE = 'dunno' | |
return | |
end | |
-- pack our table of aps into a string | |
-- we'll need this below to make our request | |
local t = {} | |
for k, v in pairs(aps) do | |
table.insert(t, v) | |
end | |
-- setup the request | |
conn = net.createConnection(net.TCP, 0) | |
good = false; | |
-- we received data | |
conn:on("receive", function(conn, payload) | |
local s = 0; | |
local e = 0; | |
-- extact the first line | |
s, e = string.find(payload, "\r") | |
local line = string.sub(payload, 0, s) | |
-- it should be the status line, if not, bounce | |
s, e = string.find(line, 'HTTP/1.') | |
if s ~= 1 then | |
return | |
end | |
-- ok, we have the status line, chop out the response code | |
s, e = string.find(line, " ", e+2) | |
code = tonumber(string.sub(line, s+1, s+4)) | |
-- set our disply mode based on the result | |
if code == 200 then | |
MODE = 'nice' | |
elseif code == 210 then | |
MODE = 'naughty' | |
else | |
MODE = 'dunno' | |
end | |
print(MODE) | |
good = true | |
end) | |
conn:on("connection", function(c) | |
-- send our request to the clown | |
conn:send("GET /?aps="..table.concat(t, "|").." HTTP/1.0\r\nHost: santa-hat.appspot.com\r\n\r\n") | |
end) | |
conn:on("disconnection",function(c) | |
-- if this flag isn't set to true | |
-- then on("receive") was never called so we should go to 'dunno' state | |
if good == false then | |
MODE = 'dunno' | |
print("closed bad") | |
else | |
-- if the flag is set, we are good, bounce | |
print("closed good") | |
end | |
end) | |
-- hardcoded ip for cnelson.org so we are more likely to get a response | |
-- if we don't have to deal with possible DNS failures | |
print("Getting crime info...") | |
conn:connect(THE_CLOWN_PORT, THE_CLOWN) | |
end | |
-- variables to manage state between calls to draw() | |
iter = nil | |
iname = '' | |
last_status = nil | |
last_btn = nil | |
-- this function is called every 5ms, it should update the LED strip | |
function draw() | |
-- trigger a check / mode change if the WIFI connection status has changed | |
local status = wifi.sta.status() | |
if status == 5 and status ~= last_status then | |
print("Conneced to AP. Triggering Crime Check") | |
wifi.sta.getap(geoloc) | |
end | |
if status ~= 5 and status ~= last_status then | |
print("WIFI Disconnected Switching to dunno") | |
MODE = 'dunno' | |
end | |
last_status = status | |
-- manually switch display modes if the button is pressed | |
local btn = gpio.read(3) | |
if btn == 0 and btn ~= last_btn then | |
if MODE == 'nice' then | |
print("Manually switching to naughty") | |
MODE = 'naughty' | |
elseif MODE =='naughty' then | |
print("Manually switching to dunno") | |
MODE = 'dunno' | |
else | |
print("Manually switching to nice") | |
MODE = 'nice' | |
end | |
end | |
last_btn = btn | |
-- if we are nice or naughty, then stay there until something changes our mode | |
-- this could be a button press, or new crime data | |
if MODE == 'nice' then | |
if iname ~= 'nice' then | |
iter = nice() | |
iname = 'nice' | |
end | |
iter() | |
elseif MODE == 'naughty' then | |
if iname ~= 'naughty' then | |
iter = naughty() | |
iname = 'naughty' | |
end | |
iter() | |
else | |
-- we are 'dunno' if we got here | |
-- if we were in a different mode, switch to red | |
if iname ~= 'r' and iname ~= 'g' and iname ~= 'w' then | |
iter = white() | |
iname = 'w' | |
end | |
-- rotate through our color fill | |
if iter() == nil then | |
if iname == 'w' then | |
iter = green() | |
iname ='g' | |
elseif iname == 'g' then | |
iter = red() | |
iname = 'r' | |
else | |
iter = white() | |
iname = 'w' | |
end | |
end | |
end | |
-- kick off another frame in 5 ms | |
tmr.alarm(0, 5, 0, draw) | |
end | |
-- check for new crime data every 5 minutes | |
tmr.alarm(6, 1000*60*5, 1, function() wifi.sta.getap(geoloc) end) | |
-- update the led every 5 miliseconds | |
tmr.alarm(0, 5, 0, draw) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
"""Accept a list of APs, then: | |
* Do geolocation with google's API, | |
* Lookup crime in that area with spotcrime's API | |
""" | |
# DO NOT USE ANY OF THIS CODE IN A PRODUCTION SYSTEM, IT'S JOKE CODE FOR A JOKE HAT | |
import datetime | |
import json | |
import urllib2 | |
from flask import Flask, request | |
app = Flask(__name__) | |
# our API keys | |
GEO_API_KEY = 'GOOGLE-API-KEY' | |
CRIME_API_KEY = 'SPOTCRIME-API-KEY' | |
# stash the last request in memory for debugging | |
last_geo = None | |
last_check = None | |
last_crimes = None | |
# our results | |
NICE = ('nice', 200) | |
NAUGHTY = ('naughty', 210) | |
DUNNO = ('dunno', 211) | |
@app.route('/') | |
def index(): | |
"""Given a list of APs, do geolocatoin to find lat/lng, | |
then use lat/lng to lookup crime statistics for the area in question | |
""" | |
global last_geo, last_check, last_crimes | |
# this is a string like: authmode,rssi,bssid,channel|authmode,rssi,bssid,channel|... | |
# this isn't JSON or some other structed formated as the callers of the API are | |
# likely to be very small / dumb devices | |
# so lets not assume they have a JSON library available, | |
# and let them send a simple delimited string instead | |
aps = request.args.get('aps', '') | |
wifiaps = [] | |
# get the list of APs | |
for ap in aps.split('|'): | |
# and the values for each one | |
try: | |
authmode, rssi, bssid, channel = ap.split(',') | |
except ValueError: | |
continue | |
# back into the data structure the API expects | |
wifiaps.append({ | |
"macAddress": bssid.upper(), | |
"signalStrength": int(rssi), | |
"age": 0, | |
"channel": int(channel), | |
}) | |
#can't geo locate with no APs | |
if len(wifiaps) == 0: | |
return DUNNO | |
# make the api call | |
url = 'https://www.googleapis.com/geolocation/v1/geolocate?key={0}'.format(GEO_API_KEY) | |
req = urllib2.Request( | |
url, | |
json.dumps({"wifiAccessPoints": wifiaps}), | |
{'Content-Type': 'application/json'} | |
) | |
geo = json.loads(urllib2.urlopen(req).read()) | |
# some test coordinates: | |
# | |
# coalinga | |
# geo = { | |
# 'location': { | |
# 'lat': 34.137726, | |
# 'lng': -118.358772 | |
# } | |
# } | |
# east oakland | |
# geo = { | |
# 'location': { | |
# 'lat': 37.779691, | |
# 'lng': -122.218603 | |
# } | |
# } | |
# now that we have the lat/lng, lookup nearby crime | |
crime_url = 'http://api.spotcrime.com/crimes.json?key={0}&lat={1}&lon={2}&radius={3}'.format( | |
CRIME_API_KEY, | |
geo['location']['lat'], | |
geo['location']['lng'], | |
0.01 | |
) | |
resp = urllib2.urlopen(crime_url) | |
crimes = json.loads(resp.read()) | |
last_geo = geo | |
last_check = datetime.datetime.now() | |
last_crimes = None | |
# if there are no crimes, then say 'dunno' not 'nice' | |
# as no place as _no crimes... it's likely a hole in the API's data sources | |
if len(crimes['crimes']) == 0: | |
# no data | |
return DUNNO | |
# figure out if naughty or nice | |
# yeah, yeah all crimes are bad, but try to distinguish between | |
# 'violent crimes' and 'property crimes' | |
naughty_crimes = ['Assault', 'Shooting', 'Robbery', 'Arson'] | |
nice_crimes = ['Burglary', 'Vandalism', 'Arrest', 'Theft'] | |
naughty = 0 | |
nice = 0 | |
# get a list of all the types of crime we have | |
types = [c['type'] for c in crimes['crimes']] | |
# build a dict where k = type, and v = the number of crimes of that type | |
summary = dict((i, types.count(i)) for i in types) | |
# remove the outlier, and then count our crimes | |
if len(summary) > 1: | |
del summary[sorted(summary.items(), key=lambda x: x[1], reverse=True)[0][0]] | |
last_crimes = summary | |
print summary | |
# add up our naughty crimes | |
for c in naughty_crimes: | |
naughty += summary.get(c, 0) | |
# and our nice crimes | |
for c in nice_crimes: | |
nice += summary.get(c, 0) | |
# return our status | |
if naughty >= nice: | |
return NAUGHTY | |
else: | |
return NICE | |
@app.route('/info') | |
def info(): | |
"""Show debugging info for the last request | |
yes this leaks your lat/lng. Enjoy being stalked : ) | |
""" | |
if last_geo is not None: | |
lg = '<a href="https://www.google.com/maps/@{0},{1},15z">{0}, {1}</a>'.format( | |
last_geo['location']['lat'], | |
last_geo['location']['lng'] | |
) | |
else: | |
lg = 'None' | |
if last_crimes is not None: | |
lc = ', '.join([kk+': '+str(vv) for kk, vv in last_crimes.items()]) | |
else: | |
lc = 'None' | |
return """ | |
<p><strong>Last Geo: </strong> {0}</p> | |
<p><strong>Last Crimes: </strong> {1}</p> | |
<p><strong>Last Check: </strong> {2}</p> | |
""".format(lg, lc, last_check) | |
if __name__ == "__main__": | |
app.run(host='0.0.0.0', port=8080, debug=False) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment