Setup Trinket M0 with CircuitPython

We'll need to get our board setup so we can run the CircuitPython code. Let's walk through these steps to get the latest version of CircuitPython onto your board. 

The Mu Python Editor

Mu is a simple Python editor that works with Adafruit CircuitPython hardware. It's written in Python and works on Windows, MacOS, Linux and Raspberry Pi. The serial console is built right in so you get immediate feedback from your board's serial output! While you can use any text editor with your code, Mu makes it super simple.

Installing or upgrading CircuitPython

You should ensure you have CircuitPython 5.0 or greater on your board. Plug your board in with a known good data + power cable (not the cheesy USB cable that comes with USB power packs, they are power only). You should see a new flash drive pop up.

If the drive is CIRCUITPY, then open the boot_out.txt file to ensure the version number is 5.0 or greater. 

Adafruit CircuitPython 5.0.0-alpha.5 on 2019-11-04; Adafruit Trinket M0 with samd21e18

If the version is less than 5 -or- you only get a drive named TRINKETBOOT then follow the Trinket M0 guide on installing CircuitPython.

Download the Adafruit CircuitPython Library Bundle

In order to run the code, we'll need to download a few libraries. Libraries contain code to help interface with hardware a lot easier for us.

Use the Trinket M0 page on Installing Libraries to get the library that matches the major version of CircuitPython you are using noted above.

To run the code for this project, we need the two libraries in the Required Libraries list below. Unzip the library bundle and search for the libraries. Drag and drop the files into a folder named lib on the CIRCUITPY drive (create the folder if it is not already on the Trinkey M0).

Required Libraries 

  • neopixel.mpy
  • adafruit_dotstar.mpy

Once we have all the files we need, a directory listing will look similar to below as far as files and directories.

Upload Code

This project offers two different code.py sketches. Both programs can be used with either display. 

Click on the download link below to grab the main code directly from GitHub. Rename the file to code.py and drop it onto the CIRCUITPY main (root) directory. The code will run properly when all of the files have been uploaded including libraries.

Use any text editor or favorite IDE to modify the code. We suggest using Mu as noted above.

# pylint: disable=import-error

"""
NeoPixel goggles code for CircuitPython

With a rotary encoder attached (pins are declred in the "Initialize
hardware" section of the code), you can select animation modes and
configurable attributes (color, brightness, etc.). TAP the encoder
button to switch between modes/settings, HOLD the encoder button to
toggle between PLAY and CONFIGURE states.

With no rotary encoder attached, you can select an animation mode
and configure attributes in the "Configurable defaults" section
(including an option to auto-cycle through the animation modes).

Things to Know:
- FancyLED library is NOT used here because it's a bit too much for the
  Trinket M0 to handle (animation was very slow).
- Animation modes are all monochromatic (single color, varying only in
  brightness). More a design decision than a technical one...of course
  NeoPixels can be individual colors, but folks like customizing and the
  monochromatic approach makes it easier to select a color. Also keeps the
  code a bit simpler, since Trinket space & performance is limited.
- Animation is monotonic time driven; there are no sleep() calls. This
  ensures that animation is constant-time regardless of the hardware or
  CircuitPython performance over time, or other goings on (e.g. garbage
  collection), only the frame rate (smoothness) varies; overall timing
  remains consistent.
"""

from math import modf, pi, sin
from random import getrandbits
from time import monotonic
from digitalio import DigitalInOut, Direction
from richbutton import RichButton
from rotaryio import IncrementalEncoder
import adafruit_dotstar
import board
import neopixel

# Configurable defaults

PIXEL_HUE = 0.0         # Red at start
PIXEL_BRIGHTNESS = 0.4  # 40% brightness at start
PIXEL_GAMMA = 2.6       # Controls brightness linearity
RING_1_OFFSET = 10      # Alignment of top pixel on 1st NeoPixel ring
RING_2_OFFSET = 10      # Alignment of top pixel on 2nd NeoPixel ring
RING_2_FLIP = True      # If True, reverse order of pixels on 2nd ring
CYCLE_INTERVAL = 0      # If >0 auto-cycle through play modes @ this interval
SPEED = 1.0             # Initial animation speed for modes that use it
XRAY_BITS = 0x0821      # Initial bit pattern for "X-ray" mode

# Things you probably don't want to change, unless adding new modes

PLAY_MODE_SPIN = 0               # Revolving pulse
PLAY_MODE_XRAY = 1               # Watchmen-inspired "X-ray goggles"
PLAY_MODE_SCAN = 2               # Scanline effect
PLAY_MODE_SPARKLE = 3            # Random dots
PLAY_MODES = 4                   # Number of PLAY modes
PLAY_MODE = PLAY_MODE_SPIN       # Initial PLAY mode

CONFIG_MODE_COLOR = 0            # Setting color (hue)
CONFIG_MODE_BRIGHTNESS = 1       # Setting brightness
CONFIG_MODE_ALIGN = 2            # Top pixel indicator
CONFIG_MODES = 3                 # Number of CONFIG modes
CONFIG_MODE = CONFIG_MODE_COLOR  # Initial CONFIG mode
CONFIGURING = False              # NOT configuring at start
# CONFIG_MODE_ALIGN is only used to test the values of RING_1_OFFSET and
# RING_2_OFFSET. The single lit pixel should appear at the top of each ring.
# If it does not, adjust each of those two values (integer from 0 to 15)
# until the pixel appears at the top (or physically reposition the rings).
# Some of the animation modes rely on the two rings being aligned a certain
# way. Once adjusted, you can reduce the value of CONFIG_MODES and this
# mode will be skipped in config state.

# Initialize hardware - PIN DEFINITIONS APPEAR HERE

# Turn off onboard DotStar LED
DOTSTAR = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1)
DOTSTAR.brightness = 0

# Turn off onboard discrete LED
LED = DigitalInOut(board.D13)
LED.direction = Direction.OUTPUT
LED.value = 0

# Declare NeoPixels on pin D0, 32 pixels long. Set to max brightness because
# on-the-fly brightness slows down NeoPixel lib, so we'll do our own here.
PIXELS = neopixel.NeoPixel(board.D0, 32, brightness=1.0, auto_write=False)

# Declare rotary encoder on pins D4 and D3, and click button on pin D2.
# If encoder behaves backwards from what you want, swap pins here.
ENCODER = IncrementalEncoder(board.D4, board.D3)
ENCODER_BUTTON = RichButton(board.D2)


def set_pixel(pixel_num, brightness):
    """Set one pixel in both 16-pixel rings. Pass in pixel index (0 to 15)
       and relative brightness (0.0 to 1.0). Actual resulting brightness
       will be a function of global brightness and gamma correction."""
    # Clamp passed brightness to 0.0-1.0 range,
    # apply global brightness and gamma correction
    brightness = max(min(brightness, 1.0), 0.0) * PIXEL_BRIGHTNESS
    brightness = pow(brightness, PIXEL_GAMMA) * 255.0
    # local_color is adjusted brightness applied to global PIXEL_COLOR
    local_color = (
        int(PIXEL_COLOR[0] * brightness + 0.5),
        int(PIXEL_COLOR[1] * brightness + 0.5),
        int(PIXEL_COLOR[2] * brightness + 0.5))
    # Roll over pixel_num as needed to 0-15 range, then store color
    pixel_num_wrapped = (pixel_num + RING_1_OFFSET) & 15
    PIXELS[pixel_num_wrapped] = local_color
    # Determine corresponding pixel for second ring. Mirror direction if
    # configured for such, correct for any rotational difference, then
    # perform similar roll-over as above before storing color.
    if RING_2_FLIP:
        pixel_num = 15 - pixel_num
    pixel_num_wrapped = 16 + ((pixel_num + RING_2_OFFSET) & 15)
    PIXELS[pixel_num_wrapped] = local_color


def triangle_wave(pos, peak=0.5):
    """Return a brightness level (0.0 to 1.0) corresponding to a position
       (0.0 to 1.0) within a triangle wave (spanning 0.0 to 1.0) with wave's
       peak brightness at a given position (0.0 to 1.0) within its span.
       Positions outside the wave's span return 0.0."""
    if 0.0 <= pos < 1.0:
        if pos <= peak:
            return pos / peak
        return (1.0 - pos) / (1.0 - peak)
    return 0.0


def hue_to_rgb(hue):
    """Given a hue value as a float, where the fractional portion
       (0.0 to 1.0) indicates the actual hue (starting from red at 0,
       to green at 1/3, to blue at 2/3, and back to red at 1.0),
       return an RGB color as a 3-tuple with values from 0.0 to 1.0."""
    hue = modf(hue)[0]
    sixth = (hue * 6.0) % 6.0
    ramp = modf(sixth)[0]
    if sixth < 1.0:
        return (1.0, ramp, 0.0)
    if sixth < 2.0:
        return (1.0 - ramp, 1.0, 0.0)
    if sixth < 3.0:
        return (0.0, 1.0, ramp)
    if sixth < 4.0:
        return (0.0, 1.0 - ramp, 1.0)
    if sixth < 5.0:
        return (ramp, 0.0, 1.0)
    return (1.0, 0.0, 1.0 - ramp)


def random_bits():
    """Generate random bit pattern, avoiding adjacent set bits (w/wrap)"""
    pattern = getrandbits(16)
    pattern |= (pattern & 1) << 16   # Replicate bit 0 at bit 16
    return pattern & ~(pattern >> 1) # Mask out adjacent set bits


# Some last-minute state initialization

POS = 0                              # Initial swirl animation position
PIXEL_COLOR = hue_to_rgb(PIXEL_HUE)  # Initial color
ENCODER_PRIOR = ENCODER.position     # Initial encoder position
TIME_PRIOR = monotonic()             # Initial time
LAST_CYCLE_TIME = TIME_PRIOR         # For mode auto-cycling
SPARKLE_BITS_PREV = 0                # First bits for sparkle animation
SPARKLE_BITS_NEXT = 0                # Next bits for sparkle animation
PREV_WEIGHT = 2                      # Force initial sparkle refresh


# Main loop

while True:
    ACTION = ENCODER_BUTTON.action()
    if ACTION is RichButton.TAP:
        # Encoder button tapped, cycle through play or config modes:
        if CONFIGURING:
            CONFIG_MODE = (CONFIG_MODE + 1) % CONFIG_MODES
        else:
            PLAY_MODE = (PLAY_MODE + 1) % PLAY_MODES
    elif ACTION is RichButton.DOUBLE_TAP:
        # DOUBLE_TAP not currently used, but this is where it would go.
        pass
    elif ACTION is RichButton.HOLD:
        # Encoder button held, toggle between PLAY and CONFIG modes:
        CONFIGURING = not CONFIGURING
    elif ACTION is RichButton.RELEASE:
        # RELEASE not currently used (play/config state changes when HOLD
        # is detected), but this is where it would go.
        pass

    # Process encoder input. Code always uses the ENCODER_CHANGE value
    # for relative adjustments.
    ENCODER_POSITION = ENCODER.position
    ENCODER_CHANGE = ENCODER_POSITION - ENCODER_PRIOR
    ENCODER_PRIOR = ENCODER_POSITION

    # Same idea, but for elapsed time (so time-based animation continues
    # at the next position, it doesn't jump around as when multiplying
    # monotonic() by SPEED.
    TIME_NOW = monotonic()
    TIME_CHANGE = TIME_NOW - TIME_PRIOR
    TIME_PRIOR = TIME_NOW

    if CONFIGURING:
        # In config mode, different pixel patterns indicate which
        # adjustment is being made (e.g. alternating pixels = hue mode).
        if CONFIG_MODE is CONFIG_MODE_COLOR:
            PIXEL_HUE = modf(PIXEL_HUE + ENCODER_CHANGE * 0.01)[0]
            PIXEL_COLOR = hue_to_rgb(PIXEL_HUE)
            for i in range(0, 16):
                set_pixel(i, i & 1)  # Turn on alternating pixels
        elif CONFIG_MODE is CONFIG_MODE_BRIGHTNESS:
            PIXEL_BRIGHTNESS += ENCODER_CHANGE * 0.025
            PIXEL_BRIGHTNESS = max(min(PIXEL_BRIGHTNESS, 1.0), 0.0)
            for i in range(0, 16):
                set_pixel(i, (i & 2) >> 1)  # Turn on pixel pairs
        elif CONFIG_MODE is CONFIG_MODE_ALIGN:
            C = 1      # First pixel on
            for i in range(0, 16):
                set_pixel(i, C)
                C = 0  # All other pixels off
    else:
        # In play mode. Auto-cycle animations if CYCLE_INTERVAL is set.
        if CYCLE_INTERVAL > 0:
            if TIME_NOW - LAST_CYCLE_TIME > CYCLE_INTERVAL:
                PLAY_MODE = (PLAY_MODE + 1) % PLAY_MODES
                LAST_CYCLE_TIME = TIME_NOW

        if PLAY_MODE is PLAY_MODE_XRAY:
            # In XRAY mode, encoder selects random bit patterns
            if abs(ENCODER_CHANGE) > 1:
                XRAY_BITS = random_bits()
            # Unset bits pulsate ever-so-slightly
            DIM = 0.42 + sin(monotonic() * 2) * 0.08
            for i in range(16):
                if XRAY_BITS & (1 << i):
                    set_pixel(i, 1.0)
                else:
                    set_pixel(i, DIM)
        else:
            # In all other modes, encoder adjusts speed/direction
            SPEED += ENCODER_CHANGE * 0.05
            SPEED = max(min(SPEED, 4.0), -4.0)
            POS += TIME_CHANGE * SPEED
            if PLAY_MODE is PLAY_MODE_SPIN:
                for i in range(16):
                    frac = modf(POS + i / 15.0)[0]  # 0.0-1.0 around ring
                    if frac < 0:
                        frac = 1.0 + frac
                    set_pixel(i, triangle_wave(frac, 0.5 - SPEED * 0.125))
            elif PLAY_MODE is PLAY_MODE_SCAN:
                if POS >= 0:
                    S = 2.0 - modf(POS)[0] * 4.0
                else:
                    S = 2.0 - (1.0 + modf(POS)[0]) * 4.0
                for i in range(16):
                    Y = sin((i / 7.5 + 0.5) * pi)  # Pixel Y coord
                    D = 0.5 - abs(Y - S) * 0.6     # Distance to scanline
                    set_pixel(i, triangle_wave(D))
            elif PLAY_MODE is PLAY_MODE_SPARKLE:
                NEXT_WEIGHT = modf(abs(POS * 2.0))[0]
                if SPEED < 0:
                    NEXT_WEIGHT = 1.0 - NEXT_WEIGHT
                if NEXT_WEIGHT < PREV_WEIGHT:
                    SPARKLE_BITS_PREV = SPARKLE_BITS_NEXT
                    while True:
                        SPARKLE_BITS_NEXT = random_bits()
                        if not SPARKLE_BITS_NEXT & SPARKLE_BITS_PREV:
                            break  # No bits in common, good!
                PREV_WEIGHT = 1.0 - NEXT_WEIGHT
                for i in range(16):
                    bit = 1 << i
                    if SPARKLE_BITS_PREV & bit:
                        result = PREV_WEIGHT
                    elif SPARKLE_BITS_NEXT & bit:
                        result = NEXT_WEIGHT
                    else:
                        result = 0
                    set_pixel(i, result)
                PREV_WEIGHT = NEXT_WEIGHT
    PIXELS.show()

Double Check

See the directory listing above and double check that you have all the files listed to make this project function. If any are missing or in an incorrect directory, move them so they're in the right places.

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

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

Text editor powered by tinymce.