lights.py
  1  # SPDX-FileCopyrightText: 2018 Tony DiCola for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  # ESP8266 MicroPython smart holiday lights project code.
  6  # This will animate NeoPixels that can be controlled from the included
  7  # lights.html web page.
  8  # Author: Tony DiCola
  9  # License: MIT License
 10  import machine
 11  import neopixel
 12  import utime
 13  import ujson
 14  
 15  
 16  # Static configuration that never changes:
 17  PIXEL_PIN   = machine.Pin(15, machine.Pin.OUT)  # Pin connected to the NeoPixels.
 18  PIXEL_COUNT = 32                                # Number of NeoPixels.
 19  CONFIG_FILE = 'config.json'                     # Name of animation config file.
 20  
 21  
 22  # Mirror the colors to make a ramp up and ramp down with no repeated colors.
 23  def mirror(values):
 24      # Add the input values in reverse order to the end of the array.
 25      # However slice off the very first and very last items (the [1:-1] syntax)
 26      # to prevent the first and last values from repeating.
 27      # For example an input of:
 28      #  [1, 2, 3]
 29      # Returns:
 30      #  [1, 2, 3, 2]
 31      # Instead of returning:
 32      #  [1, 2, 3, 3, 2, 1]
 33      # Which would duplicate 3 and 1 as you loop through the elements.
 34      values.extend(list(reversed(values))[1:-1])
 35      return values
 36  
 37  # Linear interpolation helper:
 38  def _lerp(x, x0, x1, y0, y1):
 39      return y0 + (x - x0) * ((y1 - y0)/(x1 - x0))
 40  
 41  
 42  # Animation functions:
 43  def blank(config, np, pixel_count):  # pylint: disable=unused-argument, redefined-outer-name
 44      # Turn off all the pixels.
 45      np.fill((0,0,0))
 46      np.write()
 47  
 48  
 49  def solid(config, np, pixel_count):  # pylint: disable=unused-argument, redefined-outer-name
 50      # Solid pulse of all pixels at the same color.
 51      colors = config['colors']
 52      elapsed = utime.ticks_ms() // config['period_ms']
 53      current = elapsed % len(colors)
 54      np.fill(colors[current])
 55      np.write()
 56  
 57  
 58  def chase(config, np, pixel_count):  # pylint: disable=unused-argument, redefined-outer-name
 59      # Chasing animation of pixels through different colors.
 60      colors = config['colors']
 61      elapsed = utime.ticks_ms() // config['period_ms']
 62      for i in range(PIXEL_COUNT):
 63          current = (elapsed+i) % len(colors)
 64          np[i] = colors[current]
 65      np.write()
 66  
 67  
 68  def smooth(config, np, pixel_count):  # pylint: disable=unused-argument, redefined-outer-name
 69      # Smooth pulse of all pixels at the same color.  Interpolates inbetween colors
 70      # for smoother animation.
 71      colors = config['colors']
 72      period_ms = config['period_ms']
 73      ticks = utime.ticks_ms()
 74      step = ticks // period_ms
 75      offset = ticks % period_ms
 76      color0 = colors[step % len(colors)]
 77      color1 = colors[(step+1) % len(colors)]
 78      color = (int(_lerp(offset, 0, period_ms, color0[0], color1[0])),
 79               int(_lerp(offset, 0, period_ms, color0[1], color1[1])),
 80               int(_lerp(offset, 0, period_ms, color0[2], color1[2])))
 81      np.fill(color)
 82      np.write()
 83  
 84  
 85  # Setup code:
 86  # Initialize NeoPixels and turn them off.
 87  np = neopixel.NeoPixel(PIXEL_PIN, PIXEL_COUNT)
 88  np.fill((0,0,0))
 89  np.write()
 90  
 91  # Try loading the animation configuration, otherwise fall back to a blank default.
 92  try:
 93      with open(CONFIG_FILE, 'r') as infile:
 94          config = ujson.loads(infile.read())
 95  except OSError:
 96      # Couldn't load the config file, so fall back to a default blank animation.
 97      config = {
 98          'colors': [[0,0,0]],
 99          'mirror_colors': False,
100          'period_ms': 250,
101          'animation': 'blank'
102      }
103  
104  # Mirror the color array if necessary.
105  if config['mirror_colors']:
106      config['colors'] = mirror(config['colors'])
107  
108  # Determine which animation function should be called.
109  animation = globals().get(config['animation'], blank)
110  
111  # Main loop code:
112  while True:
113      animation(config, np, PIXEL_COUNT)
114      utime.sleep(0.01)