Code with CircuitPython

Let's look at the code in CircuitPython to control the dial and lock.

We'll be using the Adafruit Circuit Playground Express library, as well as the Adafruit_Motor library. These will give us the commands we need to read the accelerometer for rotation angle of the entire board, play sounds, light up NeoPixels, as well as to control the servo motor's rotation.

Get Ready!

  1. First, make sure you're familiar with the basics of using CircuitPython on the Circuit Playground Express. Follow this guide to familiarize yourself.
  2. Then, install CircuitPython on your board by following these instructions.
  3. The last thing to do to prepare is to install the library bundle onto your board as shown here. The libraries give us what we need to code easily with high level commands!

Download the latest bundle from this link.

Once you've uncompressed the contents of the zip file, drag its contents to your Circuit Playground Express lib directory.

Download the Combo Dial Python Code

Copy the code below, and paste it into a new text document in your text editor, or in the Mu code editor for CircuitPython.

then save it to you Circuit Playground Express board as main.py.

# Combo Dial Safe
# for Adafruit Circuit Playground express
# with CircuitPython

import time

import board
import pulseio
from adafruit_motor import servo
from adafruit_circuitplayground.express import cpx

pwm = pulseio.PWMOut(board.A3, duty_cycle=2 ** 15, frequency=50)

#  plug red servo wire to VOUT, brown to GND, yellow to A3
servo = servo.Servo(pwm)

cpx.pixels.brightness = 0.05  # set brightness value


def unlock_servo():
    servo.angle = 180


def lock_servo():
    servo.angle = 90


correct_combo = ['B', 'D', 'C']  # this is where to set the combo
entered_combo = []  # this will be used to store attempts
current_dial_position = 'X'

cpx.red_led = 1  # turn off the on-board red LED while locked
lock_servo()  # lock the servo

while True:
    x_float, y_float, z_float = cpx.acceleration  # read acceleromter
    x = int(x_float)  # make int of it
    y = int(y_float)
    z = int(z_float)

    # four simple rotation positions, A-D
    # the combination entries are based on which letter is facing up
    #
    #              A
    #            .___.
    #         .         .
    #     D  .           .  B
    #        .           .
    #         .         .
    #            .|_|.
    #              C

    if x == 0 and y == 9:
        current_dial_position = 'A'  # used to store dial position
        cpx.pixels.fill((0, 0, 255))

    if x == 9 and y == 0:
        current_dial_position = 'B'
        cpx.pixels.fill((80, 0, 80))

    if x == 0 and y == -9:
        current_dial_position = 'C'
        cpx.pixels.fill((255, 70, 0))

    if x == -9 and y == 0:
        current_dial_position = 'D'
        cpx.pixels.fill((255, 255, 255))

    # press the right/B button to lock the servo
    if cpx.button_b:  # this is a more Pythonic way to check button status
        print('Locked/Reset')
        cpx.red_led = 1
        cpx.pixels.fill((50, 10, 10))
        lock_servo()
        cpx.play_tone(120, 0.4)
        cpx.pixels.fill((0, 0, 0))
        entered_combo = []  # clear this for next time around
        time.sleep(1)

    # press the left/A button to enter the current position as a combo entry
    if cpx.button_a:  # this means the button has been pressed
        # grab the current_dial_position value and add to the list
        entered_combo.append(current_dial_position)
        dial_msg = 'Dial Position: ' + \
                   str(entered_combo[(len(entered_combo) - 1)])
        print(dial_msg)
        cpx.play_tone(320, 0.3)  # beep
        time.sleep(1)  # slow down button checks

    if len(entered_combo) == 3:
        if entered_combo == correct_combo:  # they match!
            print('Correct! Unlocked.')
            cpx.red_led = 0  # turn off the on board LED
            cpx.pixels.fill((0, 255, 0))
            unlock_servo()
            cpx.play_tone(440, 1)
            time.sleep(3)
            entered_combo = []  # clear this for next time around

        else:
            print('Incorret combination.')
            cpx.pixels.fill((255, 0, 0))
            cpx.play_tone(180, 0.3)  # beep
            cpx.play_tone(130, 1)  # boop
            time.sleep(3)
            entered_combo = []  # clear this for next time around

First, the servo should go to the locked position. Now, it awaits your combination.

  • Turn to the pink quadrant, then press the A button
  • Turn to the white quadrant, then press the A button
  • Finally, turn to the orange quadrant and press the A button

It beeps, opens the lock, and turns the NeoPixels green. Success!

If you enter the incorrect combination, you'll get a sadder sounding beep, and the lights turn red. Try again!

Also note, you can press the B button to reset and try from the start at any time. The B button also re-locks the servo after it's been unlocked. Try it now!

The Combo Dial Box Code in Detail

Let's dive into the code now!

Library Import

First, we'll import a few libraries that give us access to advanced functions we'll need that don't exist in the base CircuitPython.

These code snippets aren't meant to be run on their own, they serve to explain the full program already listed above
Download: file
import board
import pulseio
from adafruit_motor import servo
from adafruit_circuitplayground.express import cpx

Next, we'll create a PWMOut object on Pin A3:

pwm = pulseio.PWMOut(board.A3, duty_cycle=2 ** 15, frequency=50)

and create a servo object:

servo = servo.Servo(pwm)

We'll also set the NeoPixels to a 0.05 brightness.

Download: file
pwm = pulseio.PWMOut(board.A3, duty_cycle=2 ** 15, frequency=50)

#  plug red servo wire to VOUT, brown to GND, yellow to A3
servo = servo.Servo(pwm)

cpx.pixels.brightness = 0.05  # set brightness value

Servo Locking & Unlocking Helpers

For clarity later on in our code, we'll define a couple of functions for the servo positions:

Download: file
def unlock_servo():
    servo.angle = 180

def lock_servo():
    servo.angle = 90

This is helpful, because the angle that is the "locked" and "unlocked" position is relative to the servo position, so it isn't always apparent which is which. By using clear names like unlock_servo and lock_servo, the code is easier to understand.

Combination Storage

Next, we'll set variable to store the correct_combo , and another to hold the attempted combinations when in use, the entered_combo. We'll be able to compare those two variables to each other to see whether or not the safe should unlock after the final position is entered.

We also will make a variable called current_dial_position to store the current dial position based upon the accelerometer reading before it is entered into the entered_combo list. It starts off with a dummy value of 'X' which is not a position on the dial.

Download: file
correct_combo = ['B', 'D', 'C']  # this is where to set the combo
entered_combo = []  # this will be used to store attempts
current_dial_position = 'X'

Finish Setting Up

Final stage of setup is to turn on the on-board red LED, and lock the servo:

Download: file
cpx.red_led = 1  # turn off the on-board red LED while locked
lock_servo()  # lock the servo

The Main Loop

Now, we get to the main loop that will run over and over again. We'll set up a set of variables that read the built-in accelerometer and convert the values to easy-to-use integers. 

Download: file
    x_float, y_float, z_float = cpx.acceleration  # read acceleromter
    x = int(x_float)  # make int of it
    y = int(y_float)
    z = int(z_float)

Here's a visual representation of how we'll break the board's rotation up into four quadrants:

Download: file
    # four simple rotation positions, A-D
    # the combination entries are based on which letter is facing up
    #
    #              A
    #            .___.
    #         .         .
    #     D  .           .  B
    #        .           .
    #         .         .
    #            .|_|.
    #              C

To determine the orientation, we'll compare the x and y axis values from the cpx.acceleration query against these four conditions:

Download: file
if x == 0 and y == 9:
        current_dial_position = 'A'  # used to store dial position
        cpx.pixels.fill((0, 0, 255))

    if x == 9 and y == 0:
        current_dial_position = 'B'
        cpx.pixels.fill((80, 0, 80))

    if x == 0 and y == -9:
        current_dial_position = 'C'
        cpx.pixels.fill((255, 70, 0))

    if x == -9 and y == 0:
        current_dial_position = 'D'
        cpx.pixels.fill((255, 255, 255))

Since gravity is -9.8 m/s2, a value reading of 9 on the y-axis means that the A quadrant of the board is facing "up" away from the gravitational pull of our planet Earth.

Using this method, we can determine the orientation of the board as it rotates around its local z-axis.

Button Time!

Next, we'll set up the code for reacting to button presses. When the B button is pressed, we'll lock the servo and reset the board to the clear state, ready to accept a new combination attempt.

Download: file
# press the right/B button to lock the servo
    if cpx.button_b:  # this is a more Pythonic way to check button status
        print('Locked/Reset')
        cpx.red_led = 1
        cpx.pixels.fill((50, 10, 10))
        lock_servo()
        cpx.play_tone(120, 0.4)
        cpx.pixels.fill((0, 0, 0))
        entered_combo = []  # clear this for next time around

        time.sleep(1)

That bit of code queries the button, and if the result comes back True, meaning it's been pressed, it will:

  • turn on the on-board red LED
  • turn all of the NeoPixels red
  • lock the servo
  • play a sad beeping sound
  • turn off the NeoPixels
  • clear the enteredCombo list
  • pause for a second

Next, we'll set up button A so that we can enter combination attempts.

Download: file
# press the left/A button to enter the current position as a combo entry
    if cpx.button_a is True:  # this means the button has been pressed
        # grab the current_dial_position value and add to the list
        enteredCombo.append(current_dial_position)
        dial_msg = 'Dial Position: ' + str(entered_combo[(len(entered_combo)-1)])
        print(dial_msg)
        cpx.play_tone(320, 0.3)  # beep
        time.sleep(1)  # slow down button checks

Again, the button is queried, and if it returns True, the code below will run.

First, it will append the current_dial_position to the entered_combo list, based on the current orientation of the board at the time the button is pressed.

If this is the first time pressing A since the list was cleared, this value will go into the first position in the list. If it's the second time, it goes to the second position, and so on.

We include some print statements so that you can look at text feedback in the REPL if you like. These are not necessary for the program to run, just informational.

Then, we play a beep to indicate that the attempt has been entered, and finally, take a short pause.

Did We Dial Right?

The next bit of code is used to determine when the third entry has been made in the entered_combo list, and then test the result against the hard coded correct_combo list.

Download: file
if len(entered_combo) == 3:
        if entered_combo == correct_combo:  # they match!
            print('Correct! Unlocked.')
            cpx.red_led = 0  # turn off the on board LED
            cpx.pixels.fill((0, 255, 0))
            unlock_servo()
            cpx.play_tone(440, 1)
            time.sleep(3)
            entered_combo = []  # clear this for next time around

First, the length of the list is checked, and when it reaches three, the code below is executed.

Got it Right!

Then, the two lists are compared. If the combination is correct:

  • a victorious message is printed
  • the red LED turns off
  • all the NeoPixels fill with a happy green
  • the servo is unlocked
  • a friendly beep is played
  • there's a short pause
  • the entered_combo list is cleared for next time

Try Again!

The last bit of code deals with the case where the two lists do NOT match. This means the incorrect combination was entered. So:

  • The 'Incorrect combination.' message is printed
  • All NeoPixels fill with angry red
  • A disapproving pair of tones plays
  • there's a short pause
  • the entered_combo list is cleared for next time
This guide was first published on Dec 11, 2017. It was last updated on Dec 11, 2017. This page (Code with CircuitPython) was last updated on Jul 15, 2019.