separate start and init sequences in statemachine

This commit is contained in:
petrucci4prez 2017-06-07 01:42:58 -04:00
parent 964445ca77
commit 766b5f6c81
8 changed files with 133 additions and 121 deletions

View File

@ -31,15 +31,17 @@ class Blinkenlights(ExceptionThread):
pwm.stop() # required to avoid core dumps when process terminates pwm.stop() # required to avoid core dumps when process terminates
super().__init__(target=blinkLights, daemon=True) super().__init__(target=blinkLights, daemon=True)
self.start()
logger.debug('Starting LED on pin %s', self._pin)
def setCyclePeriod(self, cyclePeriod): def start(self):
self._sleeptime = cyclePeriod/20/2 ExceptionThread.start(self)
logger.debug('Starting LED on pin %s', self._pin)
def stop(self): def stop(self):
self._stopper.set() self._stopper.set()
logger.debug('Stopping LED on pin %s', self._pin) logger.debug('Stopping LED on pin %s', self._pin)
def setCyclePeriod(self, cyclePeriod):
self._sleeptime = cyclePeriod/20/2
def __del__(self): def __del__(self):
self.stop() self.stop()

View File

@ -11,7 +11,7 @@ import stateMachine
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class KeypadListener(): class KeypadListener:
''' '''
Interface for standard numpad device. Capabilities include: Interface for standard numpad device. Capabilities include:
- accepting numeric input - accepting numeric input
@ -35,20 +35,11 @@ class KeypadListener():
82: '0', 83: '.' 82: '0', 83: '.'
} }
devPath = '/dev/input/by-id/usb-04d9_1203-event-kbd'
waitForPath(devPath, logger)
self._dev = InputDevice(devPath)
self._dev.grab()
numKeySound = soundLib.soundEffects['numKey'] numKeySound = soundLib.soundEffects['numKey']
ctrlKeySound = soundLib.soundEffects['ctrlKey'] ctrlKeySound = soundLib.soundEffects['ctrlKey']
wrongPassSound = soundLib.soundEffects['wrongPass'] wrongPassSound = soundLib.soundEffects['wrongPass']
backspaceSound = soundLib.soundEffects['backspace'] backspaceSound = soundLib.soundEffects['backspace']
self._clearBuffer()
def getInput(): def getInput():
while 1: while 1:
r, w, x = select([self._dev], [], []) r, w, x = select([self._dev], [], [])
@ -112,11 +103,24 @@ class KeypadListener():
ctrlKeySound.play() ctrlKeySound.play()
self._dev.set_led(ecodes.LED_NUML, 0 if soundLib.volume > 0 else 1) self._dev.set_led(ecodes.LED_NUML, 0 if soundLib.volume > 0 else 1)
self._resetCountdown = None
self._listener = ExceptionThread(target=getInput, daemon=True) self._listener = ExceptionThread(target=getInput, daemon=True)
self._resetCountdown = None
self._clearBuffer()
def start(self):
devPath = '/dev/input/by-id/usb-04d9_1203-event-kbd'
waitForPath(devPath, logger)
self._dev = InputDevice(devPath)
self._dev.grab()
self._listener.start() self._listener.start()
logger.debug('Started keypad device') logger.debug('Started keypad listener')
def resetBuffer(self):
self._stopResetCountdown
self._clearBuffer()
def _startResetCountdown(self): def _startResetCountdown(self):
self._resetCountdown = CountdownTimer(30, self._clearBuffer) self._resetCountdown = CountdownTimer(30, self._clearBuffer)
@ -126,10 +130,6 @@ class KeypadListener():
self._resetCountdown.stop() self._resetCountdown.stop()
self._resetCountdown = None self._resetCountdown = None
def resetBuffer(self):
self._stopResetCountdown
self._clearBuffer()
def _clearBuffer(self): def _clearBuffer(self):
self._buf = '' self._buf = ''
@ -170,7 +170,9 @@ class PipeListener(ExceptionThread):
callback(msg, logger) callback(msg, logger)
super().__init__(target=listen, daemon=True) super().__init__(target=listen, daemon=True)
self.start()
def start(self):
ExceptionThread.start(self)
logger.debug('Started pipe listener at path %s', self._path) logger.debug('Started pipe listener at path %s', self._path)
def __del__(self): def __del__(self):

View File

@ -36,6 +36,7 @@ if __name__ == '__main__':
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BCM)
stateMachine = StateMachine() stateMachine = StateMachine()
stateMachine.start()
signal.signal(signal.SIGTERM, sigtermHandler) signal.signal(signal.SIGTERM, sigtermHandler)

View File

@ -22,12 +22,12 @@ def lowPassFilter(pin, targetVal, period=0.001):
return GPIO.input(pin) == targetVal return GPIO.input(pin) == targetVal
def setupGPIO(name, pin, GPIOEvent, callback): def _initGPIO(name, pin, GPIOEvent, callback):
logger.info('setting up \"%s\" on pin %s', name, pin) logger.info('setting up \"%s\" on pin %s', name, pin)
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(pin, GPIOEvent, callback=callback, bouncetime=500) GPIO.add_event_detect(pin, GPIOEvent, callback=callback, bouncetime=500)
def setupMotionSensor(pin, location, action): def startMotionSensor(pin, location, action):
name = 'MotionSensor@' + location name = 'MotionSensor@' + location
def trip(channel): def trip(channel):
@ -36,9 +36,9 @@ def setupMotionSensor(pin, location, action):
action() action()
logger.debug('waiting %s for %s to power on', INIT_DELAY, name) logger.debug('waiting %s for %s to power on', INIT_DELAY, name)
CountdownTimer(INIT_DELAY, partial(setupGPIO, name, pin, GPIO.RISING, trip)) CountdownTimer(INIT_DELAY, partial(_initGPIO, name, pin, GPIO.RISING, trip))
def setupDoorSensor(pin, action, sound=None): def startDoorSensor(pin, action, sound=None):
def trip(channel): def trip(channel):
nonlocal closed nonlocal closed
val = GPIO.input(pin) val = GPIO.input(pin)
@ -56,5 +56,5 @@ def setupDoorSensor(pin, action, sound=None):
sound.play() sound.play()
action() action()
setupGPIO('DoorSensor', pin, GPIO.BOTH, trip) _initGPIO('DoorSensor', pin, GPIO.BOTH, trip)
closed = GPIO.input(pin) closed = GPIO.input(pin)

View File

@ -88,6 +88,8 @@ class SoundLib:
self._ttsQueue = queue.Queue() self._ttsQueue = queue.Queue()
self._stopper = Event() self._stopper = Event()
def start(self):
self._startMonitor() self._startMonitor()
def changeVolume(self, volumeDelta): def changeVolume(self, volumeDelta):

View File

@ -6,18 +6,16 @@ from collections import namedtuple
from auxilary import CountdownTimer, resetUSBDevice from auxilary import CountdownTimer, resetUSBDevice
from config import stateFile from config import stateFile
from sensors import setupDoorSensor, setupMotionSensor from sensors import startDoorSensor, startMotionSensor
from gmail import intruderAlert from gmail import intruderAlert
from listeners import KeypadListener, PipeListener from listeners import KeypadListener, PipeListener
from blinkenLights import Blinkenlights from blinkenLights import Blinkenlights
from soundLib import SoundLib from soundLib import SoundLib
from webInterface import initWebInterface from webInterface import startWebInterface
from stream import Camera, FileDump from stream import Camera, FileDump
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
resetUSBDevice('1-1')
class SIGNALS: class SIGNALS:
ARM = 1 ARM = 1
INSTANT_ARM = 2 INSTANT_ARM = 2
@ -69,7 +67,12 @@ class State:
class StateMachine: class StateMachine:
def __init__(self): def __init__(self):
self._lock = Lock()
self.soundLib = SoundLib() self.soundLib = SoundLib()
self.LED = Blinkenlights(17)
self.camera = Camera()
self.fileDump = FileDump()
def startTimer(t, sound): def startTimer(t, sound):
self._timer = CountdownTimer(t, partial(self.selectState, SIGNALS.TIMOUT), sound) self._timer = CountdownTimer(t, partial(self.selectState, SIGNALS.TIMOUT), sound)
@ -131,35 +134,6 @@ class StateMachine:
(self.states.triggered, SIGNALS.ARM): self.states.armed, (self.states.triggered, SIGNALS.ARM): self.states.armed,
} }
self._lock = Lock()
self.LED = Blinkenlights(17)
self.camera = Camera()
def action():
if self.currentState == self.states.armed:
self.selectState(SIGNALS.TRIGGER)
self.fileDump = FileDump()
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)
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 = { secretTable = {
"dynamoHum": partial(self.selectState, SIGNALS.DISARM), "dynamoHum": partial(self.selectState, SIGNALS.DISARM),
"zombyWoof": partial(self.selectState, SIGNALS.ARM), "zombyWoof": partial(self.selectState, SIGNALS.ARM),
@ -186,7 +160,39 @@ class StateMachine:
passwd = '5918462' passwd = '5918462'
) )
initWebInterface(self) def start(self):
resetUSBDevice('1-1')
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)
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() self.currentState.entry()

109
stream.py
View File

@ -68,7 +68,48 @@ class ThreadedPipeline:
self._stopper = Event() self._stopper = Event()
def start(self, play=True): def start(self, play=True):
self._startPipeline(play) pName = self._pipeline.get_name()
stateChange = self._pipeline.set_state(Gst.State.PAUSED)
_gstPrintMsg(pName, 'Setting to PAUSED', level=logging.INFO)
if stateChange == Gst.StateChangeReturn.FAILURE:
_gstPrintMsg(pName, 'Cannot set to PAUSE', level=logging.INFO)
self._eventLoop(block=False, doProgress=False, targetState=Gst.State.VOID_PENDING)
# we should always end up here because live
elif stateChange == Gst.StateChangeReturn.NO_PREROLL:
_gstPrintMsg(pName, 'Live and does not need preroll')
elif stateChange == Gst.StateChangeReturn.ASYNC:
_gstPrintMsg(pName, 'Prerolling')
try:
_eventLoop(block=True, doProgress=True, targetState=Gst.State.PAUSED)
except GstException:
_gstPrintMsg(pName, 'Does not want to preroll', level=logging.ERROR)
raise SystemExit
elif stateChange == Gst.StateChangeReturn.SUCCESS:
_gstPrintMsg(pName, 'Is prerolled')
# this should always succeed...
try:
self._eventLoop(block=False, doProgress=True, targetState=Gst.State.PLAYING)
except GstException:
_gstPrintMsg(pName, 'Does not want to preroll', level=logging.ERROR)
raise SystemExit
# ...and end up here
else:
if play:
_gstPrintMsg(pName, 'Setting to PLAYING', level=logging.INFO)
# ...and since this will ALWAYS be successful...
if self._pipeline.set_state(Gst.State.PLAYING) == Gst.StateChangeReturn.FAILURE:
_gstPrintMsg(pName, 'Cannot set to PLAYING', level=logging.ERROR)
err = self._pipeline.get_bus().pop_filtered(Gst.MessageType.Error)
_processErrorMessage(pName, msgSrcName, err)
# ...we end up here and loop until Tool releases their next album
try:
self._mainLoop()
except:
raise GstException
# TODO: this might not all be necessary # TODO: this might not all be necessary
def stop(self): def stop(self):
@ -248,50 +289,6 @@ class ThreadedPipeline:
def _mainLoop(self): def _mainLoop(self):
self._eventLoop(block=True, doProgress=False, targetState=Gst.State.PLAYING) self._eventLoop(block=True, doProgress=False, targetState=Gst.State.PLAYING)
def _startPipeline(self, play):
pName = self._pipeline.get_name()
stateChange = self._pipeline.set_state(Gst.State.PAUSED)
_gstPrintMsg(pName, 'Setting to PAUSED', level=logging.INFO)
if stateChange == Gst.StateChangeReturn.FAILURE:
_gstPrintMsg(pName, 'Cannot set to PAUSE', level=logging.INFO)
self._eventLoop(block=False, doProgress=False, targetState=Gst.State.VOID_PENDING)
# we should always end up here because live
elif stateChange == Gst.StateChangeReturn.NO_PREROLL:
_gstPrintMsg(pName, 'Live and does not need preroll')
elif stateChange == Gst.StateChangeReturn.ASYNC:
_gstPrintMsg(pName, 'Prerolling')
try:
_eventLoop(block=True, doProgress=True, targetState=Gst.State.PAUSED)
except GstException:
_gstPrintMsg(pName, 'Does not want to preroll', level=logging.ERROR)
raise SystemExit
elif stateChange == Gst.StateChangeReturn.SUCCESS:
_gstPrintMsg(pName, 'Is prerolled')
# this should always succeed...
try:
self._eventLoop(block=False, doProgress=True, targetState=Gst.State.PLAYING)
except GstException:
_gstPrintMsg(pName, 'Does not want to preroll', level=logging.ERROR)
raise SystemExit
# ...and end up here
else:
if play:
_gstPrintMsg(pName, 'Setting to PLAYING', level=logging.INFO)
# ...and since this will ALWAYS be successful...
if self._pipeline.set_state(Gst.State.PLAYING) == Gst.StateChangeReturn.FAILURE:
_gstPrintMsg(pName, 'Cannot set to PLAYING', level=logging.ERROR)
err = self._pipeline.get_bus().pop_filtered(Gst.MessageType.Error)
_processErrorMessage(pName, msgSrcName, err)
# ...we end up here and loop until Tool releases their next album
try:
self._mainLoop()
except:
raise GstException
class Camera(ThreadedPipeline): class Camera(ThreadedPipeline):
''' '''
Class for usb camera. The 'video' and 'audio' flags are meant for testing. Class for usb camera. The 'video' and 'audio' flags are meant for testing.
@ -304,11 +301,12 @@ class Camera(ThreadedPipeline):
X = 1 is used by the Janus WebRTC interface and X = 2 is used by the X = 1 is used by the Janus WebRTC interface and X = 2 is used by the
FileDump class below. FileDump class below.
''' '''
_vPath = '/dev/video0'
_aPath = 'hw:1,0'
def __init__(self, video=True, audio=True): def __init__(self, video=True, audio=True):
super().__init__('camera') super().__init__('camera')
vPath = '/dev/video0'
if video: if video:
vSource = Gst.ElementFactory.make("v4l2src", "videoSource") vSource = Gst.ElementFactory.make("v4l2src", "videoSource")
vConvert = Gst.ElementFactory.make("videoconvert", "videoConvert") vConvert = Gst.ElementFactory.make("videoconvert", "videoConvert")
@ -318,7 +316,7 @@ class Camera(ThreadedPipeline):
vRTPPay = Gst.ElementFactory.make("rtph264pay", "videoRTPPayload") vRTPPay = Gst.ElementFactory.make("rtph264pay", "videoRTPPayload")
vRTPSink = Gst.ElementFactory.make("multiudpsink", "videoRTPSink") vRTPSink = Gst.ElementFactory.make("multiudpsink", "videoRTPSink")
vSource.set_property('device', vPath) vSource.set_property('device', self._vPath)
vRTPPay.set_property('config-interval', 1) vRTPPay.set_property('config-interval', 1)
vRTPPay.set_property('pt', 96) vRTPPay.set_property('pt', 96)
vRTPSink.set_property('clients', '127.0.0.1:9001,127.0.0.1:9002') vRTPSink.set_property('clients', '127.0.0.1:9001,127.0.0.1:9002')
@ -342,7 +340,7 @@ class Camera(ThreadedPipeline):
aRTPPay = Gst.ElementFactory.make("rtpopuspay", "audioRTPPayload") aRTPPay = Gst.ElementFactory.make("rtpopuspay", "audioRTPPayload")
aRTPSink = Gst.ElementFactory.make("multiudpsink", "audioRTPSink") aRTPSink = Gst.ElementFactory.make("multiudpsink", "audioRTPSink")
aSource.set_property('device', 'hw:1,0') aSource.set_property('device', self._aPath)
aRTPSink.set_property('clients', '127.0.0.1:8001,127.0.0.1:8002') aRTPSink.set_property('clients', '127.0.0.1:8001,127.0.0.1:8002')
aCaps = Gst.Caps.from_string('audio/x-raw,rate=48000,channels=1') aCaps = Gst.Caps.from_string('audio/x-raw,rate=48000,channels=1')
@ -355,9 +353,10 @@ class Camera(ThreadedPipeline):
_linkElements(aEncode, aRTPPay) _linkElements(aEncode, aRTPPay)
_linkElements(aRTPPay, aRTPSink) _linkElements(aRTPPay, aRTPSink)
waitForPath(vPath) # video is on usb, so wait until it comes back after we hard reset the bus def start(self):
# video is on usb, so wait until it comes back after we hard reset the bus
self.start() waitForPath(self._vPath)
ThreadedPipeline.start(self, play=False)
class FileDump(ThreadedPipeline): class FileDump(ThreadedPipeline):
''' '''
@ -379,7 +378,7 @@ class FileDump(ThreadedPipeline):
logger.error('Attempting to init FileDump without gluster mounted. Aborting') logger.error('Attempting to init FileDump without gluster mounted. Aborting')
raise SystemExit raise SystemExit
self._savePath = os.path.join(gluster.mountpoint + '/video') self._savePath = os.path.join(gluster.mountpoint, 'video')
mkdirSafe(self._savePath, logger) mkdirSafe(self._savePath, logger)
@ -424,10 +423,10 @@ class FileDump(ThreadedPipeline):
_linkElements(mux, self.sink) _linkElements(mux, self.sink)
def start(self):
# TODO: there is probably a better way to init than starting up to PAUSE # TODO: there is probably a better way to init than starting up to PAUSE
# and then dropping back down to NULL # and then dropping back down to NULL
self.start(play=False) ThreadedPipeline.start(self, play=False)
self._pipeline.post_message(Gst.Message.new_request_state(self._pipeline, Gst.State.NULL)) self._pipeline.post_message(Gst.Message.new_request_state(self._pipeline, Gst.State.NULL))
def addInitiator(self, identifier): def addInitiator(self, identifier):

View File

@ -20,7 +20,7 @@ class TTSForm(FlaskForm):
# TODO: fix random connection fails (might be an nginx thing) # TODO: fix random connection fails (might be an nginx thing)
@async(daemon=True) @async(daemon=True)
def initWebInterface(stateMachine): def startWebInterface(stateMachine):
siteRoot = Blueprint('siteRoot', __name__, static_folder='static', static_url_path='') siteRoot = Blueprint('siteRoot', __name__, static_folder='static', static_url_path='')
@siteRoot.route('/', methods=['GET', 'POST']) @siteRoot.route('/', methods=['GET', 'POST'])