/ CircuitPython_Goggles / code.py
code.py
1 # SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 # pylint: disable=import-error 6 7 8 # NeoPixel goggles code for CircuitPython 9 # 10 # With a rotary encoder attached (pins are declred in the "Initialize 11 # hardware" section of the code), you can select animation modes and 12 # configurable attributes (color, brightness, etc.). TAP the encoder 13 # button to switch between modes/settings, HOLD the encoder button to 14 # toggle between PLAY and CONFIGURE states. 15 # 16 # With no rotary encoder attached, you can select an animation mode 17 # and configure attributes in the "Configurable defaults" section 18 # (including an option to auto-cycle through the animation modes). 19 # 20 # Things to Know: 21 # - FancyLED library is NOT used here because it's a bit too much for the 22 # Trinket M0 to handle (animation was very slow). 23 # - Animation modes are all monochromatic (single color, varying only in 24 # brightness). More a design decision than a technical one...of course 25 # NeoPixels can be individual colors, but folks like customizing and the 26 # monochromatic approach makes it easier to select a color. Also keeps the 27 # code a bit simpler, since Trinket space & performance is limited. 28 # - Animation is monotonic time driven; there are no sleep() calls. This 29 # ensures that animation is constant-time regardless of the hardware or 30 # CircuitPython performance over time, or other goings on (e.g. garbage 31 # collection), only the frame rate (smoothness) varies; overall timing 32 # remains consistent. 33 34 from math import modf, pi, sin 35 from random import getrandbits 36 from time import monotonic 37 from digitalio import DigitalInOut, Direction 38 from richbutton import RichButton 39 from rotaryio import IncrementalEncoder 40 import adafruit_dotstar 41 import board 42 import neopixel 43 44 # Configurable defaults 45 46 PIXEL_HUE = 0.0 # Red at start 47 PIXEL_BRIGHTNESS = 0.4 # 40% brightness at start 48 PIXEL_GAMMA = 2.6 # Controls brightness linearity 49 RING_1_OFFSET = 10 # Alignment of top pixel on 1st NeoPixel ring 50 RING_2_OFFSET = 10 # Alignment of top pixel on 2nd NeoPixel ring 51 RING_2_FLIP = True # If True, reverse order of pixels on 2nd ring 52 CYCLE_INTERVAL = 0 # If >0 auto-cycle through play modes @ this interval 53 SPEED = 1.0 # Initial animation speed for modes that use it 54 XRAY_BITS = 0x0821 # Initial bit pattern for "X-ray" mode 55 56 # Things you probably don't want to change, unless adding new modes 57 58 PLAY_MODE_SPIN = 0 # Revolving pulse 59 PLAY_MODE_XRAY = 1 # Watchmen-inspired "X-ray goggles" 60 PLAY_MODE_SCAN = 2 # Scanline effect 61 PLAY_MODE_SPARKLE = 3 # Random dots 62 PLAY_MODES = 4 # Number of PLAY modes 63 PLAY_MODE = PLAY_MODE_SPIN # Initial PLAY mode 64 65 CONFIG_MODE_COLOR = 0 # Setting color (hue) 66 CONFIG_MODE_BRIGHTNESS = 1 # Setting brightness 67 CONFIG_MODE_ALIGN = 2 # Top pixel indicator 68 CONFIG_MODES = 3 # Number of CONFIG modes 69 CONFIG_MODE = CONFIG_MODE_COLOR # Initial CONFIG mode 70 CONFIGURING = False # NOT configuring at start 71 # CONFIG_MODE_ALIGN is only used to test the values of RING_1_OFFSET and 72 # RING_2_OFFSET. The single lit pixel should appear at the top of each ring. 73 # If it does not, adjust each of those two values (integer from 0 to 15) 74 # until the pixel appears at the top (or physically reposition the rings). 75 # Some of the animation modes rely on the two rings being aligned a certain 76 # way. Once adjusted, you can reduce the value of CONFIG_MODES and this 77 # mode will be skipped in config state. 78 79 # Initialize hardware - PIN DEFINITIONS APPEAR HERE 80 81 # Turn off onboard DotStar LED 82 DOTSTAR = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1) 83 DOTSTAR.brightness = 0 84 85 # Turn off onboard discrete LED 86 LED = DigitalInOut(board.D13) 87 LED.direction = Direction.OUTPUT 88 LED.value = 0 89 90 # Declare NeoPixels on pin D0, 32 pixels long. Set to max brightness because 91 # on-the-fly brightness slows down NeoPixel lib, so we'll do our own here. 92 PIXELS = neopixel.NeoPixel(board.D0, 32, brightness=1.0, auto_write=False) 93 94 # Declare rotary encoder on pins D4 and D3, and click button on pin D2. 95 # If encoder behaves backwards from what you want, swap pins here. 96 ENCODER = IncrementalEncoder(board.D4, board.D3) 97 ENCODER_BUTTON = RichButton(board.D2) 98 99 100 def set_pixel(pixel_num, brightness): 101 """Set one pixel in both 16-pixel rings. Pass in pixel index (0 to 15) 102 and relative brightness (0.0 to 1.0). Actual resulting brightness 103 will be a function of global brightness and gamma correction.""" 104 # Clamp passed brightness to 0.0-1.0 range, 105 # apply global brightness and gamma correction 106 brightness = max(min(brightness, 1.0), 0.0) * PIXEL_BRIGHTNESS 107 brightness = pow(brightness, PIXEL_GAMMA) * 255.0 108 # local_color is adjusted brightness applied to global PIXEL_COLOR 109 local_color = ( 110 int(PIXEL_COLOR[0] * brightness + 0.5), 111 int(PIXEL_COLOR[1] * brightness + 0.5), 112 int(PIXEL_COLOR[2] * brightness + 0.5)) 113 # Roll over pixel_num as needed to 0-15 range, then store color 114 pixel_num_wrapped = (pixel_num + RING_1_OFFSET) & 15 115 PIXELS[pixel_num_wrapped] = local_color 116 # Determine corresponding pixel for second ring. Mirror direction if 117 # configured for such, correct for any rotational difference, then 118 # perform similar roll-over as above before storing color. 119 if RING_2_FLIP: 120 pixel_num = 15 - pixel_num 121 pixel_num_wrapped = 16 + ((pixel_num + RING_2_OFFSET) & 15) 122 PIXELS[pixel_num_wrapped] = local_color 123 124 125 def triangle_wave(pos, peak=0.5): 126 """Return a brightness level (0.0 to 1.0) corresponding to a position 127 (0.0 to 1.0) within a triangle wave (spanning 0.0 to 1.0) with wave's 128 peak brightness at a given position (0.0 to 1.0) within its span. 129 Positions outside the wave's span return 0.0.""" 130 if 0.0 <= pos < 1.0: 131 if pos <= peak: 132 return pos / peak 133 return (1.0 - pos) / (1.0 - peak) 134 return 0.0 135 136 137 def hue_to_rgb(hue): 138 """Given a hue value as a float, where the fractional portion 139 (0.0 to 1.0) indicates the actual hue (starting from red at 0, 140 to green at 1/3, to blue at 2/3, and back to red at 1.0), 141 return an RGB color as a 3-tuple with values from 0.0 to 1.0.""" 142 hue = modf(hue)[0] 143 sixth = (hue * 6.0) % 6.0 144 ramp = modf(sixth)[0] 145 if sixth < 1.0: 146 return (1.0, ramp, 0.0) 147 if sixth < 2.0: 148 return (1.0 - ramp, 1.0, 0.0) 149 if sixth < 3.0: 150 return (0.0, 1.0, ramp) 151 if sixth < 4.0: 152 return (0.0, 1.0 - ramp, 1.0) 153 if sixth < 5.0: 154 return (ramp, 0.0, 1.0) 155 return (1.0, 0.0, 1.0 - ramp) 156 157 158 def random_bits(): 159 """Generate random bit pattern, avoiding adjacent set bits (w/wrap)""" 160 pattern = getrandbits(16) 161 pattern |= (pattern & 1) << 16 # Replicate bit 0 at bit 16 162 return pattern & ~(pattern >> 1) # Mask out adjacent set bits 163 164 165 # Some last-minute state initialization 166 167 POS = 0 # Initial swirl animation position 168 PIXEL_COLOR = hue_to_rgb(PIXEL_HUE) # Initial color 169 ENCODER_PRIOR = ENCODER.position # Initial encoder position 170 TIME_PRIOR = monotonic() # Initial time 171 LAST_CYCLE_TIME = TIME_PRIOR # For mode auto-cycling 172 SPARKLE_BITS_PREV = 0 # First bits for sparkle animation 173 SPARKLE_BITS_NEXT = 0 # Next bits for sparkle animation 174 PREV_WEIGHT = 2 # Force initial sparkle refresh 175 176 177 # Main loop 178 179 while True: 180 ACTION = ENCODER_BUTTON.action() 181 if ACTION is RichButton.TAP: 182 # Encoder button tapped, cycle through play or config modes: 183 if CONFIGURING: 184 CONFIG_MODE = (CONFIG_MODE + 1) % CONFIG_MODES 185 else: 186 PLAY_MODE = (PLAY_MODE + 1) % PLAY_MODES 187 elif ACTION is RichButton.DOUBLE_TAP: 188 # DOUBLE_TAP not currently used, but this is where it would go. 189 pass 190 elif ACTION is RichButton.HOLD: 191 # Encoder button held, toggle between PLAY and CONFIG modes: 192 CONFIGURING = not CONFIGURING 193 elif ACTION is RichButton.RELEASE: 194 # RELEASE not currently used (play/config state changes when HOLD 195 # is detected), but this is where it would go. 196 pass 197 198 # Process encoder input. Code always uses the ENCODER_CHANGE value 199 # for relative adjustments. 200 ENCODER_POSITION = ENCODER.position 201 ENCODER_CHANGE = ENCODER_POSITION - ENCODER_PRIOR 202 ENCODER_PRIOR = ENCODER_POSITION 203 204 # Same idea, but for elapsed time (so time-based animation continues 205 # at the next position, it doesn't jump around as when multiplying 206 # monotonic() by SPEED. 207 TIME_NOW = monotonic() 208 TIME_CHANGE = TIME_NOW - TIME_PRIOR 209 TIME_PRIOR = TIME_NOW 210 211 if CONFIGURING: 212 # In config mode, different pixel patterns indicate which 213 # adjustment is being made (e.g. alternating pixels = hue mode). 214 if CONFIG_MODE is CONFIG_MODE_COLOR: 215 PIXEL_HUE = modf(PIXEL_HUE + ENCODER_CHANGE * 0.01)[0] 216 PIXEL_COLOR = hue_to_rgb(PIXEL_HUE) 217 for i in range(0, 16): 218 set_pixel(i, i & 1) # Turn on alternating pixels 219 elif CONFIG_MODE is CONFIG_MODE_BRIGHTNESS: 220 PIXEL_BRIGHTNESS += ENCODER_CHANGE * 0.025 221 PIXEL_BRIGHTNESS = max(min(PIXEL_BRIGHTNESS, 1.0), 0.0) 222 for i in range(0, 16): 223 set_pixel(i, (i & 2) >> 1) # Turn on pixel pairs 224 elif CONFIG_MODE is CONFIG_MODE_ALIGN: 225 C = 1 # First pixel on 226 for i in range(0, 16): 227 set_pixel(i, C) 228 C = 0 # All other pixels off 229 else: 230 # In play mode. Auto-cycle animations if CYCLE_INTERVAL is set. 231 if CYCLE_INTERVAL > 0: 232 if TIME_NOW - LAST_CYCLE_TIME > CYCLE_INTERVAL: 233 PLAY_MODE = (PLAY_MODE + 1) % PLAY_MODES 234 LAST_CYCLE_TIME = TIME_NOW 235 236 if PLAY_MODE is PLAY_MODE_XRAY: 237 # In XRAY mode, encoder selects random bit patterns 238 if abs(ENCODER_CHANGE) > 1: 239 XRAY_BITS = random_bits() 240 # Unset bits pulsate ever-so-slightly 241 DIM = 0.42 + sin(monotonic() * 2) * 0.08 242 for i in range(16): 243 if XRAY_BITS & (1 << i): 244 set_pixel(i, 1.0) 245 else: 246 set_pixel(i, DIM) 247 else: 248 # In all other modes, encoder adjusts speed/direction 249 SPEED += ENCODER_CHANGE * 0.05 250 SPEED = max(min(SPEED, 4.0), -4.0) 251 POS += TIME_CHANGE * SPEED 252 if PLAY_MODE is PLAY_MODE_SPIN: 253 for i in range(16): 254 frac = modf(POS + i / 15.0)[0] # 0.0-1.0 around ring 255 if frac < 0: 256 frac = 1.0 + frac 257 set_pixel(i, triangle_wave(frac, 0.5 - SPEED * 0.125)) 258 elif PLAY_MODE is PLAY_MODE_SCAN: 259 if POS >= 0: 260 S = 2.0 - modf(POS)[0] * 4.0 261 else: 262 S = 2.0 - (1.0 + modf(POS)[0]) * 4.0 263 for i in range(16): 264 Y = sin((i / 7.5 + 0.5) * pi) # Pixel Y coord 265 D = 0.5 - abs(Y - S) * 0.6 # Distance to scanline 266 set_pixel(i, triangle_wave(D)) 267 elif PLAY_MODE is PLAY_MODE_SPARKLE: 268 NEXT_WEIGHT = modf(abs(POS * 2.0))[0] 269 if SPEED < 0: 270 NEXT_WEIGHT = 1.0 - NEXT_WEIGHT 271 if NEXT_WEIGHT < PREV_WEIGHT: 272 SPARKLE_BITS_PREV = SPARKLE_BITS_NEXT 273 while True: 274 SPARKLE_BITS_NEXT = random_bits() 275 if not SPARKLE_BITS_NEXT & SPARKLE_BITS_PREV: 276 break # No bits in common, good! 277 PREV_WEIGHT = 1.0 - NEXT_WEIGHT 278 for i in range(16): 279 bit = 1 << i 280 if SPARKLE_BITS_PREV & bit: 281 result = PREV_WEIGHT 282 elif SPARKLE_BITS_NEXT & bit: 283 result = NEXT_WEIGHT 284 else: 285 result = 0 286 set_pixel(i, result) 287 PREV_WEIGHT = NEXT_WEIGHT 288 PIXELS.show()