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