diff --git a/blinkenLights.py b/blinkenLights.py index 123e115..e32a2dc 100644 --- a/blinkenLights.py +++ b/blinkenLights.py @@ -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() + + 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 stop(self): - self._stopper.set() - logger.debug('Stopping LED on pin %s', self._pin) - def __del__(self): self.stop() diff --git a/listeners.py b/listeners.py index 96b122f..80c79ab 100644 --- a/listeners.py +++ b/listeners.py @@ -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], [], []) @@ -111,12 +102,25 @@ 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) @@ -125,10 +129,6 @@ class KeypadListener(): if self._resetCountdown is not None and self._resetCountdown.is_alive(): 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): diff --git a/main.py b/main.py index 42873d4..848a5f3 100755 --- a/main.py +++ b/main.py @@ -36,6 +36,7 @@ if __name__ == '__main__': GPIO.setmode(GPIO.BCM) stateMachine = StateMachine() + stateMachine.start() signal.signal(signal.SIGTERM, sigtermHandler) diff --git a/sensors.py b/sensors.py index 620e837..6d5958f 100644 --- a/sensors.py +++ b/sensors.py @@ -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) diff --git a/soundLib.py b/soundLib.py index 54436e0..e083610 100644 --- a/soundLib.py +++ b/soundLib.py @@ -88,6 +88,8 @@ class SoundLib: self._ttsQueue = queue.Queue() self._stopper = Event() + + def start(self): self._startMonitor() def changeVolume(self, volumeDelta): @@ -100,7 +102,7 @@ class SoundLib: def speak(self, text): self._ttsQueue.put_nowait(text) - + @async(daemon=False) def _fader(self, lowerVolume, totalDuration, fadeDuration=0.2, stepSize=5): alarm = self.soundEffects['triggered'] diff --git a/stateMachine.py b/stateMachine.py index 3aa9639..c46dde5 100644 --- a/stateMachine.py +++ b/stateMachine.py @@ -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,10 +160,42 @@ 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() - + def selectState(self, signal): with self._lock: nextState = self.currentState.next(signal) diff --git a/stream.py b/stream.py index 2220498..8b87fca 100644 --- a/stream.py +++ b/stream.py @@ -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): @@ -247,50 +288,6 @@ class ThreadedPipeline: @async(daemon=True) 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): ''' @@ -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,10 +353,11 @@ 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 + 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) - self.start() - class FileDump(ThreadedPipeline): ''' Pipeline that takes audio and input from two udp ports, muxes them, and @@ -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) @@ -423,11 +422,11 @@ class FileDump(ThreadedPipeline): _linkElements(vQueue, mux) _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): diff --git a/webInterface.py b/webInterface.py index ff5b84f..944eef9 100644 --- a/webInterface.py +++ b/webInterface.py @@ -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'])