init
This commit is contained in:
commit
a93f116aae
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
||||||
|
recipientList:
|
||||||
|
- natedwarshuis@gmail.com
|
||||||
|
state: disarmed
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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.
Binary file not shown.
Binary file not shown.
|
@ -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.
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Camera 1</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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>
|
|
@ -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')
|
Loading…
Reference in New Issue