pyledriver/stateMachine.py

225 lines
6.2 KiB
Python
Raw Normal View History

2016-12-30 02:51:56 -05:00
import RPi.GPIO as GPIO
2017-06-08 01:43:14 -04:00
import time, logging, enum
2016-12-30 02:51:56 -05:00
from threading import Lock
from functools import partial
from collections import namedtuple
2017-06-08 01:43:14 -04:00
from enum import Enum, auto
2016-12-30 02:51:56 -05:00
from auxilary import CountdownTimer, resetUSBDevice
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__)
2017-06-08 01:43:14 -04:00
class SIGNALS(enum.Enum):
ARM = enum.auto()
INSTANT_ARM = enum.auto()
DISARM = enum.auto()
TIMOUT = enum.auto()
TRIGGER = enum.auto()
2016-12-30 02:51:56 -05:00
class State:
2017-06-08 02:15:50 -04:00
def __init__(self, stateMachine, name, entryCallbacks=[], exitCallbacks=[], sound=None):
2016-12-30 02:51:56 -05:00
self.stateMachine = stateMachine
self.name = name
self.entryCallbacks = entryCallbacks
self.exitCallbacks = exitCallbacks
2017-06-08 02:15:50 -04:00
sfx = stateMachine.soundLib.soundEffects
self._sound = sfx[name] if not sound and name in sfx else 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-08 02:15:50 -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-08 02:15:50 -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):
2017-06-08 01:43:14 -04:00
if signal in SIGNALS:
2017-06-08 02:15:50 -04:00
s = (self, signal)
t = self.stateMachine.transitionTable
return self if s not in t else t[s]
2017-06-08 01:43:14 -04:00
else:
raise Exception('Illegal signal')
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.soundLib = SoundLib()
self.LED = Blinkenlights(17)
self.camera = Camera()
self.fileDump = FileDump()
2016-12-30 02:51:56 -05:00
2017-06-08 01:02:31 -04:00
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.debug('Secret pipe listener received invalid secret')
self.secretListener = PipeListener(
callback = secretCallback,
name = 'secret'
)
self.keypadListener = KeypadListener(
stateMachine = self,
2017-06-08 01:22:40 -04:00
callbackDisarm = partial(self.selectState, SIGNALS.DISARM),
callbackArm = partial(self.selectState, SIGNALS.ARM),
2017-06-08 01:02:31 -04:00
soundLib = self.soundLib,
passwd = '5918462'
)
2016-12-30 02:51:56 -05:00
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
2017-06-03 19:11:06 -04:00
2017-06-08 02:15:50 -04:00
blinkingLED = partial(self.LED.setBlink, True)
sfx = self.soundLib.soundEffects
2017-06-03 19:11:06 -04:00
stateObjs = [
2016-12-30 02:51:56 -05:00
State(
self,
name = 'disarmed',
2017-06-08 02:15:50 -04:00
entryCallbacks = [partial(self.LED.setBlink, False)]
2016-12-30 02:51:56 -05:00
),
State(
self,
2017-06-08 02:15:50 -04:00
name = 'disarmedCountdown',
entryCallbacks = [blinkingLED, partial(startTimer, 30, sfx['disarmedCountdown'])],
2016-12-30 02:51:56 -05:00
exitCallbacks = [stopTimer]
),
State(
self,
2017-06-08 02:15:50 -04:00
name = 'armed',
entryCallbacks = [blinkingLED]
2016-12-30 02:51:56 -05:00
),
State(
self,
name = 'armedCountdown',
2017-06-08 02:15:50 -04:00
entryCallbacks = [blinkingLED, partial(startTimer, 30, sfx['armedCountdown'])],
2016-12-30 02:51:56 -05:00
exitCallbacks = [stopTimer]
),
State(
self,
name = 'triggered',
2017-06-08 02:15:50 -04:00
entryCallbacks = [blinkingLED, intruderAlert]
2016-12-30 02:51:56 -05:00
)
2017-06-03 19:11:06 -04:00
]
2017-06-08 02:15:50 -04:00
for s in stateObjs:
s.entryCallbacks.append(self.keypadListener.resetBuffer)
2017-06-03 19:11:06 -04:00
self.states = namedtuple('States', [s.name for s in stateObjs])(*stateObjs)
2016-12-30 02:51:56 -05:00
self.currentState = getattr(self.states, stateFile['state'])
2016-12-30 02:51:56 -05:00
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,
}
def start(self):
2017-06-08 01:02:31 -04:00
resetUSBDevice('1-1', logger)
self.soundLib.start()
self.LED.start()
self.keypadListener.start()
self.secretListener.start()
self.camera.start()
self.fileDump.start()
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()
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
2016-12-30 02:51:56 -05:00
logger.info('state changed to %s', self.currentState)
def __del__(self):
2017-06-03 19:11:06 -04:00
for i in ['LED', 'camera', 'fileDump', 'soundLib', 'secretListener', 'keypadListener']:
try:
getattr(self, i).__del__()
except AttributeError:
pass