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
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 start(self):
ExceptionThread.start(self)
logger.debug('Starting LED on pin %s', self._pin)
def stop(self):
self._stopper.set()
logger.debug('Stopping LED on pin %s', self._pin)
def setCyclePeriod(self, cyclePeriod):
self._sleeptime = cyclePeriod/20/2
def __del__(self):
self.stop()

View File

@ -11,7 +11,7 @@ import stateMachine
logger = logging.getLogger(__name__)
class KeypadListener():
class KeypadListener:
'''
Interface for standard numpad device. Capabilities include:
- accepting numeric input
@ -35,20 +35,11 @@ class KeypadListener():
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']
ctrlKeySound = soundLib.soundEffects['ctrlKey']
wrongPassSound = soundLib.soundEffects['wrongPass']
backspaceSound = soundLib.soundEffects['backspace']
self._clearBuffer()
def getInput():
while 1:
r, w, x = select([self._dev], [], [])
@ -112,11 +103,24 @@ class KeypadListener():
ctrlKeySound.play()
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._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()
logger.debug('Started keypad device')
logger.debug('Started keypad listener')
def resetBuffer(self):
self._stopResetCountdown
self._clearBuffer()
def _startResetCountdown(self):
self._resetCountdown = CountdownTimer(30, self._clearBuffer)
@ -126,10 +130,6 @@ class KeypadListener():
self._resetCountdown.stop()
self._resetCountdown = None
def resetBuffer(self):
self._stopResetCountdown
self._clearBuffer()
def _clearBuffer(self):
self._buf = ''
@ -170,7 +170,9 @@ class PipeListener(ExceptionThread):
callback(msg, logger)
super().__init__(target=listen, daemon=True)
self.start()
def start(self):
ExceptionThread.start(self)
logger.debug('Started pipe listener at path %s', self._path)
def __del__(self):

View File

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

View File

@ -22,12 +22,12 @@ def lowPassFilter(pin, targetVal, period=0.001):
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)
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):
def startMotionSensor(pin, location, action):
name = 'MotionSensor@' + location
def trip(channel):
@ -36,9 +36,9 @@ def setupMotionSensor(pin, location, action):
action()
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):
nonlocal closed
val = GPIO.input(pin)
@ -56,5 +56,5 @@ def setupDoorSensor(pin, action, sound=None):
sound.play()
action()
setupGPIO('DoorSensor', pin, GPIO.BOTH, trip)
_initGPIO('DoorSensor', pin, GPIO.BOTH, trip)
closed = GPIO.input(pin)

View File

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

View File

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

109
stream.py
View File

@ -68,7 +68,48 @@ class ThreadedPipeline:
self._stopper = Event()
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
def stop(self):
@ -248,50 +289,6 @@ class ThreadedPipeline:
def _mainLoop(self):
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 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
FileDump class below.
'''
_vPath = '/dev/video0'
_aPath = 'hw:1,0'
def __init__(self, video=True, audio=True):
super().__init__('camera')
vPath = '/dev/video0'
if video:
vSource = Gst.ElementFactory.make("v4l2src", "videoSource")
vConvert = Gst.ElementFactory.make("videoconvert", "videoConvert")
@ -318,7 +316,7 @@ class Camera(ThreadedPipeline):
vRTPPay = Gst.ElementFactory.make("rtph264pay", "videoRTPPayload")
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('pt', 96)
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")
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')
aCaps = Gst.Caps.from_string('audio/x-raw,rate=48000,channels=1')
@ -355,9 +353,10 @@ class Camera(ThreadedPipeline):
_linkElements(aEncode, aRTPPay)
_linkElements(aRTPPay, aRTPSink)
waitForPath(vPath) # video is on usb, so wait until it comes back after we hard reset the bus
self.start()
def start(self):
# video is on usb, so wait until it comes back after we hard reset the bus
waitForPath(self._vPath)
ThreadedPipeline.start(self, play=False)
class FileDump(ThreadedPipeline):
'''
@ -379,7 +378,7 @@ class FileDump(ThreadedPipeline):
logger.error('Attempting to init FileDump without gluster mounted. Aborting')
raise SystemExit
self._savePath = os.path.join(gluster.mountpoint + '/video')
self._savePath = os.path.join(gluster.mountpoint, 'video')
mkdirSafe(self._savePath, logger)
@ -424,10 +423,10 @@ class FileDump(ThreadedPipeline):
_linkElements(mux, self.sink)
def start(self):
# TODO: there is probably a better way to init than starting up to PAUSE
# 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))
def addInitiator(self, identifier):

View File

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