diff --git a/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.fzz b/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.fzz new file mode 100644 index 000000000..bf0ab4f8f Binary files /dev/null and b/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.fzz differ diff --git a/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.ino b/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.ino new file mode 100644 index 000000000..af0607401 --- /dev/null +++ b/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.ino @@ -0,0 +1,419 @@ +/*------------------------------------------------------------------------ + Gemma "Firewalker Lite" sneakers sketch. + Uses the following Adafruit parts (X2 for two shoes): + + - Gemma 3V microcontroller (adafruit.com/product/1222 or 2470) + - 150 mAh LiPoly battery (#1317) or larger + - Medium vibration sensor switch (#2384) + - 60/m NeoPixel RGB LED strip (#1138 or #1461) + - LiPoly charger such as #1304 (only one is required, unless you want + to charge both shoes at the same time) + + Needs Adafruit_NeoPixel library: github.com/adafruit/Adafruit_NeoPixel + + THIS CODE USES FEATURES SPECIFIC TO THE GEMMA MICROCONTROLLER BOARD AND + WILL NOT COMPILE OR RUN ON MOST OTHERS. OK on basic Trinket but NOT + Pro Trinket nor anything else. VERY specific to the Gemma! + + Adafruit invests time and resources providing this open source code, + please support Adafruit and open-source hardware by purchasing + products from Adafruit! + + Written by Phil Burgess / Paint Your Dragon for Adafruit Industries. + MIT license, all text above must be included in any redistribution. + See 'COPYING' file for additional notes. + ------------------------------------------------------------------------*/ + +#include +#include +#include +#include + +// CONFIGURABLE STUFF ------------------------------------------------------ + +#define NUM_LEDS 40 // Actual number of LEDs in NeoPixel strip +#define CIRCUMFERENCE 40 // Shoe circumference, in pixels, may be > NUM_LEDS +#define FPS 50 // Animation frames per second +#define LED_DATA_PIN 1 // NeoPixels are connected here +#define MOTION_PIN 0 // Vibration switch from here to GND +// CIRCUMFERENCE is distinct from NUM_LEDS to allow a gap (if desired) in +// LED strip around perimeter of shoe (might not want any on inside egde) +// so animation is spatially coherent (doesn't jump across gap, but moves +// as if pixels were in that space). CIRCUMFERENCE can be equal or larger +// than NUM_LEDS, but not less. + +// The following features are OPTIONAL and require ADDITIONAL COMPONENTS. +// Only ONE of these may be enabled due to limited pins on Gemma: + +// BATTERY LEVEL graph on power-up: requires two resistors of same value, +// 10K or higher, connected to pin D2 -- one goes to Vout pin, other to GND. +//#define BATT_LVL_PIN 2 // Un-comment this line to enable battery level. +// Pin number cannot be changed, this is the only AREF pin on ATtiny85. +// Empty and full thresholds (millivolts) used for battery level display: +#define BATT_MIN_MV 3350 // Some headroom over battery cutoff near 2.9V +#define BATT_MAX_MV 4000 // And little below fresh-charged battery near 4.1V +// Graph works only on battery power; USB power will always show 100% full. + +// POWER DOWN NeoPixels when idle to prolong battery: requires P-channel +// power MOSFET + 220 Ohm resistor, is a 'high side' switch to NeoPixel +V. +// DO NOT do this w/N-channel on GND side, could DESTROY strip! +//#define LED_PWR_PIN 2 // Un-comment this for NeoPixel power-down. +// Could be moved to other pin on Trinket to allow both options together. + +// GLOBAL STUFF ------------------------------------------------------------ + +Adafruit_NeoPixel strip(NUM_LEDS, LED_DATA_PIN); +void sleep(void); // Power-down function +extern const uint8_t gamma[]; // Table at the bottom of this code + +// INTERMISSION explaining the animation code. This works procedurally -- +// using mathematical functions -- rather than moving discrete pixels left +// or right incrementally. This sometimes confuses people because they go +// looking for some "setpixel(x+1, color)" type of code and can't find it. +// Also makes extensive use of fixed-point math, where discrete integer +// values are used to represent fractions (0.0 to 1.0) without relying on +// floating-point math, which just wouldn't even fit in Gemma's limited +// code space, RAM or speed. The reason the animation's done this way is +// to produce smoother, more organic motion...when pixels are the smallest +// discrete unit, animation tends to have a stuttery, 1980s quality to it. +// We can do better! +// Picture the perimeter of the shoe in two different coordinate spaces: +// One is "pixel space," each pixel spans exactly one integer unit, from +// zero to N-1 when there are N pixels. This is how NeoPixels are normally +// addressed. Now, overlaid on this, imagine another coordinate space, +// spanning the same physical width (the perimeter of the shoe, or length +// of the LED strip), but this one has 256 discrete steps (8 bits)...finer +// resolution than the pixel steps...and we do most of the math using +// these units rather than pixel units. It's then possible to move things +// by fractions of a pixel, but render each whole pixel by taking a sample +// at its approximate center in the alternate coordinate space. +// More explanation further in the code. +// +// |Pixel|Pixel|Pixel| ... |Pixel|Pixel|Pixel|<- end of strip +// | 0 | 1 | 2 | | 3 | 4 | N-1 | +// |0... ... ...255|<- fixed-point space +// +// So, inspired by the mothership in Close Encounters of the Third Kind, +// the animation in this sketch is a series of waves moving around the +// perimeter and interacting as they cross. They're triangle waves, +// height proportional to LED brightness, determined by the time since +// motion was last detected. +// +// <- /\ /\ -> <- /\ Pretend these are triangle waves +// ____/ \_____/ \_____/ \____ <- moving in 8-bit coordinate space. + +struct { + uint8_t center; // Center point of wave in fixed-point space (0 - 255) + int8_t speed; // Distance to move between frames (-128 - +127) + uint8_t width; // Width from peak to bottom of triangle wave (0 - 128) + uint8_t hue; // Current wave hue (color) see comments later + uint8_t hueTarget; // Final hue we're aiming for + uint8_t r, g, b; // LED RGB color calculated from hue +} wave[] = { + { 0, 3, 60 }, // Gemma can animate 3 of these on 40 LEDs at 50 FPS + { 0, -5, 45 }, // More LEDs and/or more waves will need lower FPS + { 0, 7, 30 } +}; +// Note that the speeds of each wave are different prime numbers. This +// avoids repetition as the waves move around the perimeter...if they were +// even numbers or multiples of each other, there'd be obvious repetition +// in the pattern of motion...beat frequencies. +#define N_WAVES (sizeof(wave) / sizeof(wave[0])) + +// ONE-TIME INITIALIZATION ------------------------------------------------- + +void setup() { +#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000L) + clock_prescale_set(clock_div_1); // Allow 16 MHz Trinket too +#endif +#ifdef POWER_PIN + pinMode(POWER_PIN, OUTPUT); + digitalWrite(POWER_PIN, LOW); // Power-on LED strip +#endif + strip.begin(); // Allocate NeoPixel buffer + strip.clear(); // Make sure strip is clear + strip.show(); // before measuring battery + +#ifdef BATT_LVL_PIN + // Battery monitoring code does some low-level Gemma-specific stuff... + int i, prev; + uint8_t count; + uint16_t mV; + + pinMode(BATT_LVL_PIN, INPUT); // No pullup + + // Select AREF (PB0) voltage reference + Bandgap (1.8V) input + ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2); // AREF, Bandgap input + ADCSRA = _BV(ADEN) | // Enable ADC + _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0); // 1/128 prescale (125 KHz) + delay(1); // Allow 1 ms settling time as per datasheet + // Bandgap readings may still be settling. Repeated readings are + // taken until four concurrent readings stabilize within 5 mV. + for(prev=9999, count=0; count<4; ) { + for(ADCSRA |= _BV(ADSC); ADCSRA & _BV(ADSC); ); // Start, await ADC conv + i = ADC; // Result + mV = i ? (1100L * 1023 / i) : 0; // Scale to millivolts + if(abs((int)mV - prev) <= 5) count++; // +1 stable reading + else count = 0; // too much change, start over + prev = mV; + } + ADCSRA = 0; // ADC off + mV *= 2; // Because 50% resistor voltage divider (to work w/3.3V MCU) + + uint8_t lvl = (mV >= BATT_MAX_MV) ? NUM_LEDS : // Full (or nearly) + (mV <= BATT_MIN_MV) ? 1 : // Drained + 1 + ((mV - BATT_MIN_MV) * NUM_LEDS + (NUM_LEDS / 2)) / + (BATT_MAX_MV - BATT_MIN_MV + 1); // # LEDs lit (1-NUM_LEDS) + for(uint8_t i=0; i= 200) rampingUp = false; + } else { + // If not ramping up, we're ramping down. This too uses a low-pass + // filter so it eases out, but with different constants so it's a + // slower fade. Also, no addition here because we want it rounding + // down toward zero... + if(!(brightness = (brightness * 15) / 16)) { // Hit zero? + sleep(); // Turn off animation + return; // Start over at top of loop() on wake + } + } + + // Wave positions and colors are updated... + for(w=0; w> 8; // More fixed-point math + g += (wave[w].g * n) >> 8; // Wave color is scaled by 'n' + b += (wave[w].b * n) >> 8; // >>8 is equiv to /256 + } else { // Fade RGB color to white + // In HSV colorspace, this would be tweaking 'saturation' + n = (uint16_t)(y - 128) * 2; // 0-255 affects white level + m = 256 * n; + n = 256 - n; // 1-256 affects RGB level + r += (m + wave[w].r * n) >> 8; + g += (m + wave[w].g * n) >> 8; + b += (m + wave[w].b * n) >> 8; + } + } + } + + // r,g,b are 16-bit types that accumulate brightness from all waves + // that affect this pixel; may exceed 255. Now clip to 0-255 range: + if(r > 255) r = 255; + if(g > 255) g = 255; + if(b > 255) b = 255; + // Store resulting RGB value and we're done with this pixel! + strip.setPixelColor(i, r, g, b); + } + + // Once rendering is complete, a second pass is made through pixel data + // applying gamma correction, for more perceptually linear colors. + // https://learn.adafruit.com/led-tricks-gamma-correction + uint8_t *pixels = strip.getPixels(); // Pointer to LED strip buffer + for(i=0; i> 1]); + return ((hue & 1) ? (h & 15) : (h >> 4)) * 17; +} + +// Gamma-correction table (see earlier comments). It's big and ugly +// and would interrupt trying to read the code, so I put it down here. +const uint8_t gamma[] PROGMEM = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, + 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, + 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, + 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, + 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, + 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, + 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, + 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, + 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, + 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, + 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, + 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 }; diff --git a/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.py b/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.py new file mode 100644 index 000000000..bcda95050 --- /dev/null +++ b/Gemma_Firewalker_Lite_Sneakers/Gemma_Firewalker_Lite_Sneakers.py @@ -0,0 +1,256 @@ +# Gemma "Firewalker Lite" sneakers +# - Uses the following Adafruit parts (X2 for two shoes): +# * Gemma M0 3V microcontroller (#3501) +# * 150 mAh LiPoly battery (#1317) or larger +# * Medium vibration sensor switch (#2384) +# * 60/m NeoPixel RGB LED strip (#1138 or #1461) +# * LiPoly charger such as #1304 +# +# - originally written by Phil Burgess for Gemma using Arduino +# * https://learn.adafruit.com/gemma-led-sneakers + +import board +import neopixel +import time +import digitalio +try: + import urandom as random +except ImportError: + import random + +# Declare a NeoPixel object on led_pin with num_leds as pixels +# No auto-write. +led_pin = board.D1 # Which pin your pixels are connected to +num_leds = 40 # How many LEDs you have +circumference = 40 # Shoe circumference, in pixels, may be > NUM_LEDS +frames_per_second = 50 # Animation frames per second +brightness = 0 # Current wave height +strip = neopixel.NeoPixel(led_pin, num_leds, brightness=1, auto_write=False) +offset = 0 + +# vibration sensor +motion_pin = board.D0 # Pin where vibration switch is connected +pin = digitalio.DigitalInOut(motion_pin) +pin.direction = digitalio.Direction.INPUT +pin.pull = digitalio.Pull.UP +ramping_up = False + +center = 0 # Center point of wave in fixed-point space (0 - 255) +speed = 1 # Distance to move between frames (-128 - +127) +width = 2 # Width from peak to bottom of triangle wave (0 - 128) +hue = 3 # Current wave hue (color) see comments later +hue_target = 4 # Final hue we're aiming for +red = 5 # LED RGB color calculated from hue +green = 6 # LED RGB color calculated from hue +blue = 7 # LED RGB color calculated from hue + +y = 0 +brightness = 0 +count = 0 + +# Gemma can animate 3 of these on 40 LEDs at 50 FPS +# More LEDs and/or more waves will need lower +wave = [0] * 8, [0] * 8, [0] * 8 + +# Note that the speeds of each wave are different prime numbers. +# This avoids repetition as the waves move around the +# perimeter...if they were even numbers or multiples of each +# other, there'd be obvious repetition in the pattern of motion... +# beat frequencies. +n_waves = len(wave) + +# 90 distinct hues (0-89) around color wheel +hue_table = [ 255, 255, 255, 255, 255, 255, 255, 255, 237, 203, + 169, 135, 101, 67, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 18, 52, 86, 120, 154, 188, 222, + 255, 255, 255, 255, 255, 255, 255, 255 ] + +# Gamma-correction table +gammas = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, + 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, + 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, + 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, + 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, + 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, + 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, + 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, + 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, + 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, + 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, + 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 ] + +def h2rgb(hue): + # return value + ret = 0 + + hue %= 90 + h = hue_table[hue >> 1] + + if ( hue & 1 ): + ret = h & 15 + else: + ret = ( h >> 4 ) + + return ( ret * 17 ) + + +def wave_setup(): + global wave + + wave = [ [ 0, 3, 60, 0, 0, 0, 0, 0 ], + [ 0, -5, 45, 0, 0, 0, 0, 0 ], + [ 0, 7, 30, 0, 0, 0, 0, 0 ] ] + + # assign random starting colors to waves + for w in range(n_waves): + wave[w][hue] = wave[w][hue_target] = 90 + random.randint(0,90) + wave[w][red] = h2rgb(wave[w][hue] - 30) + wave[w][green] = h2rgb(wave[w][hue]) + wave[w][blue] = h2rgb(wave[w][hue] + 30) + + +def vibration_detector(): + while ( True ): + if not pin.value: + return(True) + +while ( True ) : + + # wait for vibration sensor to trigger + if ( ramping_up == False ): + ramping_up = vibration_detector() + wave_setup() + + # But it's not just a straight shot that it ramps up. + # This is a low-pass filter...it makes the brightness + # value decelerate as it approaches a target (200 in + # this case). 207 is used here because integers round + # down on division and we'd never reach the target; + # it's an ersatz ceil() function: ((199*7)+200+7)/8 = 200; + brightness = int( ((brightness * 7) + 207) / 8 ) + count += 1 + + if ( count == ( circumference + num_leds + 5) ): + ramping_up = False + count = 0 + + # Wave positions and colors are updated... + for w in range(n_waves): + # Move wave; wraps around ends, is OK! + wave[w][center] += wave[w][speed] + + # Hue not currently changing? + if ( wave[w][hue] == wave[w][hue_target] ): + + # There's a tiny random chance of picking a new hue... + if ( not random.randint(frames_per_second * 4, 255) ): + # Within 1/3 color wheel + wave[w][hue_target] = random.randint(wave[w][hue] - 30, wave[w][hue] + 30) + + # This wave's hue is currently shifting... + else: + + if ( wave[w][hue] < wave[w][hue_target] ): + wave[w][hue] += 1 # Move up or + else: + wave[w][hue] -= 1 # down as needed + + # Reached destination? + if ( wave[w][hue] == wave[w][hue_target] ): + wave[w][hue] = 90 + wave[w][hue] % 90 # Clamp to 90-180 range + wave[w][hue_target] = wave[w][hue] # Copy to target + + wave[w][red] = h2rgb( wave[w][hue] - 30 ) + wave[w][green] = h2rgb( wave[w][hue] ) + wave[w][blue] = h2rgb( wave[w][hue] + 30) + + # Now render the LED strip using the current + # brightness & wave states. + # Each LED in strip is visited just once... + for i in range(num_leds): + + # Transform 'i' (LED number in pixel space) to the + # equivalent point in 8-bit fixed-point space (0-255) + # "* 256" because that would be + # the start of the (N+1)th pixel + # "+ 127" to get pixel center. + x = (i * 256 + 127) / circumference + + # LED assumed off, but wave colors will add up here + r = g = b = 0 + + # For each item in wave[] array... + for w in range(n_waves): + # Calculate distance from pixel center to wave + # center point, using both signed and unsigned + # 8-bit integers... + d1 = int( abs(x - wave[w][center]) ) + d2 = int( abs(x - wave[w][center]) ) + + # Then take the lesser of the two, resulting in + # a distance (0-128) + # that 'wraps around' the ends of the strip as + # necessary...it's a contiguous ring, and waves + # can move smoothly across the gap. + if ( d2 < d1 ): + d1 = d2 # d1 is pixel-to-wave-center distance + + # d2 distance, relative to wave width, is then + # proportional to the wave's brightness at this + # pixel (basic linear y=mx+b stuff). + # Is distance within wave's influence? + # d2 is opposite; distance to wave's end + if ( d1 < wave[w][width] ): + d2 = wave[w][width] - d1 + y = int ( brightness * d2 / wave[w][width] ) # 0 to 200 + + # y is a brightness scale value -- + # proportional to, but not exactly equal + # to, the resulting RGB value. + if ( y < 128 ): # Fade black to RGB color + # In HSV colorspace, this would be + # tweaking 'value' + n = int(y * 2 + 1) # 1-256 + r += ( wave[w][red] * n ) >> 8 # More fixed-point math + g += ( wave[w][green] * n ) >> 8 # Wave color is scaled by 'n' + b += ( wave[w][blue] * n ) >> 8 # >>8 is equiv to /256 + else: # Fade RGB color to white + # In HSV colorspace, this would be tweaking 'saturation' + n = int( ( y - 128 ) * 2 ) # 0-255 affects white level + m = 256 * n + n = 256 - n # 1-256 affects RGB level + r += (m + wave[w][red] * n) >> 8 + g += (m + wave[w][green] * n) >> 8 + b += (m + wave[w][blue] * n) >> 8 + + # r,g,b are 16-bit types that accumulate brightness + # from all waves that affect this pixel; may exceed + # 255. Now clip to 0-255 range: + if ( r > 255 ): + r = 255 + if ( g > 255 ): + g = 255 + if ( b > 255 ): + b = 255 + + # Store resulting RGB value and we're done with + # this pixel! + strip[i] = (r, g, b) + + # Once rendering is complete, a second pass is made + # through pixel data applying gamma correction, for + # more perceptually linear colors. + # https://learn.adafruit.com/led-tricks-gamma-correction + for j in range( num_leds ): + ( red_gamma, green_gamma, blue_gamma ) = strip[j] + red_gamma = gammas[red_gamma] + green_gamma = gammas[green_gamma] + blue_gamma = gammas[blue_gamma] + strip[j] = (red_gamma, green_gamma, blue_gamma) + + strip.show()