CircuitPython Code Walkthrough

There are a few (literally) moving parts that can make it seem a little overwhelming. We're going to go over what the different sections are doing so that it will make a little more sense.

First we'll import the libraries:

Download: file
import time
import displayio
import terminalio
import adafruit_imageload
from adafruit_display_text.label import Label
from adafruit_featherwing import minitft_featherwing
from adafruit_motorkit import MotorKit
from adafruit_motor import stepper

Then we setup the DC Motor FeatherWing and MiniTFT FeatherWing. The MiniTFT FeatherWing helper has built-in functionality that sets up our ability to use TFT display without any additional lines of code! To access the TFT later, we'll call minitft.display

Download: file
#setup stepper motor
kit = MotorKit()

#setup minitft featherwing
minitft = minitft_featherwing.MiniTFTFeatherWing()

These are our bitmap file locations. By doing this, we're telling the code where to look for what file we're referring to. All of the bitmaps are available on GitHub in the same folder as the code and can be dropped directly onto your Feather M4 to load properly.

Download: file
#setup bitmap file locations
five_minBMP = "/5min_bmp.bmp"
ten_minBMP = "/10min_bmp.bmp"
twenty_minBMP = "/20min_bmp.bmp"
hourBMP = "/60min_bmp.bmp"
runningBMP = "/camSlide_bmp.bmp"
reverseqBMP = "/reverseQ_bmp.bmp"
backingUpBMP = "/backingup_bmp.bmp"
stopBMP = "/stopping_bmp.bmp"

All of these are variables for the state machines that we'll use later in the loop. To start, they'll be set to zero.

Download: file
#variables for state machines in loop
mode = 0
onOff = 0
pause = 0
stop = 0
z = 0

The displayio library utilizes image groups to hold your different images that you want to display with your project. Here groups are setup for all of the different bitmaps.

Download: file
#image groups
five_minGroup = displayio.Group(max_size=20)
ten_minGroup = displayio.Group(max_size=20)
twenty_minGroup = displayio.Group(max_size=20)
hourGroup = displayio.Group(max_size=20)
reverseqGroup = displayio.Group(max_size=20)
backingUpGroup = displayio.Group(max_size=20)
stopGroup = displayio.Group(max_size=20)
progBarGroup = displayio.Group(max_size=20)

The setup continues here for the images, with this portion setting up the files to be loaded into the program and letting the program know that they are bitmaps.

Download: file
#bitmap setup for all of the menu screens
five_minBG, five_minPal = adafruit_imageload.load(five_minBMP,
                                                  bitmap=displayio.Bitmap,
                                                  palette=displayio.Palette)
five_minDis = displayio.TileGrid(five_minBG, pixel_shader=five_minPal)
ten_minBG, ten_minPal = adafruit_imageload.load(ten_minBMP,
                                                bitmap=displayio.Bitmap,
                                                palette=displayio.Palette)
ten_minDis = displayio.TileGrid(ten_minBG, pixel_shader=ten_minPal)
twenty_minBG, twenty_minPal = adafruit_imageload.load(twenty_minBMP,
                                                      bitmap=displayio.Bitmap,
                                                      palette=displayio.Palette)
twenty_minDis = displayio.TileGrid(twenty_minBG, pixel_shader=twenty_minPal)
hourBG, hourPal = adafruit_imageload.load(hourBMP,
                                          bitmap=displayio.Bitmap,
                                          palette=displayio.Palette)
hourDis = displayio.TileGrid(hourBG, pixel_shader=hourPal)
runningBG, runningPal = adafruit_imageload.load(runningBMP,
                                                bitmap=displayio.Bitmap,
                                                palette=displayio.Palette)
runningDis = displayio.TileGrid(runningBG, pixel_shader=runningPal)
reverseqBG, reverseqPal = adafruit_imageload.load(reverseqBMP,
                                                  bitmap=displayio.Bitmap,
                                                  palette=displayio.Palette)
reverseqDis = displayio.TileGrid(reverseqBG, pixel_shader=reverseqPal)
backingUpBG, backingUpPal = adafruit_imageload.load(backingUpBMP,
                                                    bitmap=displayio.Bitmap,
                                                    palette=displayio.Palette)
backingUpDis = displayio.TileGrid(backingUpBG, pixel_shader=backingUpPal)
stopBG, stopPal = adafruit_imageload.load(stopBMP,
                                          bitmap=displayio.Bitmap,
                                          palette=displayio.Palette)
stopDis = displayio.TileGrid(stopBG, pixel_shader=stopPal)

While the slider is sliding, the time remaining will be displayed and will update. This portion sets up the area on the TFT screen where that will be shown.

Download: file
#setup for timer display when camera is sliding
text_area = Label(terminalio.FONT, text='      ')
text_area.x = 55
text_area.y = 65

This is the last of the displayio setup for our bitmaps. All of the setup that we just did in the above lines are now being added to the groups that we created. Notice that the text_area for the timer display is added to the progBarGroup along with the runningDis that holds the "in progress" bitmap so that they'll be shown at the same time on the screen.

Download: file
#adding the bitmaps to the image groups so they can be displayed
five_minGroup.append(five_minDis)
ten_minGroup.append(ten_minDis)
twenty_minGroup.append(twenty_minDis)
hourGroup.append(hourDis)
progBarGroup.append(runningDis)
progBarGroup.append(text_area)
reverseqGroup.append(reverseqDis)
backingUpGroup.append(backingUpDis)
stopGroup.append(stopDis)

We'll be using state machines for the buttons on the MiniTFT FeatherWing too. Here the default states for all of the buttons are set to None to later be referenced in the loop.

Download: file
#setting button states on minitft featherwing to None
down_state = None
up_state = None
a_state = None
b_state = None
select_state = None

Here are some arrays that hold the different settings for the different slider speed options. In the loop, they'll match up with the mode state. We have the bitmap that will show for the selection of the mode, the delay used in the for loop for the stepper motor, the duration of the slide in seconds and the beginning timer display when a slide starts.

Download: file
#arrays to match up with the different slide speeds
#graphics menu array
graphics = [five_minGroup, ten_minGroup, twenty_minGroup, hourGroup]
#delay for the stepper motor
speed = [0.0154, 0.034, 0.0688, 0.2062]
#time duration for the camera slide
slide_duration = [300, 600, 1200, 3600]
#beginning timer display
slide_begin = ["5:00", "10:00", "20:00", "60:00"]

These are increments used to display the time remaining during a slide. Basically when the stepper reaches a number of microsteps that matches one of the twenty options below, it will grab the time remaining and update the display. The check is another state that will count up through the slide_checkin array in the loop.

Download: file
#stepper motor steps that corresponds with the timer display
slide_checkin = [860, 1720, 2580, 3440, 4300, 5160,
                 6020, 6880, 7740, 8600, 9460, 10320,
                 11180, 12040, 12900, 13760, 14620, 15480,
                 16340, 17195]
#variable that counts up through the slide_checkin array
check = 0

Here are some final flight check items before the loop begins. We have a call for time.monotonic() which will give us the beginning time of the program and then the TFT displays the first bitmap, which on startup is the five minute slide option in the main menu.

Download: file
#  start time
begin = time.monotonic()
print(begin)
#  when feather is powered up it shows the initial graphic splash
minitft.display.show(graphics[mode])

Here comes the loop! First, we setup the buttons on the MiniTFT FeatherWing.

Download: file
while True:
	#  setup minitft featherwing buttons
	buttons = minitft.buttons

Here the rest of the button state machine is defined so that when a button is pressed it changes the state from None.

Download: file
#  define the buttons' state changes

if not buttons.down and down_state is None:
 	down_state = "pressed"
if not buttons.up and up_state is None:
    up_state = "pressed"
if not buttons.select and select_state is None:
    select_state = "pressed"
if not buttons.a and a_state is None:
    a_state = "pressed"
if not buttons.b and b_state is None:
    b_state = "pressed"

We finally start to get into some action here with all of our prep paying off. The up and down buttons on the MiniTFT FeatherWing are setup to that when they're pressed they'll scroll through the different bitmap graphics that display the possible slider speeds available. When this is done the mode state is updated. There's also some logic built-in with the pause and onOff states that if the slider is either actively sliding or waiting to either reverse or go back to the main menu then the up and down buttons are blocked from triggering anything. Otherwise our states could get very confused.

Download: file
#scroll down to change slide duration and graphic
        if buttons.down and down_state == "pressed":
            #blocks the button if the slider is sliding or
            #in an inbetween state
            if pause == 1 or onOff == 1:
                mode = mode
                down_state = None
            else:
                mode += 1
                down_state = None
                if mode > 3:
                    mode = 0
                print("Mode:,", mode)
                minitft.display.show(graphics[mode])

        #scroll up to change slide duration and graphic
        if buttons.up and up_state == "pressed":
            #blocks the button if the slider is sliding or
            #in an inbetween state
            if pause == 1 or onOff == 1:
                mode = mode
                up_state = None
            else:
                mode -= 1
                up_state = None
                if mode < 0:
                    mode = 3
                print("Mode: ", mode)
                minitft.display.show(graphics[mode])

This little snippet ensures that the slider options pop back up after you return to the main menu.

Download: file
#workaround so that the menu graphics show after a slide is finished
        if mode == mode and pause == 0 and onOff == 0:
            minitft.display.show(graphics[mode])

Here is it, the moment you've been waiting for: the actual camera slide. A slide is initiated with the select button or if the state of z is 2, but more on z later. First there's some built-in logic just like we have for the up and down buttons that if the slider is paused or actively sliding then nothing will happen.

Download: file
#starts slide
if buttons.select and select_state == "pressed" or z == 2:
  #blocks the button if the slider is sliding or
  #in an inbetween state
  if pause == 1 or onOff == 1:
    #print("null")
    select_state = None

But, if all the states are properly aligned, then we'll begin a camera slide in the selected time limit. First, the graphic changes to show the progBarGroup, which if you remember contains the timer text as well. Then time.monotonic() is called and stored in press. The initial time from our array is shown, the button is reset, onOff is set to 1 to indicate that the slider is sliding and z is set to 0 (again, more on z later).

Download: file
else:
  #shows the slider is sliding graphic
  minitft.display.show(progBarGroup)
  #gets time of button press
  press = time.monotonic()
  print(press)
  #displays initial timer
  text_area.text = slide_begin[mode]
  #resets button
  select_state = None
  #changes onOff state
  onOff += 1
  #changes z state
  z = 0
  if onOff > 1:
    onOff = 0

Now comes the stepper motor and most mathematical portion of the code. We begin with a for loop that has the range of microsteps required to run the length of the slider. time.monotonic() is called again and this time stored as start. Why call it again so soon? There is a slight delay between when the select button is pressed and when the stepper motor begins stepping, so in order to make sure everything is on the same page time-wise we're going to create a real_time which will equal start minus press, which should equal zero initially and then hold the actual time that the slider has been sliding. This means that we can use that to keep track of how much time has passed since the actual slide begin.

This is used further to display the time on the TFT. end is used to hold the time remaining, which is gathered by taking real_time and subtracting it from the total time of the slide duration. We then sort it into minutes and seconds to get our clock style display, which comes a little later.

With all this time talk though we can't forget the star of the show: the stepper motor. The stepper is using microsteps for the most accurate stepping and will step 17,200 microsteps to reach the end of the slide rail. The time.sleep() delay affects how fast or slow this is accomplished. The bigger the delay, the slower it slides. A lot of testing was done to ensure an accurate slide duration to match up with 5, 10, 20 and 60 minutes.

Download: file
#number of steps for the length of the aluminum extrusions
for i in range(17200):
  #for loop start time
  start = time.monotonic()
  #gets actual duration time
  real_time = start - press
  #creates a countdown from the slide's length
  end = slide_duration[mode] - real_time
  # /60 since time is in seconds
  mins_remaining = end / 60
  if mins_remaining < 0:
    mins_remaining += 60
    #gets second(s) count
    total_sec_remaining = mins_remaining * 60
    #formats to clock time
    mins_remaining, total_sec_remaining = divmod(end, 60)
    #microstep for the stepper
    kit.stepper1.onestep(style=stepper.MICROSTEP)
    #delay determines speed of the slide
    time.sleep(speed[mode])

This portion of the code is for the time remaining timer that is displayed on the TFT. If you remember earlier in the code we setup an array that divides the 17,200 steps by 20. Here when the variable i matches one of those numbers in the array, it triggers the displayed text on the TFT to update. It also increases the check state by one so that it can get ready to wait for the next step check-in.

Download: file
if i == slide_checkin[check]:
  #check-in for time remaining based on motor steps
  print("0%d:%d" %
        (mins_remaining, total_sec_remaining))
  print(check)
  if total_sec_remaining < 10:
    text_area.text = "%d:0%d" % (mins_remaining, total_sec_remaining)
    else:
      text_area.text = "%d:%d" % (mins_remaining, total_sec_remaining)
      check = check + 1
      if check > 19:
        check = 0

All slides must come to an end and this final bit helps to make it a smooth one. First the graphic on the TFT is changed once there's ten seconds left to the "stopping" graphic. After the for loop for the stepper ends, the stepper is put into the release state and then our software states are updated. pause is set to 1 to indicate that the slider is in an in-between state, onOff is set to 0 since the slider is stopped, stop is set to 1 since we're stopped and check is set to 0 to reset for the next slide. There is then a 2 second delay as a safety measure and the TFT is updated again to show our next two options: reverse the slide or return to main menu.

Download: file
if end < 10:
#displays the stopping graphic for the last 10 secs.
	minitft.display.show(stopGroup)
#changes states after slide has completed
kit.stepper1.release()
pause = 1
onOff = 0
stop = 1
check = 0
#delay for safety
time.sleep(2)
#shows choice menu
minitft.display.show(reverseqGroup)

The choices presented on the TFT are selected with either the A or B button. First, we'll take a look at the B button which corresponds with the Main Menu option.

Here is the B button is only activated when pressed if the state of stop is 1, which it is after a slide has ended. Inside this if statement are two other if statements tied to more state machine logic, this time using z. The state of z tracks where the camera is on the slider when it is not sliding; either "home" in the default position or at the opposite end, like it would be after running one slide. The default state of z is 0 and it hasn't changed yet in our code, so 0 will mean that the camera has completed a slide and is not in the home position.

When the camera is not in the home position and the B button is pressed,  the camera backs up quickly back to home using double steps on the stepper motor, which is much faster than the microsteps we've been using so far. While this is happening, the TFT's graphic is changed to show the Backing Up graphic. After the back up has completed, the states of pause and stop are reset to 0 so that the main menu options are displayed again and can be selected to start a new slide.

Download: file
#b is defined to return to the menu
#only active if the slider is in the 'stopped' state
if buttons.b and b_state == "pressed" and stop == 1:
  #z defines location of the camera on the slider
  #0 means that it is opposite the motor
  if z == 0:
    b_state = None
    time.sleep(1)
    minitft.display.show(backingUpGroup)
    #delay for safety
    time.sleep(2)
    #brings camera back to 'home' at double speed
    for i in range(1145):
      kit.stepper1.onestep(direction=stepper.BACKWARD, style=stepper.DOUBLE)
      time.sleep(1)
      kit.stepper1.release()
      #changes states
      pause = 0
      stop = 0

If z's state is 1, that means that the camera is in the home position. You'll see shortly when the state of z is changed during a reverse slide. If the camera is in the home position and you select that you want to return to the main menu the stepper doesn't step at all and really the only thing that changes are that the states of pause, stop and z are all reset to 0 to prep for the next slide that you initiate via the main menu.

Download: file
#1 means that the camera is next to the motor
if z == 1:
  b_state = None
  time.sleep(2)
  #changes states
  pause = 0
  stop = 0
  z = 0

But what if instead of returning to the main menu you want to have more slider fun and slide back in reverse? Well you'll press the A button to initiate that reverse slide mode. Much like with the B button, what happens when you press A is determined by the state of z, or the position of the camera on the slider. You can see in our first part of the code that is controlled by the A button, if z is equal to 1, or in the home position, then the states of stop and pause are set to 0 and z is set to 2. If you remember in our original portion of the code that steps the stepper, the if statement that starts the whole thing ends with or if z == 2. By having this in place, we can reuse that piece of code to work with our reverse option with the slider.

Download: file
#a is defined to slide in reverse of the prev. slide
#only active if the slider is in the 'stopped' state
if buttons.a and a_state == "pressed" and stop == 1:
  #z defines location of the camera on the slider
  #1 means that the camera is next to the motor
  if z == 1:
    a_state = None
    time.sleep(2)
    stop = 0
    pause = 0
    #2 allows the 'regular' slide loop to run
    #as if the 'select' button has been pressed
    z = 2

But if A is pressed and z is set to 0, or not at home, then the portion below runs. It is basically a copy of the original earlier piece of code but with a very important twist: the stepper steps in reverse but with all the same logic built-in along with the time options as well to display the timer on the TFT. The only other difference is that at the end when the slide is stopping, the state of z is set to 1, to show that the camera is in the home position and the state logic will continue to work properly.

Download: file
#0 means that the camera is opposite the motor
if z == 0:
  a_state = None
  #same script as the 'regular' slide loop
  time.sleep(2)
  minitft.display.show(progBarGroup)
  press = time.monotonic()
  print(press)
  text_area.text = slide_begin[mode]
  onOff += 1
  pause = 0
  stop = 0
  if onOff > 1:
    onOff = 0

    for i in range(17200):
      start = time.monotonic()
      real_time = start - press
      end = slide_duration[mode] - real_time
      mins_remaining = end / 60
      if mins_remaining < 0:
        mins_remaining += 60
        total_sec_remaining = mins_remaining * 60
        mins_remaining, total_sec_remaining = divmod(end, 60)
        #only difference is that the motor is stepping backwards
        kit.stepper1.onestep(direction=stepper.BACKWARD, style=stepper.MICROSTEP)
        time.sleep(speed[mode])
        if i == slide_checkin[check]:
          print("0%d:%d" %
                (mins_remaining, total_sec_remaining))
          if total_sec_remaining < 10:
            text_area.text = "%d:0%d" % (mins_remaining, total_sec_remaining)
            else:
              text_area.text = "%d:%d" % (mins_remaining, total_sec_remaining)
              check = check + 1
              if check > 19:
                check = 0
                if end < 10:
                  minitft.display.show(stopGroup)
                  #state changes
                  kit.stepper1.release()
                  pause = 1
                  onOff = 0
                  stop = 1
                  z = 1
                  check = 0
                  time.sleep(2)
                  minitft.display.show(reverseqGroup)

And that is the CircuitPython code! It's a bit long, but hopefully this walk-through helped things make sense and will also be helpful when writing your own code for future projects.

This guide was first published on Dec 18, 2019. It was last updated on Dec 18, 2019.

This page (Code Walkthrough) was last updated on Apr 10, 2021.

Text editor powered by tinymce.