refactoring that circuit python code

In the last post I’d written some Python (Circuit Python, to be exact) code that would attach to the three push buttons on the Featherwing OLED display and change state based on whether the button was pressed/pushed in or not. The way I had it written it simply printed what button was either pressed or had been released. The code was scattered all over, with initialization at the top and a huge block down in the bottom inside the while True polling loop. I tried to refactor that mess by pulling out the switch polling code into a function, the better to organize and make it easier to extend. That’s when I had to declare the three button boolean variables as global (a_pressed, b_pressed, and c_pressed). I hate global and it always triggers me to pull everything into a class. Thus I combined all the loose button code into the Buttons class.

I now have all the button code in one convenient location, the class definition. Furthermore I have __init__ performing all initialization in one location. I took my original function, check_button_states(), and turned it into a class function. Because the state booleans are now part of the class, there is no global declaration. If there is one aspect of writing classes in Python it’s the use of self everywhere. Or maybe, after all this time, I still don’t fully understand self. One “feature” I removed were the print statements I had sprinkled in the button state tests. I turned all those hard coded function calls into function variables, and assigned then no-ops in the form of lambda : None inline functions. Normally an operation on None results in a exception, except in this one case. Instead it does no operation, thus the no-op. If you instantiate the class and call check_button_states(), absolutely nothing will appear to happen. If you want it to do any work you need to assign functions to each of the classes function variables, such as the examples in lines 103 to 109. Once again I use lambda to assign print statements to each function call to replicate the hard-wired functionality of the last version. In the near future I’ll assign functions that actually do something truly useful.

To slim down the code that was writing to all the NeoPixels, all the color tuples were put into a Python array, then the function cycle_color() in the prior code was renamed to cycle_colors(), and the older code placed inside an outer loop that iterates over that array of NeoPixel color tuples. That replaces all six of the older calls in the infinite while loop with just one call.

Looking at the bottom of the code, lines 111 to 113, everything except two function calls has been moved out of that infinite while loop.

One of the problems I have with CircuitPython is its lack of support for either threading or concurrency. Everything is polled inside a while True loop and all work is performed out of that loop. I’ve done a fair amount of work with MicroPython, from which CircuitPython is forked, and MicroPython supports basic threading as well as very basic concurrency in the form of its simplified implementation of big Python’s async/await ( ).

I would love to have cycle_colors() as a coroutine, as all it does is cycle the colors on all the NeoPixels. The button polling and subsequent actions would still run out of the infinite while loop. My solution may be to add an argument to the call to cycle_colors(), a function reference, in this case the class function check_button_states(). In its current form there is a very long delay between button checks. I could re-write cycle_colors() to accept that reference to function, and call it inside the loop writing to the NeoPixel ring, probably right before the call to time.sleep(0.3). That means if anything is to be done with the button state changes then it needs to complete very quickly. Not an ideal situation by any means.

Ah well. This is a hobby, isn’t it?

import time
import board
import neopixel
from digitalio import DigitalInOut, Direction, Pull

# Declare a class for all three buttons, A, B, and C.
class Buttons():
    def __init__(self):
        self.a_pressed = False
        self.b_pressed = False
        self.c_pressed = False

        self.a_pressed_func =  lambda : None
        self.a_released_func = lambda : None
        self.b_pressed_func =  lambda : None
        self.b_released_func = lambda : None
        self.c_pressed_func =  lambda : None
        self.c_released_func = lambda : None

        self.a_button = DigitalInOut(board.D9)
        self.a_button.direction = Direction.INPUT
        self.a_button.pull = Pull.UP

        self.b_button = DigitalInOut(board.D6)
        self.b_button.direction = Direction.INPUT
        self.b_button.pull = Pull.UP

        self.c_button = DigitalInOut(board.D5)
        self.c_button.direction = Direction.INPUT
        self.c_button.pull = Pull.UP

    # Check when a button has been pressed, and when it's
    # released after being pressed.
    def check_button_states(self):
        if (self.a_button.value is not True):
            if (not self.a_pressed):
                self.a_pressed = True
            if (self.a_pressed):
                self.a_pressed = False

        if (self.b_button.value is not True):
            if (not self.b_pressed):
                self.b_pressed = True
            if (self.b_pressed):
                self.b_pressed = False

        if (self.c_button.value is not True):
            if (not self.c_pressed):
                self.c_pressed = True
            if (self.c_pressed):
                self.c_pressed = False

# For the single neopixel on the Feather itself.
s_neopixel = neopixel.NeoPixel(
    board.NEOPIXEL, 1, brightness=0.2, auto_write=False, pixel_order=neopixel.GRB

# For the 12 neopixel ring attached to the Feather.
neopixel_ring = neopixel.NeoPixel(
    board.D10, 12, brightness=0.2, auto_write=True, pixel_order=neopixel.GRB

# All the NeoPixel colors we want to display.
# Even though there are four values in each color tuple,
# the library code will know how to handle the late value,
# which is white, and not send it to the GRB only
# NeoPixels on the ring.
neopixel_colors = [
    (64,0,0,0),     # red
    (0,64,0,0),     # green
    (0,0,64,0),     # blue
    (64,16,0,0),    # orange
    (0,32,32,0),    # cyan
    (0,0,0,0)       # black

# Send colors to all our NeoPixels.
def cycle_colors():
    for color in neopixel_colors:
        for i in range(len(neopixel_ring)):
            neopixel_ring[i-1] = (0,0,0,0) # turn LED off
            neopixel_ring[i] = color

buttons = Buttons()
buttons.a_pressed_func  = lambda : print('A button pressed')
buttons.a_released_func = lambda : print('A button released')
buttons.b_pressed_func  = lambda : print('B button pressed')
buttons.b_released_func = lambda : print('B button released')
buttons.c_pressed_func  = lambda : print('C button pressed')
buttons.c_released_func = lambda : print('C button released')

while True: