CircuitPython Code

This project is written in CircuitPython. The following will walk you through getting your PyPortal CIRCUITPY drive setup to run the code and give an overview of what's going on in the code.

This code requires some CircuitPython libraries to function. These libraries are not included with CIrcuitPython, so you'll need to load them yourself before the code will work.

CircuitPython Libraries

The first thing you'll need to do is load the necessary libraries onto your PyPortal. You can do this two ways: copy the entire CircuitPython Library Bundle onto your CIRCUITPY drive, or copy the libraries individually. We recommend copying only the individual libraries you need.

The libraries needed for this project are:

  • Adafruit CircuitPython NeoPixel
  • Adafruit CircuitPython PyPortal
  • Adafruit CircuitPython SDCard
  • Adafruit CircuitPython Touchscreen
  • Adafruit CircuitPython Display Button
  • Adafruit CircuitPython Bitmap Font
  • Adafruit CircuitPython Bus Device
  • Adafruit CircuitPython ESP32SPI
  • Adafruit CircuitPython Display Shapes
  • Adafruit CircuitPython Display Text

Download the latest CircuitPython Library Bundle to your computer and open the zip file. Find the lib folder. If copying the individual libraries, create a lib folder on your CIRCUITPY drive and copy the following files and folders into it:

  • neopixel.py
  • adafruit_pyportal.mpy
  • adafruit_sdcard.mpy
  • adafruit_touchscreen.mpy
  • adafruit_button.mpy
  • adafruit_bitmap_font
  • adafruit_bus_device
  • adafruit_esp32spi
  • adafruit_display_shapes
  • adafruit_display_text

Before continuing, ensure that you have at least the files neopixel.mpy, adafruit_pyportal.mpy, adafruit_sdcard.mpy, adafruit_touchscreen.mpy and adafruit_button.mpy, and the folders adafruit_bitmap_font, adafruit_bus_device, adafruit_esp32spi, adafruit_display_shapes and adafruit_display_text on your CIRCUITPY drive in the /lib folder. Once those are copied, you're all set to continue!

Secrets

The PyPortal has the ability to connect to your WiFi. Even though no network access is needed for this project, the PyPortal library still expects there to be a secrets.py file on your device. However, you do not need to include any credentials in the file for this project to work. The following is an example of a secrets.py file that will work with this project. Add the following file to your CIRCUITPY drive before continuing.

# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it

secrets = {
    }

The Code

This code sets a background color on your display, and then renders 12 colored buttons in a 4x3 grid. The color of the button is the color that you will set the NeoPIxels, e.g. the red button turns the NeoPixels on red. The light sensor is used as a toggle to switch between controlling the first strip, the second strip or both at the same time. Let's take a look!

import time
import board
from adafruit_pyportal import PyPortal
from adafruit_button import Button
import neopixel
import analogio

# Set the background color
BACKGROUND_COLOR = 0x443355

# Set the NeoPixel brightness
BRIGHTNESS = 0.3

light_sensor = analogio.AnalogIn(board.LIGHT)

strip_1 = neopixel.NeoPixel(board.D4, 30, brightness=BRIGHTNESS)
strip_2 = neopixel.NeoPixel(board.D3, 30, brightness=BRIGHTNESS)

# Turn off NeoPixels to start
strip_1.fill(0)
strip_2.fill(0)

# Setup PyPortal without networking
pyportal = PyPortal(default_bg=BACKGROUND_COLOR)

# Button colors
RED = (255, 0, 0)
ORANGE = (255, 34, 0)
YELLOW = (255, 170, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
VIOLET = (153, 0, 255)
MAGENTA = (255, 0, 51)
PINK = (255, 51, 119)
AQUA = (85, 125, 255)
WHITE = (255, 255, 255)
OFF = (0, 0, 0)

spots = [
    {'label': "1", 'pos': (10, 10), 'size': (60, 60), 'color': RED},
    {'label': "2", 'pos': (90, 10), 'size': (60, 60), 'color': ORANGE},
    {'label': "3", 'pos': (170, 10), 'size': (60, 60), 'color': YELLOW},
    {'label': "4", 'pos': (250, 10), 'size': (60, 60), 'color': GREEN},
    {'label': "5", 'pos': (10, 90), 'size': (60, 60), 'color': CYAN},
    {'label': "6", 'pos': (90, 90), 'size': (60, 60), 'color': BLUE},
    {'label': "7", 'pos': (170, 90), 'size': (60, 60), 'color': VIOLET},
    {'label': "8", 'pos': (250, 90), 'size': (60, 60), 'color': MAGENTA},
    {'label': "9", 'pos': (10, 170), 'size': (60, 60), 'color': PINK},
    {'label': "10", 'pos': (90, 170), 'size': (60, 60), 'color': AQUA},
    {'label': "11", 'pos': (170, 170), 'size': (60, 60), 'color': WHITE},
    {'label': "12", 'pos': (250, 170), 'size': (60, 60), 'color': OFF}
    ]

buttons = []
for spot in spots:
    button = Button(x=spot['pos'][0], y=spot['pos'][1],
                    width=spot['size'][0], height=spot['size'][1],
                    style=Button.SHADOWROUNDRECT,
                    fill_color=spot['color'], outline_color=0x222222,
                    name=spot['label'])
    pyportal.splash.append(button.group)
    buttons.append(button)

mode = 0
mode_change = None

# Calibrate light sensor on start to deal with different lighting situations
# If the mode change isn't responding properly, reset your PyPortal to recalibrate
initial_light_value = light_sensor.value
while True:
    if light_sensor.value < (initial_light_value * 0.3) and mode_change is None:
        mode_change = "mode_change"
    if light_sensor.value > (initial_light_value * 0.5) and mode_change == "mode_change":
        mode += 1
        mode_change = None
        if mode > 2:
            mode = 0
        print(mode)
    touch = pyportal.touchscreen.touch_point
    if touch:
        for button in buttons:
            if button.contains(touch):
                print("Touched", button.name)
                if mode == 0:
                    strip_1.fill(button.fill_color)
                elif mode == 1:
                    strip_2.fill(button.fill_color)
                elif mode == 2:
                    strip_1.fill(button.fill_color)
                    strip_2.fill(button.fill_color)
                break
    time.sleep(0.05)

Setup

First you import the necessary libraries. Note that you're not importing all the libraries you copied to your CIRCUITPY drive - the libraries you do import rely on the ones that you don't.

Next there are a couple of variables you can set. Set BACKGROUND_COLOR to whatever you'd like for the background color using a hex color value. It defaults to a grey. Set BRIGHTNESS to the brightness you'd like for your NeoPixels, using a number 0 - 1 where the number represents a percentage brightness, e.g. 1 is 100%. It defaults to 0.3 or 30%.

Download: file
# Set the background color
BACKGROUND_COLOR = 0x443355

# Set the NeoPixel brightness
BRIGHTNESS = 0.3

Then you setup the light sensor and the NeoPixel strips for use, and turn the NeoPixels off in the event that they were on.

Then you setup the PyPortal for use without networking. This is where the background color is set.

Download: file
light_sensor = analogio.AnalogIn(board.LIGHT)

strip_1 = neopixel.NeoPixel(board.D4, 30, brightness=BRIGHTNESS)
strip_2 = neopixel.NeoPixel(board.D3, 30, brightness=BRIGHTNESS)

# Turn off NeoPixels to start
strip_1.fill(0)
strip_2.fill(0)

# Setup PyPortal without networking
pyportal = PyPortal(default_bg=BACKGROUND_COLOR)

Next is a series of variables that set the button colors using an RGB tuple, e.g. (red, green, blue). In this case, red, green and blue are a whole number between 0 and 255, where 0 is off and 255 is maximum. You can choose any colors you want. If you decide to change the variable names, you'll need to update them in the next section.

Download: file
# Button colors
RED = (255, 0, 0)
ORANGE = (255, 34, 0)
YELLOW = (255, 170, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
VIOLET = (153, 0, 255)
MAGENTA = (255, 0, 51)
PINK = (255, 51, 119)
AQUA = (85, 125, 255)
WHITE = (255, 255, 255)
OFF = (0, 0, 0)

The next section is a list that provides the information needed to create the buttons. Each line contains the following information for each button:

  • The button label. In this case, they are numbered 1-12.
  • The position of the button, using (x, y) coordinates.
  • The size of the button, using (x, y) coordinates. Each button is 60 x 60 pixels.
  • The color to set the button, using the color variables from above.
Download: file
spots = [
    {'label': "1", 'pos': (10, 10), 'size': (60, 60), 'color': RED},
    {'label': "2", 'pos': (90, 10), 'size': (60, 60), 'color': ORANGE},
    {'label': "3", 'pos': (170, 10), 'size': (60, 60), 'color': YELLOW},
    {'label': "4", 'pos': (250, 10), 'size': (60, 60), 'color': GREEN},
    {'label': "5", 'pos': (10, 90), 'size': (60, 60), 'color': CYAN},
    {'label': "6", 'pos': (90, 90), 'size': (60, 60), 'color': BLUE},
    {'label': "7", 'pos': (170, 90), 'size': (60, 60), 'color': VIOLET},
    {'label': "8", 'pos': (250, 90), 'size': (60, 60), 'color': MAGENTA},
    {'label': "9", 'pos': (10, 170), 'size': (60, 60), 'color': PINK},
    {'label': "10", 'pos': (90, 170), 'size': (60, 60), 'color': AQUA},
    {'label': "11", 'pos': (170, 170), 'size': (60, 60), 'color': WHITE},
    {'label': "12", 'pos': (250, 170), 'size': (60, 60), 'color': OFF}
    ]

This information is then used to create the buttons by creating one button at a time and appending that button to the buttons list for later use.

Download: file
buttons = []
for spot in spots:
    button = Button(x=spot['pos'][0], y=spot['pos'][1],
                    width=spot['size'][0], height=spot['size'][1],
                    style=Button.SHADOWROUNDRECT,
                    fill_color=spot['color'], outline_color=0x222222,
                    name=spot['label'])
    pyportal.splash.append(button.group)
    buttons.append(button)

Next, set the mode to 0 and set mode_change = None. This will be used in the toggle code.

Finally, you obtain the initial_light_value from the light sensor. This ambient light value is used to "calibrate" the light sensor to use as a toggle.

Download: file
mode = 0
mode_change = None

initial_light_value = light_sensor.value

This "calibration" is necessary because initial testing found that a hard-coded value for the threshold of the toggle code meant that in different lighting situations, waving a hand over the sensor did not trigger the mode change properly. If it was too bright, then it never dropped below the hard-coded threshold even if the sensor was covered. If it was too dim, then it was constantly below the threshold and did not have the opportunity to trigger. Instead, the code "calibrates" on startup by obtaining an initial value, and then uses a percentage of that initial value as the thresholds. If you find that waving your hand over your PyPortal or covering the sensor is not triggering the mode change, you can recalibrate anytime by restarting your PyPortal.

Main Loop

The first part of the main loop is the toggle code. There are three modes: mode 0 which is controlling strip 1, mode 1 which is controlling strip 2, and mode 2 controlling both strips together. This code toggles through these modes in that order.

The amount of light reaching the sensor decreases as your hand begins to cover it until your hand is centered over it, and then it begins to increase again as your hand moves away from the sensor. The code uses this to create a toggle that only triggers once for each time your hand waves over the sensor.

The current light value is compared to 30% of the initial light value, and when it drops below that threshold, the mode change sequence is started by setting mode_change = "mode_change". As the current light value increases to greater than 50% of the initial light value, the mode is increased by 1, and mode_change is set back to None. If the mode reaches greater than 2, it is set to 0, to begin the cycle again. The mode is printed to the serial console as it changes.

Download: file
if light_sensor.value < (initial_light_value * 0.3) and mode_change is None:
    mode_change = "mode_change"
if light_sensor.value > (initial_light_value * 0.5) and mode_change == "mode_change":
    mode += 1
    mode_change = None
    if mode > 2:
        mode = 0
    print(mode)

The final section sets up touch on the display, and then checks to see if you've touched within the bounds of a button. If a particular button "contains" the point you've touched on the display, it prints Touched and the name of the button (its number, 1-12) to the serial console. It checks to see which mode is currently active, and if it's 0, it sets strip 1 to the button color, if it's 1, it sets strip 2 to the button color, and if it's 3, it sets both strips to the button color.

Download: file
touch = pyportal.touchscreen.touch_point
if touch:
    for button in buttons:
        if button.contains(touch):
            print("Touched", button.name)
            if mode == 0:
                strip_1.fill(button.fill_color)
            elif mode == 1:
                strip_2.fill(button.fill_color)
            elif mode == 2:
                strip_1.fill(button.fill_color)
                strip_2.fill(button.fill_color)
            break
time.sleep(0.05)

That's what goes into creating a NeoPixel color picker with PyPortal and CircuitPython!

Try changing the colors and setting the strips to different combinations to fit the effect you're looking for. Have fun!

This guide was first published on Mar 27, 2019. It was last updated on Mar 27, 2019. This page (CircuitPython Code) was last updated on Jun 16, 2019.