Skip to content

Commit 0c4117c

Browse files
authored
Merge pull request adafruit#1892 from PaintYourDragon/main
Add “Fire” demo for EyeLights (CircuitPython and Arduino)
2 parents 15146ad + 6d96c70 commit 0c4117c

File tree

3 files changed

+266
-0
lines changed

3 files changed

+266
-0
lines changed

EyeLights_Fire/EyeLights_Fire/.ledglasses_nrf52840.test.only

Whitespace-only changes.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
#include <Adafruit_IS31FL3741.h> // For LED driver
12+
13+
Adafruit_EyeLights_buffered glasses; // Buffered for smooth animation
14+
15+
// The raster data is intentionally one row taller than the LED matrix.
16+
// Each frame, random noise is put in the bottom (off matrix) row. There's
17+
// also an extra column on either side, to avoid needing edge clipping when
18+
// neighboring pixels (left, center, right) are averaged later.
19+
float data[6][20]; // 2D array where elements are accessed as data[y][x]
20+
21+
// Each element in the raster is a single value representing brightness.
22+
// A pre-computed lookup table maps these to RGB colors. This one happens
23+
// to have 32 elements, but as we're not on an actual paletted hardware
24+
// framebuffer it could be any size really (with suitable changes throughout).
25+
uint32_t colormap[32];
26+
#define GAMMA 2.6
27+
28+
// Crude error handler, prints message to Serial console, flashes LED
29+
void err(char *str, uint8_t hz) {
30+
Serial.println(str);
31+
pinMode(LED_BUILTIN, OUTPUT);
32+
for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1);
33+
}
34+
35+
void setup() { // Runs once at program start...
36+
37+
// Initialize hardware
38+
Serial.begin(115200);
39+
if (! glasses.begin()) err("IS3741 not found", 2);
40+
41+
// Configure glasses for reduced brightness, enable output
42+
glasses.setLEDscaling(0xFF);
43+
glasses.setGlobalCurrent(20);
44+
glasses.enable(true);
45+
46+
memset(data, 0, sizeof data);
47+
48+
for(uint8_t i=0; i<32; i++) {
49+
float n = i * 3.0 / 31.0; // 0.0 <= n <= 3.0 from start to end of map
50+
float r, g, b;
51+
if (n <= 1) { // 0.0 <= n <= 1.0 : black to red
52+
r = n; // r,g,b are initially calculated 0 to 1 range
53+
g = b = 0.0;
54+
} else if (n <= 2) { // 1.0 <= n <= 2.0 : red to yellow
55+
r = 1.0;
56+
g = n - 1.0;
57+
b = 0.0;
58+
} else { // 2.0 <= n <= 3.0 : yellow to white
59+
r = g = 1.0;
60+
b = n - 2.0;
61+
}
62+
// Gamma correction linearizes perceived brightness, then scale to
63+
// 0-255 for LEDs and store as a 'packed' RGB color.
64+
colormap[i] = (uint32_t(pow(r, GAMMA) * 255.0) << 16) |
65+
(uint32_t(pow(g, GAMMA) * 255.0) << 8) |
66+
uint32_t(pow(b, GAMMA) * 255.0);
67+
}
68+
}
69+
70+
// Linearly interpolate a range of brightnesses between two LEDs of
71+
// one eyeglass ring, mapping through the global color table. LED range
72+
// is non-inclusive; the first and last LEDs (which overlap matrix pixels)
73+
// are not set. led2 MUST be > led1. LED indices may be >= 24 to 'wrap
74+
// around' the seam at the top of the ring.
75+
void interp(bool isRight, int led1, int led2, float level1, float level2) {
76+
int span = led2 - led1 + 1; // Number of LEDs
77+
float delta = level2 - level1; // Difference in brightness
78+
for (int led = led1 + 1; led < led2; led++) { // For each LED in-between,
79+
float ratio = (float)(led - led1) / span; // interpolate brightness level
80+
uint32_t color = colormap[min(31, int(level1 + delta * ratio))];
81+
if (isRight) glasses.right_ring.setPixelColor(led % 24, color);
82+
else glasses.left_ring.setPixelColor(led % 24, color);
83+
}
84+
}
85+
86+
void loop() { // Repeat forever...
87+
// At the start of each frame, fill the bottom (off matrix) row
88+
// with random noise. To make things less strobey, old data from the
89+
// prior frame still has about 1/3 'weight' here. There's no special
90+
// real-world significance to the 85, it's just an empirically-
91+
// derived fudge factor that happens to work well with the size of
92+
// the color map.
93+
for (uint8_t x=1; x<19; x++) {
94+
data[5][x] = 0.33 * data[5][x] + 0.67 * ((float)random(1000) / 1000.0) * 85.0;
95+
}
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 (uint8_t y=0; y<5; y++) { // Current row of pixels
106+
float *y1 = &data[y + 1][0]; // One row down
107+
for (uint8_t x = 1; x < 19; x++) { // 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.drawPixel(x - 1, y, glasses.color565(colormap[min(31, int(data[y][x]))]));
115+
// Remember that the LED matrix uses GFX-style "565" colors,
116+
// hence the round trip through color565() here, whereas the LED
117+
// rings (referenced in interp()) use NeoPixel-style 24-bit colors
118+
// (those can reference colormap[] directly).
119+
}
120+
}
121+
122+
// That's all well and good for the matrix, but what about the extra
123+
// LEDs in the rings? Since these don't align to the pixel grid,
124+
// rather than trying to extend the raster data and filter it in
125+
// somehow, we'll fill those arcs with colors interpolated from the
126+
// endpoints where rings and matrix intersect. Maybe not perfect,
127+
// but looks okay enough!
128+
interp(false, 7, 17, data[4][8], data[4][1]); // Left ring bottom
129+
interp(false, 21, 29, data[0][2], data[1][8]); // Left ring top
130+
interp(true, 7, 17, data[4][18], data[4][11]); // Right ring bottom
131+
interp(true, 19, 27, data[1][11], data[0][17]); // Right ring top
132+
133+
glasses.show();
134+
delay(25);
135+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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

Comments
 (0)