Skip to content

Commit 9370807

Browse files
authored
Merge pull request #107 from robotpy/timed-state-guarantee
Magicbot: guarantee that timed_state will execute at least once
2 parents 8a3444d + 348752a commit 9370807

File tree

2 files changed

+69
-18
lines changed

2 files changed

+69
-18
lines changed

magicbot/state_machine.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ def timed_state(f=None, *, duration=None, next_state=None, first=False, must_fin
9696
'''
9797
If this decorator is applied to a function in an object that inherits
9898
from :class:`.StateMachine`, it indicates that the function
99-
is a state that will run for a set amount of time unless interrupted
99+
is a state that will run for a set amount of time unless interrupted.
100+
101+
It is guaranteed that a timed_state will execute at least once, even if
102+
it expires prior to being executed.
100103
101104
The decorated function can have the following arguments in any order:
102105
@@ -170,8 +173,10 @@ def default_state(f=None):
170173
If this decorator is applied to a method in an object that inherits
171174
from :class:`.StateMachine`, it indicates that the method
172175
is a default state; that is, if no other states are executing, this
173-
state will execute. There can only be a single default state in a
174-
StateMachine object.
176+
state will execute. If the state machine is always executing, the
177+
default state will never execute.
178+
179+
There can only be a single default state in a StateMachine object.
175180
176181
The decorated function can have the following arguments in any order:
177182
@@ -346,7 +351,7 @@ def _build_states(self):
346351
raise InvalidWrapperError(errmsg)
347352

348353
# Can't define states that are named the same as things in the
349-
# base class, will cause issues. Catch it early.
354+
# base class, will cause issues. Catch it early.
350355
if hasattr(StateMachine, state.name):
351356
raise InvalidStateName("cannot have a state function named '%s'" % state.name)
352357

@@ -514,21 +519,21 @@ def execute(self):
514519
new_state_start = tm
515520

516521
# determine if the time has passed to execute the next state
517-
# -> intentionally comes first,
518-
if state is not None and state.expires < tm:
519-
520-
previous_state = state
522+
# -> intentionally comes first
523+
if state is not None and state.ran and state.expires < tm:
524+
new_state_start = state.expires
521525

522526
if state.next_state is None:
523-
state = None
527+
# If the state expires and it's the last state, if the machine
528+
# is still engaged then it should cycle back to the beginning
529+
if self.__should_engage:
530+
self.next_state(self.__first)
531+
state = self.__state
532+
else:
533+
state = None
524534
else:
525535
self.next_state(state.next_state)
526-
new_state_start = state.expires
527536
state = self.__state
528-
529-
# Reset the expired time to prevent the state from expiring
530-
# immediately if it's ran a second time
531-
previous_state.expires = 0xffffffff
532537

533538
# deactivate the current state unless engage was called or
534539
# must_finish was set
@@ -583,7 +588,7 @@ def on_enable(self):
583588
self.__engaged = True
584589

585590
def on_iteration(self, tm):
586-
# TODO, remove the on_iteration function in 2017?
591+
# TODO, remove the on_iteration function in 2017?
587592

588593
# Only engage the state machine until its execution finishes, otherwise
589594
# it will just keep repeating

tests/test_magicbot_sm.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def third_state(self):
128128
assert sm.current_state == 'first_state'
129129
assert sm.is_executing
130130

131-
sm.execute()
131+
sm.execute()
132132
sm.execute()
133133
assert not sm.is_executing
134134
assert sm.current_state == ''
@@ -265,7 +265,7 @@ class _TM(StateMachine):
265265
def first_state(self):
266266
self.next_state(self.second_state)
267267

268-
@state
268+
@state
269269
def second_state(self):
270270
self.done()
271271

@@ -284,7 +284,7 @@ def second_state(self):
284284
def test_next_fn2(wpitime):
285285
class _TM(StateMachine):
286286

287-
@state
287+
@state
288288
def second_state(self):
289289
pass
290290

@@ -502,3 +502,49 @@ def defaultState(self, initial_call):
502502
assert sm.didDefault == True
503503
assert sm.defaultInit == False
504504
assert sm.didDone == False
505+
506+
def test_short_timed_state(wpitime):
507+
'''
508+
Tests two things:
509+
- A timed state that expires before it executes
510+
- Ensures that the default state won't execute if the machine is always
511+
executing
512+
'''
513+
514+
class _SM(StateMachine):
515+
516+
def __init__(self):
517+
self.executed = []
518+
519+
@default_state
520+
def d(self):
521+
self.executed.append('d')
522+
523+
@state(first=True)
524+
def a(self):
525+
self.executed.append('a')
526+
self.next_state('b')
527+
528+
@timed_state(duration=.01)
529+
def b(self):
530+
self.executed.append('b')
531+
532+
sm = _SM()
533+
setup_tunables(sm, 'cname')
534+
assert sm.current_state == ''
535+
assert not sm.is_executing
536+
537+
for _ in [1, 2, 3, 4]:
538+
sm.engage()
539+
sm.execute()
540+
assert sm.current_state == 'b'
541+
542+
wpitime.now += 0.02
543+
544+
sm.engage()
545+
sm.execute()
546+
assert sm.current_state == 'b'
547+
548+
wpitime.now += 0.02
549+
550+
assert sm.executed == ['a', 'b', 'a', 'b', 'a', 'b', 'a', 'b']

0 commit comments

Comments
 (0)