This commit is contained in:
petrucci4prez 2016-12-30 02:51:56 -05:00
commit a93f116aae
52 changed files with 1360 additions and 0 deletions

83
auxilary.py Normal file
View File

@ -0,0 +1,83 @@
import time, psutil, yaml
from subprocess import check_output, DEVNULL, CalledProcessError
from threading import Thread, Event
class ConfigFile():
def __init__(self, path):
self._path = path
with open(self._path, 'r') as f:
self._dict = yaml.safe_load(f)
def __getitem__(self, key):
return self._dict[key]
def __setitem__(self, key, value):
self._dict[key] = value
def sync(self):
with open(self._path, 'w') as f:
yaml.dump(self._dict, f, default_flow_style=False)
def freeBusyPath(path, logger=None):
# check if any other processes are using file path
# if found, politely ask them to exit, else nuke them
# NOTE: fuser sends diagnostic info (eg filenames and modes...which we
# don't want) to stderr. This is weird, but let's me route to /dev/null
# so I don't have to parse it later
try:
stdout = check_output(['fuser', path], universal_newlines=True, stderr=DEVNULL)
except CalledProcessError:
logger.debug('%s not in use. Execution may continue', path)
else:
# assume stdout is one PID first
try:
processes = [psutil.Process(int(stdout))]
# else assume we have multiple PIDs separated by arbitrary space
except ValueError:
processes = [psutil.Process(int(s)) for s in stdout.split()]
for p in processes:
if logger:
logger.warning('%s in use by PID %s. Sending SIGTERM', path, p.pid)
p.terminate()
dead, alive = psutil.wait_procs(processes, timeout=10)
for p in alive:
if logger:
logger.warning('Failed to terminate PID %s. Sending SIGKILL', p.pid)
p.kill()
class async:
def __init__(self, daemon=False):
self._daemon = daemon
def __call__(self, f):
def wrapper(*args, **kwargs):
t = Thread(target=f, daemon=self._daemon, args=args, kwargs=kwargs)
t.start()
return wrapper
class CountdownTimer(Thread):
def __init__(self, countdownSeconds, action, sound=None):
self._stopper = Event()
def countdown():
for i in range(countdownSeconds, 0, -1):
if self._stopper.isSet():
return None
if sound and i < countdownSeconds:
sound.play()
time.sleep(1)
action()
super().__init__(target=countdown, daemon=True)
self.start()
def stop(self):
self._stopper.set()
def __del__(self):
self.stop()

44
blinkenLights.py Normal file
View File

@ -0,0 +1,44 @@
import RPi.GPIO as GPIO
import time, logging
from threading import Thread, Event
from itertools import chain
logger = logging.getLogger(__name__)
class Blinkenlights(Thread):
def __init__(self, pin, cyclePeriod=2):
self._stopper = Event()
self._pin = pin
self.blink = False
self.setCyclePeriod(cyclePeriod) #cyclePeriod is length of one blink cycle in seconds
GPIO.setup(pin, GPIO.OUT)
pwm = GPIO.PWM(self._pin, 60)
def blinkLights():
pwm.start(0)
while not self._stopper.isSet():
t = self._sleeptime
if self.blink:
for dc in chain(range(100, -1, -5), range(0, 101, 5)):
pwm.ChangeDutyCycle(dc)
time.sleep(t)
else:
pwm.ChangeDutyCycle(100)
time.sleep(t)
pwm.stop() # required to avoid core dumps when process terminates
super().__init__(target=blinkLights, daemon=True)
self.start()
logger.debug('Starting LED on pin %s', self._pin)
def setCyclePeriod(self, cyclePeriod):
self._sleeptime = cyclePeriod/20/2
def stop(self):
self._stopper.set()
logger.debug('Stopping LED on pin %s', self._pin)
def __del__(self):
self.stop()

82
camera.py Normal file
View File

@ -0,0 +1,82 @@
import cv2, time
from threading import RLock
from sharedLogging import SlaveLogger
from auxilary import freeBusyPath
class Camera:
def __init__(self, queue):
self._lock = RLock()
self._index = 0
self._logger = SlaveLogger(__name__, 'DEBUG', queue)
freeBusyPath('/dev/video{}'.format(self._index), self._logger)
# NOTE: we use 0-255 on forms instead of floats because they look nicer
logitechProperties = {
'FPS': 25, # integer from 10 to 30 in multiples of 5
'BRIGHTNESS': 127/255, # float from 0 to 1
'CONTRAST': 32/255, # float from 0 to 1
'SATURATION': 32/255, # float from 0 to 1
'GAIN': 64/255, # float from 0 to 1
}
self._properties = {}
self.setProps(**logitechProperties)
def getProps(self, *args):
return {prop: self._video.get(getattr(cv2, 'CAP_PROP_' + prop)) for prop in args}
def setProps(self, *args, **kwargs):
# silly hack, need to reset the videoCapture object every time
# we change settings (they can only be changed once apparently)
self._lock.acquire()
try:
if hasattr(self, '_video'):
self._video.release()
self._video = cv2.VideoCapture(self._index)
for prop, val in kwargs.items():
self._properties[prop] = val
self._video.set(getattr(cv2, 'CAP_PROP_' + prop), val)
self._logger.debug('set %s to %s', prop, val)
finally:
self._lock.release()
# the reset code here could be put in seperate thread to accellarate
def getFrame(self):
frame = None
self._lock.acquire()
try:
# will try 3 attempts to grab frame, will reset on failure
i = 3
while i > 0:
if self._video.isOpened():
success, image = self._video.read()
ret, jpeg = cv2.imencode('.jpg', image)
frame = jpeg.tobytes()
break
else:
time.sleep(5)
self.reset()
i -= 1
# after 3 fails return the dummy frame
if not frame:
with open('noimage.jpg', 'rb') as f:
time.sleep(1)
frame = f.read()
finally:
self._lock.release()
return frame
def reset(self):
self.setProps(**self._properties)
self._logger.debug('camera reset')
def __del__(self):
try:
self._video.release()
self._logger.debug('Release camera at index %s', self._index)
except AttributeError:
self._logger.debug('Failed to release camera at index %s', self._index)

3
config.yaml Normal file
View File

@ -0,0 +1,3 @@
recipientList:
- natedwarshuis@gmail.com
state: disarmed

143
listeners.py Normal file
View File

@ -0,0 +1,143 @@
import logging, os, sys, stat
from threading import Thread
from evdev import InputDevice, ecodes
from select import select
from auxilary import freeBusyPath
import stateMachine
logger = logging.getLogger(__name__)
class KeypadListener(Thread):
def __init__(self, stateMachine, callbackDisarm, callbackArm, soundLib, passwd):
ctrlKeys = { 69: 'NUML', 98: '/', 14: 'BS', 96: 'ENTER'}
volKeys = { 55: '*', 74: '-', 78: '+'}
numKeys = {
71: '7', 72: '8', 73: '9',
75: '4', 76: '5', 77: '6',
79: '1', 80: '2', 81: '3',
82: '0', 83: '.'
}
devPath = '/dev/input/by-id/usb-04d9_1203-event-kbd'
freeBusyPath(devPath, logger)
self._dev = InputDevice(devPath)
self._dev.grab()
numKeySound = soundLib.soundEffects['numKey']
ctrlKeySound = soundLib.soundEffects['ctrlKey']
wrongPassSound = soundLib.soundEffects['wrongPass']
backspaceSound = soundLib.soundEffects['backspace']
self.resetBuffer()
def getInput():
while 1:
r, w, x = select([self._dev], [], [])
for event in self._dev.read():
if event.type == 1 and event.value == 1:
# numeral input
if event.code in numKeys:
if stateMachine.currentState != stateMachine.states.disarmed:
self._buf = self._buf + numKeys[event.code]
numKeySound.play()
# ctrl input
elif event.code in ctrlKeys:
val = ctrlKeys[event.code]
# disarm if correct passwd
if val=='ENTER':
if stateMachine.currentState == stateMachine.states.disarmed:
ctrlKeySound.play()
else:
if self._buf == '':
ctrlKeySound.play()
elif self._buf == passwd:
callbackDisarm()
else:
self.resetBuffer()
wrongPassSound.play()
# arm
elif val == 'NUML':
callbackArm()
ctrlKeySound.play()
# delete last char in buffer
elif val == 'BS':
self._buf = self._buf[:-1]
backspaceSound.play()
# reset buffer
elif val == '/':
self.resetBuffer()
backspaceSound.play()
# volume input
elif event.code in volKeys:
val = volKeys[event.code]
if val == '+':
soundLib.changeVolume(10)
elif val == '-':
soundLib.changeVolume(-10)
elif val == '*':
soundLib.mute()
ctrlKeySound.play()
self._dev.set_led(ecodes.LED_NUML, 0 if soundLib.volume > 0 else 1)
super().__init__(target=getInput, daemon=True)
self.start()
logger.debug('Started keypad device')
# TODO: make timer to clear buffer if user doesn't clear it
def resetBuffer(self):
self._buf = ''
def __del__(self):
try:
self._dev.ungrab()
logger.debug('Released keypad device')
except IOError:
logger.debug('Failed to release keypad device')
except AttributeError:
pass
# TODO: this code gets really confused if the pipe is deleted
class PipeListener(Thread):
def __init__(self, callback, path):
self._path = path
if os.path.exists(self._path):
if not stat.S_ISFIFO(os.stat(self._path)[0]):
os.remove(self._path)
os.mkfifo(self._path)
else:
os.mkfifo(self._path)
os.chmod(self._path, 0o0777)
def listenForSecret():
while 1:
with open(self._path, 'r') as f:
msg = f.readline()[:-1]
callback(msg, logger)
super().__init__(target=listenForSecret, daemon=True)
self.start()
logger.debug('Started pipe listener at path %s', self._path)
def __del__(self):
if os.path.exists(self._path):
os.remove(self._path)
logger.debug('Cleaned up pipe listener at path %s', self._path)

95
main.py Executable file
View File

@ -0,0 +1,95 @@
#! /bin/python
import sys, os, time, signal, traceback
import RPi.GPIO as GPIO
from queue import Queue
from multiprocessing.managers import BaseManager, DictProxy
def clean():
GPIO.cleanup()
try:
stateMachine.__del__()
except NameError:
pass
try:
webInterface.stop() # Kill process 1
except NameError:
pass
try:
logger.info('Terminated root process - PID: %s', os.getpid())
logger.stop()
except NameError:
pass
try:
manager.__del__() # kill process 2
except NameError:
pass
def sigtermHandler(signum, stackFrame):
logger.info('Caught SIGTERM')
clean()
exit()
class ResourceManager(BaseManager):
def __init__(self):
super().__init__()
from camera import Camera
from microphone import Microphone
self.register('Camera', Camera)
self.register('Queue', Queue)
self.register('Dict', dict, DictProxy)
def __del__(self):
self.shutdown()
if __name__ == '__main__':
try:
os.chdir(os.path.dirname(os.path.realpath(__file__)))
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
manager = ResourceManager()
manager.start() # Child process 1
loggerQueue = manager.Queue() # used to buffer logs
camera = manager.Camera(loggerQueue)
stateDict = manager.Dict() # used to hold state info
ttsQueue = manager.Queue() # used as buffer for TTS Engine
from sharedLogging import MasterLogger
logger = MasterLogger(__name__, 'DEBUG', loggerQueue)
from notifier import criticalError
from stateMachine import StateMachine
stateMachine = StateMachine(camera, ttsQueue, stateDict)
from webInterface import WebInterface
webInterface = WebInterface(camera, stateDict, ttsQueue, loggerQueue)
webInterface.start() # Child process 2
signal.signal(signal.SIGTERM, sigtermHandler)
while 1:
time.sleep(31536000)
except Exception:
t = 'Exception caught:\n' + traceback.format_exc()
try:
criticalError(t)
except NameError:
pass
try:
logger.critical(t)
except NameError:
print('[__main__] [CRITICAL] Logger not initialized, using print for console output:\n' + t)
clean()

33
microphone.py Normal file
View File

@ -0,0 +1,33 @@
import pyaudio
CHUNK = 4096
class Microphone:
def __init__(self):
print('aloha bra')
self._pa = pyaudio.PyAudio()
self._stream = self._pa.open(
format = pyaudio.paInt16,
channels = 1,
rate = 48000,
input = True,
frames_per_buffer = CHUNK
)
def getFrame(self):
frame = self._stream.read(CHUNK)
print(len(frame))
return frame
def __del__(self):
try:
self._stream.stop_stream()
self._stream.close()
except AttributeError:
pass
try:
self._pa.terminate()
except AttributeError:
pass

BIN
noimage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

63
notifier.py Normal file
View File

@ -0,0 +1,63 @@
import logging, time
from auxilary import async, ConfigFile
from smtplib import SMTP
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
logger = logging.getLogger(__name__)
COMMASPACE=', '
RECIPIENT_LIST=ConfigFile('config.yaml')['recipientList']
GMAIL_USER='natedwarshuis@gmail.com'
GMAIL_PWD='bwsasfxqjbookmed'
def getNextDate():
m = datetime.now().month + 1
y = datetime.now().year
y = y + 1 if m > 12 else y
return datetime(year=y, month=m%12, day=1, hour=12, minute=0)
@async(daemon=True)
def scheduleAction(action):
while 1:
nextDate = getNextDate()
sleepTime = nextDate - datetime.today()
logger.info('Next monthly test scheduled at %s (%s)', nextDate, sleepTime)
time.sleep(sleepTime.days * 86400 + sleepTime.seconds)
action()
@async(daemon=False)
def sendEmail(subject, body):
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = GMAIL_USER
msg['To'] = COMMASPACE.join(RECIPIENT_LIST)
msg.attach(MIMEText(body, 'plain'))
s = SMTP('smtp.gmail.com', 587)
s.starttls()
s.login(GMAIL_USER, GMAIL_PWD)
s.send_message(msg)
s.quit()
def monthlyTest():
subject = 'harrison4hegemon - automated monthly test'
body = 'this is an automated message - please do not reply\n\nin the future this may have useful information'
sendEmail(subject, body)
logger.info('Sending monthly test to email list')
def intruderAlert():
subject = 'harrison4hegemon - intruder detected'
body = 'intruder detected - alarm was tripped on ' + time.strftime("%H:%M:%S - %d/%m/%Y")
sendEmail(subject, body)
logger.info('Sending intruder alert to email list')
def criticalError(err):
subject = 'harrison4hegemon - critical error'
sendEmail(subject, err)
logger.info('Sending critical error to email list')
scheduleAction(monthlyTest)

26
remoteServer.py Normal file
View File

@ -0,0 +1,26 @@
from async import async
#~ from logger import logGeneric
import socket
from ftplib import FTP
from io import BytesIO
from functools import partial
def buildUploader(host, port, user, passwd):
@async(daemon=False)
def uploader(filepath, filename, buf):
retries = 3
ftp = FTP()
while retries > 0:
try:
ftp.connect(host=host, port=port)
ftp.login(user=user, passwd=passwd)
ftp.cwd(filepath)
ftp.storbinary('STOR ' + filename, BytesIO(buf))
break
except IOError:
retries =- 1
#~ logGeneric('remoteServer: Failed to upload file. ' + str(retries) + ' retries left...', 0)
ftp.quit()
return uploader

60
sensors.py Normal file
View File

@ -0,0 +1,60 @@
import RPi.GPIO as GPIO
import logging, time
from functools import partial
from auxilary import CountdownTimer
logger = logging.getLogger(__name__)
# this should never be higher than INFO or motion will never be logged
logger.setLevel(logging.DEBUG)
# delay GPIO init to avoid false positive during powerup
INIT_DELAY = 60
def lowPassFilter(pin, targetVal, period=0.001):
divisions = 10
sleepTime = period/divisions
for i in range(0, divisions):
time.sleep(sleepTime)
if GPIO.input(pin) != targetVal:
return False
return GPIO.input(pin) == targetVal
def setupGPIO(name, pin, GPIOEvent, callback):
logger.info('setting up \"%s\" on pin %s', name, pin)
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(pin, GPIOEvent, callback=callback, bouncetime=500)
def setupMotionSensor(pin, location, action):
name = 'MotionSensor@' + location
def trip(channel):
if lowPassFilter(pin, 1):
logger.info('detected motion: ' + location)
action()
logger.debug('waiting %s for %s to power on', INIT_DELAY, name)
CountdownTimer(INIT_DELAY, partial(setupGPIO, name, pin, GPIO.RISING, trip))
def setupDoorSensor(pin, action, sound=None):
def trip(channel):
nonlocal closed
val = GPIO.input(pin)
if val != closed:
if lowPassFilter(pin, val):
closed = val
if closed:
logger.info('door closed')
if sound:
sound.play()
else:
logger.info('door opened')
if sound:
sound.play()
action()
setupGPIO('DoorSensor', pin, GPIO.BOTH, trip)
closed = GPIO.input(pin)

34
sharedLogging.py Normal file
View File

@ -0,0 +1,34 @@
import logging
from logging.handlers import TimedRotatingFileHandler, QueueListener, QueueHandler
def SlaveLogger(name, level, queue):
logger = logging.getLogger(name)
logger.setLevel(getattr(logging, level))
logger.addHandler(QueueHandler(queue))
logger.propagate = False
return logger
class MasterLogger():
def __init__(self, name, level, queue):
consoleFormat = logging.Formatter('[%(name)s] [%(levelname)s] %(message)s')
fileFormat = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')
console = logging.StreamHandler()
console.setFormatter(consoleFormat)
rotatingFile = TimedRotatingFileHandler('/mnt/glusterfs/pyledriver/logs/pyledriver-log', when='midnight')
rotatingFile.setFormatter(fileFormat)
logging.basicConfig(level=getattr(logging, level), handlers=[QueueHandler(queue)])
logger = logging.getLogger(name)
# since the logger module sucks and doesn't allow me to init
# a logger in a subclass, need to "fake" object inheritance
for i in ['debug', 'info', 'warning', 'error', 'critical']:
setattr(self, i, getattr(logger, i))
self.queListener = QueueListener(queue, console, rotatingFile)
self.queListener.start()
def stop(self):
self.queListener.stop()

199
soundLib.py Normal file
View File

@ -0,0 +1,199 @@
import logging, os, hashlib, queue, threading, time, psutil
from pygame import mixer
from subprocess import call
from collections import OrderedDict
from auxilary import async
logger = logging.getLogger(__name__)
# TODO: figure out why we have buffer underruns
# TODO: why does the mixer segfault? (at least I think that's the cuprit)
class SoundEffect(mixer.Sound):
def __init__(self, path, volume=None, loops=0):
super().__init__(path)
self.path = path
self.volume = volume
if volume:
self.set_volume(volume)
self.loops = loops
def play(self, loops=None):
loops = loops if loops else self.loops
mixer.Sound.play(self, loops=loops)
def set_volume(self, volume, force=False):
# Note: force only intended to be used by fader
if not self.volume or force:
mixer.Sound.set_volume(self, volume)
class TTSSound(SoundEffect):
def __init__(self, path):
super().__init__(path, volume=1.0, loops=0)
self.size = os.path.getsize(path)
def __del__(self):
if os.path.exists(self.path):
os.remove(self.path)
class TTSCache(OrderedDict):
def __init__(self, memLimit):
super().__init__()
self._memLimit = memLimit
self._memUsed = 0
def __setitem__(self, key, value):
if type(value) != TTSSound:
raise TypeError
OrderedDict.__setitem__(self, key, value)
self._memUsed += value.size
self._maintainMemLimit()
def __delitem__(self, key):
self._memUsed -= self[key].size
OrderedDict.__delitem__(self, key)
def clear(self):
logger.debug('Clearing TTS Cache')
OrderedDict.clear(self)
self._memUsed = 0
def _maintainMemLimit(self):
while self._memUsed > self._memLimit:
OrderedDict.popitem(self, last=False)
class SoundLib:
_sentinel = None
def __init__(self, ttsQueue):
mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=1024)
mixer.init()
self.soundEffects = {
'disarmedCountdown': SoundEffect(path='soundfx/smb_coin.wav'),
'disarmed': SoundEffect(path='soundfx/smb_pause.wav'),
'armed': SoundEffect(path='soundfx/smb_powerup.wav'),
'armedCountdown': SoundEffect(path='soundfx/smb_jump-small.wav'),
'triggered': SoundEffect(path='soundfx/alarms/burgler_alarm.ogg', volume=1.0, loops=-1),
'door': SoundEffect(path='soundfx/smb_pipe.wav'),
'numKey': SoundEffect(path='soundfx/smb_bump.wav'),
'ctrlKey': SoundEffect(path='soundfx/smb_fireball.wav'),
'wrongPass': SoundEffect(path='soundfx/smb_fireworks.wav'),
'backspace': SoundEffect(path='soundfx/smb_breakblock.wav'),
}
self._ttsSounds = TTSCache(psutil.virtual_memory().total * 0.001)
self.volume = 100
self._applyVolumesToSounds(self.volume)
self._ttsQueue = ttsQueue
self._stop = threading.Event()
self._startMonitor()
def changeVolume(self, volumeDelta):
newVolume = self.volume + volumeDelta
if newVolume >= 0 and newVolume <= 100:
self._applyVolumesToSounds(newVolume)
def mute(self):
self._applyVolumesToSounds(0)
def speak(self, text):
basename = hashlib.md5(text.encode()).hexdigest()
if basename in self._ttsSounds:
self._ttsSounds.move_to_end(basename)
else:
path = '/tmp/' + basename
call(['espeak', '-a180', '-g8', '-p75', '-w', path, text])
self._ttsSounds[basename] = TTSSound(path)
self._fader(
lowerVolume=0.1,
totalDuration=self._ttsSounds[basename].get_length()
)
self._ttsSounds[basename].play()
logger.debug('TTS engine received "%s"', text)
@async(daemon=False)
def _fader(self, lowerVolume, totalDuration, fadeDuration=0.2, stepSize=5):
alarm = self.soundEffects['triggered']
alarmVolume = alarm.volume
alarmVolumeDelta = alarmVolume - lowerVolume
masterVolume = self.volume
masterVolumeDelta = self.volume - lowerVolume
sleepFadeTime = fadeDuration / stepSize
for i in range(0, stepSize):
if alarmVolumeDelta > 0:
alarm.set_volume(alarmVolume - alarmVolumeDelta * i / stepSize, force=True)
if masterVolumeDelta > 0:
self._applyVolumesToSounds(masterVolume - masterVolumeDelta * i / stepSize)
time.sleep(sleepFadeTime)
time.sleep(totalDuration - 2 * fadeDuration)
for i in range(stepSize - 1, -1, -1):
if alarmVolumeDelta > 0:
alarm.set_volume(alarmVolume - alarmVolumeDelta * i / stepSize, force=True)
if masterVolumeDelta > 0:
self._applyVolumesToSounds(masterVolume - masterVolumeDelta * i / stepSize)
time.sleep(sleepFadeTime)
# will not change sounds that have preset volume
def _applyVolumesToSounds(self, volume):
self.volume = volume
v = volume/100
s = self.soundEffects
for name, sound in s.items():
sound.set_volume(v)
def _ttsMonitor(self):
q = self._ttsQueue
has_task_done = hasattr(q, 'task_done')
while not self._stop.isSet():
try:
text = self._ttsQueue.get(True)
if text is self._sentinel:
break
self.speak(text)
if has_task_done:
q.task_done()
except queue.Empty:
pass
# There might still be records in the queue.
while 1:
try:
text = self._ttsQueue.get(False)
if text is self._sentinel:
break
self.speak(text)
if has_task_done:
q.task_done()
except queue.Empty:
break
def _startMonitor(self):
self._thread = t = threading.Thread(target=self._ttsMonitor, daemon=True)
t.start()
logger.debug('Starting TTS Queue Monitor')
def _stopMonitor(self):
self._stop.set()
self._ttsQueue.put_nowait(self._sentinel)
self._thread.join()
self._thread = None
logger.debug('Stopping TTS Queue Monitor')
def __del__(self):
mixer.quit()
self._stopMonitor()
self._ttsSounds.clear()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
soundfx/beep-07.wav Normal file

Binary file not shown.

BIN
soundfx/beep-08b.wav Normal file

Binary file not shown.

13
soundfx/beep-09b.wav Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=iso-8859-1">
<title>Image Download</title>
</head>
<body>
<h1>Download Failed</h1>
<p>The error message is: Requested file is not available</p>
</body>
</html>

Binary file not shown.

BIN
soundfx/smb2_enter_door.wav Normal file

Binary file not shown.

BIN
soundfx/smb2_shrink.wav Normal file

Binary file not shown.

BIN
soundfx/smb2_throw.wav Normal file

Binary file not shown.

BIN
soundfx/smb_1-up.wav Normal file

Binary file not shown.

BIN
soundfx/smb_breakblock.wav Normal file

Binary file not shown.

BIN
soundfx/smb_bump.wav Normal file

Binary file not shown.

BIN
soundfx/smb_coin.wav Normal file

Binary file not shown.

BIN
soundfx/smb_fireball.wav Normal file

Binary file not shown.

BIN
soundfx/smb_fireworks.wav Normal file

Binary file not shown.

BIN
soundfx/smb_flagpole.wav Normal file

Binary file not shown.

BIN
soundfx/smb_jump-small.wav Normal file

Binary file not shown.

BIN
soundfx/smb_jump-super.wav Normal file

Binary file not shown.

BIN
soundfx/smb_kick.wav Normal file

Binary file not shown.

BIN
soundfx/smb_mariodie.wav Normal file

Binary file not shown.

BIN
soundfx/smb_pause.wav Normal file

Binary file not shown.

BIN
soundfx/smb_pipe.wav Normal file

Binary file not shown.

BIN
soundfx/smb_powerup.wav Normal file

Binary file not shown.

Binary file not shown.

BIN
soundfx/smb_stomp.wav Normal file

Binary file not shown.

BIN
soundfx/smb_vine.wav Normal file

Binary file not shown.

BIN
soundfx/smb_warning.wav Normal file

Binary file not shown.

230
stateMachine.py Normal file
View File

@ -0,0 +1,230 @@
import RPi.GPIO as GPIO
import time, logging
from datetime import datetime
from threading import Lock
from functools import partial
from collections import namedtuple
from auxilary import CountdownTimer, ConfigFile
from sensors import setupDoorSensor, setupMotionSensor
from notifier import intruderAlert
from listeners import KeypadListener, PipeListener
from blinkenLights import Blinkenlights
from soundLib import SoundLib
logger = logging.getLogger(__name__)
class SIGNALS:
ARM = 1
INSTANT_ARM = 2
DISARM = 3
TIMOUT = 4
TRIGGER = 5
class State:
def __init__(self, stateMachine, name, entryCallbacks=[], exitCallbacks=[], blinkLED=True, sound=None):
self.stateMachine = stateMachine
self.name = name
self.entryCallbacks = entryCallbacks
self.exitCallbacks = exitCallbacks
self.blinkLED = blinkLED
if not sound and name in stateMachine.soundLib.soundEffects:
self.sound = stateMachine.soundLib.soundEffects[name]
else:
self.sound = sound
def entry(self):
logger.debug('entering ' + self.name)
#~ if self.sound:
#~ self.sound.play()
#~ self.stateMachine.LED.blink = self.blinkLED
#~ self.stateMachine.keypadListener.resetBuffer()
for c in self.entryCallbacks:
c()
def exit(self):
logger.debug('exiting ' + self.name)
#~ if self.sound:
#~ self.sound.stop()
for c in self.exitCallbacks:
c()
def next(self, signal):
t = (self, signal)
return self if t not in self.stateMachine.transitionTable else self.stateMachine.transitionTable[t]
def __str__(self):
return self.name
def __eq__(self, other):
return self.name == other
def __hash__(self):
return hash(self.name)
class StateMachine:
def __init__(self, camera, ttsQueue, sharedState):
self.sharedState = sharedState
self.soundLib = SoundLib(ttsQueue)
self._cfg = ConfigFile('config.yaml')
def startTimer(t, sound):
self._timer = CountdownTimer(t, partial(self.selectState, SIGNALS.TIMOUT), sound)
def stopTimer():
if self._timer.is_alive():
self._timer.stop()
self._timer = None
States = namedtuple('States', ['disarmed', 'disarmedCountdown', 'armed', 'armedCountdown', 'triggered'])
self.states = States(
State(
self,
name = 'disarmed',
blinkLED = False
),
State(
self,
name='disarmedCountdown',
entryCallbacks = [partial(startTimer, 30, self.soundLib.soundEffects['disarmedCountdown'])],
exitCallbacks = [stopTimer]
),
State(
self,
name = 'armed'
),
State(
self,
name = 'armedCountdown',
entryCallbacks = [partial(startTimer, 30, self.soundLib.soundEffects['armedCountdown'])],
exitCallbacks = [stopTimer]
),
State(
self,
name = 'triggered',
entryCallbacks = [intruderAlert]
)
)
self.currentState = getattr(self.states, self._cfg['state'])
self.transitionTable = {
(self.states.disarmed, SIGNALS.ARM): self.states.disarmedCountdown,
(self.states.disarmed, SIGNALS.INSTANT_ARM): self.states.armed,
(self.states.disarmedCountdown, SIGNALS.DISARM): self.states.disarmed,
(self.states.disarmedCountdown, SIGNALS.TIMOUT): self.states.armed,
(self.states.disarmedCountdown, SIGNALS.INSTANT_ARM): self.states.armed,
(self.states.armed, SIGNALS.DISARM): self.states.disarmed,
(self.states.armed, SIGNALS.TRIGGER): self.states.armedCountdown,
(self.states.armedCountdown, SIGNALS.DISARM): self.states.disarmed,
(self.states.armedCountdown, SIGNALS.ARM): self.states.armed,
(self.states.armedCountdown, SIGNALS.TIMOUT): self.states.triggered,
(self.states.triggered, SIGNALS.DISARM): self.states.disarmed,
(self.states.triggered, SIGNALS.ARM): self.states.armed,
}
self._lock = Lock()
#~ self.LED = Blinkenlights(17)
def action():
if self.currentState == self.armed:
self.selectState(SIGNALS.TRIGGER)
def actionVideo(pin):
if self.currentState in (self.armed, self.armedCountdown, self.triggered):
self.selectState(SIGNALS.TRIGGER)
while GPIO.input(pin):
path = '/mnt/glusterfs/pyledriver/images/%s.jpg'
with open(path % datetime.now(), 'wb') as f:
f.write(camera.getFrame())
time.sleep(0.2)
#~ setupMotionSensor(5, 'Nate\'s room', action)
#~ setupMotionSensor(19, 'front door', action)
#~ setupMotionSensor(26, 'Laura\'s room', action)
#~ setupMotionSensor(6, 'deck window', partial(actionVideo, 6))
#~ setupMotionSensor(13, 'kitchen bar', partial(actionVideo, 13))
#~ setupDoorSensor(22, action, self.soundLib.soundEffects['door'])
secretTable = {
"dynamoHum": partial(self.selectState, SIGNALS.DISARM),
"zombyWoof": partial(self.selectState, SIGNALS.ARM),
"imTheSlime": partial(self.selectState, SIGNALS.INSTANT_ARM)
}
#~ def secretCallback(secret, logger):
#~ if secret in secretTable:
#~ secretTable[secret]()
#~ logger.debug('Secret pipe listener received: \"%s\"', secret)
#~ elif logger:
#~ logger.error('Secret pipe listener received invalid secret')
#~ self.secretListener = PipeListener(
#~ callback = secretCallback,
#~ path = '/tmp/secret'
#~ )
#~ def ttsCallback(text, logger):
#~ self.soundLib.speak(text)
#~ logger.debug('TTS pipe listener received text: \"%s\"', text)
#~ self.ttsListener = PipeListener(
#~ callback = ttsCallback,
#~ path = '/tmp/tts'
#~ )
#~ self.keypadListener = KeypadListener(
#~ stateMachine = self,
#~ callbackDisarm = partial(self.selectState, 'disarm'),
#~ callbackArm = partial(self.selectState, 'arm'),
#~ soundLib = self.soundLib,
#~ passwd = '5918462'
#~ )
self.sharedState['name'] = self.currentState.name
self.currentState.entry()
def selectState(self, signal):
self._lock.acquire() # make state transitions threadsafe
try:
nextState = self.currentState.next(signal)
if nextState != self.currentState:
self.currentState.exit()
self.currentState = nextState
self.currentState.entry()
finally:
self._lock.release()
self.sharedState['name'] = self.currentState.name
self._cfg['state'] = self.currentState.name
self._cfg.sync()
logger.info('state changed to %s', self.currentState)
def __del__(self):
try:
self.LED.__del__()
except AttributeError:
pass
try:
self.soundLib.__del__()
except AttributeError:
pass
try:
self.pipeListener.__del__()
except AttributeError:
pass
try:
self.keypadListener.__del__()
except AttributeError:
pass

49
static/index.css Normal file
View File

@ -0,0 +1,49 @@
html, body {
margin: 0px;
min-height: 100%;
font-family: DejaVu-Sans;
}
.titlebar {
color: #eee;
font-size: 32px;
text-align: center;
padding: 10px;
margin-bottom: 12px;
background: #2b2b2b;
}
#state, #FPSValue {
color: #7196D9;
}
.camera_container {
margin: auto;
display: block;
width: 960px;
}
.camera {
float: left;
width: 640px;
}
.controls {
color: #111;
height: 418px;
width: 300px;
padding: 30px;
border: 1px solid #dcdcdc;
margin-left: 660px;
background: #f7f7f7;
}
.controls form {
width: 80%;
margin: auto;
padding: 0px 30px 10px 30px;
}
.controls form:not(:first-child) {
border-top: 2px solid #888;
}

76
stream.py Executable file
View File

@ -0,0 +1,76 @@
#! /bin/python
from auxilary import async
import gi, time
gi.require_version('Gst', '1.0')
from gi.repository import Gst
Gst.init(None)
pipe = Gst.Pipeline.new("streamer")
bus = pipe.get_bus()
vidSrc = Gst.ElementFactory.make("v4l2src", "vidSrc")
vidConv = Gst.ElementFactory.make("videoconvert", "vidConv")
vidScale = Gst.ElementFactory.make("videoscale", "vidScale")
vidClock = Gst.ElementFactory.make("clockoverlay", "vidClock")
vidEncode = Gst.ElementFactory.make("omxh264enc", "vidEncode")
vidParse = Gst.ElementFactory.make("h264parse", "vidParse")
mux = Gst.ElementFactory.make("mp4mux", "mux")
#~ sink = Gst.ElementFactory.make("tcpserversink", "sink")
sink = Gst.ElementFactory.make("filesink", "sink")
vidSrc.set_property('device', '/dev/video0')
#~ sink.set_property('host', '0.0.0.0')
#~ sink.set_property('port', 8080)
sink.set_property('location', '/home/alarm/testicle.mp4')
vidRawCaps = Gst.Caps.from_string('video/x-raw,width=320,height=240,framerate=30/1')
parseCaps = Gst.Caps.from_string('video/x-h264,stream-format=avc')
pipe.add(vidSrc, vidConv, vidScale, vidClock, vidEncode, vidParse, mux, sink)
print(vidSrc.link(vidConv))
print(vidConv.link(vidScale))
print(vidScale.link_filtered(vidClock, vidRawCaps))
print(vidClock.link(vidEncode))
print(vidEncode.link(vidParse))
print(vidParse.link_filtered(mux, parseCaps))
print(mux.link(sink))
pipe.set_state(Gst.State.PLAYING)
#~ signal.signal(signal.SIGTERM, exit())
def terminate():
pipe.set_state(Gst.State.NULL)
exit()
@async(daemon=True)
def errorHandler():
while 1:
msg = bus.timed_pop_filtered(1e18, Gst.MessageType.ERROR)
print('howdy')
print(msg.parse_error())
terminate()
@async(daemon=True)
def eosHandler():
while 1:
msg = bus.timed_pop_filtered(1e18, Gst.MessageType.EOS)
print('EOS reached')
terminate()
try:
errorHandler()
eosHandler()
while 1:
time.sleep(3600)
except KeyboardInterrupt:
pass

8
templates/framerate.html Normal file
View File

@ -0,0 +1,8 @@
<html>
<head>
<title>Camera 1</title>
</head>
<body>
</body>
</html>

35
templates/index.html Normal file
View File

@ -0,0 +1,35 @@
<html>
<link rel="stylesheet" href="{{ url_for('camPage.static', filename='index.css') }}">
<head>
<title>Camera 1</title>
</head>
<body>
<div class="titlebar">
<b>Pyledriver Status: </b><span id='state'>{{ state }}</span>
</div>
<div class="camera_container">
<div class="camera">
<img id="bg" src="{{ url_for('camPage.videoFeed') }}">
</div>
<div class="controls">
<form action="{{ url_for('camPage.index') }}" method='post' name='framerate'>
{{ camForm.hidden_tag() }}
<p>Current Framerate: <span id='FPSvalue'>{{ fps }}</span></p>
<p>Set Framerate {{ camForm.fps }}</p>
<p>{{ camForm.submitFPS }}</p>
</form>
<form action="{{ url_for('camPage.index') }}" method='post' name='text_to_speech'>
{{ ttsForm.hidden_tag() }}
<p>Speak your mind</p>
<p>{{ ttsForm.tts }}</p>
<p>{{ ttsForm.submitTTS }}</p>
</form>
<form action="{{ url_for('camPage.index') }}" method='post' name='text_to_speech'>
{{ ttsForm.hidden_tag() }}
<p>If something goes wrong...</p>
<p>{{ resetForm.submitReset }}</p>
</form>
</div>
</div>
</body>
</html>

84
webInterface.py Normal file
View File

@ -0,0 +1,84 @@
from multiprocessing import Process
from sharedLogging import SlaveLogger
from flask import Flask, render_template, Response, Blueprint, redirect, url_for
from flask_wtf import FlaskForm
from wtforms.fields import SelectField, StringField, SubmitField
from wtforms.validators import InputRequired
class CamForm(FlaskForm):
fps = SelectField(choices=[(i, '%s fps' % i) for i in range(10, 31, 5)], coerce=int)
submitFPS = SubmitField('Set')
class TTSForm(FlaskForm):
tts = StringField(validators=[InputRequired()])
submitTTS = SubmitField('Speak')
class ResetForm(FlaskForm):
submitReset = SubmitField('Reset')
# TODO: fix random connection fails (might be an nginx thing)
# TODO: show camera failed status here somewhere
class WebInterface(Process):
def __init__(self, camera, stateDict, ttsQueue, loggerQueue):
self._moduleLogger = SlaveLogger(__name__, 'INFO', loggerQueue)
self._flaskLogger = SlaveLogger('werkzeug', 'ERROR', loggerQueue)
camPage = Blueprint('camPage', __name__, static_folder='static', template_folder='templates')
def generateFrame():
while 1:
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + camera.getFrame() + b'\r\n')
@camPage.route('/', methods=['GET', 'POST'])
@camPage.route('/index', methods=['GET', 'POST'])
def index():
props = camera.getProps('FPS')
fps = int(props['FPS'])
camForm = CamForm(fps = props['FPS'])
ttsForm = TTSForm()
resetForm = ResetForm()
if camForm.validate_on_submit() and camForm.submitFPS.data:
camera.setProps(FPS=camForm.fps.data)
return redirect(url_for('camPage.index'))
if ttsForm.validate_on_submit() and ttsForm.submitTTS.data:
ttsQueue.put_nowait(ttsForm.tts.data)
return redirect(url_for('camPage.index'))
if resetForm.validate_on_submit() and resetForm.submitReset.data:
camera.reset()
return redirect(url_for('camPage.index'))
return render_template(
'index.html',
camForm=camForm,
ttsForm=ttsForm,
resetForm=resetForm,
fps=fps,
state=stateDict['name']
)
@camPage.route('/videoFeed')
def videoFeed():
return Response(generateFrame(), mimetype='multipart/x-mixed-replace; boundary=frame')
self._app = Flask(__name__)
self._app.secret_key = '3276d68dac56985bea352325125641ff'
self._app.register_blueprint(camPage, url_prefix='/pyledriver')
super().__init__(daemon=True)
def run(self):
# TODO: not sure exactly how threaded=True works, intended to enable
# multiple connections. May want to use something more robust w/ camera
# see here: https://blog.miguelgrinberg.com/post/video-streaming-with-flask
self._moduleLogger.info('Started web interface')
self._app.run(debug=False, threaded=True)
def stop(self):
self.terminate()
self.join()
self._moduleLogger.info('Terminated web interface')