If we make use of CircuitPython's object oriented (OO) capabilities we can make a far cleaner implementation. Each state becomes a class. We can use inheritance to create a simple machine that manages transitions. The machine just needs to track it's current state, not caring what exactly it is. As before, the pause feature in this project adds a little wrinkle to this.

For simplicity, everything before the main loop stays more-or-less the same (of course we don't need the state constants). The main loop will be replaced by what's described below. 

State

Before we go further we need to look at the abstract State base class. This defines the interface that all state conform to (in Python this is by convention, in some languages this is enforced by the compiler). It also contains any functionality that is common to all states.

Looking at the class below we see 5 pieces of a state's interface:

constructor - This is expected. While the implementation here does nothing , it could. The concrete state classes call this before doing any of their own initialization.
name - this property returns the name of the class. Here it returns an empty string since the State class is abstract and should never be instantiated. Subclasses return their name.
enter - This is called when a state becomes the active state (i.e. as it is entered).
exit - This is called when a state ceases to be the active state (i.e. when it is exited).
update - This is called on the active state each time through the main loop. Each state's update method calls this first, before doing it's own processing. You can see that is how pausing is implemented. When the active state is updated, this is executed first, checking for a switch press and pausing if it has been detected. Notice how a boolean is returned. That signals whether pausing has happened. If so, the "active" state is no longer active and should skip the rest of its update. States that are not pausable (paused and waiting) do not call this in their update method.

class State(object):

    def __init__(self):
        pass

    @property
    def name(self):
        return ''

    def enter(self, machine):
        pass

    def exit(self, machine):
        pass

    def update(self, machine):
        if switch.fell:
            machine.paused_state = machine.state.name
            machine.pause()
            return False
        return True

The State Machine

A state machine implementation is a class that manages states and the transitions between them. Using inheritance in OO and especially in a dynamically typed language like Python, the machine can be happily unaware of what the states are as long as they have a consistent interface. The way the paused state works in this project muddies that slightly in that the machine is responsible for the state interactions with the paused state. Because of that the paused state can be seen as almost part of the machine. As such they'll be discussed together.

The machine has a constructor that simply initializes some instance variables. Some are machine related, some are used to implement pausing, but a few are here because it's a common place to coordinate between states. In this case we have some variables used to coordinate between the fireworks states. A better way would be to use a composite state for this: a state that has a state machine embedded in it. That adds a little more complexity than desired for a fairly introductory guide.

Similarly, there are methods for adding states to the machine (add_state), transitioning to a different state (go_to_state), and updating each time through the loop (update). There are two methods for pausing (pause and resume_state), as well as a fireworks support method (reset_fireworks).

class StateMachine(object):

    def __init__(self):
        self.state = None
        self.states = {}
        self.firework_color = 0
        self.firework_step_time = 0
        self.burst_count = 0
        self.shower_count = 0
        self.firework_stop_time = 0
        self.paused_state = None
        self.pixels = []
        self.pixel_index = 0

    def add_state(self, state):
        self.states[state.name] = state

    def go_to_state(self, state_name):
        if self.state:
            log('Exiting %s' % (self.state.name))
            self.state.exit(self)
        self.state = self.states[state_name]
        log('Entering %s' % (self.state.name))
        self.state.enter(self)

    def update(self):
        if self.state:
            log('Updating %s' % (self.state.name))
            self.state.update(self)

    # When pausing, don't exit the state
    def pause(self):
        self.state = self.states['paused']
        log('Pausing')
        self.state.enter(self)

    # When resuming, don't re-enter the state
    def resume_state(self, state_name):
        if self.state:
            log('Exiting %s' % (self.state.name))
            self.state.exit(self)
        self.state = self.states[state_name]
        log('Resuming %s' % (self.state.name))

    def reset_fireworks(self):
        """As indicated, reset the fireworks system's variables."""
        self.firework_color = random_color()
        self.burst_count = 0
        self.shower_count = 0
        self.firework_step_time = time.monotonic() + 0.05
        strip.fill(0)
        strip.show()

Paused State

When the paused state is entered, it grabs the time the switch was pressed (since it can only be entered in response to the switch being pressed, we know it was just pressed). This is used in update to determine how long the switch has been held. Audio and servo are stopped.

In update (note that the base class's update is NOT called) a switch press is checked for. If it's found audio and servo are resumed and the machine is told to go back to the state that was paused (notably without entering it). If the switch wasn't just pressed but is pressed, it must still be pressed from the entry to the paused state. If it's been a second since then, the system is reset by transitioning to the raising state.

class PausedState(State):

    def __init__(self):
        self.switch_pressed_at = 0
        self.paused_servo = 0

    @property
    def name(self):
        return 'paused'

    def enter(self, machine):
        State.enter(self, machine)
        self.switch_pressed_at = time.monotonic()
        if audio.playing:
            audio.pause()
        self.paused_servo = servo.throttle
        servo.throttle = 0.0

    def exit(self, machine):
        State.exit(self, machine)

    def update(self, machine):
        if switch.fell:
            if audio.paused:
                audio.resume()
            servo.throttle = self.paused_servo
            self.paused_servo = 0.0
            machine.resume_state(machine.paused_state)
        elif not switch.value:
            if time.monotonic() - self.switch_pressed_at > 1.0:
                machine.go_to_state('raising')

Now we can look at each other state in turn.

Waiting State

This state is responsible for doing nothing. Until, that is, the user presses the switch or the time reaches 10 seconds to midnight on New Year's Eve. At that point it causes a transition to the dropping state.

class WaitingState(State):
  
    @property
    def name(self):
        return 'waiting'

    def enter(self, machine):
        State.enter(self, machine)

    def exit(self, machine):
        State.exit(self, machine)

    def almost_NY(self):
        t = rtc.datetime
        return (t.tm_mday == 31 and
                t.tm_mon == 12 and
                t.tm_hour == 23 and
                t.tm_min == 59 and
                t.tm_sec == 50)

    def update(self, machine):
        if switch.fell or self.almost_NY():
            machine.go_to_state('dropping')

Dropping State

The dropping state has quite a bit going on.

It starts with a constructor that allocates a couple variables for the rainbow effect, as well as one to hold the time at which to stop the drop.

The enter method is responsible for starting the countdown audio file, turning on the servo to drop the ball, starting the rainbow effect, and setting the time to end the drop.

The exit method stops the audio and servo, as well as setting up for the fireworks effect.

Finally, update checks if it's time to end the drop. If so it has the machine transition to the burst state. Otherwise, if it's time to advance the rainbow effects it does so.

class DroppingState(State):

    def __init__(self):
        self.rainbow = None
        self.rainbow_time = 0
        self.drop_finish_time = 0

    @property
    def name(self):
        return 'dropping'

    def enter(self, machine):
        State.enter(self, machine)
        now = time.monotonic()
        start_playing('./countdown.wav')
        servo.throttle = DROP_THROTTLE
        self.rainbow = rainbow_lamp(range(0, 256, 2))
        self.rainbow_time = now + 0.1
        self.drop_finish_time = now + DROP_DURATION

    def exit(self, machine):
        State.exit(self, machine)
        now = time.monotonic()
        servo.throttle = 0.0
        stop_playing()
        machine.reset_fireworks()
        machine.firework_stop_time = now + FIREWORKS_DURATION

    def update(self, machine):
        if State.update(self, machine):
            now = time.monotonic()
            if now >= self.drop_finish_time:
                machine.go_to_state('burst')
            if now >= self.rainbow_time:
                next(self.rainbow)
                self.rainbow_time = now + 0.1

Burst and Shower States

These two go together to make the fireworks effect reminiscent of an explosion followed by a shower of twinkling sparks.

Burst is pretty simple. Nothing happens when it's entered, but when it exits, it sets the count for Shower. Each time through the loop its update method calls burst to advance the effect (same as the brute force implementation). When burst eventually returns True, it triggers a transition to the shower state.

Shower is somewhat similar. On exit it resets the fireworks system (which will generate a different random color). In updateshower is called to advance the effect. When shower eventually returns True, the machine transitions to one of two states: idle if it's time for the fireworks effect to finished, back to burst if it's not.

These states bounce back and forth until the fireworks has gone on long enough: explosion, shower, repeat.

class BurstState(State):

    @property
    def name(self):
        return 'burst'

    def enter(self, machine):
        State.enter(self, machine)

    def exit(self, machine):
        State.exit(self, machine)
        machine.shower_count = 0

    def update(self, machine):
        if State.update(self, machine):
            if burst(machine, time.monotonic()):
                machine.go_to_state('shower')


# Show a shower of sparks following an explosion

class ShowerState(State):

    @property
    def name(self):
        return 'shower'

    def enter(self, machine):
        State.enter(self, machine)

    def exit(self, machine):
        State.exit(self, machine)
        machine.reset_fireworks()

    def update(self, machine):
        if State.update(self, machine):
            if shower(machine, time.monotonic()):
                if now >= machine.firework_stop_time:
                    machine.go_to_state('idle')
                else:
                    machine.go_to_state('burst')

Idle State

As we saw above, once the fireworks effect is finished, the machine moves into the idle state. This state does absolutely nothing beyond calling the base class' methods. State's update method will kick the machine into paused (and possibly raising after a second) when the switch is pressed.

class IdleState(State):

    @property
    def name(self):
        return 'idle'

    def enter(self, machine):
        State.enter(self, machine)

    def exit(self, machine):
        State.exit(self, machine)

    def update(self, machine):
        State.update(self, machine)

Raising State

This state is transitioned to when the switch is held for at least a second and remains the active state until it is released at which time the machine moves to the waiting state to start all over again.

The enter method resets the NeoPixel strip, stops the audio, and turns on the servo to pull the ball back up. On exit, the servo is stopped. The update method simply watches for the switch to be released, at which point the machine moves to the waiting state.

class RaisingState(State):

    @property
    def name(self):
        return 'raising'

    def enter(self, machine):
        State.enter(self, machine)
        strip.fill(0)
        strip.brightness = 1.0
        strip.show()
        if audio.playing:
            audio.stop()
        servo.throttle = RAISE_THROTTLE

    def exit(self, machine):
        State.exit(self, machine)
        servo.throttle = 0.0

    def update(self, machine):
        if State.update(self, machine):
            if switch.rose:
                machine.go_to_state('waiting')

The Main Loop

All that's left is to set up the machine and start looping.

machine = StateMachine()
machine.add_state(WaitingState())
machine.add_state(DroppingState())
machine.add_state(BurstState())
machine.add_state(ShowerState())
machine.add_state(IdleState())
machine.add_state(RaisingState())
machine.add_state(PausedState())

machine.go_to_state('waiting')

while True:
    switch.update()
    machine.update()

Discussion

By using classes to split the code into chunks specific to each state, and managing them in a separate machine class we've broken the code into small pieces, each of which is quite simple and understandable. Furthermore, separating out entry, exit, and update into separate methods cuts things into even smaller and more understandable pieces. What happens when a state is entered? Look in its class' entry method. This regularity of structure also helps make managing the states similarly simple and understandable.

It's also far easier to add states to the machine.

This guide was first published on Dec 29, 2018. It was last updated on Dec 29, 2018.

This page (Using Classes) was last updated on Apr 17, 2021.

Text editor powered by tinymce.