pyledriver/stateMachine.py

267 lines
7.2 KiB
Python
Raw Normal View History

2016-12-30 02:51:56 -05:00
import RPi.GPIO as GPIO
import time, logging, enum, os
2016-12-30 02:51:56 -05:00
from threading import Lock
from functools import partial
from collections import namedtuple
from exceptionThreading import ExceptionThread
from config import stateFile
from sensors import startDoorSensor, startMotionSensor
2017-05-31 00:41:42 -04:00
from gmail import intruderAlert
2016-12-30 02:51:56 -05:00
from listeners import KeypadListener, PipeListener
from blinkenLights import Blinkenlights
from soundLib import SoundLib
from webInterface import startWebInterface
2017-06-03 01:28:57 -04:00
from stream import Camera, FileDump
2016-12-30 02:51:56 -05:00
logger = logging.getLogger(__name__)
class _SIGNALS(enum.Enum):
2017-06-08 01:43:14 -04:00
ARM = enum.auto()
INSTANT_ARM = enum.auto()
DISARM = enum.auto()
TIMOUT = enum.auto()
TRIGGER = enum.auto()
class _CountdownTimer(ExceptionThread):
'''
Launches thread which self terminates after some time (given in seconds).
Termination triggers some action (a function). Optionally, a sound can be
assigned to each 'tick'
'''
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()
def _resetUSBDevice(device):
'''
Resets a USB device using the de/reauthorization method. This is really
crude but works beautifully
'''
devpath = os.path.join('/sys/bus/usb/devices/' + device + '/authorized')
with open(devpath, 'w') as f:
f.write('0')
with open(devpath, 'w') as f:
f.write('1')
logger.debug('Reset USB device: %s', devpath)
2016-12-30 02:51:56 -05:00
class _State:
2017-06-10 01:18:36 -04:00
def __init__(self, name, entryCallbacks=[], exitCallbacks=[], sound=None):
2016-12-30 02:51:56 -05:00
self.name = name
self.entryCallbacks = entryCallbacks
self.exitCallbacks = exitCallbacks
2017-06-10 01:13:51 -04:00
self._transTbl = {}
2016-12-30 02:51:56 -05:00
2017-06-10 01:18:36 -04:00
self._sound = sound
2016-12-30 02:51:56 -05:00
def entry(self):
2017-05-30 02:11:15 -04:00
logger.info('entering ' + self.name)
2017-06-10 01:18:36 -04:00
if self._sound:
self._sound.play()
2016-12-30 02:51:56 -05:00
for c in self.entryCallbacks:
c()
def exit(self):
2017-05-30 02:11:15 -04:00
logger.info('exiting ' + self.name)
2017-06-10 01:18:36 -04:00
if self._sound:
self._sound.stop()
2016-12-30 02:51:56 -05:00
for c in self.exitCallbacks:
c()
def next(self, signal):
if signal in _SIGNALS:
2017-06-10 01:13:51 -04:00
return self if signal not in self._transTbl else self._transTbl[signal]
2017-06-08 01:43:14 -04:00
else:
raise Exception('Illegal signal')
2017-06-10 01:13:51 -04:00
def addTransition(self, signal, state):
2017-06-10 02:15:16 -04:00
self._transTbl[signal] = state
2016-12-30 02:51:56 -05:00
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):
self._lock = Lock()
self._managed = []
self.soundLib = self._addManaged(SoundLib())
self.fileDump = self._addManaged(FileDump())
self._addManaged(Camera())
2016-12-30 02:51:56 -05:00
# add signals to self to avoid calling partial every time
for sig in _SIGNALS:
2017-06-10 01:56:42 -04:00
setattr(self, sig.name, partial(self.selectState, sig))
2017-06-08 01:02:31 -04:00
secretTable = {
'dynamoHum': self.DISARM,
'zombyWoof': self.ARM,
'imTheSlime': self.INSTANT_ARM
2017-06-08 01:02:31 -04:00
}
def secretCallback(secret, logger):
if secret in secretTable:
secretTable[secret]()
logger.debug('Secret pipe listener received: \"%s\"', secret)
elif logger:
logger.debug('Secret pipe listener received invalid secret')
self._addManaged(PipeListener(callback=secretCallback, name= 'secret'))
2017-06-08 01:02:31 -04:00
keypadListener = self._addManaged(
KeypadListener(
stateMachine = self,
callbackDisarm = self.DISARM,
callbackArm = self.ARM,
passwd = '5918462'
)
2017-06-08 01:02:31 -04:00
)
2016-12-30 02:51:56 -05:00
def startTimer(t, sound):
self._timer = _CountdownTimer(t, self.TIMOUT, sound)
2016-12-30 02:51:56 -05:00
def stopTimer():
if self._timer.is_alive():
self._timer.stop()
self._timer = None
2017-06-03 19:11:06 -04:00
LED = self._addManaged(Blinkenlights(17))
blinkingLED = partial(LED.setBlink, True)
2017-06-08 02:15:50 -04:00
sfx = self.soundLib.soundEffects
2017-06-03 19:11:06 -04:00
stateObjs = [
_State(
2016-12-30 02:51:56 -05:00
name = 'disarmed',
entryCallbacks = [partial(LED.setBlink, False)],
2017-06-10 01:18:36 -04:00
sound = sfx['disarmed']
2016-12-30 02:51:56 -05:00
),
_State(
2017-06-08 02:15:50 -04:00
name = 'disarmedCountdown',
2017-06-10 01:18:36 -04:00
entryCallbacks = [blinkingLED, partial(startTimer, 30, sfx['disarmedCountdown'])],
exitCallbacks = [stopTimer],
sound = sfx['disarmedCountdown']
2016-12-30 02:51:56 -05:00
),
_State(
2017-06-08 02:15:50 -04:00
name = 'armed',
2017-06-10 01:18:36 -04:00
entryCallbacks = [blinkingLED],
sound = sfx['armed']
2016-12-30 02:51:56 -05:00
),
_State(
2016-12-30 02:51:56 -05:00
name = 'armedCountdown',
2017-06-10 01:18:36 -04:00
entryCallbacks = [blinkingLED, partial(startTimer, 30, sfx['armedCountdown'])],
exitCallbacks = [stopTimer],
sound = sfx['armedCountdown']
2016-12-30 02:51:56 -05:00
),
_State(
2016-12-30 02:51:56 -05:00
name = 'triggered',
2017-06-10 01:18:36 -04:00
entryCallbacks = [blinkingLED, intruderAlert],
sound = sfx['triggered']
2016-12-30 02:51:56 -05:00
)
2017-06-03 19:11:06 -04:00
]
2017-06-10 01:56:42 -04:00
for obj in stateObjs:
obj.entryCallbacks.append(keypadListener.resetBuffer)
2017-06-08 02:15:50 -04:00
2017-06-10 01:56:42 -04:00
self.states = st = namedtuple('States', [obj.name for obj in stateObjs])(*stateObjs)
2016-12-30 02:51:56 -05:00
st.disarmed.addTransition( _SIGNALS.ARM, st.disarmedCountdown)
st.disarmed.addTransition( _SIGNALS.INSTANT_ARM, st.armed)
2016-12-30 02:51:56 -05:00
st.disarmedCountdown.addTransition( _SIGNALS.DISARM, st.disarmed)
st.disarmedCountdown.addTransition( _SIGNALS.TIMOUT, st.armed)
st.disarmedCountdown.addTransition( _SIGNALS.INSTANT_ARM, st.armed)
2017-06-10 01:13:51 -04:00
st.armed.addTransition( _SIGNALS.DISARM, st.disarmed)
st.armed.addTransition( _SIGNALS.TRIGGER, st.armedCountdown)
2017-06-10 01:13:51 -04:00
st.armedCountdown.addTransition( _SIGNALS.DISARM, st.disarmed)
st.armedCountdown.addTransition( _SIGNALS.TIMOUT, st.triggered)
st.armedCountdown.addTransition( _SIGNALS.ARM, st.armed)
st.armedCountdown.addTransition( _SIGNALS.INSTANT_ARM, st.armed)
2017-06-10 01:13:51 -04:00
st.triggered.addTransition( _SIGNALS.DISARM, st.disarmed)
st.triggered.addTransition( _SIGNALS.ARM, st.armed)
st.triggered.addTransition( _SIGNALS.INSTANT_ARM, st.armed)
2017-06-10 01:13:51 -04:00
self.currentState = getattr(self.states, stateFile['state'])
2016-12-30 02:51:56 -05:00
def __enter__(self):
_resetUSBDevice('1-1')
self._startManaged()
def action():
if self.currentState == self.states.armed:
self.selectState(_SIGNALS.TRIGGER)
sensitiveStates = (self.states.armed, self.states.armedCountdown, self.states.triggered)
def actionVideo(pin):
if self.currentState in sensitiveStates:
self.selectState(_SIGNALS.TRIGGER)
self.fileDump.addInitiator(pin)
while GPIO.input(pin) and self.currentState in sensitiveStates:
time.sleep(0.1)
self.fileDump.removeInitiator(pin)
2016-12-30 02:51:56 -05:00
startMotionSensor(5, 'Nate\'s room', action)
startMotionSensor(19, 'front door', action)
startMotionSensor(26, 'Laura\'s room', action)
startMotionSensor(6, 'deck window', partial(actionVideo, 6))
startMotionSensor(13, 'kitchen bar', partial(actionVideo, 13))
startDoorSensor(22, action, self.soundLib.soundEffects['door'])
startWebInterface(self)
self.currentState.entry()
2017-06-10 01:13:51 -04:00
def __exit__(self, exception_type, exception_value, traceback):
self._stopManaged()
2016-12-30 02:51:56 -05:00
def selectState(self, signal):
with self._lock:
2016-12-30 02:51:56 -05:00
nextState = self.currentState.next(signal)
if nextState != self.currentState:
self.currentState.exit()
self.currentState = nextState
self.currentState.entry()
stateFile['state'] = self.currentState.name
def _addManaged(self, obj):
self._managed.append(obj)
return obj
def _startManaged(self):
for m in self._managed:
m.start()
def _stopManaged(self):
for m in self._managed:
m.stop()