Harvesting a stack of hard drive platters and discovering that four Neopixel strips could stand vertically inside the central hole suggested this overall structure:

The model includes a parameter for the number of strips, but not everything respects that. I’m not sure I’ll ever make a three-LED column and five strips won’t fit, so it probably doesn’t matter.
The central pillar holds everything together:

The Neopixel strips slide into those slots, which turned out to be too small to actually print, because the molten plastic pretty much squeezed the slots closed. Some deft pull saw action enlarged them enough to pass the strips, at the cost of tedious hand-fitting and considerable hidden ugliness. Printing the slots slightly larger bangs against the (lack of) printer resolution, because there’s not much wiggle room between the tiny slots and the outer diameter of the column:

The three alignment pin holes along each edge sit 6.944 mm on center, which is what you get when you divide the nominal 1 meter strip length by 144 Neopixels. I’m using knockoff Neopixels from halfway around the planet, but they’re probably pretty close to the real thing (also from halfway around the planet, I’m sure).
All those parts laid out on the platform, along with a fourth set of spacers in case I drop one:

And they print in cyan PETG just like you’d expect:

The round base (on the right) prints bottom-side-up, with bridging from the rim to the central pillar, and came out looking just fine. The top doesn’t have the central post and the pillar doesn’t have the top recess shown in the model: those tweaks will appear in the next iteration.
Each tiny triangular spacer gets an alignment pin glued into its inner surface, then four of them get glued to the pillar. This crash test dummy pillar worked out the dimensions, so it’s squat and ugly:

It’s clamped to a glass plate (smooth side up!) to force the spacers onto on a plane, with the other clamps smashing them against the pillar. All the other spacers get glued in situ atop each platter as it’s installed, which is a definite downside.
Installing the Neopixels before assembling the platters seemed to be the right way to go:

After that, just stack ’em up:

I dry-assembled the upper two spacer sets, so I could pull it apart in case that seemed necessary. Turned out to be a good idea.
And then screw the lid on top to see what it looks like:

That top screw should be a pan-head or something similarly smooth, rather than a random PC case screw. The sacrificial hard drives provided a bunch of Torx screws that would surely look better; most are far too small.
I thought a taller stack would be appropriate, but I kinda like the short, squat aspect ratio.
Now for some wiring…
The OpenSCAD source code:
// Hard Drive Platter Mood Light // Ed Nisley KE4ZNU November 2015 Layout = "Show"; // Build Show Pixel LEDString Platters Pillar Spacers TopCap Base ShowDisks = 2; // number of disks in Show layout //- Extrusion parameters must match reality! ThreadThick = 0.20; ThreadWidth = 0.40; HoleWindage = 0.2; Protrusion = 0.1; // make holes end cleanly inch = 25.4; function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); //---------------------- // Dimensions ID = 0; OD = 1; LENGTH = 2; Platter = [25.0,95.0,1.27]; // hard drive platters LEDStringCount = 3; // number of LEDs on each strip (Show mode looks odd for less than 3) LEDStripCount = 4; // number of strips (verify locating pin holes & suchlike) WireSpace = 1.0; // allowance for wiring along strip ends BaseSize = [40,14,3.0]; // overall base plate outside engine controller slot Pixel = [13.0, 1000 / 144, 0.5]; // smallest indivisible unit of LED strip PixelMargin = [1.0, 1.0, 2.0]; // LED and circuitry atop the strip BeamAngle = 120; // LED viewing angle BeamShape = [ [0,0], [Platter[OD]*cos(BeamAngle/2),-Platter[OD]*sin(BeamAngle/2)], [Platter[OD]*cos(BeamAngle/2), Platter[OD]*sin(BeamAngle/2)] ]; PillarSides = 12*4; PillarCore = Platter[ID] - 2*(Pixel[2] + PixelMargin[2] + 2.0); // LED channel distance across pillar centerline PillarLength = LEDStringCount*Pixel[1] + Platter[LENGTH]; echo(str("Pillar core size: ",PillarCore)); echo(str(" ... length:"),PillarLength); Cap = [Platter[ID] + 4.0,Platter[ID] + 4.0 + 10*2*ThreadWidth,2*WireSpace + 6*ThreadThick]; // cap over top of pillar CapSides = 16; Base = [Platter[ID] + 10.0,0.5*Platter[OD],8.0]; BaseSides = 16; Screw = [2.0,3.0,20.0]; // screws used to secure cap & pillar Spacer = [Platter[ID],(Platter[ID] + 2*8),(Pixel[1] - Platter[LENGTH])]; echo(str("Spacer OD: ",Spacer[OD])); echo(str(" ... thick:",Spacer[LENGTH])); LEDStripProfile = [ [0,0], [Pixel[0]/2,0], [Pixel[0]/2,Pixel[2]], [(Pixel[0]/2 - PixelMargin[0]),Pixel[2]], [(Pixel[0]/2 - PixelMargin[0]),(Pixel[2] + PixelMargin[2])], [-(Pixel[0]/2 - PixelMargin[0]),(Pixel[2] + PixelMargin[2])], [-(Pixel[0]/2 - PixelMargin[0]),Pixel[2]], [-Pixel[0]/2,Pixel[2]], [-Pixel[0]/2,0] ]; //---------------------- // Useful routines module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2); FixDia = Dia / cos(180/Sides); cylinder(r=(FixDia + HoleWindage)/2, h=Height, $fn=Sides); } //- Locating pin hole with glue recess // Default length is two pin diameters on each side of the split PinOD = 1.70; module LocatingPin(Dia=PinOD,Len=0.0) { PinLen = (Len != 0.0) ? Len : (4*Dia); translate([0,0,-ThreadThick]) PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4); translate([0,0,-2*ThreadThick]) PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4); translate([0,0,-(PinLen/2 + ThreadThick)]) PolyCyl(Dia,(PinLen + 2*ThreadThick),4); } //---------------------- // Pieces //-- LED strips module OnePixel() { render() rotate([-90,0,0]) rotate(180) // align result the way you'd expect from the dimensions difference() { linear_extrude(height=Pixel[1],convexity=3) polygon(points=LEDStripProfile); translate([-Pixel[0]/2,Pixel[2],-PixelMargin[0]]) cube([Pixel[0],2*PixelMargin[2],2*PixelMargin[0]]); translate([-Pixel[0]/2,Pixel[2],Pixel[1]-PixelMargin[0]]) cube([Pixel[0],2*PixelMargin[2],2*PixelMargin[0]]); } } module LEDString(n = LEDStringCount) { for (i=[0:n-1]) translate([0,i*Pixel[1]]) // resize([0,Pixel[1] + 2*Protrusion,0]) OnePixel(); } //-- Stack of hard drive platters module Platters(n = LEDStringCount + 1) { color("gold",0.4) for (i=[0:n-1]) { translate([0,0,i*Pixel[1]]) difference() { cylinder(d=Platter[OD],h=Platter[LENGTH],center=false,$fn=PillarSides); cylinder(d=Platter[ID],h=3*Platter[LENGTH],center=true,$fn=PillarSides); } } } //-- Pillar holding the LED strips module Pillar() { difflen = PillarLength + 2*Protrusion; // render(convexity=5) difference() { linear_extrude(height=PillarLength,convexity=4) difference() { rotate(180/(12*4)) circle(d=Platter[ID] - 1*ThreadWidth,$fn=PillarSides); for (i=[0:LEDStripCount-1]) // clearance for LED beamwidth, may not actually cut surface rotate(i*360/LEDStripCount) translate([PillarCore/2,0,0]) polygon(points=BeamShape); for (i=[0:LEDStripCount-1]) // LED front clearance rotate(i*360/LEDStripCount) translate([(PillarCore/2 + Pixel[2]),(Pixel[0] - 2*PixelMargin[0])/2]) rotate(-90) square([Pixel[0] - 2*PixelMargin[0],Platter[ID]]); } for (i=[0:LEDStripCount-1]) // LED strip slots rotate(i*360/LEDStripCount) translate([PillarCore/2,0,-Protrusion]) linear_extrude(height=difflen,convexity=2) rotate(-90) polygon(points=LEDStripProfile); for (i=[0,90]) // wiring recess on top surface rotate(i) translate([0,0,(PillarLength - (WireSpace/2 - Protrusion))]) cube([(PillarCore + 2*Protrusion),Pixel[0] - 2*PixelMargin[0],WireSpace],center=true); for (i=[0:LEDStripCount-1]) // wiring recess on bottom surface rotate(i*90) translate([PillarCore/2 - (WireSpace - Protrusion)/2,0,WireSpace/2 - Protrusion]) cube([WireSpace + Protrusion,Pixel[0] - 2*PixelMargin[0],WireSpace],center=true); for (j=[0:LEDStringCount-1]) // platter spacer alignment pins for (i=[0:LEDStripCount-1]) rotate(i*360/LEDStripCount + 180/LEDStripCount) translate([(Platter[ID] - 1*ThreadWidth)/2,0,(j*Pixel[1] + Pixel[1]/2 + Platter[LENGTH]/2)]) rotate([0,90,0]) rotate(45) LocatingPin(); translate([0,0,-Protrusion]) // central screw hole rotate(180/4) PolyCyl(Screw[ID],difflen,4); if (false) for (i=[-1,1]) // vertical wire channels rotate(i*360/LEDStripCount + 180/LEDStripCount) translate([PillarCore/2 - 2.0,0,-Protrusion]) PolyCyl(2.0,difflen,4); for (i=[-1,1]) // locating pins rotate(i*360/LEDStripCount - 180/LEDStripCount) translate([PillarCore/2 - 2.0,0,0]) LocatingPin(); } } //-- Spacers to separate platters module Spacers() { difference() { linear_extrude(height=Spacer[LENGTH],convexity=4) difference() { rotate(180/PillarSides) circle(d=Spacer[OD],$fn=PillarSides); for (i=[0:LEDStripCount-1]) // clearance for LED beamwidth, may not actually cut surface rotate(i*360/LEDStripCount) translate([PillarCore/2,0,0]) polygon(points=BeamShape); for (i=[0:LEDStripCount-1]) // LED front clearance rotate(i*360/LEDStripCount) translate([(PillarCore/2 + Pixel[2]),(Pixel[0] - 2*PixelMargin[0])/2]) rotate(-90) square([Pixel[0] - 2*PixelMargin[0],Platter[ID]]); rotate(180/PillarSides) circle(d=Spacer[ID],$fn=PillarSides); // central pillar fits in the hole } for (i=[0:LEDStripCount-1]) rotate(i*360/LEDStripCount + 180/LEDStripCount) translate([Platter[ID]/2,0,(Pixel[1] - Platter[LENGTH])/2]) rotate([0,90,0]) rotate(45) LocatingPin(); } } //-- Cap over top of pillar module TopCap() { difference() { cylinder(d1=(Cap[OD] + Cap[ID])/2,d2=Cap[OD],h=Cap[LENGTH],$fn=CapSides); // outer lid translate([0,0,-Protrusion]) PolyCyl(Screw[ID],Cap[LENGTH] + WireSpace + Protrusion,4); // screw hole translate([0,0,Cap[LENGTH] - 2*WireSpace]) difference() { cylinder(d=Cap[ID],h=2*Cap[LENGTH],$fn=CapSides); // cutout cylinder(d=2*Screw[OD],h=Cap[LENGTH],$fn=CapSides); // boss } translate([0,0,Cap[LENGTH] - ThreadThick]) cylinder(d=Cap[ID]/2,h=ThreadThick + Protrusion,$fn=CapSides); // recess boss } } //-- Base below pillar module Base() { SideWidth = 0.5*Base[OD]*sin(180/BaseSides); // close enough difference() { union() { difference() { cylinder(d=Base[OD],h=Base[LENGTH],$fn=BaseSides); // outer base translate([0,0,6*ThreadThick]) // main cutout cylinder(d=Base[ID],h=Base[LENGTH],$fn=BaseSides); translate([-SideWidth/2,0,6*ThreadThick]) // cable port cube([SideWidth,Base[OD],Base[LENGTH]]); } translate([0,0,Base[LENGTH]/2]) // pillar support is recessed below rim cube([PillarCore,PillarCore,Base[LENGTH] - ThreadThick],center=true); } for (i=[0:LEDStripCount-1]) // wiring recesses rotate(i*90) translate([PillarCore/2 - (WireSpace - Protrusion)/2,0,Base[LENGTH] - WireSpace/2]) cube([WireSpace + Protrusion,PillarCore - 4*WireSpace,WireSpace],center=true); translate([0,0,-Protrusion]) PolyCyl(Screw[ID],2*Base[LENGTH],4); // screw hole translate([0,0,-Protrusion]) // screw head recess PolyCyl(8.5,5.0 + Protrusion,$fn=6); for (i=[-1,1]) // locating pins rotate(i*360/LEDStripCount - 180/LEDStripCount) translate([PillarCore/2 - 2.0,0,Base[LENGTH] - ThreadThick]) LocatingPin(); } } //---------------------- // Build it if (Layout == "Pixel") OnePixel(); if (Layout == "LEDString") LEDString(LEDStringCount); if (Layout == "Platters") Platters(LEDStringCount + 1); if (Layout == "Pillar") Pillar(LEDStringCount); if (Layout == "TopCap") TopCap(); if (Layout == "Base") Base(); if (Layout == "Spacers") Spacers(); if (Layout == "Show") { Pillar(); for (i=[0:LEDStripCount-1]) // LED strips rotate(i*360/LEDStripCount) translate([PillarCore/2,0,Platter[LENGTH]/2]) rotate([90,0,90]) color("lightblue") LEDString(); if (true) for (j=[0:max(1,ShowDisks - 2)]) // spacers translate([0,0,(j*Pixel[1] + Platter[LENGTH])]) color("cyan") Spacers(); for (j=[0:max(2,ShowDisks - 2)]) // spacer alignment pins for (i=[0:LEDStripCount-1]) rotate(i*360/LEDStripCount + 180/LEDStripCount) translate([(Platter[ID] - 1*ThreadWidth)/2,0,(j*Pixel[1] + Pixel[1]/2 + Platter[LENGTH]/2)]) rotate([0,90,0]) rotate(45) color("Yellow",0.25) LocatingPin(Len=4); translate([0,0,PillarLength + 3*Cap[LENGTH]]) rotate([180,0,0]) TopCap(); translate([0,0,-3*Base[LENGTH]]) Base(); if (ShowDisks > 0) Platters(ShowDisks); } // Ad-hoc build layout if (Layout == "Build") { Pillar(); translate([0,Cap[OD],0]) TopCap(); translate([0,-Base[OD],Base[LENGTH]]) rotate([0,180,0]) Base(); Ybase = Spacer[OD] * (LEDStringCount%2 ? (LEDStringCount - 1) : (LEDStringCount - 2)) / 4; for (i=[0:LEDStringCount]) // build one extra set of spacers! translate([(i%2 ? 1 : -1)*(Spacer[OD] + Base[OD])/2, // alternate X sides to shrink Y space (i%2 ? i-1 : i)*Spacer[OD]/2 - Ybase, // same Y for even-odd pairs in X 0]) Spacers(); }
The original doodles showing this might work, along with some ideas that wouldn’t:



Nifty. Are they going to spin? Is it going to have a slip-ring?
Nope, it’s a static display: all the electronics attach to the plastic hub that clamps the disks.
I thought about mounting the head arm off to one side and having the Arduino wave it around, but came to my senses; the disk spacing doesn’t work out neatly and it’d need those intense stator magnets.
Watching the LEDs change color is as good as it gets… [grin]