Being cheap and lazy, I remembered that I had an addressable 1m LED strip that I bought several years ago and never really tried for anything interesting. After a night's hacking, I came up with a few different animations:
High art, it is not, but I picked up some new ideas, especially about how to sort colors.
I also learned that these strips have highly variable builds. Not only were the colors of the wires completely different from their documentation (but easy to figure out from the physical layout), but even the colors of the LEDs were hooked up in a different order than the WS2801 data sheet would have implied.
The code is fairly trivial, but given the reaction of my wife and sons, I think I got a pretty good return on those 450 lines of code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Derived from Nathan Seidle's LED strip demo code | |
* | |
* You will need to connect 5V/Gnd from the Arduino (USB power seems to be sufficient). | |
* | |
* For the data pins, please pay attention to the arrow printed on the strip. You will need to connect to | |
* the end that is the begining of the arrows (data connection)---> | |
* | |
* The documented wire colors are very unreliable. Follow the labels printed on the LED strip. | |
* | |
* Color sorting adapted from http://www.alanzucconi.com/2015/09/30/colour-sorting/ | |
* Color conversion adapted from https://github.com/ratkins/RGBConverter | |
*/ | |
int SDI = 2; // Data | |
int CKI = 3; // Clock | |
int ledPin = 13; //On board LED | |
// #define SINGLE_ANIMATION BubbleSortAnimation | |
// #define ROTATING_ANIMATIONS | |
#define STRIP_LENGTH 32 //32 LEDs on this strip | |
long strip_colors[STRIP_LENGTH]; | |
class Animation { | |
public: | |
virtual ~Animation() {} | |
virtual void generateNextFrame() { } | |
virtual bool suggestSwitch() { return animationTimeout(); } | |
virtual int speed() { return 250; } | |
virtual int finalHold() { return 1; } | |
protected: | |
long randomColor(byte maxRed=0xFF, byte maxGreen=0xFF, byte maxBlue=0xFF); | |
}; | |
Animation * gCurrentAnimation; | |
unsigned long gAnimationTime; | |
class ShiftRandomAnimation : public Animation { | |
public: | |
ShiftRandomAnimation(); | |
virtual void generateNextFrame(); | |
virtual int speed() { return 250; } | |
}; | |
class SnowAnimation : public Animation { | |
public: | |
SnowAnimation(); | |
virtual void generateNextFrame(); | |
virtual bool suggestSwitch(); | |
virtual int speed() { return 30; } | |
private: | |
int height; | |
int density; | |
long color; | |
}; | |
class CompromiseAnimation : public Animation { | |
public: | |
CompromiseAnimation(); | |
virtual void generateNextFrame(); | |
virtual bool suggestSwitch(); | |
virtual int speed() { return 100; } | |
virtual int finalHold() { return 500; } | |
private: | |
long startColor; | |
long endColor; | |
long curColor; | |
int startPos; | |
int endPos; | |
int curPos; | |
int direction; | |
long compromiseColor(long end, long start); | |
}; | |
class NoiseAnimation : public Animation { | |
public: | |
NoiseAnimation(); | |
virtual void generateNextFrame(); | |
virtual int speed() { return 100; } | |
private: | |
int noisiness; | |
}; | |
class BubbleSortAnimation : public Animation { | |
public: | |
BubbleSortAnimation(); | |
virtual void generateNextFrame(); | |
virtual bool suggestSwitch() { return sorted; } | |
virtual int speed() { return 20; } | |
virtual int finalHold() { return 2000; } | |
private: | |
bool sorted; | |
}; | |
void startNextAnimation() | |
{ | |
if (gCurrentAnimation) | |
delay(gCurrentAnimation->finalHold()); | |
gAnimationTime = millis(); | |
for(int x = 0 ; x < STRIP_LENGTH ; x++) | |
strip_colors[x] = 0; | |
delete gCurrentAnimation; | |
#ifdef SINGLE_ANIMATION | |
gCurrentAnimation = new SINGLE_ANIMATION; | |
#else | |
#ifdef ROTATING_ANIMATIONS | |
static int anim = -1; | |
switch (anim = (anim+1)%5) { | |
#else | |
switch (random(5)) { | |
#endif | |
case 1: | |
gCurrentAnimation = new SnowAnimation; | |
break; | |
case 2: | |
gCurrentAnimation = new NoiseAnimation; | |
break; | |
case 3: | |
gCurrentAnimation = new CompromiseAnimation; | |
break; | |
case 4: | |
gCurrentAnimation = new BubbleSortAnimation; | |
break; | |
default: | |
gCurrentAnimation = new ShiftRandomAnimation; | |
break; | |
} | |
#endif | |
post_frame(); | |
} | |
bool animationTimeout() | |
{ | |
unsigned long now = millis(); | |
if (now-gAnimationTime > 30000) | |
return true; | |
else if ((now ^ gAnimationTime) & 0x8000000) | |
return true; | |
else | |
return false; | |
} | |
void setup() { | |
pinMode(SDI, OUTPUT); | |
pinMode(CKI, OUTPUT); | |
pinMode(ledPin, OUTPUT); | |
randomSeed(analogRead(0)); | |
startNextAnimation(); | |
delay(2000); | |
} | |
void loop() { | |
gCurrentAnimation->generateNextFrame(); | |
post_frame(); //Push the current color frame to the strip | |
digitalWrite(ledPin, HIGH); // set the LED on | |
delay(gCurrentAnimation->speed()); | |
digitalWrite(ledPin, LOW); // set the LED off | |
delay(gCurrentAnimation->speed()); | |
if (gCurrentAnimation->suggestSwitch()) { | |
delay(2000); | |
startNextAnimation(); | |
} | |
} | |
//Takes the current strip color array and pushes it out | |
void post_frame (void) { | |
//Each LED requires 24 bits of data | |
//MSB: R7, R6, R5..., G7, G6..., B7, B6... B0 | |
//Once the 24 bits have been delivered, the IC immediately relays these bits to its neighbor | |
//Pulling the clock low for 500us or more causes the IC to post the data. | |
for(int LED_number = 0 ; LED_number < STRIP_LENGTH ; LED_number++) { | |
long this_led_color = strip_colors[LED_number]; //24 bits of color data | |
// My LED strip actually has colors ordered BBBBBBBBGGGGGGGGRRRRRRRR | |
this_led_color = ((this_led_color & 0xFF) << 16) | (this_led_color & 0xFF00L) | ((this_led_color & 0xFF0000L) >> 16); | |
for(byte color_bit = 23 ; color_bit != 255 ; color_bit--) { | |
//Feed color bit 23 first (red data MSB) | |
digitalWrite(CKI, LOW); //Only change data when clock is low | |
long mask = 1L << color_bit; | |
//The 1'L' forces the 1 to start as a 32 bit number, otherwise it defaults to 16-bit. | |
if(this_led_color & mask) | |
digitalWrite(SDI, HIGH); | |
else | |
digitalWrite(SDI, LOW); | |
digitalWrite(CKI, HIGH); //Data is latched when clock goes high | |
} | |
} | |
//Pull clock low to put strip into reset/post mode | |
digitalWrite(CKI, LOW); | |
delayMicroseconds(500); //Wait for 500us to go into reset | |
} | |
long | |
Animation::randomColor(byte maxRed, byte maxGreen, byte maxBlue) | |
{ | |
return (random(maxRed) << 16) | |
| (random(maxGreen) << 8) | |
| (random(maxBlue)); | |
} | |
ShiftRandomAnimation::ShiftRandomAnimation() | |
{ | |
//Pre-fill the color array with known values | |
strip_colors[0] = 0xFF0000; //Bright Red | |
strip_colors[1] = 0x00FF00; //Bright Green | |
strip_colors[2] = 0x0000FF; //Bright Blue | |
strip_colors[3] = 0x010000; //Faint red | |
strip_colors[4] = 0x800000; //1/2 red (0x80 = 128 out of 256) | |
} | |
//Throws random colors down the strip array | |
void | |
ShiftRandomAnimation::generateNextFrame() { | |
int x; | |
//First, shuffle all the current colors down one spot on the strip | |
for(x = (STRIP_LENGTH - 1) ; x > 0 ; x--) | |
strip_colors[x] = strip_colors[x - 1]; | |
//Now form a new RGB color | |
long new_color = randomColor(); | |
strip_colors[0] = new_color; //Add the new random color to the strip | |
} | |
SnowAnimation::SnowAnimation() | |
: height(STRIP_LENGTH-1), density(random(10, 20)) | |
{ | |
color = randomColor(0x3F, 0x3F) | 0xC0C000; // Blue-ish | |
} | |
void | |
SnowAnimation::generateNextFrame() | |
{ | |
for (int i=height; i>0; --i) { | |
strip_colors[i] = strip_colors[i-1]; | |
} | |
strip_colors[0] = (random(100) < density) ? color : 0; | |
if (strip_colors[height]) { | |
--height; | |
} | |
} | |
bool | |
SnowAnimation::suggestSwitch() | |
{ | |
return height < (STRIP_LENGTH>>2); | |
} | |
CompromiseAnimation::CompromiseAnimation() | |
: startColor(randomColor()), endColor(randomColor()), startPos(0), endPos(STRIP_LENGTH-1), curPos(startPos), direction(-1) | |
{ | |
switch (random(6)) { | |
case 0: | |
startColor &= 0xFF0000; | |
endColor &= 0x00FF00; | |
break; | |
case 1: | |
startColor &= 0xFF0000; | |
endColor &= 0x0000FF; | |
break; | |
case 2: | |
startColor &= 0x00FF00; | |
endColor &= 0x0000FF; | |
break; | |
case 3: | |
startColor &= 0x00FF00; | |
endColor &= 0xFF0000; | |
break; | |
case 4: | |
startColor &= 0x0000FF; | |
endColor &= 0xFF0000; | |
break; | |
case 5: | |
startColor &= 0x0000FF; | |
endColor &= 0x00FF00; | |
break; | |
} | |
curColor = startColor; | |
strip_colors[startPos] = startColor; | |
strip_colors[endPos--] = endColor; | |
} | |
long | |
CompromiseAnimation::compromiseColor(long end, long start) | |
{ | |
long newRed = (((end >> 16) & 0xFF)*9+((start >> 16) & 0xFF)) / 10; | |
long newGreen = (((end >> 8) & 0xFF)*9+((start >> 8) & 0xFF)) / 10; | |
long newBlue = (( end & 0xFF)*9+( start & 0xFF)) / 10; | |
return (newRed << 16) | (newGreen << 8) | newBlue; | |
} | |
void | |
CompromiseAnimation::generateNextFrame() | |
{ | |
strip_colors[curPos] = curColor; | |
if (direction < 0) { | |
if (curPos == startPos) { | |
startColor = curColor; | |
direction = 1; | |
curColor = compromiseColor(endColor, startColor); | |
curPos = ++startPos; | |
} else { | |
if (curPos < endPos) { | |
strip_colors[curPos+1] = 0; | |
} | |
--curPos; | |
} | |
} else { | |
if (curPos == endPos) { | |
endColor = curColor; | |
direction = -1; | |
curColor = compromiseColor(startColor, endColor); | |
curPos = --endPos; | |
} else { | |
if (curPos > startPos) { | |
strip_colors[curPos-1] = 0; | |
} | |
++curPos; | |
} | |
} | |
} | |
bool | |
CompromiseAnimation::suggestSwitch() | |
{ | |
return startPos >= endPos; | |
} | |
NoiseAnimation::NoiseAnimation() | |
: noisiness(random(20, 60)) | |
{ | |
for (int i=0; i<STRIP_LENGTH; ++i) | |
strip_colors[i] = randomColor(); | |
} | |
void | |
NoiseAnimation::generateNextFrame() | |
{ | |
for (int i=0; i<STRIP_LENGTH; ++i) | |
if (random(100) < noisiness) | |
strip_colors[i] = randomColor(); | |
} | |
BubbleSortAnimation::BubbleSortAnimation() | |
: sorted(false) | |
{ | |
for (int i=0; i<STRIP_LENGTH; ++i) | |
strip_colors[i] = randomColor(); | |
} | |
void | |
rgb2hsv(long color, float rgb[], float hsv[]) { | |
float r = ((color >> 16) & 0xFF) / 255.0; | |
float g = ((color >> 8) & 0xFF) / 255.0; | |
float b = ( color & 0xFF) / 255.0; | |
float cmax = max(r,g); cmax = max(cmax,b); | |
float cmin = min(r,g); cmin = min(cmin,b); | |
float h; | |
float s; | |
float v = cmax; | |
float d = cmax - cmin; | |
s = cmax == 0 ? 0 : d / cmax; | |
if (cmax == cmin) { | |
h = 0; // achromatic | |
} else { | |
if (cmax == r) { | |
h = (g - b) / d + (g < b ? 6 : 0); | |
} else if (cmax == g) { | |
h = (b - r) / d + 2; | |
} else if (cmax == b) { | |
h = (r - g) / d + 4; | |
} | |
h /= 6; | |
} | |
rgb[0] = r; | |
rgb[1] = g; | |
rgb[2] = b; | |
hsv[0] = h; | |
hsv[1] = s; | |
hsv[2] = v; | |
} | |
long | |
colorIndex(long color, int h_quant, int l_quant, int v_quant) | |
{ | |
float hsv[3], rgb[3]; | |
rgb2hsv(color, rgb, hsv); | |
float lum = sqrt( .241 * rgb[0] + .691 * rgb[1] + .068 * rgb[2]); | |
long hq = int(hsv[0] * h_quant); | |
long lq = l_quant - int(lum * l_quant); | |
long vq = int(hsv[2] * v_quant); | |
#if 0 | |
if (hq % 2 == 1) { | |
vq = v_quant - vq; | |
lq = l_quant - lq; | |
} | |
#endif | |
return (hq << 20) | (lq << 10) | vq; | |
} | |
long | |
colorIndex(long color) | |
{ | |
return colorIndex(color, 8, 8, 256); | |
} | |
void | |
BubbleSortAnimation::generateNextFrame() | |
{ | |
for (int i=0; i<STRIP_LENGTH-1; ++i) | |
if (colorIndex(strip_colors[i]) < colorIndex(strip_colors[i+1])) { | |
long swap = strip_colors[i]; | |
strip_colors[i] = strip_colors[i+1]; | |
strip_colors[i+1] = swap; | |
return; | |
} | |
sorted = true; | |
} |