Fallback mode works now (software side)
[saartuer.git] / statemachine.py
index 0fc24bdf08e1c4d9b91c80752cf6c6cc97d5e704..cd1b04ce93f89a929cec1c05d5d871e6c5c2e118 100644 (file)
@@ -1,6 +1,6 @@
 from libtuer import ThreadFunction, logger, fire_and_forget
 from actor import Actor
 from libtuer import ThreadFunction, logger, fire_and_forget
 from actor import Actor
-import os, random, time
+import os, random, time, threading
 
 # logger.{debug,info,warning,error,critical}
 
 
 # logger.{debug,info,warning,error,critical}
 
@@ -11,7 +11,7 @@ def play_sound (what):
                logger.error("StateMachine: Unable to list sound files in %s" % (SOUNDS_DIRECTORY+what))
                return
        soundfile = SOUNDS_DIRECTORY + what + '/' + random.choice(soundfiles)
                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: ")
+       fire_and_forget_cmd ([SOUNDS_PLAYER,soundfile], logger.error, "StateMachine: ")
 
 
 # StateUnlocking constants
 
 
 # StateUnlocking constants
@@ -22,6 +22,9 @@ OPEN_REPEAT_NUMBER = 3
 CLOSE_REPEAT_TIMEOUT = 7
 CLOSE_REPEAT_NUMBER = 3
 
 CLOSE_REPEAT_TIMEOUT = 7
 CLOSE_REPEAT_NUMBER = 3
 
+# StateFallback constants
+FALLBACK_LEAVE_DELAY_LOCK = 5 # seconds
+
 # 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")),\
 # 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")),\
@@ -99,38 +102,103 @@ class StateMachine():
                        else:
                                raise Exception("Unknown command number: %d" % ev)
        
                        else:
                                raise Exception("Unknown command number: %d" % ev)
        
-       class AbstractLockedState(State):
+       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'''
                '''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:
                def handle_pins_event(self):
                        if not self.pins().door_locked:
-                               return StateAboutToOpen(self.state_machine)
-                       return super().handle_pins_event
+                               logger.info("Door unlocked, space is about to open")
+                               return StateMachine.StateAboutToOpen(self.state_machine)
+                       return super().handle_pins_event()
        
        
-       class AbstractUnlockedState(State):
+       class AbstractUnlockedState(AbstractNonStartState):
                '''A state with invariant "The space is unlocked", switching to StateZu when the space becomes locked'''
                '''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:
                def handle_pins_event(self):
                        if self.pins().door_locked:
+                               logger.info("Door locked, closing space")
                                if self.pins().space_active:
                                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.info("StateMachine: door manually locked, but space switch is still on - going to StateZu")
+                                       logger.warning("StateMachine: door manually locked, but space switch is still on - going to StateZu")
                                        play_sound("manual_lock")
                                        play_sound("manual_lock")
-                               return StateZu(self.state_machine)
-                       return super().handle_pins_event
+                               return StateMachine.StateZu(self.state_machine)
+                       return super().handle_pins_event()
        
        class StateStart(State):
        
        class StateStart(State):
+               def __init__(self, sm, nervlist = None, fallback=False):
+                       super().__init__(sm, nervlist)
+                       self.fallback = fallback
                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):
                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, switch to a proper state
+                               if self.fallback:
+                                       logger.info("Going to StateFallback because running in fallback mode")
+                                       return StateMachine.StateFallback(self.state_machine)
                                if pins.door_locked:
                                if pins.door_locked:
+                                       logger.info("All sensors got a value, switching to a proper state: Space is closed")
                                        return StateMachine.StateZu(self.state_machine)
                                else:
                                        return StateMachine.StateZu(self.state_machine)
                                else:
+                                       logger.info("All sensors got a value, switching to a proper state: Space is (about to) open")
                                        return StateMachine.StateAboutToOpen(self.state_machine)
                        return super().handle_pins_event()
        
                                        return StateMachine.StateAboutToOpen(self.state_machine)
                        return super().handle_pins_event()
        
+       class StateFallback(State):
+               def __init__(self, sm, nervlist = None):
+                       super().__init__(sm, nervlist)
+                       self._last_blink_time = time.time()
+                       self._red_state = False
+               def handle_pins_event(self):
+                       pins = self.pins()
+                       # buzz if open and bell rang
+                       if pins.space_active and pins.bell_ringing and not self.old_pins().bell_ringing:
+                               logger.info("StateFallback: Space switch on and door bell rung => buzzing")
+                               self.actor().act(Actor.CMD_BUZZ)
+                       # set green LED according to space switch
+                       if pins.space_active:
+                               self.actor().act(Actor.CMD_GREEN_ON)
+                       else:
+                               self.actor().act(Actor.CMD_GREEN_OFF)
+                       # primitive leaving procedure if space switch turned off
+                       if not pins.space_active and self.old_pins().space_active:
+                               def _close_after_time():
+                                       time.sleep(FALLBACK_LEAVE_DELAY_LOCK)
+                                       self.actor().act(Actor.CMD_LOCK)
+                               fire_and_forget(_close_after_time)
+                       # not calling superclass because we want to stay in fallback mode
+               def handle_wakeup_event(self):
+                       # blink red LED
+                       now = time.time()
+                       if self._red_state:
+                               self.actor().act(Actor.CMD_RED_OFF)
+                               self._red_state = False
+                       else:
+                               self.actor().act(Actor.CMD_RED_ON)
+                               self._red_state = True
+                       self._last_blink_time = now
+               def handle_cmd_unlock_event(self,arg):
+                       if arg is not None:
+                               arg("298 Fallback Okay: Trying to unlock the door. The System is in fallback mode, success information is not available.")
+                       self.actor().act(Actor.CMD_UNLOCK)
+       
        class StateZu(AbstractLockedState):
                def handle_cmd_unlock_event(self,callback):
                        return StateMachine.StateUnlocking(self.state_machine, callback)
        class StateZu(AbstractLockedState):
                def handle_cmd_unlock_event(self,callback):
                        return StateMachine.StateUnlocking(self.state_machine, callback)
+               def handle_pins_event(self):
+                       if not self.old_pins().space_active and self.pins().space_active: # first thing to check: edge detection
+                               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 StateUnlocking(AbstractLockedState):
                def __init__(self,sm,callback=None):
        
        class StateUnlocking(AbstractLockedState):
                def __init__(self,sm,callback=None):
@@ -167,6 +235,7 @@ class StateMachine():
                def handle_pins_event(self):
                        pins = self.pins()
                        if pins.space_active:
                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()
        
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
@@ -177,7 +246,7 @@ class StateMachine():
                        self.last_buzzed = None
                def handle_pins_event(self):
                        pins = self.pins()
                        self.last_buzzed = None
                def handle_pins_event(self):
                        pins = self.pins()
-                       if pins.bell_ringing and not self.old_pins().bell_ringing:
+                       if pins.bell_ringing and not self.old_pins().bell_ringing: # first thing to check: edge detection
                                # someone just pressed the bell
                                logger.info("StateMachine: buzzing because of bell ringing in StateAuf")
                                self.actor().act(Actor.CMD_BUZZ)
                                # someone just pressed the bell
                                logger.info("StateMachine: buzzing because of bell ringing in StateAuf")
                                self.actor().act(Actor.CMD_BUZZ)
@@ -216,6 +285,7 @@ class StateMachine():
                        if not self.pins().door_closed:
                                return StateMachine.StateLeaving(self.state_machine)
                        if self.pins().space_active:
                        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()
        
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
@@ -225,17 +295,20 @@ class StateMachine():
                        super().__init__(sm, nervlist)
                def handle_pins_event(self):
                        if self.pins().door_closed:
                        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:
                                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()
        
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
-       def __init__(self, actor):
+       def __init__(self, actor, waker, fallback = False):
                self.actor = actor
                self.callback = ThreadFunction(self._callback, name="StateMachine")
                self.actor = actor
                self.callback = ThreadFunction(self._callback, name="StateMachine")
-               self.current_state = StateMachine.StateStart(self)
+               self.current_state = StateMachine.StateStart(self, None, fallback)
                self.pins = None
                self.old_pins = None
                self.pins = None
                self.old_pins = None
+               waker.register(lambda: self.callback(StateMachine.CMD_WAKEUP), 1.0) # wake up every second
        
        def stop (self):
                self.callback.stop()
        
        def stop (self):
                self.callback.stop()
@@ -248,6 +321,7 @@ 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:
                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:
+                       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
                        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