From: Constantin Berhard Date: Sun, 20 Oct 2013 02:00:46 +0000 (+0200) Subject: Merge branch 'master' of ralfj.de:saartuer X-Git-Url: https://git.ralfj.de/saartuer.git/commitdiff_plain/f821d08610f37224ff13df938715da4438592907?hp=36d7c6cbf03d4096480d14d1b03aa758722251f6 Merge branch 'master' of ralfj.de:saartuer --- diff --git a/actor.py b/actor.py index 8f57d9f..25939a6 100644 --- a/actor.py +++ b/actor.py @@ -3,32 +3,57 @@ import RPi.GPIO as GPIO import time class Actor: - CMD_BUZZ = 0 - CMD_UNLOCK = 1 - CMD_LOCK = 2 + 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: ("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)]), + 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 on", pin=26, tid=2, todo=[(False, 0)]), } def __init__(self): - self.act = ThreadFunction(self._act, name="Actor") - for (name, pin, todo) in self.CMDs.values(): - GPIO.setup(pin, GPIO.OUT) + # 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 _act(self, cmd): - if cmd in self.CMDs: - (name, pin, todo) = self.CMDs[cmd] - 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 _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): - self.act.stop() + for thread in self.threads.values(): + thread.stop() diff --git a/libtuer.py b/libtuer.py index 613ce4e..097a25a 100644 --- a/libtuer.py +++ b/libtuer.py @@ -1,24 +1,26 @@ import logging, logging.handlers, os, time, queue, threading, subprocess import traceback, smtplib -from email.mime.text import MIMEText +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' +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): if not isinstance(type(receivers), list): receivers = [receivers] # construct content - msg = MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8') + 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('ralfj.de') + s = smtplib.SMTP('localhost') s.sendmail(sender, receivers, msg.as_string()) s.quit() @@ -26,18 +28,20 @@ def sendeMail(subject, text, receivers, sender='sphinx@hacksaar.de', replyTo=Non class Logger: def __init__ (self): self.syslog = logging.getLogger("tuerd") - self.syslog.setLevel(syslogLevel) + 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" % ("osspd", os.getpid(), what) + thestr = "%s[%d]: %s" % ("tuerd", os.getpid(), what) # console log - print(thestr) + if lvl >= printLevel: + print(thestr) # syslog - self.syslog.log(lvl, thestr) + if lvl >= syslogLevel: + self.syslog.log(lvl, thestr) # mail log - if lvl >= mailLevel: + if lvl >= mailLevel and mailAddress is not None: sendeMail('Kritischer Türfehler', what, mailAddress) def debug(self, what): @@ -60,7 +64,7 @@ def fire_and_forget (cmd, log, prefix): with open("/dev/null", "w") as fnull: retcode = subprocess.call(cmd, stdout=fnull, stderr=fnull) if retcode is not 0: - log("%sReturn code %d at command: %s" % (prefix,retcode,str(cmd))) + logger.error("%sReturn code %d at command: %s" % (prefix,retcode,str(cmd))) t = threading.Thread(target=_fire_and_forget) t.start() diff --git a/statemachine.odg b/statemachine.odg index 883e183..d32026c 100644 Binary files a/statemachine.odg and b/statemachine.odg differ diff --git a/statemachine.py b/statemachine.py index 07d6574..7c75a7a 100644 --- a/statemachine.py +++ b/statemachine.py @@ -25,11 +25,8 @@ 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"))] - -# StateAuf constants -# time that must pass between two bell_ringing events to buzz the door again (seconds) -AUF_BELLBUZZ_REPEAT_TIME = 2 + (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 @@ -78,12 +75,16 @@ class StateMachine(): 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 OPEN event" % self.__class__.__name__) + 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 @@ -98,98 +99,111 @@ class StateMachine(): else: raise Exception("Unknown command number: %d" % ev) - class StateStart(State): + class AbstractNonStartState(State): def handle_pins_event(self): - super().handle_pins_event() - 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) + if self.pins().door_locked != (not self.pins().space_active): + self.actor().act(Actor.CMD_RED_ON) else: - return StateMachine.StateAuf(self.state_machine) + self.actor().act(Actor.CMD_RED_OFF) + return super().handle_pins_event() - class StateZu(State): + 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): - super().handle_pins_event() - pins = self.pins() - if not pins.door_locked: + if not self.pins().door_locked: + logger.info("Door unlocked, space is about to open") return StateMachine.StateAboutToOpen(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: + # FIXME the same state can be reached by first locking the door, and then activating the space. What to do then? + 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): - # intentionally not calling super() implementation return StateMachine.StateUnlocking(self.state_machine, callback) - class StateUnlocking(State): + 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 open the door" here? Are the callbacks multi-use? + # 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 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)) + 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 handle_pins_event(self): - super().handle_pins_event() - pins = self.pins() - if not pins.door_locked: - self.notify(True) - return StateMachine.StateAboutToOpen(self.state_machine) + def on_leave(self): + self.notify(not self.pins().door_locked) def handle_cmd_unlock_event(self,callback): - # intentionally not calling super() implementation # 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) + logger.critical("StateMachine: Couldn't open door after %d tries. Going back to StateZu." % OPEN_REPEAT_NUMBER) return StateMachine.StateZu(self.state_machine) - class AbstractStateWhereOpeningIsRedundant(State): + class AbstractStateWhereUnlockingIsRedundant(AbstractUnlockedState): 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.info("Received OPEN command in StateAboutToOpen. This should not be necessary.") + logger.info("StateMachine: Received UNLOCK command in %s. This should not be necessary." % self.__class__.__name__) self.actor().act(Actor.CMD_UNLOCK) - class StateAboutToOpen(AbstractStateWhereOpeningIsRedundant): + class StateAboutToOpen(AbstractStateWhereUnlockingIsRedundant): def __init__(self, sm): super().__init__(sm, ABOUTOPEN_NERVLIST) def handle_pins_event(self): - super().handle_pins_event() pins = self.pins() - if pins.door_locked: - return StateMachine.StateZu(self.state_machine) - elif pins.space_active: + if pins.space_active: + logger.info("Space activated, opening procedure completed") return StateMachine.StateAuf(self.state_machine) + return super().handle_pins_event() - class StateAuf(AbstractStateWhereOpeningIsRedundant): + class StateAuf(AbstractStateWhereUnlockingIsRedundant): def __init__(self,sm): - super().__init__(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): - 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") - self.actor().act(Actor.CMD_BUZZ) - self.last_buzzed = now + 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("space switch off - starting leaving procedure") + logger.info("StateMachine: space switch turned off - starting leaving procedure") return StateMachine.StateAboutToLeave(self.state_machine) - if pins.door_locked: - logger.info("door manually locked, but space switch on - going to StateZu") - play_sound("manual_lock") - return StateMachine.StateZu(self.state_machine) + return super().handle_pins_event() - class StateLocking(State): - # TODO: share code with StateUnlocking + 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)] @@ -201,39 +215,40 @@ class StateMachine(): 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("door manually opened during locking") + logger.warning("StateMachine: door manually opened during locking") return StateMachine.StateAboutToOpen(self.state_machine) - if pins.door_locked: - return StateMachine.StateZu(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 server is currently trying to lock the door. Try again later.") + callback("409 conflict: The sphinx 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) + 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(State): + 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().door_locked: - return StateMachine.StateZu(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(State): + 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().door_locked: - return StateMachine.StateZu(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): self.actor = actor @@ -253,6 +268,8 @@ class StateMachine(): newstate = self.current_state.handle_event(cmd,arg) # returns None or an instance of the new state self.old_pins = self.pins while newstate is not None: - logger.debug("StateMachine: new state = %s" % newstate.__class__.__name__) + assert isinstance(newstate, StateMachine.State), "I should get a state" + self.current_state.on_leave() + logger.debug("StateMachine: Doing state transition %s -> %s" % (self.current_state.__class__.__name__, 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 8c24cfd..5c91f92 100755 --- a/tuerd +++ b/tuerd @@ -2,6 +2,8 @@ import RPi.GPIO as GPIO import statemachine, actor, pins, tysock, waker from libtuer import logger +import argparse +# TODO: implement some arguments, e.g. --debug which does not send e-mails and prints debug stuff # initialize GPIO stuff GPIO.setmode(GPIO.BOARD) diff --git a/tyshell b/tyshell index c35ec0e..708ac50 100755 --- a/tyshell +++ b/tyshell @@ -32,7 +32,7 @@ def extcmd(cmd): def sendcmd(addr, cmd): def run(c): - print("Running %s..." % (cmd)) + print("206 Sending command %s..." % (cmd)) s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(addr) s.settimeout(60.0) diff --git a/tysock.py b/tysock.py index cd11214..0984b2a 100644 --- a/tysock.py +++ b/tysock.py @@ -1,4 +1,4 @@ -import socket, os, stat, struct, pwd +import socket, os, stat, struct, pwd, errno from statemachine import StateMachine from libtuer import logger SO_PEERCRED = 17 # DO - NOT - TOUCH @@ -74,4 +74,4 @@ class TySocket(): except KeyboardInterrupt: raise # forward Ctrl-C to the outside except Exception as e: - logger.error("TySocket: Something went wrong: %s" % str(e)) + logger.critical("TySocket: Something went wrong: %s" % str(e))