USB Current Probe Extender

Having gotten two answers from two USB meters, I figured it was time to get primal:

USB Current-Probe Extender - wiring
USB Current-Probe Extender – wiring

That’s a pair of USB breakout connectors and lengths of nice silicone wire (24 AWG power & 28 AWG data), with just enough slack for a Tek A6302 current probe:

USB Current-Probe Extender - in action
USB Current-Probe Extender – in action

So I can see the actual current waveform of a Glass Tile box running from a bench power supply:

Tiles 2x2 - bench supply - 50 mA-div
Tiles 2×2 – bench supply – 50 mA-div

The top trace is the firmware heartbeat from the Arduino Nano, the middle trace is the SK6812 LED data stream, and the bottom trace is the USB current at 50 mA/div. The current steps downward by about 10 mA (just after the data burst) when one of the tiles changes color and and LED shuts off.

The current probe reveals some mysteries, such as this waveform from a dirt-cheap USB charger:

Tiles 2x2 - anon white charger - 50 mA-div
Tiles 2×2 – anon white charger – 50 mA-div

I wonder why it’s ramming 100 mA current spikes into the circuit, too. At least now I can see what’s going on.

Glass Tiles: Matrix for SK6812 PCBs

Tweaking the glass tile frame for press-fit SK6812 PCBs in the bottom of the array cells:

Glass Tile Frame - cell array - openscad
Glass Tile Frame – cell array – openscad

Which looks like this with the LEDs and brass inserts installed:

Glass Tile - 2x2 array - interior
Glass Tile – 2×2 array – interior

The base holds an Arduino Nano with room for wiring under the cell array:

Glass Tile Frame - base - openscad
Glass Tile Frame – base – openscad

Which looks like this after it’s all wired up:

Glass Tile - 2x2 array - wiring
Glass Tile – 2×2 array – wiring

The weird colors showing through the inserts are from the LEDs. The red thing in the upper left is a silicone insulation snippet. Yes, that’s hot-melt glue holding the Arduino Nano in place and preventing the PCBs from getting frisky.

Soak a handful of glass tiles overnight in paint stripper:

Glass Tiles - paint stripper soak
Glass Tiles – paint stripper soak

Whereupon the adhesive slides right off with the gentle application of a razor scraper. Rinse carefully, dry thoroughly, and snap into place.

Tighten the four M3 SHCS and it’s all good:

Glass Tile - 2x2 array - operating
Glass Tile – 2×2 array – operating

So far, I’ve had two people tell me they don’t know what it is, but they want one:

Glass Tile - various versions
Glass Tile – various versions

The OpenSCAD Customizer lets you set the array size:

Glass Tile Frame - 3x3 - press-fit SK6812 LEDs
Glass Tile Frame – 3×3 – press-fit SK6812 LEDs

However, just because you can do something doesn’t mean you should:

Glass Tile Frame - 6x6 cell array - openscad
Glass Tile Frame – 6×6 cell array – openscad

Something like this might be interesting:

Glass Tile Frame - 2x6 cell array - openscad
Glass Tile Frame – 2×6 cell array – openscad

In round numbers, printing the frame takes about an hour per cell, so a 2×2 array takes three hours and 3×3 array runs around seven hours. A 6×6 frame is just not happening.

The OpenSCAD source code as a GitHub Gist:

// Illuminated Tile Grid
// Ed Nisley - KE4ZNU
// 2020-05
/* [Configuration] */
Layout = "Build"; // [Cell,CellArray,MCU,Base,Show,Build]
Shape = "Square"; // [Square, Pyramid, Cone]
Cells = [2,2];
CellDepth = 15.0;
Inserts = true;
SupportInserts = true;
/* [Hidden] */
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.1; // make holes end cleanly
ID = 0;
OD = 1;
LENGTH = 2;
Tile = [25.0 + 0.1,25.0 + 0.1,4.0];
WallThick = 4*ThreadWidth;
FloorThick = 3.0;
Flange = [2*ThreadWidth,2*ThreadWidth,0]; // ridge supporting tile
Separator = [3*ThreadWidth,3*ThreadWidth,Tile.z - 1]; // between tiles
Screw = [3.0,6.0,3.5]; // M3 SHCS, OD=head, LENGTH=head
Insert = [3.0,4.2,8.0]; // threaded brass insert
ScrewRecess = Screw[LENGTH] + 4*ThreadThick;
LEDPCB = [9.6,9.6,2.9]; // round SK6812, squared-off sides
LED = [5.0 + 2*HoleWindage,5.0 + 2*HoleWindage,1.3];
LEDOffset = [0.0,0.0,0.0]; // if offset from PCB center
CellOAL = [Tile.x,Tile.y,0] + Separator + [0,0,CellDepth] + [0,0,FloorThick];
ArrayOAL = [Cells.x*CellOAL.x,Cells.y*CellOAL.y,CellOAL.z]; // just the LED cells
BlockOAL = ArrayOAL + [2*WallThick,2*WallThick,0]; // LED cells + exterior wall
echo(str("Block OAL: ",BlockOAL));
InsertOC = ArrayOAL - [Insert[OD],Insert[OD],0] - [WallThick,WallThick,0];
echo(str("Insert OC: ",InsertOC));
TapeThick = 1.0;
Arduino = [44.0,18.0,8.0 + TapeThick]; // Arduino Nano to top of USB Mini-B plug
USBPlug = [15.0,11.0,9.0]; // USB Mini-B plug insulator
USBOffset = [0,0,5.0]; // offset from PCB base
WiringSpace = 3.5;
WiringBay = [(Cells.x - 1)*CellOAL.x + LEDPCB.x,(Cells.y - 1)*CellOAL.y + LEDPCB.x,WiringSpace];
PlateOAL = [BlockOAL.x,BlockOAL.y,FloorThick + Arduino.z + WiringSpace]; // allow wiring above Arduino
echo(str("Base Plate: ",PlateOAL));
echo(str("Screw length: ",(PlateOAL.z - ScrewRecess) + Insert.z/2," to ",(PlateOAL.z - ScrewRecess) + Insert.z));
LegendRecess = 1*ThreadThick;
//------------------------
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(d=(FixDia + HoleWindage),h=Height,$fn=Sides);
}
//-----------------------
// Base and optics in single tile
module LEDCone() {
hull() {
translate([0,0,CellDepth + Tile.z/2])
cube(Tile - 2*[Flange.x,Flange.y,0],center=true);
if (Shape == "Square") {
translate([0,0,LEDPCB.z/2])
cube([Tile.x,Tile.y,LEDPCB.z] - 2*[Flange.x,Flange.y,0],center=true);
}
else if (Shape == "Pyramid") {
translate([0,0,LEDPCB.z/2])
cube(LEDPCB,center=true);
}
else if (Shape == "Cone") {
translate([0,0,LEDPCB.z/2])
cylinder(d=1.0*LEDPCB.x,h=LED.z,center=true);
}
else {
echo(str("Whoopsie! Invalid Shape: ",Shape));
cube(5);
}
}
}
// One complete LED cell
module LEDCell() {
difference() {
translate([0,0,CellOAL.z/2])
cube(CellOAL + [Protrusion,Protrusion,0],center=true); // force overlapping adjacent sides!
translate([0,0,CellOAL.z - Separator.z + Tile.z/2])
cube(Tile,center=true);
translate([0,0,LEDPCB.z])
LEDCone();
// cube([LED.x,LED.y,CellOAL.z],center=true);
translate(-LEDOffset + [0,0,-CellOAL.z/2])
rotate(180/8)
PolyCyl(LEDPCB.x,CellOAL.z,8);
}
}
// The whole array of cells
module CellArray() {
difference() {
union() {
translate([CellOAL.x/2 - Cells.x*CellOAL.x/2,CellOAL.y/2 - Cells.y*CellOAL.y/2,0])
for (i=[0:Cells.x - 1], j=[0:Cells.y - 1])
translate([i*CellOAL.x,j*CellOAL.y,0])
LEDCell();
if (Inserts) // bosses
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,0])
rotate(180/8)
cylinder(d=Insert[OD] + 2*WallThick,h=Insert[LENGTH],$fn=8);
}
if (Inserts) // holes
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion])
rotate(180/8)
PolyCyl(Insert[OD],Insert[LENGTH] + FloorThick + Protrusion,8);
}
difference() {
translate([0,0,CellOAL.z/2])
cube(BlockOAL,center=true);
translate([0,0,CellOAL.z])
cube(ArrayOAL + [0,0,2*CellOAL.z],center=true);
}
}
// Arduino bounding box
// Origin at center bottom of PCB
module Controller() {
union() {
translate([0,0,Arduino.z/2])
cube(Arduino,center=true);
translate([Arduino.x/2 - Protrusion,-USBPlug.y/2,USBOffset.z + TapeThick - USBPlug.z/2])
cube(USBPlug + [Protrusion,0,0],center=false);
}
}
// Baseplate
module BasePlate() {
difference() {
translate([0,0,PlateOAL.z/2])
cube(PlateOAL,center=true);
translate([PlateOAL.x/2 - Arduino.x/2 - 2*WallThick,0,FloorThick])
Controller();
translate([PlateOAL.x/2 - Arduino.x/2 - 2*WallThick,0,FloorThick + PlateOAL.z/2])
cube([Arduino.x - 2*2.0,WiringBay.y,PlateOAL.z],center=true); // cutouts beside MCU
translate([0,0,PlateOAL.z - WiringBay.z + PlateOAL.z/2 - Protrusion])
cube([PlateOAL.x - 2*WallThick,WiringBay.y,PlateOAL.z],center=true); // cutout above MCU
translate([0,0,PlateOAL.z - WiringBay.z + PlateOAL.z/2 - Protrusion])
cube([WiringBay.x,PlateOAL.y - 2*WallThick,PlateOAL.z],center=true); // cutout above MCU
if (Inserts)
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion])
rotate(180/8) {
PolyCyl(Screw[ID],2*PlateOAL.z,8);
PolyCyl(Screw[OD],ScrewRecess + Protrusion,8);
}
cube([45,17.0,2*LegendRecess],center=true);
}
linear_extrude(height=2*LegendRecess) {
translate([0,1])
rotate(-0*90) mirror([1,0,0])
text(text="Ed Nisley",size=6,font="Arial:style:Bold",halign="center");
translate([0,-6.5])
rotate(-0*90) mirror([1,0,0])
text(text="softsolder.com",size=4.5,font="Arial:style:Bold",halign="center");
}
Fin = [Screw[OD]/2 - 1.5*ThreadWidth,2*ThreadWidth,ScrewRecess - ThreadThick];
if (Inserts && SupportInserts)
color("Yellow")
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,0]) {
rotate(180/8)
cylinder(d=6*ThreadWidth,h=ThreadThick,$fn=8);
for (a=[0:90:360])
rotate(a)
translate([Fin.x/2 + ThreadWidth/2,0,(ScrewRecess - ThreadThick)/2])
cube(Fin,center=true);
}
}
//-----------------------
// Build things
if (Layout == "Cell")
LEDCell();
else if (Layout == "CellArray")
CellArray();
else if (Layout == "MCU")
Controller();
else if (Layout == "Base")
BasePlate();
else if (Layout == "Show") {
translate([0,0,3*PlateOAL.z])
CellArray();
BasePlate();
translate([PlateOAL.x/2 - Arduino.x/2 - 2*WallThick,0,FloorThick])
color("Orange",0.3)
Controller();
}
else if (Layout == "Build") union() {
translate([0,0.6*BlockOAL.y,0])
CellArray();
translate([0,-0.6*BlockOAL.x,0])
rotate(90)
BasePlate();
}

Glass Tiles: Glow vs. Flash Firmware

Although it’s not obvious in a still picture, the firmware now supports both the continuously changing colors of the Nissan fog lamp (mashed with tweaks from the vacuum tube lights) and the randomly changing colors from the LED matrix, both using SK6812 LEDs rather than the failing WS2812 modules:

Glass Tile - glow vs flash
Glass Tile – glow vs flash

Flash is a misnomer, as the tiles simply change from one color to the next, but I’ve never been adept at picking catchy names. In any event, the glass tiles on the left show nice pastel shades, in contrast to the bright primary(-ish) colors appearing on the right.

The colors are random numbers from 1 to 7, because 0 produces a somewhat ugly dark cell. The SK6812 modules have a white LED in addition to the RGB LEDs in the WS2812 modules, so I replace the “additive white” R+G+B color with the more-or-less true white (warm, for these modules) LED.

The new color goes into a cell picked at random (0 through 3, for 2×2 frames), except if the cell already holds the same color, whereupon a simple XOR flips the colors, except if the cell is already full-on white, whereupon it becomes half-on white to avoid going completely dark.

The glass tiles must change colors at a much slower pace than the 8×8 LED matrix, because there are so few cells; a random delay between 500 ms and 6 s seems about right.

They look really great in a dim room!

The Arduino source code as a GitHub Gist:

// Neopixel lighting for glass tiles
// Ed Nisley - KE4ANU - May 2020
#include <Adafruit_NeoPixel.h>
#include <Entropy.h>
//----------
// Pin assignments
const byte PIN_NEO = A3; // DO - data to first Neopixel
const byte PIN_MODE = 2; // DI - select mode
const byte PIN_SPEED = 3; // DI - select speed
const byte PIN_SELECT = 4; // DO - drive adjacent pins low
const byte PIN_HEARTBEAT = 13; // DO - Arduino LED
//----------
// Constants
// number of pixels
#define PIXELS 9
// lag between adjacent pixels in degrees of slowest period
#define PIXELPHASE 45
// update LEDs only this many ms apart (minus loop() overhead)
#define UPDATEINTERVAL 50ul
// number of steps per cycle, before applying prime factors
#define RESOLUTION 500
//----------
// Globals
// instantiate the Neopixel buffer array
Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXELS, PIN_NEO, NEO_GRBW + NEO_KHZ800);
struct pixcolor_t {
unsigned int Prime;
unsigned int NumSteps;
unsigned int Step;
float StepSize;
float Phase;
byte MaxPWM;
};
unsigned int PlatterSteps;
byte PrimeList[] = {3,5,7,13,19,29};
unsigned int MaxTileTime;
// colors in each LED
enum pixcolors {RED, GREEN, BLUE, WHITE, PIXELSIZE};
struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
uint32_t UniColor;
enum dispmode {GLOW, FLASH}; // based on input pin
unsigned long UpdateMS;
unsigned long MillisNow;
unsigned long MillisThen;
//-- Select three unique primes for the color generator function
// Then compute all the step parameters based on those values
void SetColorGenerators(void) {
Pixels[RED].Prime = PrimeList[random(sizeof(PrimeList))];
do {
Pixels[GREEN].Prime = PrimeList[random(sizeof(PrimeList))];
} while (Pixels[RED].Prime == Pixels[GREEN].Prime);
do {
Pixels[BLUE].Prime = PrimeList[random(sizeof(PrimeList))];
} while (Pixels[BLUE].Prime == Pixels[RED].Prime ||
Pixels[BLUE].Prime == Pixels[GREEN].Prime);
do {
Pixels[WHITE].Prime = PrimeList[random(sizeof(PrimeList))];
} while (Pixels[WHITE].Prime == Pixels[RED].Prime ||
Pixels[WHITE].Prime == Pixels[GREEN].Prime ||
Pixels[WHITE].Prime == Pixels[BLUE].Prime);
if (!digitalRead(PIN_SPEED)) { // force fast for debugging
Pixels[RED].Prime = 3;
Pixels[GREEN].Prime = 5;
Pixels[BLUE].Prime = 7;
Pixels[WHITE].Prime = 11;
}
printf("Primes: %d %d %d %d\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime,Pixels[WHITE].Prime);
Pixels[RED].MaxPWM = 255;
Pixels[GREEN].MaxPWM = 255;
Pixels[BLUE].MaxPWM = 255;
Pixels[WHITE].MaxPWM = 255;
unsigned int PhaseSteps = (unsigned int) ((PIXELPHASE / 360.0) *
RESOLUTION * (unsigned int) max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime));
printf("Pixel phase offset: %d deg = %d steps\r\n",(int)PIXELPHASE,PhaseSteps);
for (byte c=0; c < PIXELSIZE; c++) {
Pixels[c].NumSteps = RESOLUTION * Pixels[c].Prime; // steps per cycle
Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // radians per step
Pixels[c].Step = random(Pixels[c].NumSteps); // current step
Pixels[c].Phase = PhaseSteps * Pixels[c].StepSize;; // phase in radians for this color
printf(" c: %d Steps: %5d Init: %5d Phase: %3d deg",c,Pixels[c].NumSteps,Pixels[c].Step,(int)(Pixels[c].Phase * 360.0 / TWO_PI));
printf(" PWM: %d\r\n",Pixels[c].MaxPWM);
}
}
//-- Helper routine for printf()
int s_putc(char c, FILE *t) {
Serial.write(c);
}
//------------------
// Set the mood
void setup() {
pinMode(PIN_HEARTBEAT,OUTPUT);
digitalWrite(PIN_HEARTBEAT,LOW); // show we arrived
pinMode(PIN_MODE,INPUT_PULLUP);
pinMode(PIN_SPEED,INPUT_PULLUP);
pinMode(PIN_SELECT,OUTPUT);
digitalWrite(PIN_SELECT,LOW); // drive adjacent pins
Serial.begin(57600);
fdevopen(&s_putc,0); // set up serial output for printf()
printf("\r\nAlgorithmic Art Light - Glass Tiles\r\nEd Nisley - KE4ZNU - May 2020\r\n");
printf("Display mode: %s\r\n",digitalRead(PIN_MODE) == GLOW ? "Glow" : "Flash");
printf("Speed: %s\r\n",digitalRead(PIN_SPEED) ? "Normal" : "Override");
Entropy.initialize(); // start up entropy collector
// set up pixels
strip.begin();
strip.show();
// lamp test
printf("Lamp test: flash full-on colors\r\n");
uint32_t FullRGBW = strip.Color(255,255,255,255);
uint32_t FullRGB = strip.Color(255,255,255,0);
uint32_t FullR = strip.Color(255,0,0,0);
uint32_t FullG = strip.Color(0,255,0,0);
uint32_t FullB = strip.Color(0,0,255,0);
uint32_t FullW = strip.Color(0,0,0,255);
uint32_t FullOff = strip.Color(0,0,0,0);
uint32_t TestColors[] = {FullR,FullG,FullB,FullRGB,FullW,FullRGBW,FullOff};
for (byte i=0; i < sizeof(TestColors)/sizeof(uint32_t) ; i++) {
printf(" color: %08lx\r\n",TestColors[i]);
for (int j=0; j < strip.numPixels(); j++) {
strip.setPixelColor(j,TestColors[i]);
}
strip.show();
delay(1000);
}
// while (1) {continue;}; // all LEDs constant for burn-in testing
// get an actual random number
uint32_t rn = Entropy.random();
printf("Random seed: %08lx\r\n",rn);
randomSeed(rn);
// set up the color generators
SetColorGenerators();
MaxTileTime = (digitalRead(PIN_SPEED) ? 6 : 1) * (1000.0 / UPDATEINTERVAL);
UpdateMS = UPDATEINTERVAL;
MillisNow = MillisThen = millis();
}
//------------------
// Run the mood
void loop() {
MillisNow = millis();
if ((MillisNow - MillisThen) >= UpdateMS) { // time for color change?
digitalWrite(PIN_HEARTBEAT,HIGH);
if (digitalRead(PIN_MODE) == GLOW) {
boolean CycleRun = false; // check to see if all cycles have ended
for (byte c=0; c < PIXELSIZE; c++) { // compute next increment for each color
if (++Pixels[c].Step >= Pixels[c].NumSteps) {
Pixels[c].Step = 0;
printf("Cycle %5d steps %5d at %8ld delta %8ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow - MillisThen));
}
else {
CycleRun = true; // this color is still cycling
}
}
if (!CycleRun) {
printf("All cycles ended: setting new color generator values\r\n");
SetColorGenerators();
}
for (int i=0; i < strip.numPixels(); i++) { // for each pixel
byte Value[PIXELSIZE];
for (byte c=0; c < PIXELSIZE; c++) { // ... for each color
Value[c] = (Pixels[c].MaxPWM / 2.0) * (1.0 + sin(Pixels[c].Step * Pixels[c].StepSize - i*Pixels[c].Phase));
}
byte WhiteBias = min(min(Value[RED],Value[GREEN]),Value[BLUE]); // hack to reduce power
UniColor = strip.Color((Value[RED] - WhiteBias) * Pixels[RED].MaxPWM/255,
(Value[GREEN] - WhiteBias) * Pixels[GREEN].MaxPWM/255,
(Value[BLUE] - WhiteBias) * Pixels[BLUE].MaxPWM/255,
WhiteBias * Pixels[WHITE].MaxPWM/255);
strip.setPixelColor(i,UniColor);
}
}
else {
byte c = random(1,8); // exclude 0 = all off, to avoid darkness
printf("Color %d ",c);
byte r = c & 0x04 ? 0xff : 0;
byte g = c & 0x02 ? 0xff : 0;
byte b = c & 0x01 ? 0xff : 0;
byte w = 0;
if (c == 7) { // use white LED instead of R+G+B
r = g = b = 0;
w = 0xff;
}
UniColor = strip.Color(r, g, b, w);
byte i = random(strip.numPixels());
printf("at %d ",i);
if (UniColor == strip.getPixelColor(i)) { // flip color
printf("^ ");
if (w) { // white becomes dim
w = 0x7f;
UniColor = strip.Color(r, g, b, w);
}
else
UniColor ^= 0xffffff00l; // other colors flip
}
else {
printf(" ");
}
strip.setPixelColor(i,UniColor);
UpdateMS = random(10,MaxTileTime) * UPDATEINTERVAL; // pick time for next update
printf("delay: %6ld ms\r\n",UpdateMS);
}
strip.show(); // send out precomputed colors
MillisThen = MillisNow;
digitalWrite(PIN_HEARTBEAT,LOW);
}
}
view raw GlassTiles.ino hosted with ❤ by GitHub

More WS2812 Failures

Even though I’m using what seem to be good-quality parts, one of the WS2812 RGB LEDs in a Glass Tile frame died:

Glass Tile - 2x2 - first WS2812B failure
Glass Tile – 2×2 – first WS2812B failure

It passed the Josh Sharpie Test:

Glass Tile - WS2812 failure - PCB unknown
Glass Tile – WS2812 failure – PCB unknown

After building the third Glass Tile unit, one of the LEDs didn’t light up due to an easily diagnosed problem:

Glass Tile - WS2812 failure - PCB cold solder - as found
Glass Tile – WS2812 failure – PCB cold solder – as found

A closer look:

Glass Tile - WS2812 failure - PCB cold solder
Glass Tile – WS2812 failure – PCB cold solder

Shortly thereafter, the Nissan Fog Lamp developed an obvious beam problem:

Nissan Fog Lamp - failed WS2812 effect
Nissan Fog Lamp – failed WS2812 effect

The WS2812 had the proper voltages / signals at all its pins and was still firmly stuck to the central “heatsink”:

Nissan Fog Lamp - failed WS2812 detail
Nissan Fog Lamp – failed WS2812 detail

It also passed the Josh Sharpie Test:

Glass Tile - WS2812 failure - tape - unknown
Glass Tile – WS2812 failure – tape – unknown

I’m particularly surprised by this one, because eleven of the twelve flex-PCB WS2812s in the Hard Drive Platter light have been running continuously for years with no additional failures.

The alert reader will note the common factor: no matter what substrate the LED is (supposed to be) soldered to, no matter when I bought it, no matter what it’s wired into, a WS2812 will fail.

They’re all back in operation:

Glowing Algorithmic Art
Glowing Algorithmic Art

Although nobody knows for how long …

Obviously, it’s time to refresh my programmable RGB LED stockpile!

Glass Tiles: 2×2 Matrix

Start with a single cell holding a glass tile over a WS2812 RGB LED:

Glass Tile - 1x1 cell test - purple phase
Glass Tile – 1×1 cell test – purple phase

A bit of OpenSCAD tinkering produces a simple 2×2 array with square interiors as a test piece:

Glass Tile - 2x2 - PETG strings
Glass Tile – 2×2 – PETG strings

The excessive stringing and the booger in the upper-left cell come from absurdly thin infill tucked into the too-thin walls; Slic3r doesn’t (seem to) have a “minimum infill width” setting and it’ll desperately try to fit infill between two nearly adjacent perimeter threads.

The little support spiders under the LED PCB recesses snapped right out, though, so I got that part right:

Glass Tile - 2x2 - support spiders
Glass Tile – 2×2 – support spiders

The perimeter threads around the LED aperture aren’t quite fused, because it was only one layer thick and that’s not enough.

A quick test with two LEDs showed the white PETG let far too much light bleed between the cells, which was no surprise from the single cell test piece.

Fortunately, it’s all parametric, so a bit more tinkering produced a slightly chunkier matrix with a base for an Arduino Nano and M3 threaded brass inserts for the screws holding it together:

Glass Tile Frame - 2x2 - Arduino Nano base - solid model
Glass Tile Frame – 2×2 – Arduino Nano base – solid model

Those two parts require about three hours of printing, much faster than I could produce them by milling pockets into aluminum or black acrylic slabs, and came out with minimal stringing.

A little cleanup, some epoxy work, and a few dabs of solder later:

Glass Tile - 2x2 - Arduino wiring
Glass Tile – 2×2 – Arduino wiring

An initial lamp test showed the white-ish glass tiles aren’t all quite the same color:

Glass Tile - 2x2 - white color variation
Glass Tile – 2×2 – white color variation

I thought it was an LED color variation, too, but the slightly blue tint in the lower left corner followed the tile.

The blurred horizontal strip across the middle is adhesive tape holding the tiles in place; I was reluctant to glue them in before being sure this whole thing would work. A peek into the future, though, shows it’s got potential:

Glass Tile - 2x2 - first two units
Glass Tile – 2×2 – first two units

They do give off a definite Windows logo vibe, don’t they?

The OpenSCAD source code as a GitHub Gist:

// Illuminated Tile Grid
// Ed Nisley - KE4ZNU
// 2020-05
/* [Configuration] */
Layout = "Build"; // [Cell,CellArray,MCU,Base,Show,Build]
Shape = "Square"; // [Square, Pyramid, Cone]
Cells = [2,2];
CellDepth = 15.0;
Support = true;
Inserts = true;
/* [Hidden] */
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.1; // make holes end cleanly
ID = 0;
OD = 1;
LENGTH = 2;
Tile = [25.0 + 0.1,25.0 + 0.1,4.0];
WallThick = 3*ThreadWidth;
Flange = [4*ThreadWidth,4*ThreadWidth,0]; // ridge supporting tile
Separator = [3*ThreadWidth,3*ThreadWidth,Tile.z - 1]; // between tiles
Screw = [3.0,6.0,3.5]; // M3 SHCS, OD=head, LENGTH=head
Insert = [3.0,4.2,8.0]; // threaded brass insert
PCB = [15.0,8.0,2.5];
LED = [5.0 + 2*HoleWindage,5.0 + 2*HoleWindage,1.0];
LEDOffset = [0.0,(PCB.y - LED.y)/2 - 0.5,0.0]; // slight offset from +Y PCB edge
CellOAL = [Tile.x,Tile.y,0] + Separator + [0,0,CellDepth] + [0,0,WallThick] + [0,0,PCB.z];
ArrayOAL = [Cells.x*CellOAL.x,Cells.y*CellOAL.y,CellOAL.z]; // just the LED cells
BlockOAL = ArrayOAL + [2*WallThick,2*WallThick,0]; // LED cells + exterior wall
echo(str("Block OAL: ",BlockOAL));
InsertOC = ArrayOAL - [Insert[OD],Insert[OD],0] - [2*WallThick,2*WallThick,0];
echo(str("Insert OC: ",InsertOC));
TapeThick = 1.0;
Arduino = [44.0,18.0,8.0 + TapeThick]; // Arduino Nano to top of USB Mini-B plug
USBPlug = [15.0,11.0,8.5]; // USB Mini-B plug insulator
USBOffset = [0,0,5.5]; // offset from PCB base
WiringBay = [BlockOAL.x - 4*WallThick,38.0,3.0];
PlateOAL = [BlockOAL.x,BlockOAL.y,WallThick + Arduino.z + WiringBay.z];
echo(str("Base Plate: ",PlateOAL));
//------------------------
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(d=(FixDia + HoleWindage),h=Height,$fn=Sides);
}
//-----------------------
// Base and optics in single tile
module LEDCone() {
hull() {
translate([0,0,CellDepth + Tile.z/2])
cube(Tile - [Flange.x,Flange.y,0],center=true);
if (Shape == "Square") {
translate([0,0,LED.z/2])
cube([Tile.x,Tile.y,LED.z] - [Flange.x,Flange.y,0],center=true);
}
else if (Shape == "Pyramid") {
translate([0,0,LED.z/2])
cube(LED,center=true);
}
else if (Shape == "Cone") {
translate([0,0,LED.z/2])
cylinder(d=1.5*LED.x,h=LED.z,center=true);
}
else {
echo(str("Whoopsie! Invalid Shape: ",Shape));
cube(5);
}
}
}
// One complete LED cell
module LEDCell() {
difference() {
translate([0,0,CellOAL.z/2])
cube(CellOAL,center=true);
translate([0,0,CellOAL.z - Separator.z + Tile.z/2])
cube(Tile,center=true);
translate([0,0,PCB.z + WallThick])
LEDCone();
cube([LED.x,LED.y,CellOAL.z],center=true);
translate(-LEDOffset + [0,0,PCB.z/2 - Protrusion/2])
cube(PCB + [0,0,Protrusion],center=true);
}
if (Support)
color("Yellow") render()
translate(-LEDOffset) {
// translate([0,0,ThreadThick/2])
// cube([PCB.x - 2*ThreadWidth,PCB.y - 2*ThreadWidth,ThreadThick],center=true);
intersection() {
translate([0,0,(PCB.z - ThreadThick)/2])
cube([PCB.x - 2*ThreadWidth,PCB.y - 2*ThreadWidth,PCB.z - ThreadThick],center=true);
union() { for (a=[0:22.5:359])
rotate(a)
translate([PCB.x/2,0,PCB.z/2])
cube([PCB.x,2*ThreadWidth,PCB.z],center=true); }
}
}
}
// The whole array of cells
module CellArray() {
difference() {
union() {
translate([CellOAL.x/2 - Cells.x*CellOAL.x/2,CellOAL.y/2 - Cells.y*CellOAL.y/2,0])
for (i=[0:Cells.x - 1], j=[0:Cells.y - 1])
translate([i*CellOAL.x,j*CellOAL.y,0])
LEDCell();
if (Inserts) // bosses
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,0])
rotate(180/8)
cylinder(d=Insert[OD] + 3*WallThick,h=Insert[LENGTH],$fn=8);
}
if (Inserts) // holes
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion])
rotate(180/8)
PolyCyl(Insert[OD],Insert[LENGTH] + WallThick + Protrusion,8);
}
difference() {
translate([0,0,CellOAL.z/2])
cube(BlockOAL,center=true);
translate([0,0,CellOAL.z])
cube(ArrayOAL + [0,0,2*CellOAL.z],center=true);
}
}
// Arduino bounding box
// Origin at center bottom of PCB
module Controller() {
union() {
translate([0,0,Arduino.z/2])
cube(Arduino,center=true);
translate([Arduino.x/2 - Protrusion,-USBPlug.y/2,USBOffset.z + TapeThick - USBPlug.z/2])
cube(USBPlug + [Protrusion,0,0],center=false);
}
}
// Baseplate
module BasePlate() {
difference() {
translate([0,0,PlateOAL.z/2])
cube(PlateOAL,center=true);
translate([0,0,WallThick])
Controller();
translate([0,0,WallThick + PlateOAL.z/2])
cube([Arduino.x - 2*2.0,WiringBay.y,PlateOAL.z],center=true);
translate([0,0,PlateOAL.z - WiringBay.z + WiringBay.z/2])
cube(WiringBay + [0,0,2*Protrusion],center=true);
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion])
rotate(180/8) {
PolyCyl(Screw[ID],2*PlateOAL.z,8);
PolyCyl(Screw[OD],Screw[LENGTH] + 4*ThreadThick + Protrusion,8);
}
translate([0,0,ThreadThick-Protrusion])
cube([17.0,45,2*ThreadThick],center=true);
}
linear_extrude(height=2*ThreadWidth + Protrusion) {
translate([1,0,-Protrusion])
rotate(-90) mirror([1,0,0])
text(text="Ed Nisley",size=6,font="Arial:style:Bold",halign="center");
translate([-6.5,0,-Protrusion])
rotate(-90) mirror([1,0,0])
text(text="softsolder.com",size=4.5,font="Arial:style:Bold",halign="center");
}
if (Support)
color("Yellow")
for (i=[-1,1], j=[-1,1])
translate([i*InsertOC.x/2,j*InsertOC.y/2,0])
for (a=[0:45:135])
rotate(a)
translate([0,0,(Screw[LENGTH] - ThreadThick)/2])
cube([Screw[OD] - 2*ThreadWidth,2*ThreadWidth,Screw[LENGTH] - ThreadThick],center=true);
}
//-----------------------
// Build things
if (Layout == "Cell")
LEDCell();
else if (Layout == "CellArray")
CellArray();
else if (Layout == "MCU")
Controller();
else if (Layout == "Base")
BasePlate();
else if (Layout == "Show") {
translate([0,0,PlateOAL.z + 10])
CellArray();
BasePlate();
}
else if (Layout == "Build") {
translate([0,0.6*BlockOAL.y,0])
CellArray();
translate([0,-0.6*BlockOAL.y,0])
rotate(90)
BasePlate();
}

Glass Tiles: Single Test Cell

A single glass tile rests on the ridge around the pyramidal interior:

Glass Tile Frame - pyramid cell
Glass Tile Frame – pyramid cell

The bottom has a cutout for the WS2812 PCB, with some in-the-model support for simplicity:

Glass Tile Frame - pyramid cell - bottom
Glass Tile Frame – pyramid cell – bottom

Which becomes this in real life:

Glass Tile - 1x1 cell test - pyramid PETG strings
Glass Tile – 1×1 cell test – pyramid PETG strings

There’s plenty of PETG hair inside the opening, which seems like a Bad Thing all around.

Cleaning out the worst of the fur, taping a WS2812 LED into the opening, and dropping a white-ish tile in place:

Glass Tile - 1x1 cell test - purple phase
Glass Tile – 1×1 cell test – purple phase

Obviously, JPG compression wasn’t built with a finely textured granular surface in mind:

Glass Tile - 1x1 cell test - blue phase
Glass Tile – 1×1 cell test – blue phase

But it looks really nice in a dim room!

With a physical object in hand, it’s obvious the pyramidal interior adds exactly zero value:

  • Direct rays in the beam from the WS2812 don’t hit the walls
  • Light outside the beam doesn’t contribute much after hitting those irregular walls

So the next pass should be just a hollow box with tweaked tile & PCB measurement: rapid prototyping in full effect!

Glass Tiles: Proof of Concept

Extract some victims from a square foot of glass tiles:

Glass Tiles - as sold
Glass Tiles – as sold

Wire an old WS2812 breakout board (the new ones are much larger) to an Arduino Nano running the Nissan Fog Lamp firmware:

Glass Tile - backlight blue - setup
Glass Tile – backlight blue – setup

Aaaand it looks like this might actually work:

Glass Tile - backlight blue
Glass Tile – backlight blue

The WS2812 “beam” illuminates the 25 mm square tile without too much vignetting at about 15 mm.

The bottom tile is white-ish, the top is gray-ish, and they look different enough to justify using only one color in each array:

Glass Tile - backlight neutral
Glass Tile – backlight neutral

Now, for some solid modeling …