Rotation State

We want to use motion to change some settings on our little creature friend Spoka. In this case, we're going to use rotating the lamp to the left and to the right as two separate inputs. We plan to use shake as an input, so we need to make sure that our rotation motion isn't mistaken for a shake motion. There are various ways we could use orientation as a limiting factor, but many of them make using the actual input difficult and inconsistent. We want our lamp to work easily every time!

So, we're going to require a series of events to occur in a particular order for the input to be accepted.  How will we accomplish this? We're going to create a state machine.

State Machines

Put simply, a state machine looks for a series of inputs. When it reads an input, it changes state. Each step specifies the next state. When all of the required steps have been completed, in the correct order, it returns the desired result.

Consider purchasing a soda from a vending machine. First, you are required to insert the correct amount of money. If you only insert half, nothing happens. The correct input here is the full cost of the soda. Once you've inserted that, the next step is to choose which soda you want. The soda machine waits for this input before continuing. If you choose a cola, the machine will then proceed to dispense a cola. Once that is complete, the machine returns to it's original state: waiting for the correct amount of money to be inserted so it can repeat the steps once again.

How does this apply to code? If you would like to require a series of inputs in a certain order, instead of a single input, you'll want to create a state machine. For example, if you wanted to use rotating your Circuit Playground Express as an input, you could set it so any time it is rotated to the left, it recognises that as an input and spams the result anytime it's in that position. However, if you wanted to have it see rotating left as an input once, and require it be rotated back before allowing the left rotation input to work again, you'd need a state machine. That's what we're going to do!

The State of Spoka

The first thing we need to do is decide what we want our state machine to look like. First, we want our code to first require a rotation of approximately 90 degrees to the left or right of the upright position. Next, it should require the lamp to be held in that rotated state for one second. Last, it should require the lamp to be placed in either the opposite rotation state or the upright state before the next rotation input in the same direction can be started.

Here is a list of the steps for using the left rotation as an input:

  1. Begin upright or rotated to the right.
  2. Rotate 90 degrees to the left of the upright position.
  3. Hold for one second.
  4. Rotate upright or to the right.

The right rotation will be the same steps, with right swapped for left.

We'll start with the left rotation input. Here is what our code looks like. Download the file and load it on your CPX

import time

from adafruit_circuitplayground.express import cpx


# pylint: disable=redefined-outer-name


def upright(x, y, z):
    x_up = abs(x) < accel_threshold
    y_up = abs(y) < accel_threshold
    z_up = abs(9.8 - z) < accel_threshold
    return x_up and y_up and z_up


def left_side(x, y, z):
    x_side = abs(9.8 - x) < accel_threshold
    y_side = abs(y) < accel_threshold
    z_side = abs(z) < accel_threshold

    return x_side and y_side and z_side


state = None
hold_end = None

accel_threshold = 2
hold_time = 1

while True:
    x, y, z = cpx.acceleration
    if left_side(x, y, z):
        if state is None or not state.startswith("left"):
            hold_end = time.monotonic() + hold_time
            state = "left"
            print("Entering state 'left'")
        elif (state == "left"
              and hold_end is not None
              and time.monotonic() >= hold_end):
            state = "left-done"
            print("Entering state 'left-done'")
    elif upright(x, y, z):
        if state != "upright":
            hold_end = None
            state = "upright"
            print("Entering state 'upright'")

Let's check this out! Connect to the REPL so you can view the print statements. Begin with the board flat, facing upright. You may see "Entering state 'upright'". Rotate the board 90 degrees to the left. You should see "Entering state 'left'". Hold for 1 second, or until you see "Entering state 'left-done'". Then rotate back to facing upright, and look for "Entering state 'upright'". Now you've used our state machine!

So let's take a look at the code.

Functions

First, we create the functions that define what "left" and "upright" mean.

def upright(x, y, z):
    return abs(x) < accel_threshold and abs(y) < accel_threshold and abs(9.8 - z) < accel_threshold


def left_side(x, y, z):
    return abs(9.8 - x) < accel_threshold and abs(y) < accel_threshold and abs(z) < accel_threshold

To identify the orientation of the board, we're using the accelerometer. The accelerometer provides an (x, y, z) tuple, which is the acceleration value, in meters per second squared (m/s2), currently applied to the x, y and z axes. We can use this to determine what direction the board is pointing. When the board is flat with the front facing upright, acceleration returns (0, 0, 9.8). This is because there is 0 acceleration on the x and y axes, and 9.8m/s2 (gravity!) on the z axis. So, the first function uses this information to tell the code that upright means when the board is flat and facing up. The same concept applies to left_side, with the values altered to match the values when the board is pointed to the left. As gravity is -9.8m/s2, we take the absolute value of (x, y, z) using abs() to avoid dealing with negative numbers in our math.

Variables

Next, we create some variables for use later: state and hold_end. Then, we set hold_time to 1, and accel_threshold to 2.

state = None
hold_end = None

accel_threshold = 2
hold_time = 1

hold_time is the amount of time you must hold the board in the rotated state to continue to the next step in the state machine. accel_threshold is the number used to determine how close to exactly-rotated you must have the board to activate the input. If the accel_threshold is 0, then it must be rotated to exactly the right spot to activate it. Experimentation led us to use 2, which provides a range for the orientation that successfully activates the input. If you find you're having trouble finding the correct angle to hold the board, you can increase this number. accel_threshold must be between 0 and 9.8. Be aware that the higher the number, the easier it is to activate the input, so you may mistakenly activate it if you set the number too high. At 9.8, any movement registers as a rotation. It is not recommended to set it that high.

The Loop

Next we begin our loop. The first thing we do is call and assign acceleration. Next, we begin our state machine. At any given point in time, the code is looking to see whether the board is pointing to the left_side or upright. Within that, we begin to use that information to work through the steps of our input.

If we rotate to the left side, it checks to make sure that the state is either None (as we assigned on startup), or doesn't start with "left".

if left_side(x, y, z):
        if state is None or not state.startswith("left"):
            hold_end = time.monotonic() + hold_time
            state = "left"
            print("Entering state 'left'")

If either one of these conditions is met, it assigns hold_end = time.monotonic() + hold_time, and the code enters the "left" state. This meaning hold_end is equal to the current time plus the hold_time and state = "left".

If neither of the first two conditions is true, it checks for three other conditions.

        elif (state == "left"
              and hold_end is not None
              and time.monotonic() >= hold_end):
            state = "left-done"
            print("Entering state 'left-done'")

Is the state equal to "left", is hold_end not equal to None, and is the current time greater than or equal to hold_end. If all of these conditions is met, it begins to check whether or not the hold time has passed. Once it has, it enters the "left-done" state. At this point, we must return the lamp to the upright state to begin the left rotation part of our state machine again.

The last section checks to see if the board is upright.

    elif upright(x, y, z):
        if state != "upright":
            hold_end = None
            state = "upright"
            print("Entering state 'upright'")

If so, it checks to see if the state is not already upright before setting hold_end = None and state = "upright". The reason for verifying that the state is not already upright is to avoid spamming the upright position, since the lamp spends most of its time upright.

And now our state machine can begin again!

Last updated on Feb 20, 2018 Published on Feb 20, 2018