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()