CircuitPython Code

The HandRaiser uses CircuitPython to listen for commands on its USB Serial connection and respond by changing its LED's color accordingly.

Let's walk through the code showing how this works:

Download: file
import board
import adafruit_dotstar
from time import sleep

import supervisor

First we need to import some modules that we'll use.  Most of these are common: board includes definitions that are specific to the Trinket M0 like pin names, adafruit_dotstar lets use control the single color LED and the sleep() function lets us add delays to our loop.

A less common addition is the supervisor module.  It provides information and control of the Python environment itself.  In this case, we want to ask CircuitPython if there is any text available to read from the USB Serial connection (without blocking).  Thankfully, there's an attribute (runtime.serial_bytes_available) on the supervisor module runtime object that lets us check that.

Download: file
def hex2rgb(hex_code):
    red = int("0x"+hex_code[0:2], 16)
    green = int("0x"+hex_code[2:4], 16)
    blue = int("0x"+hex_code[4:6], 16)
    rgb = (red, green, blue)
    return rgb

This helper function, hex2rgb() is handy to convert Web-style hex color codes to RGB tuples.  This lets the controlling computer send commands like "#550000" for dark red or "#FFFF00" for bright yellow.

Download: file
beatArray = [0.090909091,0.097902098,0.104895105,0.118881119,0.132867133,0.146853147,
    0.153846154........

The beatArray variable holds an array of sampled values that represent a heartbeat.  It supports the "@beat" mode.

Download: file
# When we start up, make the LED black
black = (0, 0, 0)
# the color that's passed in over the text input
targetColor = black

#pos is used for all modes that cycle or progress
#it loops from 0-255 and starts over
pos = 0

#curColor is the color that will be displayed at the end of the main loop
#it is mapped using pos according to the mode
curColor = black

Here we set the initial values for the current and target color, position of the color wheel and make a constant color black that we'll use for turning off the LED.

Download: file
# the mode can be one of 
# solid - just keep the current color
# blink - alternate between black and curColor
# ramp  - transition continuously between black and curColor
# beat - pulse to a recorded heartbeat intensity
# wheel - change hue around the colorwheel (curColor is ignored)

mode='wheel'

The mode variable keeps track of which type of animation we're running.  We'll use it to change the LED color on each pass through the main loop.

Download: file
# standard function to rotate around the colorwheel
def wheel(cpos):
    # Input a value 0 to 255 to get a color value.
    # The colours are a transition r - g - b - back to r.
    if cpos < 85:
        return (int(cpos * 3), int(255 - (cpos * 3)), 0)
    elif cpos < 170:
        cpos -= 85
        return (int(255 - (cpos * 3)), 0, int(cpos * 3))
    else:
        cpos -= 170
        return (0, int(cpos * 3), int(255 - cpos * 3))

# We start by turning off pixels
pixels.fill(black)
pixels.show()

This code is common to many "blinky" projects from Adafruit - it cycles through the color wheel making a lovely rainbow pattern. 

Download: file
# Main Loop
while True:
    # Check to see if there's input available (requires CP 4.0 Alpha)
    if supervisor.runtime.serial_bytes_available:
        # read in text (@mode, #RRGGBB, %brightness, standard color)
        # input() will block until a newline is sent
        inText = input().strip()
        # Sometimes Windows sends an extra (or missing) newline - ignore them
        if inText == "":
            continue

Once the setup is complete, we have our main loop - this will continue forever, checking for new messages from the controller and changing the LED.

First, it checks with the CircuitPython Supervisor to see if there is any text to read from the USB port.  If so, it calls input() to read the message (and then strips off any whitespace like newlines or spaces).

Note that the input() function will block until it gets a carriage return (\r) and something reads the Serial port from the other side. That's why the serial_bytes_available attribute is important!
Download: file
# Process the input text - start with the presets (no #,@,etc)
        # We use startswith to not have to worry about CR vs CR+LF differences
        if inText.lower().startswith("red"):
            # set the target color to red
            targetColor = (255, 0, 0)
            # and set the mode to solid if we're in a mode that ignores targetColor
            if mode == "wheel":
                mode="solid"
# Repeat for green, yellow and black...

Now that we're processing text, look for standard colors: red, green, yellow, and black.  We set an appropriate color and make the mode "solid" if it was set to "wheel".

Download: file
# Here we're going to change the mode - which starts w/@
        elif inText.lower().startswith("@"):
            mode= inText[1:]

If the incoming message starts with an "@" symbol, change the mode to the incoming text.  Valid values of mode are "solid", "blink", "ramp", "beat", and "wheel".

Download: file
# Here we can set the brightness with a "%" symbol
        elif inText.startswith("%"):
            pctText = inText[1:]
            pct = float(pctText)/100.0
            pixels.brightness=pct

If the controller sends a message starting with "%" take the number after it as a brightness level (0-100).

Download: file
# If we get a hex code set it and go to solid
        elif inText.startswith("#"):
            hexcode = inText[1:]
            targetColor = hex2rgb(hexcode)
            if (mode == "wheel"):
                mode="solid"

Here we use the hex2rgb() function to convert any incoming #RRGGBB values into a valid color tuple before setting the color.

Download: file
# if we get a command we don't understand, set it to gray
        #we should probably just ignore it but this helps debug
        else:
            targetColor =(50, 50, 50)
            if (mode == "wheel"):
                mode="solid"

If we get a command but don't understand it, just set the color to medium gray so we have an indicator something's gone wrong.

Download: file
else:
        #If no text availble, update the color according to the mode
        if mode == 'blink':
            if curColor == black:
                curColor = targetColor
            else:
                curColor = black
            sleep(.4)
        #        print('.', end='')
            pixels.fill(curColor)
            pixels.show()   
        elif mode == 'wheel':
            sleep(.05)
            pos = (pos + 1) % 255
            pixels.fill(wheel(pos))
            pixels.show()
        elif mode == 'solid':
            pixels.fill(targetColor)
            pixels.show()
        elif mode == 'beat':
            pos = (pos + 5 ) % 106
            scaleAvg = (beatArray[(pos-2)%106] + beatArray[(pos-1)%106] + beatArray[pos] + beatArray[(pos+1)%106] + beatArray[(pos+2)%106])/5
            beatColor = tuple(int(scaleAvg*x) for x in targetColor)
            pixels.fill(beatColor)
            sleep(.025)
            pixels.show()
        elif mode == 'ramp':
            pos = ((pos + 5 ) % 255)
            scaleFactor = (2*abs(pos-127))/255
            beatColor = tuple(int(scaleFactor * x) for x in targetColor)
            pixels.fill(beatColor)
            sleep(.075)
            pixels.show()

At this point, we know that we do not have an incoming message. So, we just want to update the color of the LED based on the current mode, sleep() if appropriate and continue the loop to check for the message again.

Each of the If/Elif sections covers a different mode and simply calculates the next color, sets it and sleeps. There are lots of ways to modify these color modes or to add your own!

Complete Code

# ATMakers HandUp
# Listens to the USB Serial port and responds to incoming strings
# Sets appropriate colors on the DotStar LED

# This program uses the board package to access the Trinket's pin names
# and uses adafruit_dotstar to talk to the LED
# other boards would use the neopixel library instead

from time import sleep
import board
import adafruit_dotstar
import supervisor

# create an object for the dotstar pixel on the Trinket M0
# It's an array because it's a sequence of one pixel
pixels = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=.95)

# this function takes a standard "hex code" for a color and returns
# a tuple of (red, green, blue)
def hex2rgb(hex_code):
    red = int("0x"+hex_code[0:2], 16)
    green = int("0x"+hex_code[2:4], 16)
    blue = int("0x"+hex_code[4:6], 16)
    rgb = (red, green, blue)
    # print(rgb)
    return rgb

# This array contains digitized data for a heartbeat wave scaled to between 0 and 1.0
# It is used to create the "beat" mode. Ensure each line is <78 characters for Travis-CI
beatArray = [0.090909091,0.097902098,0.104895105,0.118881119,0.132867133,0.146853147,
             0.153846154,0.160839161,0.181818182,0.181818182,0.195804196,0.181818182,0.188811189,
             0.188811189,0.181818182,0.174825175,0.174825175,0.160839161,0.167832168,0.160839161,
             0.167832168,0.167832168,0.167832168,0.160839161,0.146853147,0.146853147,0.153846154,
             0.160839161,0.146853147,0.153846154,0.13986014,0.153846154,0.132867133,0.146853147,
             0.13986014,0.13986014,0.146853147,0.146853147,0.146853147,0.146853147,0.160839161,
             0.146853147,0.160839161,0.167832168,0.181818182,0.202797203,0.216783217,0.20979021,
             0.202797203,0.195804196,0.195804196,0.216783217,0.160839161,0.13986014,0.13986014,
             0.13986014,0.118881119,0.118881119,0.111888112,0.132867133,0.111888112,0.132867133,
             0.104895105,0.083916084,0.020979021,0,0.230769231,0.636363636,1,0.846153846,
             0.27972028,0.048951049,0.055944056,0.083916084,0.090909091,0.083916084,0.083916084,
             0.076923077,0.076923077,0.076923077,0.090909091,0.06993007,0.083916084,0.076923077,
             0.076923077,0.06993007,0.076923077,0.083916084,0.083916084,0.083916084,0.076923077,
             0.090909091,0.076923077,0.083916084,0.06993007,0.076923077,0.062937063,0.06993007,
             0.062937063,0.055944056,0.055944056,0.048951049,0.041958042,0.034965035,0.041958042,
             0.027972028]

# When we start up, make the LED black
black = (0, 0, 0)
# the color that's passed in over the text input
targetColor = black

# pos is used for all modes that cycle or progress
# it loops from 0-255 and starts over
pos = 0

# curColor is the color that will be displayed at the end of the main loop
# it is mapped using pos according to the mode
curColor = black

# the mode can be one of
# solid - just keep the current color
# blink - alternate between black and curColor
# ramp  - transition continuously between black and curColor
# beat - pulse to a recorded heartbeat intensity
# wheel - change hue around the colorwheel (curColor is ignored)

mode='wheel'

# standard function to rotate around the colorwheel
def wheel(cpos):
    # Input a value 0 to 255 to get a color value.
    # The colours are a transition r - g - b - back to r.
    if cpos < 85:
        return (int(cpos * 3), int(255 - (cpos * 3)), 0)
    elif cpos < 170:
        cpos -= 85
        return (int(255 - (cpos * 3)), 0, int(cpos * 3))
    else:
        cpos -= 170
        return (0, int(cpos * 3), int(255 - cpos * 3))

# We start by turning off pixels
pixels.fill(black)
pixels.show()

# Main Loop
while True:
    # Check to see if there's input available (requires CP 4.0 Alpha)
    if supervisor.runtime.serial_bytes_available:
        # read in text (@mode, #RRGGBB, %brightness, standard color)
        # input() will block until a newline is sent
        inText = input().strip()
        # Sometimes Windows sends an extra (or missing) newline - ignore them
        if inText == "":
            continue
        # Process the input text - start with the presets (no #,@,etc)
        # We use startswith to not have to worry about CR vs CR+LF differences
        if inText.lower().startswith("red"):
            # set the target color to red
            targetColor = (255, 0, 0)
            # and set the mode to solid if we're in a mode that ignores targetColor
            if mode == "wheel":
                mode="solid"
        # similar for green, yellow, and black
        elif inText.lower().startswith("green"):
            targetColor = (0, 255, 0)
            if mode == "wheel":
                mode="solid"
        elif inText.lower().startswith("yellow"):
            targetColor = (200, 200, 0)
            if mode == "wheel":
                mode="solid"
        elif inText.lower().startswith("black"):
            targetColor = (0, 0, 0)
            if mode == "wheel":
                mode="solid"
        # Here we're going to change the mode - which starts w/@
        elif inText.lower().startswith("@"):
            mode= inText[1:]
        # Here we can set the brightness with a "%" symbol
        elif inText.startswith("%"):
            pctText = inText[1:]
            pct = float(pctText)/100.0
            pixels.brightness=pct
        # If we get a hex code set it and go to solid
        elif inText.startswith("#"):
            hexcode = inText[1:]
            targetColor = hex2rgb(hexcode)
            if mode == "wheel":
                mode="solid"
        # if we get a command we don't understand, set it to gray
        # we should probably just ignore it but this helps debug
        else:
            targetColor =(50, 50, 50)
            if mode == "wheel":
                mode="solid"
    else:
        # If no text available, update the color according to the mode
        if mode == 'blink':
            if curColor == black:
                curColor = targetColor
            else:
                curColor = black
            sleep(.4)
            # print('.', end='')
            pixels.fill(curColor)
            pixels.show()
        elif mode == 'wheel':
            sleep(.05)
            pos = (pos + 1) % 255
            pixels.fill(wheel(pos))
            pixels.show()
        elif mode == 'solid':
            pixels.fill(targetColor)
            pixels.show()
        elif mode == 'beat':
            pos = (pos + 5 ) % 106
            scaleAvg = (beatArray[(pos-2)%106] + beatArray[(pos-1)%106] + beatArray[pos] +
                        beatArray[(pos+1)%106] + beatArray[(pos+2)%106])/5
            beatColor = tuple(int(scaleAvg*x) for x in targetColor)
            pixels.fill(beatColor)
            sleep(.025)
            pixels.show()
        elif mode == 'ramp':
            pos = ((pos + 5 ) % 255)
            scaleFactor = (2*abs(pos-127))/255
            beatColor = tuple(int(scaleFactor * x) for x in targetColor)
            pixels.fill(beatColor)
            sleep(.075)
            pixels.show()
This guide was first published on Jun 25, 2019. It was last updated on Jun 25, 2019. This page (CircuitPython Code) was last updated on Dec 05, 2019.