Reputation: 111
I've got an application running on a BeagleBone Black controlling some hardware. The user interface consists of an LCD display and a rotary encoder switch with an integrated push button.
In terms of state transitions, all what the application does is to react to push button events to switch between them:
In summary, there are two states, and the trigger is the button push, which actually toggles between the two.
The current code is an infinite loop with an if ... else condition that executes either the "manual" mode or "auto" mode actions depending on the state. A delay is added at the end of the loop. The manual
variable is updated (toggled) by an interrupt whenever a push button event is detected.
while True:
# Toggle mode when the push button event is detected
# It can also be detected asynchronously outside the loop,
# but we'll keep it on the loop for a smaller example
if gpio.event_detected(push_button):
manual_mode = not manual_mode
if manual_mode:
# MANUAL MODE:
do_user_input_actions()
else:
# AUTO mode
do_automatic_actions()
time.sleep(0.5)
This construct is similar to the C equivalent that I've used a few times before. But I was wondering of a more object-oriented way of doing the same in Python.
Essentially, it seems that the logic would lend itself to using pytransitions, particularly if I need to add more states in the future, so I'm considering porting the code to it.
I can picture the two states and the transitions:
states = ['manual', 'sweep']
transitions = [['handle_manual_input', 'auto', 'manual'],
['run_auto_test', 'manual', 'auto']]
However, I can't quite visualize what the proper way to implement the equivalent of my current infinite loop with the state check is in the pytranslations model.
Effectively, handling manual input should be done on each loop iteration, and not just on the transition from auto, and vice versa for running the auto mode.
I understand that the state machine should be left alone to just take care of the states and transitions for better code separation.
It's the model implementation that I can't quite picture: I'd welcome any guidance on how and where to run do_user_input_actions()
and do_automatic_actions()
.
Upvotes: 1
Views: 4683
Reputation: 2273
If you are willing to (re-)enter states every cycle, this should do the trick:
from transitions import Machine
from random import choice
class Model(object):
def on_enter_manual(self):
# do_user_input_actions()
print('manual')
def on_enter_auto(self):
# do_automatic_actions()
print('auto')
def event_detected(self):
# return gpio.event_detected()
# For simulation purposes randomise the return value
return choice([True, False])
states = ['manual', 'auto']
transitions = [{'trigger': 'loop', 'source': ['manual', 'auto'],
'dest': 'manual', 'conditions': 'event_detected'},
{'trigger': 'loop', 'source': ['manual', 'auto'],
'dest': 'auto'}]
model = Model()
machine = Machine(model=model, states=states,
transitions=transitions, initial='manual')
for i in range(10):
model.loop()
transitions
processes possible transitions in the order they were added. This means that the first transition is executed when event_detected
returns True
. If this is not the case, the second transition will be chosen as it has no condition.
For this solution, some tweaks are possible:
a) You could replace source:[..]
with source:'*'
to allow a loop transitions from all states. If you want to add states in the future this might be useful, but it can also backfire if you plan to use more than one trigger.
b) In case do_user_input_actions
, gpio.event_detected
, do_automatic_actions
are static methods, you can more or less omit the model by using the following transitions:
transitions = [{'trigger': 'loop', 'source': ['manual', 'auto'],
'dest': 'manual', 'conditions': gpio.event_detected,
'after': do_user_input_actions},
{'trigger': 'loop', 'source': ['manual', 'auto'],
'dest': 'auto', 'after': do_automatic_actions}]
machine = Machine(states=states, transitions=transitions, initial='manual')
Note that I passed function references rather than strings. Strings are considered to be model functions while function references can originate from anywhere. Since our model would be more or less empty, we can use the Machine
instance as model. This is only recommended when the behaviour is rather simple. A dedicated model makes handling complex configurations more maintainable. I passed the function callbacks to after
, but in this scenario it does not matter whether it is executed before, during or after the state transition.
For the sake of completeness and since you explicitly mentioned you do not want to handle user input during transitions, I would suggest a Strategy Pattern approach where we create strategy objects which can be handled in the same way, but do different tasks.
Strategies are replaced whenever a state is changed. This time we need unless
in the second transition to only go to 'auto' mode when no user input has been detected (unless
is just the convenience counterpart of conditions
). We also make use of Machine
's keyword finalize_event
which will always execute a function regardless of the success of a previous attempted transition. We could also just call Model.execute
ourselves:
from transitions import Machine
from random import choice
class AutoStrategy(object):
def execute(self):
# do_automatic_actions()
print('auto')
class UserInputStrategy(object):
def execute(self):
# do_user_input_actions()
print('manual')
class Model(object):
def __init__(self):
self.strategy = UserInputStrategy()
def execute(self):
self.strategy.execute()
def on_enter_manual(self):
# We could use a singleton here if *Strategy is stateless
self.strategy = UserInputStrategy()
def on_enter_strategy(self):
self.strategy = AutoStrategy()
def event_detected(self):
# return gpio.event_detected()
# For simulation purposes, randomise the return value
return choice([True, False])
states = ['manual', 'auto']
transitions = [{'trigger': 'loop', 'source': 'auto', 'dest': 'manual',
'conditions': 'event_detected'},
{'trigger': 'loop', 'source': 'manual', 'dest': 'auto',
'unless': 'event_detected'}]
model = Model()
machine = Machine(model=model, states=states, transitions=transitions,
initial='manual', finalize_event='execute')
for i in range(10):
model.loop()
Upvotes: 2