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