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:

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); | |
} | |
} |
I believe video production would say “fade” vs “cut,” but I know that’s a different context …
Perhaps it’d be a “cut” if all the tiles changed at once and a “fade” if one (or more?) tiles changed slowly.
It’s definitely not a “flash”, though, so that’s wrong.
Good naming is the hardest thing!
Getting fancy there! Interestingly, the 0-255 values driving addressable LEDs produce linear brightness changes, but our eyes have roughly logarithmic response, so when I do fades and other kinds of animations I usually do them in a “perceptual” range and then convert to linear right before output. You can cheat and use x^2 as that curve is pretty close.
At one point I had the ahem bright idea to use exponential steps for each color, tinkered it up, and saw what eight PWM steps looked like. Wrong implementation.
I should fire the sequences directly into
ColorHSV()
and see what happens.