magicbot StateMachine

How is a StateMachine component meant to be used in a higher level component?

For example, I have a state machine that gets kicked off by a joystick button press, and then runs to completion unless pressed by another button. I would expect my robot code to look something like


  def teleopPeriodic(self):
    if self.joystick.getRawButton(1) and not self.state_machine.is_executing:
      self.state_machine.engage()
    if self.joystick.getRawButton(2):
      self.state_machine.done()
    self.state_machine.execute()

but it doesn’t seem to be working that way and I can’t seem to find really any example in the docs.

First, you don’t need to call execute – execute is always called for you by the MagicRobot class (this is how magicbot components work, after all. Perhaps the docs could be clearer on that point).

To get the kind of behavior you’re talking about, I would specify must_finish in the state decorator, something like so:


class MyComponent(StateMachine):
    
    @state(first=True, must_finish=True)
    def do_a_thing(self):
        # do something here
        pass 

Then you could call it from MagicRobot via:


def teleopPeriodic(self):
  if self.joystick.getRawButton(1):
    self.state_machine.engage()
  if self.joystick.getRawButton(2):
    self.state_machine.done()

Once the state machine is engaged, it will continue being executed (eg, do_a_thing is called) until done() is called, at which time do_a_thing will stop being called.

Ok. I have 5 states currently, and so each must be decorated with must_finish=True. That works.

What’s the significance of must_finish=False? It seems not so useful.

My first attempt was without must_finish=True on anything:


def teleopPeriodic(self):
  self.state_machine.engage()

it ran through the state machine nicely, but when it reached the end state, it reset to the start state and went through the state machine again. This doesn’t seem useful, if I wanted my state machine to loop, I’d build the loop into the state machine.

My expectations for a state machine api:

execute - run the state machine for a single cycle unless it is in the end state, otherwise do nothing
is_finished - true iff in the end state
reset - set the current state to the start state

Compared to that, it seems engage is a combination of execute and reset, and there is no is_finished.

That’s a fair criticism. The state machine stuff was originally created for autonomous modes (and honestly, that’s really where I find it useful, I’m still working on making it more useful in teleop), so the state machine behaves as if someone was constantly calling engage() until done is called. That behavior is closer to what one expects.

In teleop, my philosophy is that “nothing should happen unless I tell it to happen”, and so the state machine follows that. If you don’t call engage, then the state machine won’t do it’s things. In my opinion this generally makes it easier to reason about what’s happening when debugging. Often the most confusing types of bugs occur when there’s some kind of stale state left in the system and you’re trying to figure out why it is still doing that thing.

it ran through the state machine nicely, but when it reached the end state, it reset to the start state and went through the state machine again. This doesn’t seem useful, if I wanted my state machine to loop, I’d build the loop into the state machine.

Hm. I see how that could happen. The looping is arguably buggy behavior. Please file an issue on github (or even better, make a pull request with a fix).

My expectations for a state machine api:

execute - run the state machine for a single cycle unless it is in the end state, otherwise do nothing
is_finished - true iff in the end state
reset - set the current state to the start state

Compared to that, it seems engage is a combination of execute and reset, and there is no is_finished.

execute is one of the functions required by the magicbot component definitions (and the component API was defined before the state machine stuff I think), so it is always called every loop, thus it can’t be used in the way that you specify. Instead, it manages the actual execution of the states if the machine is engaged.

engage is similar to what you want in execute, with the exception that the state machine effectively turns itself off if you don’t call it. If the state machine is ‘turned off’, then the function with the @default_state decorator is called.

reset could be implemented, it would be equivalent to:


def reset(self):
    first_state = look up the first state()
    self.next(first_state)

Because the state machine can branch in arbitrary paths because you can call the next() function to move to a named state, technically, there’s no end state, so is_finished isn’t really meaningful. It would be vaguely equivalent to ‘not my_state_machine.is_executing’.

The RobotPy project is intended to be a community project, so if it doesn’t do what you want then I’m certainly open to improvements. Pull requests on github are especially encouraged.