Advertisements

Hard Drive Platter Mood Light: Neopixel Firmware

Having accumulated a pile of useless hard drives, it seemed reasonable to harvest the platters and turn them into techie mood lights (remember mood lights?). Some doodling showed that four of Adafruit’s high-density Neopixel strips could stand up inside the 25 mm central hole, completely eliminating the need to putz around with PWM drivers and RGB LEDs: one wire from an Arduino Pro Mini and you’re done:

const byte PIN_NEO = 6;				// DO - data out to first Neopixel

The firmware creates three sine waves with mutually prime periods, then updates the RGB channels with raised-sine values every 10 ms. The PdBase constant defines the common conversion from milliseconds to radians:

const float PdBase = 0.05 * TWO_PI / 1000.0;	// scale time in ms to radians

The leading 0.05 = 1/20 means the sine wave will repeat every 20 s = 20000 ms.

Dividing that period by three small primes produces an RGB pattern that will repeat every 5x11x17 = 935 PdBase cycles = 18.7×103 s = 5.19 h:

const float Period[] = {PdBase/5.0,PdBase/11.0,PdBase/17.0};		// mutually prime periods

That’s languid enough for me, although I admit most of the colors look pretty much the same. Obviously, you can tune for best picture by dinking with a few constants.

A Phase array sets the starting phase to 3π/2 = -90 degrees:

float Phase[] = {3.0 * HALF_PI,3.0 * HALF_PI,3.0 * HALF_PI};		// sin(3π/2 ) = -1, so LEDs are off

Jiggling those starting phases produces a randomized initial color that’s close to dark:

	MillisNow = MillisThen = millis();
	randomSeed(MillisNow + analogRead(6) + analogRead(7));
	printf("Phases: ");
	for (byte i=0; i<3; i++) {
		Phase[i] += random(-1000,1000) * HALF_PI / 1000.0;
		printf("%d ",(int)(Phase[i]*RAD_TO_DEG));
	}
	printf(" deg\r\n");

With all that in hand, converting from time to color goes like this:

uint32_t SineColor(unsigned long t) {
byte rgb[3];

	for (byte i=0; i<3; i++) {
			rgb[i] = Intensity[i]/2.0 * (1 + sin(t * Period[i] + Phase[i]));
	}
	return strip.Color(rgb[0],rgb[1],rgb[2]);
}

The rest of the code scales neatly with the strip length defined in the magic instantiation:

Adafruit_NeoPixel strip = Adafruit_NeoPixel(12, PIN_NEO, NEO_GRB + NEO_KHZ800);

Although the colors change very slowly, shifting them all one chip toward the end of the 144 Neopixel strip at each update produces a noticeable difference that reassured me this whole mess was working:

		for (int i=strip.numPixels()-1; i>0; i--) {
			c = strip.getPixelColor(i-1);
			strip.setPixelColor(i,c);
		}

		c = SineColor(MillisNow);
		strip.setPixelColor(0,c);
		strip.show();

And with that in hand, It Just Worked…

However, it’s worth noting that each Neopixel draws a bit over 60 mA at full white, which works out to a smidge under 9 A for a 144 LED strip. Because they’re PWM devices, the LEDs are either full-on or full-off, so the peak current can actually be 9 A, regardless of any reduced duty cycle to limit the intensity.

The Adafruit driver includes an overall intensity control, but I added an Intensity array with separate values for each channel:

float Intensity[] = {128.0,128.0,128.0};							// pseudo current limit - PWM is always full current

That would allow throttling back the blue LEDs a bit to adjust the overall color temperature, but that’s definitely in the nature of fine tuning.

The Adafruit Neopixel guide recommends a honkin’ big cap right at the strip, plus a 470 Ω decoupling resistor at the first chip’s data input. I think those attempt to tamp down the problems caused by underpowered supplies and crappy wiring; running it at half intensity produced a maximum average current just under the supply’s 3 A limit.

The complete Arduino source code:

// Neopixel mood lighting for hard drive platter sculpture
// Ed Nisley - KE4ANU - November 2015

#include <Adafruit_NeoPixel.h>

//----------
// Pin assignments

const byte PIN_NEO = 6;				// DO - data out to first Neopixel

const byte PIN_HEARTBEAT = 13;		// DO - Arduino LED

//----------
// Constants

const int UPDATEMS = 10ul - 4ul;		// update LEDs only this many ms apart minus loop() overhead

const float PdBase = 0.05 * TWO_PI / 1000.0;	// scale time in ms to radians

const float Period[] = {PdBase/5.0,PdBase/11.0,PdBase/17.0};		// mutually prime periods
float Phase[] = {3.0 * HALF_PI,3.0 * HALF_PI,3.0 * HALF_PI};		// sin(3π/2 ) = -1, so LEDs are off
float Intensity[] = {128.0,128.0,128.0};							// pseudo current limit - PWM is always full current

//----------
// Globals

unsigned long MillisNow;
unsigned long MillisThen;

Adafruit_NeoPixel strip = Adafruit_NeoPixel(12, PIN_NEO, NEO_GRB + NEO_KHZ800);

uint32_t FullWhite = strip.Color(255,255,255);
uint32_t FullOff = strip.Color(0,0,0);

//--- figure color from time in ms

uint32_t SineColor(unsigned long t) {
byte rgb[3];

	for (byte i=0; i<3; i++) {
			rgb[i] = Intensity[i]/2.0 * (1 + sin(t * Period[i] + Phase[i]));
	}
	return strip.Color(rgb[0],rgb[1],rgb[2]);
}

//-- Helper routine for printf()

int s_putc(char c, FILE *t) {
  Serial.write(c);
}

//------------------
// Set the mood

void setup() {
	
uint32_t c;

	pinMode(PIN_HEARTBEAT,OUTPUT);
	digitalWrite(PIN_HEARTBEAT,LOW);	// show we arrived

	Serial.begin(57600);
	fdevopen(&s_putc,0);				// set up serial output for printf()

	printf("Mood Light with Neopixels\r\nEd Nisley - KE4ZNU - November 2015\r\n");
	
/// set up Neopixels
	
	strip.begin();
	strip.show();
	
// lamp test: run a brilliant white dot along the length of the strip
	
	printf("Lamp test: walking white\r\n");
	
	strip.setPixelColor(0,FullWhite);
	strip.show();
	delay(500);
	
	for (int i=1; i<strip.numPixels(); i++) {
		digitalWrite(PIN_HEARTBEAT,HIGH);
		strip.setPixelColor(i-1,FullOff);
		strip.setPixelColor(i,FullWhite);
		strip.show();
		digitalWrite(PIN_HEARTBEAT,LOW);
		delay(500);
	}
	
	MillisNow = MillisThen = millis();
	randomSeed(MillisNow + analogRead(6) + analogRead(7));
	printf("Phases: ");
	for (byte i=0; i<3; i++) {
		Phase[i] += random(-1000,1000) * HALF_PI / 1000.0;
		printf("%d ",(int)(Phase[i]*RAD_TO_DEG));
	}
	printf(" deg\r\n");
	
	c = SineColor(MillisNow);
	printf("Initial time: %08lx -> color: %08lx\r\n",MillisNow,c);
	
	for (int i=0; i<strip.numPixels()-1; i++) {
		strip.setPixelColor(i,c);
	}
	
	strip.show();
	
}

//------------------
// Run the mood

void loop() {
	
byte r,g,b;
uint32_t c;

	MillisNow = millis();
	if ((MillisNow - MillisThen) > UPDATEMS) {
		digitalWrite(PIN_HEARTBEAT,HIGH);
		
		for (int i=strip.numPixels()-1; i>0; i--) {
			c = strip.getPixelColor(i-1);
			strip.setPixelColor(i,c);
		}

		c = SineColor(MillisNow);
		strip.setPixelColor(0,c);
		strip.show();

		MillisThen = MillisNow;
		digitalWrite(PIN_HEARTBEAT,LOW);
	}
}

Advertisements

  1. #1 by madbodger on 2015-11-13 - 09:38

    three sine waves with mutually prime periods

    Biorhythms!

    • #2 by Ed on 2015-11-13 - 10:08

      The woo-woo is strong in this one…

  2. #3 by solaandjin on 2015-11-13 - 13:15

    Arduino floats are 32-bit, giving you 6-7 digits of precision (total). I haven’t done the math, but it’s conceivable you may see decreasing time resolution over long runtimes as the t value starts to swamp the other components.

    Also, your code has been formatted so that the loop() beginning is now at the end of the previous commented line.

    • #4 by Ed on 2015-11-13 - 14:03

      decreasing time resolution over long runtimes

      Good point; hadn’t thought of that.

      Back of the envelope: the Phase values are essentially unity and become irrelevant when t*Period exceeds 106. The Period values are all around 30×10-6 ms-1, so that’ll happen when t > 30×109 ms = 1 year.

      After that, I think the sin() function argument boils down to just shuffling the exponents to line up the significant figures and the waves keep rolling along with different periods.

      Best case: as long as the colors keep changing, nobody will notice. [grin]

      For sure, the power around here blinks often enough to keep any of that from happening.

      your code has been formatted

      Grrrr!

      Thanks…

      • #5 by solaandjin on 2015-11-13 - 15:34

        You won’t even get anywhere near there, as millis() will rollover in 50 days.

        • #6 by Ed on 2015-11-13 - 15:39

          Point taken; so much for my envelope.

          I suppose I just let it run, then, and wait for things to get weird.

  3. #7 by dithermaster on 2015-11-14 - 11:29

    Ed, I wrote math along similar lines for my holiday wreath project: https://www.youtube.com/watch?v=MjPOFIJPyIw

    I used floats in the math and I can confirm the previous commenters points that they lose precision over time. If I don’t reset the Arduino, after a week or two the animation starts getting chunkier (less updates per second due to precision loss). Makes sense; for every doubling in value a float loses another bit of low-end precision. I should add a mod or use unsigned longs for millis but at this point I just reset it when I notice it.

    Ed, I’m glad you’re making good use of the plotter!!! I really enjoyed your restoration posts and am thrilled it has a second life.

    • #8 by Ed on 2015-11-15 - 11:23

      after a week or two

      Mmmph…

      More envelope action: run a local millisecond count and a cycle count, then reset the milliseconds when the cycle count completes a Grand Cycle (5x11x17 = 935) of all three primes. That should happen when all three sin() arguments roll around to 0.000 modulo 2π and, ideally, you’d never notice the transition.

      However, a Grand Cycle requires 935x203 s = 7.48×106 s = 7.5×109 ms = 86.6 day (the first 203 comes from the arbitrary 1/20 scale factor that slows down the base period). Now that you point it out, the millisecond counter exceeds the maximum value of an unsigned long by a smidge under factor of two.

      Howzabout: 5x7x11 for a Grand Cycle of 385 and a maximum millisecond count of 3.08×106 s = 3×109 ms = 36 day = call it a month. That just fits, but probably discards too many digits.

      But, even better, the total Grand Cycle period varies with the cube of the arbitrary scale factor! I should dial that back to, oh, say 1/10, which brings the original 5x11x17 Grand Cycle down to just under 935×103 s = 1×109 ms = 11 day and life will be good, even if the 10 s period for the 1/5 prime cycle cranks up the pace.

      Well, that certainly improves my mood… [heavy sigh]

      good use of the plotter

      It was a big hit at the Mini Maker Faire, with graybeards musing “I haven’t seen one of those in, oh, 30 years” while kids picked their favorite from the plots amassed while getting the demo code running. That entire 3/4 inch pile o’ paper vanished by the end of the day!

      Obviously, I need a different demo program for next year. Maybe reducing a video image to lines to plot automated portraits? OpenCV would make that trivially easy, if I were 10 dB brighter…

      • #9 by solaandjin on 2015-11-15 - 15:27

        I have used something called StippleGen successfully to render convincing portraits in a plotter like device. I used it in points more, but it also has a TSP mode which connects all the dots that might work well if you had more spatial resolution than I was working with.

        • #10 by Ed on 2015-11-15 - 18:22

          A serious downside to the plotted-image notion is the awful racket from the pen lift solenoid: you really want to minimize the number of up-down cycles. The connect-the-dot mode would be the way to go, assuming I can dope out how to fetch and image and mash it down to the essentials.

          Yet another entry for the to-do list…

  1. Hard Drive Platter Mood Light: First Light! | The Smell of Molten Projects in the Morning
  2. Hard Drive Platter Mood Light: Improved Trigonometry | The Smell of Molten Projects in the Morning