code.py
  1  # SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  """
  6  GOOGLY EYES for Adafruit EyeLight LED glasses + driver. Pendulum physics
  7  simulation using accelerometer and math. This uses only the rings, not the
  8  matrix portion. Adapted from Bill Earl's STEAM-Punk Goggles project:
  9  https://learn.adafruit.com/steam-punk-goggles
 10  """
 11  
 12  import math
 13  import random
 14  import board
 15  import supervisor
 16  import adafruit_lis3dh
 17  import adafruit_is31fl3741
 18  from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses
 19  
 20  
 21  # HARDWARE SETUP ----
 22  
 23  i2c = board.I2C()  # Shared by both the accelerometer and LED controller
 24  
 25  # Initialize the accelerometer
 26  lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)
 27  
 28  # Initialize the IS31 LED driver, buffered for smoother animation
 29  glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
 30  
 31  
 32  # PHYSICS SETUP -----
 33  
 34  
 35  class Pendulum:
 36      """A small class for our pendulum simulation."""
 37  
 38      def __init__(self, ring, color):
 39          """Initial pendulum position, plus axle friction, are randomized
 40          so the two rings don't spin in perfect lockstep."""
 41          self.ring = ring  # Save reference to corresponding LED ring
 42          self.color = color  # (R,G,B) tuple for color
 43          self.angle = random.random()  # Position around ring, in radians
 44          self.momentum = 0
 45          self.friction = random.uniform(0.85, 0.9)  # Inverse friction, really
 46  
 47      def interp(self, pixel, scale):
 48          """Given a pixel index (0-23) and a scaling factor (0.0-1.0),
 49          interpolate between LED "off" color (at 0.0) and this item's fully-
 50          lit color (at 1.0) and set pixel to the result."""
 51          self.ring[pixel] = (
 52              (int(self.color[0] * scale) << 16)
 53              | (int(self.color[1] * scale) << 8)
 54              | int(self.color[2] * scale)
 55          )
 56  
 57      def iterate(self, xyz):
 58          """Given an accelerometer reading, run one cycle of the pendulum
 59          physics simulation and render the corresponding LED ring."""
 60          # Minus here is because LED pixel indices run clockwise vs. trigwise.
 61          # 0.05 is just an empirically-derived scaling fudge factor that looks
 62          # good; smaller values for more sluggish rings, higher = more twitch.
 63          self.momentum = (
 64              self.momentum * self.friction
 65              - (math.cos(self.angle) * xyz[2] + math.sin(self.angle) * xyz[0]) * 0.05
 66          )
 67          self.angle += self.momentum
 68  
 69          # Scale pendulum angle into pixel space
 70          midpoint = self.angle * 12 / math.pi % 24
 71          # Go around the whole ring, setting each pixel based on proximity
 72          # (this is also to erase the prior position)...
 73          for i in range(24):
 74              dist = abs(midpoint - i)  # Pixel to pendulum distance...
 75              if dist > 12:  #            If it crosses the "seam" at top,
 76                  dist = 24 - dist  #      take the shorter path.
 77              if dist > 5:  #             Not close to pendulum,
 78                  self.ring[i] = 0  #      erase pixel.
 79              elif dist < 2:  #           Close to pendulum,
 80                  self.interp(i, 1.0)  #   solid color
 81              else:  #                    Anything in-between,
 82                  self.interp(i, (5 - dist) / 3)  # interpolate
 83  
 84  
 85  # List of pendulum objects, of which there are two: one per glasses ring
 86  pendulums = [
 87      Pendulum(glasses.left_ring, (0, 20, 50)),  # Cerulean blue,
 88      Pendulum(glasses.right_ring, (0, 20, 50)),  # 50 is plenty bright!
 89  ]
 90  
 91  
 92  # MAIN LOOP ---------
 93  
 94  while True:
 95  
 96      # The try/except here is because VERY INFREQUENTLY the I2C bus will
 97      # encounter an error when accessing either the accelerometer or the
 98      # LED driver, whether from bumping around the wires or sometimes an
 99      # I2C device just gets wedged. To more robustly handle the latter,
100      # the code will restart if that happens.
101      try:
102  
103          accel = lis3dh.acceleration
104          for p in pendulums:
105              p.iterate(accel)
106  
107          glasses.show()
108  
109      # See "try" notes above regarding rare I2C errors.
110      except OSError:
111          supervisor.reload()