separate start and init sequences in statemachine
This commit is contained in:
parent
964445ca77
commit
766b5f6c81
|
@ -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()
|
||||
|
|
38
listeners.py
38
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):
|
||||
|
|
1
main.py
1
main.py
|
@ -36,6 +36,7 @@ if __name__ == '__main__':
|
|||
GPIO.setmode(GPIO.BCM)
|
||||
|
||||
stateMachine = StateMachine()
|
||||
stateMachine.start()
|
||||
|
||||
signal.signal(signal.SIGTERM, sigtermHandler)
|
||||
|
||||
|
|
10
sensors.py
10
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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
111
stream.py
111
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):
|
||||
|
|
|
@ -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'])
|
||||
|
|
Loading…
Reference in New Issue