Skip to content

Commit 6e7a846

Browse files
authored
Merge pull request #1872 from PaintYourDragon/main
Add Arduino ports of 3 EyeLights CircuitPython demos
2 parents 2c9ee4f + 77ec4e8 commit 6e7a846

10 files changed

Lines changed: 501 additions & 1 deletion

File tree

EyeLights_Accelerometer_Tap/.nrf52840.test.only

Whitespace-only changes.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
/*
6+
ACCELEROMETER INPUT DEMO: while the LED Glasses Driver has a perfectly
7+
good clicky button for input, this code shows how one might instead use
8+
the onboard accelerometer for interactions*.
9+
10+
Worn normally, the LED rings are simply lit a solid color.
11+
TAP the eyeglass frames to cycle among a list of available colors.
12+
LOOK DOWN to light the LED rings bright white -- for navigating steps
13+
or finding the right key. LOOK BACK UP to return to solid color.
14+
This uses only the rings, not the matrix portion.
15+
16+
* Like, if you have big ol' monster hands, that little button can be
17+
hard to click, y'know?
18+
*/
19+
20+
#include <Adafruit_IS31FL3741.h> // For LED driver
21+
#include <Adafruit_LIS3DH.h> // For accelerometer
22+
#include <Adafruit_Sensor.h> // For m/s^2 accel units
23+
24+
Adafruit_LIS3DH accel;
25+
Adafruit_EyeLights_buffered glasses; // Buffered for smooth animation
26+
27+
// Here's a list of colors that we cycle through when tapped, specified
28+
// as {R,G,B} values from 0-255. These are intentionally a bit dim --
29+
// both to save battery and to make the "ground light" mode more dramatic.
30+
// Rather than primary color red/green/blue sequence which is just so
31+
// over-done at this point, let's use some HALLOWEEN colors!
32+
uint8_t colors[][3] = {
33+
{27, 9, 0}, // Orange
34+
{12, 0, 24}, // Purple
35+
{5, 31, 0}, // Green
36+
};
37+
#define NUM_COLORS (sizeof colors / sizeof colors[0]) // List length
38+
uint8_t looking_down_color[] = {255, 255, 255}; // Max white
39+
40+
uint8_t color_index = 0; // Begin at first color in list
41+
uint8_t *target_color; // Pointer to color we're aiming for
42+
float interpolated_color[] = {0.0, 0.0, 0.0}; // Current color along the way
43+
float filtered_y; // De-noised accelerometer reading
44+
bool looking_down; // Set true when glasses are oriented downward
45+
sensors_event_t event; // For accelerometer conversion
46+
uint32_t last_tap_time = 0; // For accelerometer tap de-noising
47+
48+
// Crude error handler, prints message to Serial console, flashes LED
49+
void err(char *str, uint8_t hz) {
50+
Serial.println(str);
51+
pinMode(LED_BUILTIN, OUTPUT);
52+
for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1);
53+
}
54+
55+
void setup() { // Runs once at program start...
56+
57+
// Initialize hardware
58+
Serial.begin(115200);
59+
if (! accel.begin()) err("LIS3DH not found", 5);
60+
if (! glasses.begin()) err("IS3741 not found", 2);
61+
62+
// Configure accelerometer and get initial state
63+
accel.setClick(1, 100); // Set threshold for single tap
64+
accel.getEvent(&event); // Current accel in m/s^2
65+
// Check accelerometer to see if we've started in the looking-down state,
66+
// set the target color (what we're aiming for) appropriately. Only the
67+
// Y axis is needed for this.
68+
filtered_y = event.acceleration.y;
69+
looking_down = (filtered_y > 5.0);
70+
// If initially looking down, aim for the look-down color,
71+
// else aim for the first item in the color list.
72+
target_color = looking_down ? looking_down_color : colors[color_index];
73+
74+
// Configure glasses for max brightness, enable output
75+
glasses.setLEDscaling(0xFF);
76+
glasses.setGlobalCurrent(0xFF);
77+
glasses.enable(true);
78+
}
79+
80+
void loop() { // Repeat forever...
81+
82+
// interpolated_color blends from the prior to the next ("target")
83+
// LED ring colors, with a pleasant ease-out effect.
84+
for(uint8_t i=0; i<3; i++) { // R, G, B
85+
interpolated_color[i] = interpolated_color[i] * 0.97 + target_color[i] * 0.03;
86+
}
87+
// Convert separate red, green, blue to "packed" 24-bit RGB value
88+
uint32_t rgb = ((int)interpolated_color[0] << 16) |
89+
((int)interpolated_color[1] << 8) |
90+
(int)interpolated_color[2];
91+
// Fill both rings with packed color, then refresh the LEDs.
92+
glasses.left_ring.fill(rgb);
93+
glasses.right_ring.fill(rgb);
94+
glasses.show();
95+
96+
// The look-down detection only needs the accelerometer's Y axis.
97+
// This works with the Glasses Driver mounted on either temple,
98+
// with the glasses arms "open" (as when worn).
99+
accel.getEvent(&event);
100+
// Smooth the accelerometer reading the same way RGB colors are
101+
// interpolated. This avoids false triggers from jostling around.
102+
filtered_y = filtered_y * 0.97 + event.acceleration.y * 0.03;
103+
104+
// The threshold between "looking down" and "looking up" depends
105+
// on which of those states we're currently in. This is an example
106+
// of hysteresis in software...a change of direction requires a
107+
// little extra push before it takes, which avoids oscillating if
108+
// there was just a single threshold both ways.
109+
if (looking_down) { // Currently in the looking-down state...
110+
(void)accel.getClick(); // Discard any taps while looking down
111+
if (filtered_y < 3.5) { // Have we crossed the look-up threshold?
112+
target_color = colors[color_index]; // Back to list color
113+
looking_down = false; // We're looking up now!
114+
}
115+
} else { // Currently in the looking-up state...
116+
if (filtered_y > 5.0) { // Crossed the look-down threshold?
117+
target_color = looking_down_color; // Aim for white
118+
looking_down = true; // We're looking down now!
119+
} else if (accel.getClick()) {
120+
// No look up/down change, but the accelerometer registered
121+
// a tap. Compare this against the last time we sensed one,
122+
// and only do things if it's been more than half a second.
123+
// This avoids spurious double-taps that can occur no matter
124+
// how carefully the tap threshold was set.
125+
uint32_t now = millis();
126+
uint32_t elapsed = now - last_tap_time;
127+
if (elapsed > 500) {
128+
// A good tap was detected. Cycle to the next color in
129+
// the list and note the time of this tap.
130+
color_index = (color_index + 1) % NUM_COLORS;
131+
target_color = colors[color_index];
132+
last_tap_time = now;
133+
}
134+
}
135+
}
136+
}

EyeLights_Accelerometer_Tap/code.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
15
"""
26
ACCELEROMETER INPUT DEMO: while the LED Glasses Driver has a perfectly
37
good clicky button for input, this code shows how one might instead use

EyeLights_Audio_Spectrum/.nrf52840.test.only

Whitespace-only changes.
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
/*
6+
AUDIO SPECTRUM LIGHT SHOW for Adafruit EyeLights (LED Glasses + Driver).
7+
Uses onboard microphone and a lot of math to react to music.
8+
REQUIRES Adafruit_ZeroFFT LIBRARY, install via Arduino Library manager.
9+
*/
10+
11+
#include <Adafruit_IS31FL3741.h> // For LED driver
12+
#include <PDM.h> // For microphone
13+
#include <Adafruit_ZeroFFT.h> // For math
14+
15+
// FFT/SPECTRUM CONFIG ----
16+
17+
#define NUM_SAMPLES 512 // Audio & FFT buffer, MUST be a power of two
18+
#define SPECTRUM_SIZE (NUM_SAMPLES / 2) // Output spectrum is 1/2 of FFT output
19+
// Bottom of spectrum tends to be noisy, while top often exceeds musical
20+
// range and is just harmonics, so clip both ends off:
21+
#define LOW_BIN 5 // Lowest bin of spectrum that contributes to graph
22+
#define HIGH_BIN 150 // Highest bin "
23+
24+
// GLOBAL VARIABLES -------
25+
26+
Adafruit_EyeLights_buffered glasses; // LED matrix is buffered for smooth animation
27+
extern PDMClass PDM; // Microphone
28+
short audio_buf[3][NUM_SAMPLES]; // Audio input buffers, 16-bit signed
29+
uint8_t active_buf = 0; // Buffer # into which audio is currently recording
30+
volatile int samples_read = 0; // # of samples read into current buffer thus far
31+
volatile bool mic_on = false; // true when reading from mic, false when full/stopped
32+
float spectrum[SPECTRUM_SIZE]; // FFT results are stored & further processed here
33+
float dynamic_level = 10.0; // For adapting to changing audio volume
34+
int frames; // For frames-per-second calculation
35+
uint32_t start_time; // Ditto
36+
37+
struct { // Values associated with each column of the matrix
38+
int first_bin; // First spectrum bin index affecting column
39+
int num_bins; // Number of spectrum bins affecting column
40+
float *bin_weights; // List of spectrum bin weightings
41+
uint32_t color; // GFX-style 'RGB565' color for column
42+
float top; // Current column top position
43+
float dot; // Current column 'falling dot' position
44+
float velocity; // Current velocity of falling dot
45+
} column_table[18];
46+
47+
// Crude error handler, prints message to Serial console, flashes LED
48+
void err(char *str, uint8_t hz) {
49+
Serial.println(str);
50+
pinMode(LED_BUILTIN, OUTPUT);
51+
for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1);
52+
}
53+
54+
void setup() { // Runs once at program start...
55+
56+
Serial.begin(115200);
57+
//while(!Serial);
58+
if (! glasses.begin()) err("IS3741 not found", 2);
59+
60+
// FFT/SPECTRUM SETUP -----
61+
62+
uint8_t spectrum_bits = (int)log2f((float)SPECTRUM_SIZE); // e.g. 8 = 256 bin spectrum
63+
// Scale LOW_BIN and HIGH_BIN to 0.0 to 1.0 equivalent range in spectrum
64+
float low_frac = log2f((float)LOW_BIN) / (float)spectrum_bits;
65+
float frac_range = log2((float)HIGH_BIN) / (float)spectrum_bits - low_frac;
66+
// Serial.printf("%d %f %f\n", spectrum_bits, low_frac, frac_range);
67+
68+
// To keep the display lively, tables are precomputed where each column of
69+
// the matrix (of which there are few) is the sum value and weighting of
70+
// several bins from the FFT spectrum output (of which there are many).
71+
// The tables also help visually linearize the output so octaves are evenly
72+
// spaced, as on a piano keyboard, whereas the source spectrum data is
73+
// spaced by frequency in Hz.
74+
75+
for (int column=0; column<18; column++) {
76+
// Determine the lower and upper frequency range for this column, as
77+
// fractions within the scaled 0.0 to 1.0 spectrum range. 0.95 below
78+
// creates slight frequency overlap between columns, looks nicer.
79+
float lower = low_frac + frac_range * ((float)column / 18.0 * 0.95);
80+
float upper = low_frac + frac_range * ((float)(column + 1) / 18.0);
81+
float mid = (lower + upper) * 0.5; // Center of lower-to-upper range
82+
float half_width = (upper - lower) * 0.5 + 1e-2; // 1/2 of lower-to-upper range
83+
// Map fractions back to spectrum bin indices that contribute to column
84+
int first_bin = int(pow(2, (float)spectrum_bits * lower) + 1e-4);
85+
int last_bin = int(pow(2, (float)spectrum_bits * upper) + 1e-4);
86+
//Serial.printf("%d %d %d\n", column, first_bin, last_bin);
87+
float total_weight = 0.0; // Accumulate weight for this bin
88+
int num_bins = last_bin - first_bin + 1;
89+
// Allocate space for bin weights for column, stop everything if out of RAM.
90+
column_table[column].bin_weights = (float *)malloc(num_bins * sizeof(float));
91+
if (column_table[column].bin_weights == NULL) err("Malloc fail", 10);
92+
for (int bin_index = first_bin; bin_index <= last_bin; bin_index++) {
93+
// Find distance from column's overall center to individual bin's
94+
// center, expressed as 0.0 (bin at center) to 1.0 (bin at limit of
95+
// lower-to-upper range).
96+
float bin_center = log2f((float)bin_index + 0.5) / (float)spectrum_bits;
97+
float dist = fabs(bin_center - mid) / half_width;
98+
if (dist < 1.0) { // Filter out a few math stragglers at either end
99+
// Bin weights have a cubic falloff curve within range:
100+
dist = 1.0 - dist; // Invert dist so 1.0 is at center
101+
float bin_weight = (((3.0 - (dist * 2.0)) * dist) * dist);
102+
column_table[column].bin_weights[bin_index - first_bin] = bin_weight;
103+
total_weight += bin_weight;
104+
}
105+
}
106+
//Serial.println(column);
107+
// Scale bin weights so total is 1.0 for each column, but then mute
108+
// lower columns slightly and boost higher columns. It graphs better.
109+
for (int i=0; i<num_bins; i++) {
110+
column_table[column].bin_weights[i] = column_table[column].bin_weights[i] /
111+
total_weight * (0.6 + (float)i / 18.0 * 2.0);
112+
//Serial.printf(" %f\n", column_table[column].bin_weights[i]);
113+
}
114+
column_table[column].first_bin = first_bin;
115+
column_table[column].num_bins = num_bins;
116+
column_table[column].color = glasses.color565(glasses.ColorHSV(
117+
57600UL * column / 18, 255, 255)); // Red (0) to purple (57600)
118+
column_table[column].top = 6.0; // Start off bottom of graph
119+
column_table[column].dot = 6.0;
120+
column_table[column].velocity = 0.0;
121+
}
122+
123+
for (int i=0; i<SPECTRUM_SIZE; i++) spectrum[i] = 0.0;
124+
125+
// HARDWARE SETUP ---------
126+
127+
// Configure glasses for max brightness, enable output
128+
glasses.setLEDscaling(0xFF);
129+
glasses.setGlobalCurrent(0xFF);
130+
glasses.enable(true);
131+
132+
// Configure PDM mic, mono 16 KHz
133+
PDM.onReceive(onPDMdata);
134+
PDM.begin(1, 16000);
135+
136+
start_time = millis();
137+
}
138+
139+
void loop() { // Repeat forever...
140+
141+
short *audio_data; // Pointer to newly-received audio
142+
143+
while (mic_on) yield(); // Wait for next buffer to finish recording
144+
// Full buffer received -- active_buf is index to new data
145+
audio_data = &audio_buf[active_buf][0]; // New data is here
146+
active_buf = 1 - active_buf; // Swap buffers to record into other one,
147+
mic_on = true; // and start recording next batch
148+
149+
// Perform FFT operation on newly-received data,
150+
// results go back into the same buffer.
151+
ZeroFFT(audio_data, NUM_SAMPLES);
152+
153+
// Convert FFT output to spectrum. log(y) looks better than raw data.
154+
// Only LOW_BIN to HIGH_BIN elements are needed.
155+
for(int i=LOW_BIN; i<=HIGH_BIN; i++) {
156+
spectrum[i] = (audio_data[i] > 0) ? log((float)audio_data[i]) : 0.0;
157+
}
158+
159+
// Find min & max range of spectrum bin values, with limits.
160+
float lower = spectrum[LOW_BIN], upper = spectrum[LOW_BIN];
161+
for (int i=LOW_BIN+1; i<=HIGH_BIN; i++) {
162+
if (spectrum[i] < lower) lower = spectrum[i];
163+
if (spectrum[i] > upper) upper = spectrum[i];
164+
}
165+
//Serial.printf("%f %f\n", lower, upper);
166+
if (upper < 2.5) upper = 2.5;
167+
168+
// Adjust dynamic level to current spectrum output, keeps the graph
169+
// 'lively' as ambient volume changes. Sparkle but don't saturate.
170+
if (upper > dynamic_level) {
171+
// Got louder. Move level up quickly but allow initial "bump."
172+
dynamic_level = dynamic_level * 0.5 + upper * 0.5;
173+
} else {
174+
// Got quieter. Ease level down, else too many bumps.
175+
dynamic_level = dynamic_level * 0.75 + lower * 0.25;
176+
}
177+
178+
// Apply vertical scale to spectrum data. Results may exceed
179+
// matrix height...that's OK, adds impact!
180+
float scale = 15.0 / (dynamic_level - lower);
181+
for (int i=LOW_BIN; i<=HIGH_BIN; i++) {
182+
spectrum[i] = (spectrum[i] - lower) * scale;
183+
}
184+
185+
// Clear screen, filter and draw each column of the display...
186+
glasses.fill(0);
187+
for(int column=0; column<18; column++) {
188+
int first_bin = column_table[column].first_bin;
189+
// Start BELOW matrix and accumulate bin weights UP, saves math
190+
float column_top = 7.0;
191+
for (int bin_offset=0; bin_offset<column_table[column].num_bins; bin_offset++) {
192+
column_top -= spectrum[first_bin + bin_offset] * column_table[column].bin_weights[bin_offset];
193+
}
194+
// Column top positions are filtered to appear less 'twitchy' --
195+
// last data still has a 30% influence on current positions.
196+
column_top = (column_top * 0.7) + (column_table[column].top * 0.3);
197+
column_table[column].top = column_top;
198+
199+
if(column_top < column_table[column].dot) { // Above current falling dot?
200+
column_table[column].dot = column_top - 0.5; // Move dot up
201+
column_table[column].velocity = 0.0; // and clear out velocity
202+
} else {
203+
column_table[column].dot += column_table[column].velocity; // Move dot down
204+
column_table[column].velocity += 0.015; // and accelerate
205+
}
206+
207+
// Draw column and peak dot
208+
int itop = (int)column_top; // Quantize column top to pixel space
209+
glasses.drawLine(column, itop, column, itop + 20, column_table[column].color);
210+
glasses.drawPixel(column, (int)column_table[column].dot, 0xE410);
211+
}
212+
213+
glasses.show(); // Buffered mode MUST use show() to refresh matrix
214+
215+
frames += 1;
216+
uint32_t elapsed = millis() - start_time;
217+
//Serial.println(frames * 1000 / elapsed);
218+
}
219+
220+
// PDM mic interrupt handler, called when new data is ready
221+
void onPDMdata() {
222+
//digitalWrite(LED_BUILTIN, millis() & 1024); // Debug heartbeat
223+
if (int bytes_to_read = PDM.available()) {
224+
if (mic_on) {
225+
int byte_limit = (NUM_SAMPLES - samples_read) * 2; // Space remaining,
226+
bytes_to_read = min(bytes_to_read, byte_limit); // don't overflow!
227+
PDM.read(&audio_buf[active_buf][samples_read], bytes_to_read);
228+
samples_read += bytes_to_read / 2; // Increment counter
229+
if (samples_read >= NUM_SAMPLES) { // Buffer full?
230+
mic_on = false; // Stop and
231+
samples_read = 0; // reset counter for next time
232+
}
233+
} else {
234+
// Mic is off (code is busy) - must read but discard data.
235+
// audio_buf[2] is a 'bit bucket' for this.
236+
PDM.read(audio_buf[2], bytes_to_read);
237+
}
238+
}
239+
}

EyeLights_Audio_Spectrum/code.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
15
"""
26
AUDIO SPECTRUM LIGHT SHOW for Adafruit EyeLights (LED Glasses + Driver).
37
Uses onboard microphone and a lot of math to react to music.

EyeLights_Googly_Rings/.nrf52840.test.only

Whitespace-only changes.

0 commit comments

Comments
 (0)