def test_that_unhandled_event_gets_raised(self): Events = enum.Enum('Events', 'UNHANDLED') state0 = State() with pytest.raises(KeyError): with Machine(initial_state=state0) as machine: assert machine.state is state0 machine.fire(Events.UNHANDLED)
def test_minimal_sm_that_does_nothing(self): Events = enum.Enum('Events', 'MACHINE STATE') s0 = State() s0.actions[Events.STATE] = s0, None with Machine(initial_state=s0) as machine: machine.actions[Events.MACHINE] = s0, None assert machine.state is s0 assert machine.fire(Events.MACHINE) is None assert machine.state is s0 assert machine.fire(Events.STATE) is None assert machine.state is s0
def test_that_exception_can_be_suppressed(self): Events = enum.Enum('Events', 'UNHANDLED') class SuppressAllExceptions(State): def __exit__(self, exc_type, exc_val, exc_tb): return True # Suppress the exception. state0 = SuppressAllExceptions() with Machine(initial_state=state0) as machine: assert machine.state is state0 assert machine.fire(Events.UNHANDLED) is None assert machine.state is state0
def test_new_state_handles_less_events(self): Events = enum.Enum('Events', 's0 s1') s0 = State() s1 = State() s0.actions = {Events.s0: s0.action, Events.s1: s1.action} s1.actions = {Events.s0: s0.action} # Missing `s1`. with pytest.raises( AssertionError, match= ('Set of current events handled, {<Events.s0: 1>, <Events.s1: 2>},' ' not the same as set of new events, {<Events.s0: 1>}')): with Machine(initial_state=s0) as m: m.fire(Events.s1)
def test_traffic_lights(self): """ .. image:: media/TrafficLightStateDiagram.png """ class Inputs(enum.Enum): # 1. The inputs. RED_TIMEOUT = enum.auto() AMBER_TIMEOUT = enum.auto() GREEN_TIMEOUT = enum.auto() ERROR = enum.auto() class Outputs(enum.Enum): # 2. The outputs. RED = enum.auto() AMBER = enum.auto() GREEN = enum.auto() FLASHING_RED = enum.auto() flashing_red = State(ident='flashing_red', value=Outputs.FLASHING_RED) # 3. The states. red = State(ident='red', value=Outputs.RED) amber = State(ident='amber', value=Outputs.AMBER) green = State(ident='green', value=Outputs.GREEN) red.actions[ Inputs.RED_TIMEOUT] = green.action # 4a. The *state* actions. green.actions[Inputs.GREEN_TIMEOUT] = amber.action amber.actions[Inputs.AMBER_TIMEOUT] = red.action with Machine(initial_state=red) as machine: # 5. The machine. machine.actions[ Inputs. RED_TIMEOUT] = flashing_red.action # 4b. The *machine* actions. machine.actions[Inputs.AMBER_TIMEOUT] = flashing_red.action machine.actions[Inputs.GREEN_TIMEOUT] = flashing_red.action machine.actions[Inputs.ERROR] = flashing_red.action assert machine.state is red assert machine.fire( Inputs.RED_TIMEOUT ) is Outputs.GREEN # 6. Fire events and obtain outputs. assert machine.state is green assert machine.fire(Inputs.GREEN_TIMEOUT) is Outputs.AMBER assert machine.state is amber assert machine.fire(Inputs.AMBER_TIMEOUT) is Outputs.RED assert machine.state is red assert machine.fire(Inputs.AMBER_TIMEOUT) is Outputs.FLASHING_RED assert machine.state is flashing_red assert machine.fire(Inputs.ERROR) is Outputs.FLASHING_RED assert machine.state is flashing_red
def test_edge_detector(self): """ https://en.wikipedia.org/wiki/Mealy_machine .. image:: media/EdgeDetectorStateDiagram.png :alt: Edge Detector State Diagram :width: 864px :height: 720px """ Bit = enum.Enum( 'Bit', 'ZERO ONE' ) # 1. & 2. Define the inputs (in this case also the outputs). s_i = State(ident='i') # 3. Define the states. s_0 = State(ident=0) s_1 = State(ident=1) s_i.actions = { Bit.ZERO: (s_0, Bit.ZERO), Bit.ONE: (s_1, Bit.ZERO) } # 4. Define the actions. s_0.actions = {Bit.ZERO: (s_0, Bit.ZERO), Bit.ONE: (s_1, Bit.ONE)} s_1.actions = {Bit.ZERO: (s_0, Bit.ONE), Bit.ONE: (s_1, Bit.ZERO)} with Machine(initial_state=s_i) as machine: # 5. Define the machine. assert machine.state is s_i assert machine.fire( Bit.ZERO) is Bit.ZERO # 6. Fire events and obtain outputs. assert machine.state is s_0 assert machine.fire(Bit.ZERO) is Bit.ZERO assert machine.state is s_0 assert machine.fire(Bit.ONE) is Bit.ONE assert machine.state is s_1 assert machine.fire(Bit.ONE) is Bit.ZERO assert machine.state is s_1 assert machine.fire(Bit.ZERO) is Bit.ONE assert machine.state is s_0
def test_state_active_time(self): class StateTime(State): def __init__(self): super().__init__( value=self.state_active_time) # Value is a function. self._enter_time = None def state_active_time(self): return time() - self._enter_time def __enter__(self): self._enter_time = time() return self def __exit__(self, exc_type, exc_val, exc_tb): self._enter_time = None return False state_active_time = 1 s = StateTime() s.actions[state_active_time] = s.action with Machine(initial_state=s) as m: assert m.fire(state_active_time)( ) >= 0 # Value is a function which is called, 2nd `()`.
def main(): print('Traffic lights running ...') class Events: RED_TIMEOUT = 1 AMBER_TIMEOUT = 2 GREEN_TIMEOUT = 3 ERROR = 4 START = 5 start = State( ident='start' ) # Special start state to allow for initialization before operation. timer0 = 10 class FlashingRed(State): # Special fault state that should never exit. def __init__(self): super().__init__(ident='error') self.timer = Timer(timer0 + 4) self.led = LED(1) # noinspection PyUnusedLocal def toggle_with_arg( not_used ): # Toggle func that accepts an arg, because ``schedule`` *needs* an arg. self.led.toggle() self.led_tog_ref = toggle_with_arg # Store the function reference locally to avoid allocation in interrupt. def __enter__(self): self.timer.init( freq=2, callback=lambda _: schedule(self.led_tog_ref, None)) return self def __exit__(self, exc_type, exc_val, exc_tb): self.led.off() self.timer.deinit() flashing_red = FlashingRed() traffic_lights = Machine(initial_state=start) # The traffic light machine. traffic_lights.actions[ Events.RED_TIMEOUT] = flashing_red.action # Catch anything unexpected. traffic_lights.actions[Events.AMBER_TIMEOUT] = flashing_red.action traffic_lights.actions[Events.GREEN_TIMEOUT] = flashing_red.action traffic_lights.actions[Events.ERROR] = flashing_red.action traffic_lights.actions[Events.START] = flashing_red.action tl_fire_ref = traffic_lights.fire # Store the function reference locally to avoid allocation in interrupt. error = Switch() error.callback(lambda: schedule(tl_fire_ref, Events.ERROR)) class LEDState( State ): # Output is determined by ``__enter__`` and ``__exit__`` (common in embedded machines). def __init__(self, led_num, time_on, event): super().__init__(ident=led_num) # Use the LED num as the ident. self.led = LED(self.ident) # The LED to use. self.timer = Timer(timer0 + self.ident) # The timer to use. self.timeout = time_on # Time to wait before firing event. self.event = event # Event to fire at end of time_on. def __enter__(self): self.led.on() self.timer.init( freq=1 / self.timeout, callback=lambda _: schedule(tl_fire_ref, self.event)) return self def __exit__(self, exc_type, exc_value, traceback): self.led.off() self.timer.deinit() return False red = LEDState(led_num=1, time_on=3, event=Events.RED_TIMEOUT) green = LEDState(led_num=2, time_on=3, event=Events.GREEN_TIMEOUT) amber = LEDState(led_num=3, time_on=0.5, event=Events.AMBER_TIMEOUT) red.actions[Events.RED_TIMEOUT] = green.action green.actions[Events.GREEN_TIMEOUT] = amber.action amber.actions[Events.AMBER_TIMEOUT] = red.action start.actions[Events.START] = red.action with traffic_lights: _ = traffic_lights.fire( event=Events.START ) # Start the machine once all the setup is complete. while True: # Keep running timers (and other peripherals), but otherwise do nothing. wfi()