From: Constantin Berhard Date: Mon, 14 Oct 2013 19:01:46 +0000 (+0200) Subject: Merge branch 'master' of ralfj.de:saartuer X-Git-Url: https://git.ralfj.de/saartuer.git/commitdiff_plain/4f9482a7c46d5930b309a5796c39f81efa3989ae?hp=cfdd8b51f663f6449f79d40becd4403fd780fd46 Merge branch 'master' of ralfj.de:saartuer --- diff --git a/actor.py b/actor.py index 248edeb..8f57d9f 100644 --- a/actor.py +++ b/actor.py @@ -1,5 +1,6 @@ from libtuer import ThreadFunction, logger import RPi.GPIO as GPIO +import time class Actor: CMD_BUZZ = 0 @@ -7,13 +8,13 @@ class Actor: CMD_LOCK = 2 CMDs = { - CMD_BUZZ: ("buzz", 12, [(True, 0.3), (False, 2.0)]), - CMD_UNLOCK: ("unlock", 16, [(None, 0.2), (True, 0.3), (False, 1.0)]), - CMD_LOCK: ("lock", 22, [(None, 0.2), (True, 0.3), (False, 1.0)]), + CMD_UNLOCK: ("unlock", 12, [(None, 0.2), (True, 0.3), (False, 0.5)]), + CMD_LOCK: ("lock", 16, [(None, 0.2), (True, 0.3), (False, 0.5)]), + CMD_BUZZ: ("buzz", 22, [(None, 0.2), (True, 2.0), (False, 0.5)]), } def __init__(self): - self.act = ThreadFunction(self._act) + self.act = ThreadFunction(self._act, name="Actor") for (name, pin, todo) in self.CMDs.values(): GPIO.setup(pin, GPIO.OUT) @@ -23,10 +24,11 @@ class Actor: logger.info("Actor: Running command %s" % name) for (value, delay) in todo: if value is not None: + logger.debug("Setting pin %d to %d" % (pin, value)) GPIO.output(pin, value) time.sleep(delay) else: logger.error("Actor: Gut unknown command %d" % cmd) def stop(self): - pass + self.act.stop() diff --git a/libtuer.py b/libtuer.py index 1f8a034..613ce4e 100644 --- a/libtuer.py +++ b/libtuer.py @@ -1,28 +1,55 @@ import logging, logging.handlers, os, time, queue, threading, subprocess +import traceback, smtplib +from email.mime.text import MIMEText + +# Logging configuration +syslogLevel = logging.INFO +mailLevel = logging.CRITICAL # must be "larger" than syslog level! +mailAddress = 'post+tuer'+'@'+'ralfj.de' + +# Mail logging handler +def sendeMail(subject, text, receivers, sender='sphinx@hacksaar.de', replyTo=None): + if not isinstance(type(receivers), list): receivers = [receivers] + # construct content + msg = MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8') + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = ', '.join(receivers) + if replyTo is not None: + msg['Reply-To'] = replyTo + # put into envelope and send + s = smtplib.SMTP('ralfj.de') + s.sendmail(sender, receivers, msg.as_string()) + s.quit() # logging function class Logger: def __init__ (self): - self.logger = logging.getLogger("tuerd") - self.logger.setLevel(logging.INFO) - self.handler = logging.handlers.SysLogHandler(address = '/dev/log', facility = logging.handlers.SysLogHandler.LOG_LOCAL0) - self.logger.addHandler(self.handler) + self.syslog = logging.getLogger("tuerd") + self.syslog.setLevel(syslogLevel) + self.syslog.addHandler(logging.handlers.SysLogHandler(address = '/dev/log', + facility = logging.handlers.SysLogHandler.LOG_LOCAL0)) - def log (self, lvl, what): + def _log (self, lvl, what): thestr = "%s[%d]: %s" % ("osspd", os.getpid(), what) - print (thestr) - self.logger.log(lvl, thestr) + # console log + print(thestr) + # syslog + self.syslog.log(lvl, thestr) + # mail log + if lvl >= mailLevel: + sendeMail('Kritischer Türfehler', what, mailAddress) def debug(self, what): - self.log(logging.DEBUG, what) + self._log(logging.DEBUG, what) def info(self, what): - self.log(logging.INFO, what) + self._log(logging.INFO, what) def warning(self, what): - self.log(logging.WARNING, what) + self._log(logging.WARNING, what) def error(self, what): - self.log(logging.ERROR, what) + self._log(logging.ERROR, what) def critical(self, what): - self.log(logging.CRITICAL, what) + self._log(logging.CRITICAL, what) logger = Logger() @@ -42,7 +69,8 @@ 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) @@ -52,39 +80,45 @@ class ThreadFunction(): while True: (cmd, data) = self._q.get() # run command - if cmd == _CALL: + if cmd == ThreadFunction._CALL: try: self._f(*data) - except Exception: - logger.error("ThreadFunction: Got exception out of handler thread: %s" % str(e)) - elif cmd == _TERM: + 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: 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((_TERM, None)) + self._q.put((ThreadFunction._TERM, None)) self._t.join() # Thread timer-repeater class: Call a function every seconds class ThreadRepeater(): - def __init__(self, f, sleep_time): + 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(): + def _thread_func(self): while True: if self._stop: break - self._f() - time.sleep(sleep_time) + 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._stop = True diff --git a/pins.py b/pins.py index 30f9666..88a17c2 100644 --- a/pins.py +++ b/pins.py @@ -20,7 +20,7 @@ class PinWatcher(): def read(self): 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 @@ -41,31 +41,32 @@ class PinWatcher(): class PinsWatcher(): def __init__(self, state_machine): - self.pins = { + self._pins = { 'bell_ringing': PinWatcher(18, 2), - 'door_closed': PinWatcher(8, 5), - 'door_locked': PinWatcher(10, 5), - 'space_active': PinWatcher(24, 5), + '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) - - def _read(): + self._t = ThreadRepeater(self._read, 0.02, name="PinsWatcher") + + def _read(self): saw_change = False - for name in self.pins.keys(): - pin = pins[name] + 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 + 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) + 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 stop(): + def stop(self): self._t.stop() diff --git a/statemachine.py b/statemachine.py index 332f48d..07d6574 100644 --- a/statemachine.py +++ b/statemachine.py @@ -1,6 +1,6 @@ -8from libtuer import ThreadFunction, logger, fire_and_forget +from libtuer import ThreadFunction, logger, fire_and_forget from actor import Actor -import os, random +import os, random, time # logger.{debug,info,warning,error,critical} @@ -15,24 +15,25 @@ def play_sound (what): # StateUnlocking constants -OPEN_REPEAT_TIMEOUT = 8 +OPEN_REPEAT_TIMEOUT = 7 OPEN_REPEAT_NUMBER = 3 # StateLocking constants -CLOSE_REPEAT_TIMEOUT = 8 +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"))] +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"))] # StateAuf constants # time that must pass between two bell_ringing events to buzz the door again (seconds) AUF_BELLBUZZ_REPEAT_TIME = 2 # Timeout we wait after the switch was switched to "Closed", until we assume nobody will open the door and we just lock it -LEAVE_TIMEOUT = 30 +# 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/" @@ -48,11 +49,11 @@ class Nerver(): def nerv(self): if len(self.nervlist): - (time, f) = self.nervlist[0] + (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 >= time: + 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() @@ -73,7 +74,7 @@ class StateMachine(): 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) + self.actor().act(Actor.CMD_BUZZ) arg("200 okay: buzz executed") def handle_cmd_unlock_event(self,arg): if arg is not None: @@ -86,73 +87,72 @@ class StateMachine(): def actor(self): return self.state_machine.actor def handle_event(self,ev,arg): # don't override - if arg is CMD_PINS: - self.handle_pins_event() - elif arg is CMD_BUZZ: - self.handle_buzz_event(arg) - elif arg is CMD_UNLOCK: - self.handle_cmd_unlock_event(arg) - elif arg is CMD_WAKEUP: - self.handle_wakeup_event() + 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 StateStart(State): def handle_pins_event(self): super().handle_pins_event() - thepins = self.pins() - for pin in thepins: - if pin is None: - return None - if thepins.door_locked: - return StateZu + pins = self.pins() + if pins.door_locked is None or pins.door_closed is None or pins.space_dactive is None or pins.bell_ringing is None: + return None # wait till we have all sensors non-None + if pins.door_locked: + return StateMachine.StateZu(self.state_machine) else: - return StateAuf - + return StateMachine.StateAuf(self.state_machine) + class StateZu(State): def handle_pins_event(self): super().handle_pins_event() pins = self.pins() if not pins.door_locked: - return StateAboutToOpen(self.state_machine) + return StateMachine.StateAboutToOpen(self.state_machine) def handle_cmd_unlock_event(self,callback): # intentionally not calling super() implementation - return StateUnlocking(callback,self.state_machine) + return StateMachine.StateUnlocking(self.state_machine, callback) class StateUnlocking(State): - def __init__(self,callback,sm): + def __init__(self,sm,callback=None): # construct a nervlist - nervlist = [(OPEN_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_UNLOCK)) for t in xrange(OPEN_REPEAT_NUMBER)] + 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] - # FIXME: can we send "202 processing: Trying to open the door" here? Are the callbacks multi-use? + # TODO: can we send "202 processing: Trying to open the door" here? Are the callbacks multi-use? self.actor().act(Actor.CMD_UNLOCK) def notify(self, did_it_work): s = "200 okay: door open" if did_it_work else ("500 internal server error: Couldn't open door with %d tries à %f seconds" % (OPEN_REPEAT_NUMBER,OPEN_REPEAT_TIMEOUT)) for cb in self.callbacks: - if cb is not None: # FIXME why this check? shouldn't be needed + if cb is not None: cb(s) def handle_pins_event(self): super().handle_pins_event() pins = self.pins() if not pins.door_locked: self.notify(True) - return StateAboutToOpen(self.state_machine) + return StateMachine.StateAboutToOpen(self.state_machine) def handle_cmd_unlock_event(self,callback): # intentionally not calling super() implementation - # FIXME: 202 notification also here if possible + # TODO: 202 notification also here if possible self.callbacks.append(callback) def could_not_open(self): logger.critical("Couldn't open door after %d tries. Going back to StateZu." % OPEN_REPEAT_NUMBER) self.notify(False) - return StateZu(self.state_machine) + return StateMachine.StateZu(self.state_machine) class AbstractStateWhereOpeningIsRedundant(State): def handle_cmd_unlock_event(self, callback): # intentionally not calling super() implementation callback("299 redundant: Space seems to be already open. Still processing your request tough.") - logger.warning("Received OPEN command in StateAboutToOpen. This should not be necessary.") + logger.info("Received OPEN command in StateAboutToOpen. This should not be necessary.") self.actor().act(Actor.CMD_UNLOCK) class StateAboutToOpen(AbstractStateWhereOpeningIsRedundant): @@ -162,9 +162,9 @@ class StateMachine(): super().handle_pins_event() pins = self.pins() if pins.door_locked: - return StateZu(self.state_machine) + return StateMachine.StateZu(self.state_machine) elif pins.space_active: - return StateAuf(self.state_machine) + return StateMachine.StateAuf(self.state_machine) class StateAuf(AbstractStateWhereOpeningIsRedundant): def __init__(self,sm): @@ -174,6 +174,7 @@ class StateMachine(): super().handle_pins_event() pins = self.pins() if pins.bell_ringing: + # TODO: use old_pins instead of a timer now = time.time() if self.last_buzzed is None or now-self.last_buzzed < AUF_BELLBUZZ_REPEAT_TIME: logger.info("buzzing because of bell ringing in state auf") @@ -181,69 +182,63 @@ class StateMachine(): self.last_buzzed = now if not pins.space_active: logger.info("space switch off - starting leaving procedure") - return StateAboutToLeave(self.state_machine) + return StateMachine.StateAboutToLeave(self.state_machine) if pins.door_locked: - logger.error("door manually locked, but space switch on - going to StateZu") + logger.info("door manually locked, but space switch on - going to StateZu") play_sound("manual_lock") - return StateZu(self.state_machine) + return StateMachine.StateZu(self.state_machine) class StateLocking(State): - # FIXME: Why does this even have callbacks? # TODO: share code with StateUnlocking def __init__(self,sm): # construct a nervlist - nervlist = [(t*CLOSE_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_LOCK)) for t in range(1, CLOSE_REPEAT_NUMBER+1)] - nervlist += [((CLOSE_REPEAT_NUMBER+1)*CLOSE_REPEAT_TIMEOUT, self.could_not_close)] + 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) - self.callbacks=[] - # FIXME: can we send "202 processing: Trying to close the door" here? Are the callbacks multi-use? - self.tries = 0 - assert self.pins().door_closed, "Door is open while we should close it, this must not happen" - self.actor().act(Actor.CMD_LOCK) - def notify(self, did_it_work): - s = "200 okay: door closed" if did_it_work else ("500 internal server error: Couldn't close door with %d tries à %f seconds" % (CLOSE_REPEAT_NUMBER,CLOSE_REPEAT_TIMEOUT)) - for cb in self.callbacks: - if cb is not None: - cb(s) + 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 - self.notify(True) - return StateAboutToOpen(self.state_machine) + logger.warning("door manually opened during locking") + return StateMachine.StateAboutToOpen(self.state_machine) if pins.door_locked: - return SpaceZu(self.state_machine) + return StateMachine.StateZu(self.state_machine) def handle_cmd_unlock_event(self,callback): - callback("409 conflict: The server is currently trying to close the door. Try again later.") + callback("409 conflict: The server is currently trying to lock the door. Try again later.") def could_not_close(self): logger.critical("Couldn't close door after %d tries. Going back to StateAboutToOpen." % CLOSE_REPEAT_NUMBER) - self.notify(False) - return StateAboutToOpen(self.state_machine) + return StateMachine.StateAboutToOpen(self.state_machine) class StateAboutToLeave(State): def __init__(self, sm): - nervlist = [(LEAVE_TIMEOUT, lambda: StateLocking(self.state_machine))] + 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 StateLeaving(self.state_machine) + return StateMachine.StateLeaving(self.state_machine) if self.pins().door_locked: - return StateZu(self.state_machine) + return StateMachine.StateZu(self.state_machine) + if self.pins().space_active: + return StateMachine.StateAuf(self.state_machine) class StateLeaving(State): def __init__(self, sm): - nervlist = [(LEAVE_TIMEOUT, lambda: StateAboutToOpen(self.state_machine))] + nervlist = [(LEAVE_TIMEOUT, lambda: StateMachine.StateAboutToOpen(self.state_machine))] super().__init__(sm, nervlist) def handle_pins_event(self): if self.pins().door_closed: - return StateLocking(self.state_machine) + return StateMachine.StateLocking(self.state_machine) if self.pins().door_locked: - return StateZu(self.state_machine) + return StateMachine.StateZu(self.state_machine) + if self.pins().space_active: + return StateMachine.StateAuf(self.state_machine) def __init__(self, actor): self.actor = actor - self.callback = ThreadFunction(self._callback) - self.current_state = StateStart(self) + self.callback = ThreadFunction(self._callback, name="StateMachine") + self.current_state = StateMachine.StateStart(self) self.pins = None self.old_pins = None @@ -256,8 +251,8 @@ class StateMachine(): self.pins = arg # handle stuff newstate = self.current_state.handle_event(cmd,arg) # returns None or an instance of the new state - self.old_pins = self.pins # FIXME not used? + self.old_pins = self.pins while newstate is not None: - logger.info("StateMachine: new state = %s" % newstate.__class__.__name__) + logger.debug("StateMachine: new state = %s" % newstate.__class__.__name__) self.current_state = newstate newstate = self.current_state.handle_event(StateMachine.CMD_PINS, self.pins) diff --git a/tuerd b/tuerd index c3a5a43..8c24cfd 100755 --- a/tuerd +++ b/tuerd @@ -1,6 +1,7 @@ #!/usr/bin/python3 import RPi.GPIO as GPIO import statemachine, actor, pins, tysock, waker +from libtuer import logger # initialize GPIO stuff GPIO.setmode(GPIO.BOARD) @@ -17,12 +18,12 @@ try: the_socket.accept() except KeyboardInterrupt: # this is what we waited for! + logger.info("Got SIGINT, terminating...") pass # bring 'em all down the_waker.stop() the_pins.stop() -the_socket.stop() the_machine.stop() the_actor.stop() diff --git a/tyshell b/tyshell index 8d7070a..9f32725 100755 --- a/tyshell +++ b/tyshell @@ -35,9 +35,9 @@ def sendcmd(addr, cmd): print("Running %s..." % (cmd)) s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(addr) - s.settimeout(10.0) + s.settimeout(60.0) s.send(cmd.encode()) - data = s.recv(4) + data = s.recv(256) s.close() print(data.decode('utf-8')) return run diff --git a/tysock.py b/tysock.py index 3a0cc3a..cd11214 100644 --- a/tysock.py +++ b/tysock.py @@ -1,4 +1,4 @@ -import socket, os, stat +import socket, os, stat, struct, pwd from statemachine import StateMachine from libtuer import logger SO_PEERCRED = 17 # DO - NOT - TOUCH @@ -75,6 +75,3 @@ class TySocket(): raise # forward Ctrl-C to the outside except Exception as e: logger.error("TySocket: Something went wrong: %s" % str(e)) - - def stop(self): - pass diff --git a/waker.py b/waker.py index b1d475b..7b212a1 100644 --- a/waker.py +++ b/waker.py @@ -4,7 +4,7 @@ from statemachine import StateMachine class Waker(): def __init__(self, sm): self._sm = sm - self._t = ThreadRepeater(self._wake, 0.5) + self._t = ThreadRepeater(self._wake, 0.5, name="Waker") def _wake(self): self._sm.callback(StateMachine.CMD_WAKEUP)