(This article was supposed to be written a lot earlier, but food poisoning had a discouraging effect on the author.)

Every New Year's Eve, up to a million visitors crowd Times Square NYC to see a massive crystal and LED ball descend at the stroke of midnight. The ball has grown increasingly large and complex over the past few years, now 12 feet in diameter and over 32,000 LEDs. While an impressive sight, it's not easy to drop by Times Square when you live on the West Coast. And seeing the ball drop on TV, 3 hours early, just isn't the same. So why not build your own LED ball?

Dropping our own LED ball is a 4-year macetech LLC tradition (well, four drops, and three elapsed years to be precise). The first ShiftBrite LED Modules were produced in early 2008. By the end of 2008, we were doing higher volume production and had founded a new company, macetech LLC. ShiftBrites are a small PCB with an RGB LED and a PWM controller, capable of 1023 levels of brightness for each of the red, green, and blue channels. They can be chained together with 6-pin cables, and sent color commands by a microcontroller. Only 4 data pins are needed to fully control up to 255 pixels in a single chain.

NYE Ball 2008/2009 The first ball was a spur of the moment idea during Christmas vacation. I happened to have a "stellated icosahedron" made from a bunch of drinking straws (it was something to do between interviews during a few months of unemployment in 2006). A stellated icosahedron has 32 intersections where ShiftBrites could be placed...coincidentally, I had a bunch of used ShiftBrites that had been sitting out in the rain for a Christmas light display on our front fence. I had seen an article about the new six-foot, LED and crystal New Year's Eve ball, and decided to give it a try. A few minutes, an Arduino, and some zip ties later, and the ball was ready. We made a 20 foot pole out of electrical conduit, a pulley, and some light rope. Unfortunately no video of the actual drop, but the neighbors all came out to celebrate!

NYE Ball 2009/2010 The second ball was inspired by a couple of new macetech products, the ShiftBar and the Satellite Module 001. The ShiftBar allowed controlling much brighter LEDs than the ShiftBrite, and each Satellite 001 module was as bright as 15-20 ShiftBrites. Since we used six of them in a cube shape, the second ball was about three times brighter than the old ball.

The ball itself had to be something other than the original drinking straw construction, since my then-roommate/current-cofounder's cat had located the 2008/2009 ball. The cat already loved chewing wires, straws, and zip ties all on their own...

So new construction was based on the Sparkleball. A simple, yet geometrically interesting shape made from plastic drinking cups. It's in fairly widespread use by Christmas and some wedding decorators...simply attach a lot of drinking cups into a sphere, fill with lights, and hang it someplace. There are a lot of ways to go about fastening the cups together, the type of lights to use, the size of the cups, etc. For this ball, I used 120 16-ounce plastic cups. The Arduino and ShiftBars were attached to the top inner surface of the ball, and the Satellite Modules hung down to the center. We were at a friends house and couldn't set up a pole, so we threw a rope over a tree branch.





NYE Ball 2010/2011 In a way, this ball combines aspects of both previous balls. The structure is still based on plastic cups, but it has a more geometric pattern: a spherical dodecahedron. It was constructed by connecting seven cups into six-sided modules, and then connecting twenty of these modules to each other through two cups. Each module has a MegaBrite LED Module at its center, providing a greater degree of animation control compared to the LED core idea of the previous year. I tend to recycle a lot of code: this is actually the same code used on the MegaBrite Tree from the same year.





NYE Ball 2011/2012 No new ball this year! We actually saved last year's ball and will just use it again. Better than wasting another 140 plastic cups.

Implementation Details Here is some information that might help with other NYE ball projects out there.

First, the (messy) code from last year's ball (should work with any ShiftBrite/ShiftBar/MegaBrite/OctoBar project):

Put this code in your main sketch.



#include <math.h> #include <avr/pgmspace.h> #include "rgbdefs.h" #define clockpin 13 // CI #define enablepin 10 // EI #define latchpin 9 // LI #define datapin 11 #define NumLEDs 20 unsigned long SB_CommandPacket; int SB_CommandMode; int SB_BlueCommand; int SB_RedCommand; int SB_GreenCommand; int currentpattern = 2; int channelnumber = 0; int cyclecount = 0; byte fadecount = 0; byte spincount = 0; float spinspeed = 4; byte speedvalue = 0; int channelvalue[4] = {0}; int spinpos = 0; int oldred = 0; int oldgreen = 0; int oldblue = 0; int newred; int newgreen; int newblue; int LEDChannels[NumLEDs][3] = {0}; PROGMEM prog_uchar Gradients[4][24] = { {0,0,0, 25,25,0, 50,50,0, 100,100,0, 150,150,0, 200,200,0, 255,255,0, 0,0,0}, {255,0,0, 255,0,0, 255,0,0, 255,0,0, 0,0,255, 0,0,255, 0,0,255, 0,0,255}, {0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,128,0, 0,255,0, 0,255,0, 0,255,0}, {255,0,0, 255,255,0, 0,255,0, 0,255,255, 0,0,255, 255,0,255, 0,0,0, 0,0,0} }; byte PlasmaGradient[8][3] = {0}; byte treerows[11] = {1, 1, 3, 3, 5, 5, 5, 7, 7, 7, 7}; void SelectGradient(int gradient) { for (int d = 0; d < 8; d++) { PlasmaGradient[d][0] = pgm_read_byte_near(Gradients[gradient] + d * 3); PlasmaGradient[d][1] = pgm_read_byte_near(Gradients[gradient] + d * 3 + 1); PlasmaGradient[d][2] = pgm_read_byte_near(Gradients[gradient] + d * 3 + 2); } } void setup() { pinMode(datapin, OUTPUT); pinMode(latchpin, OUTPUT); pinMode(enablepin, OUTPUT); pinMode(clockpin, OUTPUT); SPCR = (1<<SPE)|(1<<MSTR)|(0<<SPR1)|(1<<SPR0); digitalWrite(latchpin, LOW); digitalWrite(enablepin, LOW); Serial.begin(9600); randomSeed(analogRead(0)); } void SB_SendPacket() { if (SB_CommandMode == B01) { SB_RedCommand = 127; SB_GreenCommand = 127; SB_BlueCommand = 127; } SPDR = SB_CommandMode << 6 | SB_BlueCommand>>4; while(!(SPSR & (1<<SPIF))); SPDR = SB_BlueCommand<<4 | SB_RedCommand>>6; while(!(SPSR & (1<<SPIF))); SPDR = SB_RedCommand << 2 | SB_GreenCommand>>8; while(!(SPSR & (1<<SPIF))); SPDR = SB_GreenCommand; while(!(SPSR & (1<<SPIF))); } void WriteLEDArray() { SB_CommandMode = B00; // Write to PWM control registers for (int h1=0;h1<NumLEDs;h1++) { SB_RedCommand = LEDChannels[NumLEDs-h1-1][0]; SB_GreenCommand = LEDChannels[NumLEDs-h1-1][1]; SB_BlueCommand = LEDChannels[NumLEDs-h1-1][2]; SB_SendPacket(); } delayMicroseconds(3); PORTB |= (1 << 1); PORTB |= (1 << 2); delayMicroseconds(3); PORTB &= ~(1 << 2); PORTB &= ~(1 << 1); SB_CommandMode = B01; // Write to current control registers for (int z = 0; z < NumLEDs; z++) SB_SendPacket(); delayMicroseconds(3); PORTB |= (1 << 1); delayMicroseconds(3); PORTB &= ~(1 << 1); } void fadeall(int rate, int fromred, int fromgreen, int fromblue, int tored, int togreen, int toblue) { for (int i = 0; i < 17; i++) { for (int j1 = 0; j1 < NumLEDs; j1++) { LEDChannels[j1][0] = (fromred * (16 - i) + tored * i)/4; LEDChannels[j1][1] = (fromgreen * (16 - i) + togreen * i)/4; LEDChannels[j1][2] = (fromblue * (16 - i) + toblue * i)/4; } WriteLEDArray(); delay(rate); } } void randomstrobe(int rate, int blinks) { for (int i = 0; i<blinks; i++) { int randomLEDx = random(0,NumLEDs); LEDChannels[randomLEDx][0] = 1023; LEDChannels[randomLEDx][1] = 1023; LEDChannels[randomLEDx][2] = 1023; WriteLEDArray(); delay(rate); for (int j1 = 0; j1 < NumLEDs; j1++) { LEDChannels[j1][0] = 0; LEDChannels[j1][1] = 0; LEDChannels[j1][2] = 0; } delay(rate); } } void noisy(int blinks, int rate, int red, int green, int blue) { for (int i = 0; i<blinks; i++) { for (int j1 = 0; j1 < NumLEDs; j1++) { int rndval = random(0,10); LEDChannels[j1][0] = rndval*red/10; LEDChannels[j1][1] = rndval*blue/10; LEDChannels[j1][2] = rndval*green/10; } WriteLEDArray(); delay(rate); } } void fillall(int red, int green, int blue) { for (int i = 0; i < NumLEDs; i++) { LEDChannels[i][0] = red; LEDChannels[i][1] = green; LEDChannels[i][2] = blue; } } void blinkall(int rate, int blinks, int red, int green, int blue) { for (int i = 0; i<blinks; i++) { for (int j1 = 0; j1 < NumLEDs; j1++) { LEDChannels[j1][0] = red; LEDChannels[j1][1] = green; LEDChannels[j1][2] = blue; } WriteLEDArray(); delay(rate); for (int j1 = 0; j1 < NumLEDs; j1++) { LEDChannels[j1][0] = 0; LEDChannels[j1][1] = 0; LEDChannels[j1][2] = 0; } WriteLEDArray(); delay(rate); } } // Mapping functions int MapToTree(int row, int col, int red, int green, int blue) { int newindex = 51; if ((row == 0) || (row == 1)) { if (col == 0) { newindex = row;; } } else if (row == 2) { if (col < 3) { newindex = 4 - col; } } else if (row == 3) { if (col < 3) { newindex = col + 5; } } else if (row == 4) { if (col < 5) { newindex = 12 - col; } } else if (row == 5) { if (col < 5) { newindex = col + 13; } } else if (row == 6) { if (col < 5) { newindex = 22 - col; } } else if (row == 7) { if (col < 7) { newindex = col + 23; } } else if (row == 8) { if (col < 7) { newindex = 36 - col; } } else if (row == 9) { if (col < 7) { newindex = col + 37; } } else if (row == 10) { if (col < 7) { newindex = 50 - col; } } LEDChannels[50-newindex][0] = red; LEDChannels[50-newindex][1] = green; LEDChannels[50-newindex][2] = blue; } void ctree(int rate, int cycles) { for (int i = 0; i < cycles; i++) { fillall(0, 800, 0); MapToTree(0,0,1023,800,0); for (int j = 0; j < 10; j++) { int randval = random(0,50); LEDChannels[randval][0] = 1023; LEDChannels[randval][1] = 0; LEDChannels[randval][2] = 0; } WriteLEDArray(); delay(rate); } } void filltree(int rate, int red, int green, int blue) { for (int i = 10; i >= 0; i--) { for (int j = 0; j < treerows[i]; j++) { MapToTree(i, j, red, green, blue); } WriteLEDArray(); delay(rate); } } RGBType GradientMap(int color) { float GradientRed = 0; float GradientGreen = 0; float GradientBlue = 0; int GradientIndex1 = color/32; int GradientIndex2 = color/32 + 1; int GradientRatio = color % 32; if (GradientIndex2 > 7) GradientIndex2 = 0; GradientRed = (PlasmaGradient[GradientIndex1][0] * (32 - GradientRatio) + PlasmaGradient[GradientIndex2][0] * (GradientRatio)) / 8160.0; GradientGreen = (PlasmaGradient[GradientIndex1][1] * (32 - GradientRatio) + PlasmaGradient[GradientIndex2][1] * (GradientRatio)) / 8160.0; GradientBlue = (PlasmaGradient[GradientIndex1][2] * (32 - GradientRatio) + PlasmaGradient[GradientIndex2][2] * (GradientRatio)) / 8160.0; RGBType RGB; RETURN_RGB(GradientRed, GradientGreen, GradientBlue); } RGBType HSV_to_RGB( HSVType HSV ) { // H is given on [0, 6] or UNDEFINED. S and V are given on [0, 1]. // RGB are each returned on [0, 1]. float h = HSV.H, s = HSV.S, v = HSV.V, m, n, f; int i; RGBType RGB; if (h == UNDEFINED) RETURN_RGB(v, v, v); i = floor(h); f = h - i; if ( !(i&1) ) f = 1 - f; // if i is even m = v * (1 - s); n = v * (1 - s * f); switch (i) { case 6: case 0: RETURN_RGB(v, n, m); case 1: RETURN_RGB(n, v, m); case 2: RETURN_RGB(m, v, n) case 3: RETURN_RGB(m, n, v); case 4: RETURN_RGB(n, m, v); case 5: RETURN_RGB(v, m, n); } } RGBType Plasma(int i, float cntOffset, float frequency, float centerX) { for(int x = 0; x < NumLEDs; x++) { float color = ((1.0 + sin(abs(i - x - centerX) / frequency + cntOffset*3.14159/180)))/2.0; HSVType plasHSV; plasHSV.H = color*6.0; plasHSV.S = 1.0; plasHSV.V = 1.0; return HSV_to_RGB(plasHSV); } } int cntHeart = 0; float cntHeartOffset = 0; // counter for rotating plasma void runAnimLoops() { RGBType mapColor; cntHeartOffset += 4; if (cntHeartOffset >= 360) cntHeartOffset = 0; for (int i = 0; i < NumLEDs; i++) { mapColor = Plasma(i, cntHeartOffset, 8.0, 0); LEDChannels[i][0] = mapColor.R*1023; LEDChannels[i][1] = mapColor.G*1023; LEDChannels[i][2] = mapColor.B*1023; } WriteLEDArray(); } void loop() { int reps; switch (random(0,6)) { case 0: for(int i = 0; i<(random(300,600)); i++) { runAnimLoops(); } break; case 1: blinkall(random(15,45), random(5,20), random(0,10)*100, random(0,10)*100, random(0,10)*100); break; case 2: noisy(random(10,30), random(30,120), random(0,10)*100, random(0,10)*100, random(0,10)*100); break; case 3: randomstrobe(random(1,25),random(50,130)); break; case 4: reps = random(3,10); for (int i = 0; i < reps; i++) { newred = random(10)*20; newgreen = random(10)*20; newblue = random(10)*20; fadeall(20,oldred,oldgreen,oldblue,newred,newgreen,newblue); oldred = newred; oldgreen = newgreen; oldblue = newblue; } break; case 5: reps = random(3,10); for(int i = 0; i < reps; i++) { fadeall(10,255,0,0,0,255,0); delay(250); fadeall(10,0,255,0,0,0,255); delay(250); fadeall(10,0,0,255,255,0,0); delay(250); } fadeall(40,255,0,0,0,0,0); break; case 6: ctree(100,400); break; case 7: for (int i = 0; i < 5; i++) { filltree(25, 1023, 0, 0); filltree(25, 1023, 1023, 0); filltree(25, 0, 1023, 0); filltree(25, 0, 1023, 1023); filltree(25, 0, 0, 1023); filltree(25, 1023, 0, 1023); filltree(25, 1023, 1023, 1023); filltree(25, 0, 0, 0); } break; } delay(1); }

Make a new tab in your sketch, name it rgbdefs.h, and put this code inside:



#define RETURN_HSV(h, s, v) {HSV.H = h; HSV.S = s; HSV.V = v; return HSV;}

#define RETURN_RGB(r, g, b) {RGB.R = r; RGB.G = g; RGB.B = b; return RGB;}

#define UNDEFINED -1

typedef struct {float R, G, B;} RGBType;

typedef struct {float H, S, V;} HSVType;

Building the pole: It's not too difficult. We used two pieces of 10 foot EMT conduit from Home Depot. The coupling isn't very strong, so we braced it with a 1x2 and some gaffer tape...you can't really see it in the dark. :) The most important thing to set the bottom end of the pole carefully. You don't want it to kick out at any time. Put in inside the center of an old tire, jam it deep into the ground, etc. We strapped it to the side of an existing brick fence. Next, you'll need guy wires...nothing too strong needed, just some twine. Use two or three guy wires to stabilize the top of the pole. You'll also need a pulley at the top, and some light rope or strong twine. Suspend the ball from the twine and make sure you can easily raise and lower it before setting everything up. You won't have time to fix it if there's a problem at midnight!

Wiring and Power: Depending on the LED modules used, the power supply can vary. If using Satellite 001's, you would be using a 12V supply. If using ShiftBrites or MegaBrites, you would use between 5.5V and 9V. A Meanwell 7.5V, 8A supply is a pretty commonly available option, plenty of power. Get your LEDs and Arduino all fastened inside the ball, leaving only a power cable running down from the ball to your power supply. Usually, you will want the ball hanging from the rope, and the power cable hanging straight below the ball to the ground. If you fasten the power cable well enough, it's also something extra to pull on in case the ball doesn't want to come down by gravity alone.

Dropping the Ball: Maybe you want to set up an automatic motorized timer system, but the simplest things often work best! Plus, you're only doing this once a year. Let other people count off the seconds, and start lowering the ball. Don't let go of the rope, and pace yourself so the ball reaches the ground at the zero count. Hurray, it's a new year!