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

5 thoughts on “Glass Tiles: Glow vs. Flash Firmware

  1. I believe video production would say “fade” vs “cut,” but I know that’s a different context …

    1. 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!

  2. 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.

    1. 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.

Comments are closed.