Skip to content

Instantly share code, notes, and snippets.

@cnelson
Created January 6, 2016 19:03
Show Gist options
  • Save cnelson/750202a0dc2c5b26b202 to your computer and use it in GitHub Desktop.
Save cnelson/750202a0dc2c5b26b202 to your computer and use it in GitHub Desktop.
Naughty or Nice Santa hat
-- 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)
#!/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