Commit 9a0250c3 authored by mar-v-in's avatar mar-v-in
Browse files

Merge branch 'master' of

parents 80273482 6b78b1cc
files = *.py tuerd tryshell tyshell
target = /opt/tuer/
all: install
.PHONY: install
cp -v $(files) $(target)
from libtuer import ThreadFunction, logger
import RPi.GPIO as GPIO
import time
class Actor:
class CMD():
def __init__(self, name, pin, tid, todo): = name = pin
self.tid = tid
self.todo = todo
# don't do the GPIO setup here, the main init did not yet run
def execute(self):"Actor: Running command %s" %
for (value, delay) in self.todo:
if value is not None:
logger.debug("Actor: Setting pin %d to %d" % (, value))
GPIO.output(, value)
if delay > 0:
CMDs = {
CMD_UNLOCK: CMD("unlock", pin=12, tid=0, todo=[(True, 0.3), (False, 0.1)]),
CMD_LOCK: CMD("lock", pin=16, tid=0, todo=[(True, 0.3), (False, 0.1)]),
CMD_BUZZ: CMD("buzz", pin=22, tid=1, todo=[(True, 2.5), (False, 0.1)]),
CMD_GREEN_ON: CMD("green on", pin=23, tid=2, todo=[(True, 0)]),
CMD_GREEN_OFF: CMD("green off", pin=23, tid=2, todo=[(False, 0)]),
CMD_RED_ON: CMD("red on", pin=26, tid=2, todo=[(True, 0)]),
CMD_RED_OFF: CMD("red off", pin=26, tid=2, todo=[(False, 0)]),
def __init__(self):
# launch threads, all running the "_execute" method
self.threads = {}
for cmd in Actor.CMDs.values():
GPIO.setup(, GPIO.OUT)
GPIO.output(, False)
if not cmd.tid in self.threads:
self.threads[cmd.tid] = ThreadFunction(self._execute, "Actor TID %d" % cmd.tid)
def _execute(self, cmd):
def act(self, cmd):
# dispatch command to correct thread
def stop(self):
for thread in self.threads.values():
import logging, logging.handlers, syslog, os import logging, logging.handlers, os, time, queue, threading, subprocess
import traceback, smtplib
import email.mime.text, email.utils
# Logging configuration
syslogLevel = logging.INFO
mailLevel = logging.CRITICAL # must be "larger" than syslog level!
mailAddress = ['post+tuer'+'@'+'', '']
printLevel = logging.DEBUG
# Mail logging handler
def sendeMail(subject, text, receivers, sender='', replyTo=None):
assert isinstance(receivers, list)
if not len(receivers): return # nothing to do
# construct content
msg = email.mime.text.MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')
msg['Subject'] = subject
msg['Date'] = email.utils.formatdate(localtime=True)
msg['From'] = sender
msg['To'] = ', '.join(receivers)
if replyTo is not None:
msg['Reply-To'] = replyTo
# put into envelope and send
s = smtplib.SMTP('localhost')
s.sendmail(sender, receivers, msg.as_string())
# logging function # logging function
class Logger: class Logger:
def __init__ (self): def __init__ (self):
import __main__ as main self.syslog = logging.getLogger("tuerd") = os.path.basename(main.__file__) self.syslog.setLevel(logging.DEBUG)
self.logger = logging.getLogger( self.syslog.addHandler(logging.handlers.SysLogHandler(address = '/dev/log',
self.logger.setLevel(logging.INFO) facility = logging.handlers.SysLogHandler.LOG_LOCAL0))
self.handler = logging.handlers.SysLogHandler(address = '/dev/log', facility = logging.handlers.SysLogHandler.LOG_LOCAL0)
self.logger.addHandler(self.handler) def _log (self, lvl, what):
def log (self, what): thestr = "%s[%d]: %s" % ("tuerd", os.getpid(), what)
thestr = "%s[%d]: %s" % (,os.getpid(),what) # console log
print (thestr) if lvl >= printLevel: print(thestr)
# syslog
if lvl >= syslogLevel:
self.syslog.log(lvl, thestr)
# mail log
if lvl >= mailLevel and mailAddress is not None:
sendeMail('Kritischer Türfehler', what, mailAddress)
def debug(self, what):
self._log(logging.DEBUG, what)
def info(self, what):
self._log(logging.INFO, what)
def warning(self, what):
self._log(logging.WARNING, what)
def error(self, what):
self._log(logging.ERROR, what)
def critical(self, what):
self._log(logging.CRITICAL, what)
logger = Logger() logger = Logger()
def log (what): # run a command asynchronously and log the return value if not 0
logger.log(what) # prefix must be a string identifying the code position where the call came from
def fire_and_forget (cmd, log, prefix):
def _fire_and_forget ():
with open("/dev/null", "w") as fnull:
retcode =, stdout=fnull, stderr=fnull)
if retcode is not 0:
logger.error("%sReturn code %d at command: %s" % (prefix,retcode,str(cmd)))
t = threading.Thread(target=_fire_and_forget)
# Threaded callback class # Threaded callback class
class ThreadFunction(): class ThreadFunction():
_CALL = 0 _CALL = 0
_TERM = 1 _TERM = 1
def __init__(self, f): def __init__(self, f, name): = name
self._f = f self._f = f
self._q = queue.Queue() self._q = queue.Queue()
self._t = threading.Thread(target=self._thread_func) self._t = threading.Thread(target=self._thread_func)
...@@ -35,17 +85,46 @@ class ThreadFunction(): ...@@ -35,17 +85,46 @@ class ThreadFunction():
while True: while True:
(cmd, data) = self._q.get() (cmd, data) = self._q.get()
# run command # run command
if cmd == _CALL: if cmd == ThreadFunction._CALL:
self._f(*data) try:
elif cmd == _TERM: self._f(*data)
except Exception as e:
logger.critical("ThreadFunction: Got exception out of handler thread %s: %s" % (, str(e)))
elif cmd == ThreadFunction._TERM:
assert data is None assert data is None
break break
else: else:
raise NotImplementedError("Command %d does not exist" % cmd) logger.error("ThreadFunction: Command %d does not exist" % cmd)
def __call__(self, *arg): def __call__(self, *arg):
self._q.put((self._CALL, arg)) self._q.put((ThreadFunction._CALL, arg))
def stop(self):
self._q.put((ThreadFunction._TERM, None))
# Thread timer-repeater class: Call a function every <sleep_time> seconds
class ThreadRepeater():
def __init__(self, f, sleep_time, name): = name
self._f = f
self._stop = False
self._sleep_time = sleep_time
self._t = threading.Thread(target=self._thread_func)
def _thread_func(self):
while True:
if self._stop:
except Exception as e:
logger.critical("ThreadRepeater: Got exception out of handler thread %s: %s" % (, str(e)))
def stop(self): def stop(self):
self._q.put((_TERM, None)) self._stop = True
self._t.join() self._t.join()
import time, socket, atexit
import queue, threading, select
from libtuer import log, ThreadFunction
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD) from collections import namedtuple
atexit.register(GPIO.cleanup) from libtuer import ThreadRepeater, logger
from statemachine import StateMachine
tuerSock = "/run/tuer.sock" class PinsState():
ringPin = 18 pass
# Main classes
class PinWatcher(): class PinWatcher():
def __init__(self, pin, histlen): def __init__(self, pin, histlen):
GPIO.setup(pin, GPIO.IN) GPIO.setup(pin, GPIO.IN)
assert histlen > 1 # otherwise our logic goes nuts... assert histlen > 1 # otherwise our logic goes nuts...
self._pin = pin = pin
self._histlen = histlen self._histlen = histlen
# state change detection # state change detection
self._state = None self.state = None
self._newstate = None # != None iff we are currently seeing a state change self._newstate = None # != None iff we are currently seeing a state change
self._newstatelen = 0 # only valid if newstate != None self._newstatelen = 0 # only valid if newstate != None
# start state change handler thread
self._callback = ThreadFunction(self.callback)
self.stop = self._callback.stop
def read(self): def read(self):
curstate = GPIO.input(self._pin) curstate = GPIO.input(
assert curstate in (0, 1) assert curstate in (0, 1)
if curstate != self._state: if curstate != self.state:
# the state is about to change # the state is about to change
if curstate == self._newstate: if curstate == self._newstate:
# we already saw this new state # we already saw this new state
self._newstatelen += 1 self._newstatelen += 1
if self._newstatelen >= self._histlen: if self._newstatelen >= self._histlen:
self._callback(self._state, curstate) # send stuff to the other thread # we saw it often enough to declare it the new state
self._state = curstate self.state = curstate
self._newstate = None self._newstate = None
return True
else: else:
# now check for how long we see this new state # now check for how long we see this new state
self._newstate = curstate self._newstate = curstate
...@@ -44,44 +37,36 @@ class PinWatcher(): ...@@ -44,44 +37,36 @@ class PinWatcher():
else: else:
# old state is preserved # old state is preserved
self._newstate = None self._newstate = None
return False
class RingWatcher(PinWatcher): class PinsWatcher():
def __init__(self): def __init__(self, state_machine):
super().__init__(ringPin, 2) self._pins = {
self.last1Event = None 'bell_ringing': PinWatcher(18, 2),
'door_closed': PinWatcher(8, 4),
'door_locked': PinWatcher(10, 4),
'space_active': PinWatcher(24, 4),
self._sm = state_machine
# start a thread doing the work
self._t = ThreadRepeater(self._read, 0.02, name="PinsWatcher")
def callback(self, oldstate, newstate): def _read(self):
if oldstate is None: saw_change = False
return # ignore the very first state change for name in self._pins.keys():
# now (oldstate, newstate) is either (0, 1) or (1, 0) pin = self._pins[name]
if newstate: if
self.last1Event = time.time() saw_change = True
elif self.last1Event is not None: logger.debug("Pin %s changed to %d" % (name, pin.state))
# how long was this pressed? if not saw_change:
timePressed = time.time() - self.last1Event return None
log("Ring button pressed for",timePressed) # create return object
if timePressed >= 1.5 and timePressed <= 3: pinsState = PinsState() for name in self._pins.keys():
setattr(pinsState, name, self._pins[name].state)
# send it to state machine
self._sm.callback(StateMachine.CMD_PINS, pinsState)
def buzz(self): def stop(self):
log("Opening door") self._t.stop()
# talk with tuerd
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
pins = [
log("entering loop")
while True:
for pin in pins:
except KeyboardInterrupt:
for pin in pins:
from libtuer import ThreadFunction, logger, fire_and_forget
from actor import Actor
import os, random, time
# logger.{debug,info,warning,error,critical}
def play_sound (what):
soundfiles = os.listdir(SOUNDS_DIRECTORY+what)
except FileNotFoundError:
logger.error("StateMachine: Unable to list sound files in %s" % (SOUNDS_DIRECTORY+what))
soundfile = SOUNDS_DIRECTORY + what + '/' + random.choice(soundfiles)
fire_and_forget ([SOUNDS_PLAYER,soundfile], logger.error, "StateMachine: ")
# StateUnlocking constants
# StateLocking constants
# StateAboutToOpen constants
ABOUTOPEN_NERVLIST = [(5, lambda : play_sound("flipswitch")), (5, lambda:play_sound("flipswitch")), (0, lambda:logger.warning("Space open but switch not flipped for 10 seconds")),\
(10, lambda:play_sound("flipswitch")), (10, lambda:play_sound("flipswitch")), (0, lambda:logger.error("Space open but switch not flipped for 30 seconds")),\
(10, lambda:play_sound("flipswitch")), (10, lambda:play_sound("flipswitch")), (6, lambda:play_sound("flipswitch")), (4, lambda:logger.critical("Space open but switch not flipped for 60 seconds")),
(59*60, lambda:logger.critical("Space open but switch not flipped for one hour"))]
# Timeout we wait after the switch was switched to "Closed", until we assume nobody will open the door and we just lock it
# ALso the time we wait after the door was opend, till we assume something went wrong and start nerving
# play_sound constants
SOUNDS_DIRECTORY = "/opt/tuer/sounds/"
SOUNDS_PLAYER = "/usr/bin/mplayer"
class Nerver():
# A little utility class used to run the nervlists. A nervlist is a list of (n, f) tuples where f() is run after n seconds.
# If f returns something, that's also returned by nerv.
def __init__(self, nervlist):
self.nervlist = list(nervlist)
self.last_event_time = time.time()
def nerv(self):
if len(self.nervlist):
(wait_time, f) = self.nervlist[0]
now = time.time()
time_gone = now-self.last_event_time
# check if the first element is to be triggered
if time_gone >= wait_time:
self.nervlist = self.nervlist[1:] # "pop" the first element, but do not modify original list
self.last_event_time = now
return f()
class StateMachine():
# commands you can send
class State():
def __init__(self, state_machine, nervlist = None):
self.state_machine = state_machine
self._nerver = None if nervlist is None else Nerver(nervlist)
def handle_pins_event(self):
pass # one needn't implement this
def handle_buzz_event(self,arg): # this shouldn't be overwritten
arg("200 okay: buzz executed")
def handle_cmd_unlock_event(self,arg):
if arg is not None:
arg("412 Precondition Failed: The current state (%s) cannot handle the UNLOCK command. Try again later." % self.__class__.__name__)
def handle_wakeup_event(self):
if self._nerver is not None:
return self._nerver.nerv()
def on_leave(self):
def pins(self):
return self.state_machine.pins
def old_pins(self):
return self.state_machine.old_pins
def actor(self):
def handle_event(self,ev,arg): # don't override
if ev == StateMachine.CMD_PINS:
return self.handle_pins_event()
elif ev == StateMachine.CMD_BUZZ:
return self.handle_buzz_event(arg)
elif ev == StateMachine.CMD_UNLOCK:
return self.handle_cmd_unlock_event(arg)
elif ev == StateMachine.CMD_WAKEUP:
return self.handle_wakeup_event()
raise Exception("Unknown command number: %d" % ev)
class AbstractNonStartState(State):
def handle_pins_event(self):
if self.pins().door_locked != (not self.pins().space_active):
return super().handle_pins_event()
class AbstractLockedState(AbstractNonStartState):
'''A state with invariant "The space is locked", switching to StateAboutToOpen when the space becomes unlocked'''
def __init__(self, sm, nervlist = None):
super().__init__(sm, nervlist)
def handle_pins_event(self):
if not self.pins().door_locked:"Door unlocked, space is about to open")
return StateMachine.StateAboutToOpen(self.state_machine)
if not self.old_pins().space_active and self.pins().space_active:"Space toggled to active while it was closed - unlocking the door")
return StateMachine.StateUnlocking(self.state_machine)
return super().handle_pins_event()
class AbstractUnlockedState(AbstractNonStartState):
'''A state with invariant "The space is unlocked", switching to StateZu when the space becomes locked'''
def __init__(self, sm, nervlist = None):
super().__init__(sm, nervlist)
def handle_pins_event(self):
if self.pins().door_locked:"Door locked, closing space")
if self.pins().space_active:
logger.warning("StateMachine: door manually locked, but space switch is still on - going to StateZu")
return StateMachine.StateZu(self.state_machine)
return super().handle_pins_event()
class StateStart(State):
def handle_pins_event(self):
pins = self.pins()
if not (pins.door_locked is None or pins.door_closed is None or pins.space_active is None or pins.bell_ringing is None):"All sensors got a value, switching to a proper state")
if pins.door_locked:
return StateMachine.StateZu(self.state_machine)
return StateMachine.StateAboutToOpen(self.state_machine)
return super().handle_pins_event()
class StateZu(AbstractLockedState):
def handle_cmd_unlock_event(self,callback):
return StateMachine.StateUnlocking(self.state_machine, callback)
class StateUnlocking(AbstractLockedState):
def __init__(self,sm,callback=None):
# construct a nervlist
nervlist = [(OPEN_REPEAT_TIMEOUT, lambda: for t in range(OPEN_REPEAT_NUMBER)]
nervlist += [(OPEN_REPEAT_TIMEOUT, self.could_not_open)]
# TODO: can we send "202 processing: Trying to unlock the door" here? Are the callbacks multi-use?
def notify(self, did_it_work):
s = "200 okay: door unlocked" if did_it_work else ("500 internal server error: Couldn't unlock door with %d tries à %f seconds" % (OPEN_REPEAT_NUMBER,OPEN_REPEAT_TIMEOUT))
for cb in self.callbacks:
if cb is not None:
def on_leave(self):
self.notify(not self.pins().door_locked)
def handle_cmd_unlock_event(self,callback):
# TODO: 202 notification also here if possible
def could_not_open(self):
logger.critical("StateMachine: Couldn't open door after %d tries. Going back to StateZu." % OPEN_REPEAT_NUMBER)
return StateMachine.StateZu(self.state_machine)
class AbstractStateWhereUnlockingIsRedundant(AbstractUnlockedState):
def handle_cmd_unlock_event(self, callback):
callback("299 redundant: Space seems to be already open. Still processing your request tough.")"StateMachine: Received UNLOCK command in %s. This should not be necessary." % self.__class__.__name__)
class StateAboutToOpen(AbstractStateWhereUnlockingIsRedundant):
def __init__(self, sm):
super().__init__(sm, ABOUTOPEN_NERVLIST)
def handle_pins_event(self):
pins = self.pins()
if pins.space_active:"Space activated, opening procedure completed")
return StateMachine.StateAuf(self.state_machine)
return super().handle_pins_event()
class StateAuf(AbstractStateWhereUnlockingIsRedundant):
def __init__(self,sm):
nervlist = [(24*60*60, lambda: logger.critical("Space is now open for 24h. Is everything all right?"))]
super().__init__(sm, nervlist)
self.last_buzzed = None
def handle_pins_event(self):
pins = self.pins()
if pins.bell_ringing and not self.old_pins().bell_ringing:
# someone just pressed the bell"StateMachine: buzzing because of bell ringing in StateAuf")
if not pins.space_active:"StateMachine: space switch turned off - starting leaving procedure")
return StateMachine.StateAboutToLeave(self.state_machine)
return super().handle_pins_event()
class StateLocking(AbstractUnlockedState):
def __init__(self,sm):
# construct a nervlist
nervlist = [(CLOSE_REPEAT_TIMEOUT, lambda: for t in range(CLOSE_REPEAT_NUMBER)]
nervlist += [(CLOSE_REPEAT_TIMEOUT, self.could_not_close)]
super().__init__(sm, nervlist)
if self.pins().door_closed: # this should always be true, but just to be sure...
def handle_pins_event(self):
pins = self.pins()
if not pins.door_closed:
# TODO play a sound? This shouldn't happen, door was opened while we are locking
logger.warning("StateMachine: door manually opened during locking")
return StateMachine.StateAboutToOpen(self.state_machine)
# TODO do anything here if the switch is activated now?
return super().handle_pins_event()
def handle_cmd_unlock_event(self,callback):
callback("409 conflict: The sphinx is currently trying to lock the door. Try again later.")
def could_not_close(self):