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

Merge branch 'master' of ralfj.de:saartuer

parents 80273482 6b78b1cc
__pycache__
files = *.py tuerd tryshell tyshell
target = /opt/tuer/
all: install
.PHONY: install
install:
cp -v $(files) $(target)
from libtuer import ThreadFunction, logger
import RPi.GPIO as GPIO
import time
class Actor:
CMD_BUZZ = 0
CMD_UNLOCK = 1
CMD_LOCK = 2
CMD_GREEN_ON = 3
CMD_GREEN_OFF = 4
CMD_RED_ON = 5
CMD_RED_OFF = 6
class CMD():
def __init__(self, name, pin, tid, todo):
self.name = name
self.pin = pin
self.tid = tid
self.todo = todo
# don't do the GPIO setup here, the main init did not yet run
def execute(self):
logger.info("Actor: Running command %s" % self.name)
for (value, delay) in self.todo:
if value is not None:
logger.debug("Actor: Setting pin %d to %d" % (self.pin, value))
GPIO.output(self.pin, value)
if delay > 0:
time.sleep(delay)
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(cmd.pin, GPIO.OUT)
GPIO.output(cmd.pin, False)
if not cmd.tid in self.threads:
self.threads[cmd.tid] = ThreadFunction(self._execute, "Actor TID %d" % cmd.tid)
def _execute(self, cmd):
Actor.CMDs[cmd].execute()
def act(self, cmd):
# dispatch command to correct thread
self.threads[Actor.CMDs[cmd].tid](cmd)
def stop(self):
for thread in self.threads.values():
thread.stop()
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'+'@'+'ralfj.de', 'vorstand@lists.hacksaar.de']
printLevel = logging.DEBUG
# Mail logging handler
def sendeMail(subject, text, receivers, sender='sphinx@hacksaar.de', 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())
s.quit()
# logging function
class Logger:
def __init__ (self):
import __main__ as main
self.name = os.path.basename(main.__file__)
self.logger = logging.getLogger(self.name)
self.logger.setLevel(logging.INFO)
self.handler = logging.handlers.SysLogHandler(address = '/dev/log', facility = logging.handlers.SysLogHandler.LOG_LOCAL0)
self.logger.addHandler(self.handler)
def log (self, what):
thestr = "%s[%d]: %s" % (self.name,os.getpid(),what)
print (thestr)
self.logger.info(thestr)
self.syslog = logging.getLogger("tuerd")
self.syslog.setLevel(logging.DEBUG)
self.syslog.addHandler(logging.handlers.SysLogHandler(address = '/dev/log',
facility = logging.handlers.SysLogHandler.LOG_LOCAL0))
def _log (self, lvl, what):
thestr = "%s[%d]: %s" % ("tuerd", os.getpid(), what)
# console log
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()
def log (what):
logger.log(what)
# run a command asynchronously and log the return value if not 0
# 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 = subprocess.call(cmd, 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)
t.start()
# Threaded callback class
class ThreadFunction():
_CALL = 0
_TERM = 1
def __init__(self, f):
def __init__(self, f, name):
self.name = name
self._f = f
self._q = queue.Queue()
self._t = threading.Thread(target=self._thread_func)
......@@ -35,17 +85,46 @@ class ThreadFunction():
while True:
(cmd, data) = self._q.get()
# run command
if cmd == _CALL:
self._f(*data)
elif cmd == _TERM:
if cmd == ThreadFunction._CALL:
try:
self._f(*data)
except Exception as e:
logger.critical("ThreadFunction: Got exception out of handler thread %s: %s" % (self.name, str(e)))
logger.debug(traceback.format_exc())
elif cmd == ThreadFunction._TERM:
assert data is None
break
else:
raise NotImplementedError("Command %d does not exist" % cmd)
logger.error("ThreadFunction: Command %d does not exist" % cmd)
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))
self._t.join()
# Thread timer-repeater class: Call a function every <sleep_time> seconds
class ThreadRepeater():
def __init__(self, f, sleep_time, name):
self.name = name
self._f = f
self._stop = False
self._sleep_time = sleep_time
self._t = threading.Thread(target=self._thread_func)
self._t.start()
def _thread_func(self):
while True:
if self._stop:
break
try:
self._f()
except Exception as e:
logger.critical("ThreadRepeater: Got exception out of handler thread %s: %s" % (self.name, str(e)))
logger.debug(traceback.format_exc())
time.sleep(self._sleep_time)
def stop(self):
self._q.put((_TERM, None))
self._stop = True
self._t.join()
#!/usr/bin/python3
import time, socket, atexit
import queue, threading, select
from libtuer import log, ThreadFunction
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
atexit.register(GPIO.cleanup)
from collections import namedtuple
from libtuer import ThreadRepeater, logger
from statemachine import StateMachine
tuerSock = "/run/tuer.sock"
ringPin = 18
class PinsState():
pass
# Main classes
class PinWatcher():
def __init__(self, pin, histlen):
GPIO.setup(pin, GPIO.IN)
assert histlen > 1 # otherwise our logic goes nuts...
self._pin = pin
self.pin = pin
self._histlen = histlen
# state change detection
self._state = None
self.state = None
self._newstate = None # != None iff we are currently seeing a state change
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):
curstate = GPIO.input(self._pin)
curstate = GPIO.input(self.pin)
assert curstate in (0, 1)
if curstate != self._state:
if curstate != self.state:
# the state is about to change
if curstate == self._newstate:
# we already saw this new state
self._newstatelen += 1
if self._newstatelen >= self._histlen:
self._callback(self._state, curstate) # send stuff to the other thread
self._state = curstate
# we saw it often enough to declare it the new state
self.state = curstate
self._newstate = None
return True
else:
# now check for how long we see this new state
self._newstate = curstate
......@@ -44,44 +37,36 @@ class PinWatcher():
else:
# old state is preserved
self._newstate = None
return False
class RingWatcher(PinWatcher):
def __init__(self):
super().__init__(ringPin, 2)
self.last1Event = None
class PinsWatcher():
def __init__(self, state_machine):
self._pins = {
'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):
if oldstate is None:
return # ignore the very first state change
# now (oldstate, newstate) is either (0, 1) or (1, 0)
if newstate:
self.last1Event = time.time()
elif self.last1Event is not None:
# how long was this pressed?
timePressed = time.time() - self.last1Event
log("Ring button pressed for",timePressed)
if timePressed >= 1.5 and timePressed <= 3:
self.buzz()
def _read(self):
saw_change = False
for name in self._pins.keys():
pin = self._pins[name]
if pin.read():
saw_change = True
logger.debug("Pin %s changed to %d" % (name, pin.state))
if not saw_change:
return None
# create return object
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):
log("Opening door")
# talk with tuerd
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(tuerSock)
s.send(b'buzz')
s.close()
# MAIN PROGRAM
pins = [
RingWatcher(),
]
try:
log("entering loop")
while True:
for pin in pins:
pin.read()
time.sleep(0.02)
except KeyboardInterrupt:
for pin in pins:
pin.stop()
def stop(self):
self._t.stop()
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):
try:
soundfiles = os.listdir(SOUNDS_DIRECTORY+what)
except FileNotFoundError:
logger.error("StateMachine: Unable to list sound files in %s" % (SOUNDS_DIRECTORY+what))
return
soundfile = SOUNDS_DIRECTORY + what + '/' + random.choice(soundfiles)
fire_and_forget ([SOUNDS_PLAYER,soundfile], logger.error, "StateMachine: ")
# StateUnlocking constants
OPEN_REPEAT_TIMEOUT = 7
OPEN_REPEAT_NUMBER = 3
# StateLocking constants
CLOSE_REPEAT_TIMEOUT = 7
CLOSE_REPEAT_NUMBER = 3
# 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
LEAVE_TIMEOUT = 20
# 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
CMD_PINS = 0
CMD_BUZZ = 1
CMD_UNLOCK = 2
CMD_WAKEUP = 3
CMD_LAST = 4
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
self.actor().act(Actor.CMD_BUZZ)
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):
pass
def pins(self):
return self.state_machine.pins
def old_pins(self):
return self.state_machine.old_pins
def actor(self):
return self.state_machine.actor
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()
else:
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):
self.actor().act(Actor.CMD_RED_ON)
else:
self.actor().act(Actor.CMD_RED_OFF)
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)
self.actor().act(Actor.CMD_GREEN_OFF)
def handle_pins_event(self):
if not self.pins().door_locked:
logger.info("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:
logger.info("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)
self.actor().act(Actor.CMD_GREEN_ON)
def handle_pins_event(self):
if self.pins().door_locked:
logger.info("Door locked, closing space")
if self.pins().space_active:
logger.warning("StateMachine: door manually locked, but space switch is still on - going to StateZu")
play_sound("manual_lock")
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):
logger.info("All sensors got a value, switching to a proper state")
if pins.door_locked:
return StateMachine.StateZu(self.state_machine)
else:
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: self.actor().act(Actor.CMD_UNLOCK)) for t in range(OPEN_REPEAT_NUMBER)]
nervlist += [(OPEN_REPEAT_TIMEOUT, self.could_not_open)]
super().__init__(sm,nervlist)
self.callbacks=[callback]
# TODO: can we send "202 processing: Trying to unlock the door" here? Are the callbacks multi-use?
self.actor().act(Actor.CMD_UNLOCK)
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:
cb(s)
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
self.callbacks.append(callback)
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.")
logger.info("StateMachine: Received UNLOCK command in %s. This should not be necessary." % self.__class__.__name__)
self.actor().act(Actor.CMD_UNLOCK)
class StateAboutToOpen(AbstractStateWhereUnlockingIsRedundant):
def __init__(self, sm):
super().__init__(sm, ABOUTOPEN_NERVLIST)
def handle_pins_event(self):
pins = self.pins()
if pins.space_active:
logger.info("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
logger.info("StateMachine: buzzing because of bell ringing in StateAuf")
self.actor().act(Actor.CMD_BUZZ)
if not pins.space_active:
logger.info("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: self.actor().act(Actor.CMD_LOCK)) 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...
self.actor().act(Actor.CMD_LOCK)
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):
logger.critical("StateMachine: Couldn't close door after %d tries. Going back to StateAboutToOpen." % CLOSE_REPEAT_NUMBER)
return StateMachine.StateAboutToOpen(self.state_machine)
class StateAboutToLeave(AbstractUnlockedState):
def __init__(self, sm):
nervlist = [(LEAVE_TIMEOUT, lambda: StateMachine.StateLocking(self.state_machine))]
super().__init__(sm, nervlist)
def handle_pins_event(self):
if not self.pins().door_closed:
return StateMachine.StateLeaving(self.state_machine)
if self.pins().space_active:
logger.info("Space re-activated, cancelling leaving procedure")
return StateMachine.StateAuf(self.state_machine)
return super().handle_pins_event()
class StateLeaving(AbstractUnlockedState):
def __init__(self, sm):
nervlist = [(LEAVE_TIMEOUT, lambda: StateMachine.StateAboutToOpen(self.state_machine))]
super().__init__(sm, nervlist)
def handle_pins_event(self):
if self.pins().door_closed:
logger.info("The space was left, locking the door")
return StateMachine.StateLocking(self.state_machine)
if self.pins().space_active:
logger.info("Space re-activated, cancelling leaving procedure")
return StateMachine.StateAuf(self.state_machine)
return super().handle_pins_event()
def __init__(self, actor):