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 8 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 8.
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).
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.
# SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries # # SPDX-License-Identifier: MIT # 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()
Page last edited January 21, 2025
Text editor powered by tinymce.