|
| 1 | +# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | + |
| 5 | +""" |
| 6 | +FIRE EFFECT for Adafruit EyeLights (LED Glasses + Driver). |
| 7 | +A demoscene classic that produces a cool analog-esque look with |
| 8 | +modest means, iteratively scrolling and blurring raster data. |
| 9 | +""" |
| 10 | + |
| 11 | +import random |
| 12 | +from supervisor import reload |
| 13 | +import board |
| 14 | +from busio import I2C |
| 15 | +import adafruit_is31fl3741 |
| 16 | +from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses |
| 17 | + |
| 18 | + |
| 19 | +# HARDWARE SETUP --------- |
| 20 | + |
| 21 | +# Manually declare I2C (not board.I2C() directly) to access 1 MHz speed... |
| 22 | +i2c = I2C(board.SCL, board.SDA, frequency=1000000) |
| 23 | + |
| 24 | +# Initialize the IS31 LED driver, buffered for smoother animation |
| 25 | +glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER) |
| 26 | +glasses.show() # Clear any residue on startup |
| 27 | +glasses.global_current = 20 # Just middlin' bright, please |
| 28 | + |
| 29 | + |
| 30 | +# INITIALIZE TABLES ------ |
| 31 | + |
| 32 | +# The raster data is intentionally one row taller than the LED matrix. |
| 33 | +# Each frame, random noise is put in the bottom (off matrix) row. There's |
| 34 | +# also an extra column on either side, to avoid needing edge clipping when |
| 35 | +# neighboring pixels (left, center, right) are averaged later. |
| 36 | +data = [[0] * (glasses.width + 2) for _ in range(glasses.height + 1)] |
| 37 | +# (2D array where elements are accessed as data[y][x], initialized to 0) |
| 38 | + |
| 39 | +# Each element in the raster is a single value representing brightness. |
| 40 | +# A pre-computed lookup table maps these to RGB colors. This one happens |
| 41 | +# to have 32 elements, but as we're not on an actual paletted hardware |
| 42 | +# framebuffer it could be any size really (with suitable changes throughout). |
| 43 | +gamma = 2.6 |
| 44 | +colormap = [] |
| 45 | +for n in range(32): |
| 46 | + n *= 3 / 31 # 0.0 <= n <= 3.0 from start to end of map |
| 47 | + if n <= 1: # 0.0 <= n <= 1.0 : black to red |
| 48 | + r = n # r,g,b are initially calculated 0 to 1 range |
| 49 | + g = b = 0 |
| 50 | + elif n <= 2: # 1.0 <= n <= 2.0 : red to yellow |
| 51 | + r = 1 |
| 52 | + g = n - 1 |
| 53 | + b = 0 |
| 54 | + else: # 2.0 <= n <= 3.0 : yellow to white |
| 55 | + r = g = 1 |
| 56 | + b = n - 2 |
| 57 | + r = int((r ** gamma) * 255) # Gamma correction linearizes |
| 58 | + g = int((g ** gamma) * 255) # perceived brightness, then |
| 59 | + b = int((b ** gamma) * 255) # scale to 0-255 for LEDs and |
| 60 | + colormap.append((r << 16) | (g << 8) | b) # store as 'packed' RGB color |
| 61 | + |
| 62 | + |
| 63 | +# UTILITY FUNCTIONS ----- |
| 64 | + |
| 65 | + |
| 66 | +def interp(ring, led1, led2, level1, level2): |
| 67 | + """Linearly interpolate a range of brightnesses between two LEDs of |
| 68 | + one eyeglass ring, mapping through the global color table. LED range |
| 69 | + is non-inclusive; the first and last LEDs (which overlap matrix pixels) |
| 70 | + are not set. led2 MUST be > led1. LED indices may be >= 24 to 'wrap |
| 71 | + around' the seam at the top of the ring.""" |
| 72 | + span = led2 - led1 + 1 # Number of LEDs |
| 73 | + delta = level2 - level1 # Difference in brightness |
| 74 | + for led in range(led1 + 1, led2): # For each LED in-between, |
| 75 | + ratio = (led - led1) / span # interpolate brightness level |
| 76 | + ring[led % 24] = colormap[min(31, int(level1 + delta * ratio))] |
| 77 | + |
| 78 | + |
| 79 | +# MAIN LOOP ------------- |
| 80 | + |
| 81 | +while True: |
| 82 | + # The try/except here is because VERY INFREQUENTLY the I2C bus will |
| 83 | + # encounter an error when accessing the LED driver, whether from bumping |
| 84 | + # around the wires or sometimes an I2C device just gets wedged. To more |
| 85 | + # robustly handle the latter, the code will restart if that happens. |
| 86 | + try: |
| 87 | + |
| 88 | + # At the start of each frame, fill the bottom (off matrix) row |
| 89 | + # with random noise. To make things less strobey, old data from the |
| 90 | + # prior frame still has about 1/3 'weight' here. There's no special |
| 91 | + # real-world significance to the 85, it's just an empirically- |
| 92 | + # derived fudge factor that happens to work well with the size of |
| 93 | + # the color map. |
| 94 | + for x in range(1, 19): |
| 95 | + data[5][x] = 0.33 * data[5][x] + 0.67 * random.random() * 85 |
| 96 | + # If this were actual SRS BZNS 31337 D3M0SC3N3 code, great care |
| 97 | + # would be taken to avoid floating-point math. But with few pixels, |
| 98 | + # and so this code might be less obtuse, a casual approach is taken. |
| 99 | + |
| 100 | + # Each row (except last) is then processed, top-to-bottom. This |
| 101 | + # order is important because it's an iterative algorithm...the |
| 102 | + # output of each frame serves as input to the next, and the steps |
| 103 | + # below (looking at the pixels below each row) are what makes the |
| 104 | + # "flames" appear to move "up." |
| 105 | + for y in range(5): # Current row of pixels |
| 106 | + y1 = data[y + 1] # One row down |
| 107 | + for x in range(1, 19): # Skip left, right columns in data |
| 108 | + # Each pixel is sort of the average of the three pixels |
| 109 | + # under it (below left, below center, below right), but not |
| 110 | + # exactly. The below center pixel has more 'weight' than the |
| 111 | + # others, and the result is scaled to intentionally land |
| 112 | + # short, making each row bit darker as they move up. |
| 113 | + data[y][x] = (y1[x] + ((y1[x - 1] + y1[x + 1]) * 0.33)) * 0.35 |
| 114 | + glasses.pixel(x - 1, y, colormap[min(31, int(data[y][x]))]) |
| 115 | + |
| 116 | + # That's all well and good for the matrix, but what about the extra |
| 117 | + # LEDs in the rings? Since these don't align to the pixel grid, |
| 118 | + # rather than trying to extend the raster data and filter it in |
| 119 | + # somehow, we'll fill those arcs with colors interpolated from the |
| 120 | + # endpoints where rings and matrix intersect. Maybe not perfect, |
| 121 | + # but looks okay enough! |
| 122 | + interp(glasses.left_ring, 7, 17, data[4][8], data[4][1]) |
| 123 | + interp(glasses.left_ring, 21, 29, data[0][2], data[2][8]) |
| 124 | + interp(glasses.right_ring, 7, 17, data[4][18], data[4][11]) |
| 125 | + interp(glasses.right_ring, 19, 27, data[2][11], data[0][17]) |
| 126 | + |
| 127 | + glasses.show() # Buffered mode MUST use show() to refresh matrix |
| 128 | + |
| 129 | + except OSError: # See "try" notes above regarding rare I2C errors. |
| 130 | + print("Restarting") |
| 131 | + reload() |
0 commit comments