This is a large (20” x 30” or so) wall clock I designed and built. A 3d printed (and glued together) array of ~1” cubes was designed so that it lines up perfectly with some cheap RGB strip LEDs. Then an arduino controls the lighting and communicates with a RTC. I designed an acrylic case and had it laser cut from pololu.com. The back and sides are transparent while the front is frosted to disperse the light. There is a microphone and a spectrum analyzer chip that gets ambient sound data to make the lights react to nearby music. This part is still a work in progress – the first spectrum analyzer chip doesn’t give great response.

A look at the firmware is below the gallery.

//this version will dynamically determine min and max for each channel and implement a tasker. also smooth out the bars a little
//other screen ideas: solid background that changes with daytime and a rainbow clock layover
// make into alarm clock. buttons would help set time and programmable sunrise time. also a switch to toggle alarm function on/off
//add 3 rotary encoders. 1) screen selection, 2) brightness, and 3) alarm/time setting
#include <AudioAnalyzer.h>
#include <FastLED.h>
//task schedule intialization stuff
//#define _TASK_MICRO_RES //uncomment for microseconds
#include <TaskScheduler.h>
//Tasks
void adjustFreqMinMax();
void getFreqs();
void drawScreen();
void keepTime();
Task adjustFreqMinMax_task(50, TASK_FOREVER, &adjustFreqMinMax);
Task getFreqs_task(30, TASK_FOREVER, &getFreqs);
Task drawScreen_task(15, TASK_FOREVER, &drawScreen);
Task keepTime_task(1000, TASK_FOREVER, &keepTime);
Scheduler runner;
Analyzer Audio = Analyzer(A3,A4,A5);//Strobe pin ->3 RST pin ->4 Analog Pin ->5
#define DATA_PIN 4
#define CLOCK_PIN 3
#define BRIGHTNESS 25
#define LED_TYPE APA102
#define COLOR_ORDER BGR
const uint8_t kMatrixWidth = 21;
const uint8_t kMatrixHeight = 13;
const bool kMatrixSerpentineLayout = true;
#define NUM_LEDS (kMatrixWidth * kMatrixHeight)
CRGB leds[NUM_LEDS];
uint16_t freqMinVals[7] = {1023,1023,1023,1023,1023,1023,1023};
uint16_t freqMaxVals[7] = {1023,1023,1023,1023,1023,1023,1023};
int FreqVal[7];
uint16_t castFreqVal[7];
uint8_t currentBarVals[7]; //actual value of bar graph based on current measurement
uint8_t displayedBarVals[7]; //bar value that is shown on screen. this will be smoothed vs currentBarVals
uint8_t screenIndex = 0; //this will control which screen is displayed at any time.
//__TIME__ is a pointer and format is 12:34:56
uint8_t fullHour = atoi(__TIME__); //save this for scaling colors over 24h period
uint8_t hour = atoi(__TIME__)%12; //us time
uint8_t minute = atoi(__TIME__+3);
uint8_t second = atoi(__TIME__+6);
//uint8_t hour = 12; //us time
//uint8_t minute = 22;
//uint8_t second = 0;
const uint8_t numberIndices[10][13][2] = { //this contains the pixels for each digit 0-9. each row is padded with the first pixel in case you accidentally iterate through the whole loop rather than the length defined in numberIndicesLength
{{0,0},{1,0},{2,0},{0,1},{2,1},{0,2},{2,2},{0,3},{2,3},{0,4},{1,4},{2,4},{0,0}},
{{2,0},{2,1},{2,2},{2,3},{2,4},{2,0},{2,0},{2,0},{2,0},{2,0},{2,0},{2,0},{2,0}},
{{0,0},{1,0},{2,0},{2,1},{0,2},{1,2},{2,2},{0,3},{0,4},{1,4},{2,4},{0,0},{0,0}},
{{0,0},{1,0},{2,0},{2,1},{0,2},{1,2},{2,2},{2,3},{0,4},{1,4},{2,4},{0,0},{0,0}},
{{0,0},{2,0},{0,1},{2,1},{0,2},{1,2},{2,2},{2,3},{2,4},{0,0},{0,0},{0,0},{0,0}},
{{0,0},{1,0},{2,0},{0,1},{0,2},{1,2},{2,2},{2,3},{0,4},{1,4},{2,4},{0,0},{0,0}},
{{0,0},{1,0},{2,0},{0,1},{0,2},{1,2},{2,2},{0,3},{2,3},{0,4},{1,4},{2,4},{0,0}},
{{0,0},{1,0},{2,0},{2,1},{2,2},{2,3},{2,4},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}},
{{0,0},{1,0},{2,0},{0,1},{2,1},{0,2},{1,2},{2,2},{0,3},{2,3},{0,4},{1,4},{2,4}},
{{0,0},{1,0},{2,0},{0,1},{2,1},{0,2},{1,2},{2,2},{2,3},{2,4},{0,0},{0,0},{0,0}}
};
const uint8_t numberIndicesLength[10] = {12,5,11,11,9,11,12,7,13,10};
void setup()
{
Serial.begin(9600); //Init the baudrate
Audio.Init();//Init module
FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN, COLOR_ORDER, DATA_RATE_MHZ(1)>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip );
FastLED.setBrightness( BRIGHTNESS );
runner.init();
runner.addTask(adjustFreqMinMax_task);
adjustFreqMinMax_task.enable();
runner.addTask(getFreqs_task);
getFreqs_task.enable();
runner.addTask(drawScreen_task);
drawScreen_task.enable();
runner.addTask(keepTime_task);
keepTime_task.enable();
}
void loop()
{
runner.execute();
}
void rainbowScreenWithClock()
{
uint8_t currentTime = millis() >> 4;
paintRainbowBackground(currentTime); //this paints the background
currentTime = millis() >> 3;
paintTime(hour, minute, 100, 1, currentTime, 0, 0); //this paints the numbers, the 1 value means use rainbow
}
uint8_t determineTimeWidth(uint8_t hour, uint8_t minute){
//not all time values will take up the same number of pixel columns. at the very least there are 3 columns between hour and minute and 1 column between MM. Max width is 12:XX which is 1+1+3+3+3+1+3 = 15
//minimum is 1:11 1+3+1+1+1 = 7
uint8_t digit1 = 0;
uint8_t digit2 = 3;
uint8_t digit3 = 3;
uint8_t digit4 = 3;
if(hour/10==1){digit1 = 2;}
if(hour%10==1){digit2 = 1;}
if(minute/10==1){digit3 = 1;}
if(minute%10==1){digit4 = 1;}
return digit1 + digit2 + 3 + digit3 + 1 + digit4;
}
void complementEqualizer()
{
uint8_t currentTime = millis() >> 5;
for (uint8_t x = 0; x < kMatrixWidth; x++)
{
for (uint8_t y = 0; y < kMatrixHeight; y++)
{
// The x * 20 and y * 10 terms determine the general
// direction and width of the rainbow, and the x * y term
// makes it curve a bit.
//leds[ XY( x, y) ] = CHSV(210-10*x+40,255,255);//(uint32_t)p CRGB::White
leds[ XY( x, y) ] = CHSV(currentTime%255,255,255);//(uint32_t)p CRGB::White
}
}
for(uint8_t x = 0;x<kMatrixWidth;x++)
{
for(uint8_t y = 0;y<kMatrixHeight;y++)
{
if(y<=kMatrixHeight-displayedBarVals[x/3])
{
//leds[ XY( x, y) ] = CRGB::Grey;
}
else
{
//leds[ XY( x, y) ] = CHSV(x*10, 255, 255); //(uint32_t)p
leds[ XY( x, y) ] = CHSV((currentTime+128)%255, 255, 255); //(uint32_t)p
}
}
}
paintTime(hour, minute, 100, 1, currentTime, (kMatrixWidth-determineTimeWidth(hour, minute))/2-1, 3); //this paints the numbers, the 1 value means use rainbow
}
void keepTime()
{
second++;
if(second>=60)
{
second = 0;
minute++;
}
if(minute>=60)
{
minute = 0;
hour++;
//hour = hour%24;
hour = hour%12;
}
}
void blankScreen()
{
for(uint16_t z=0;z<NUM_LEDS;z++)
{
leds[z] = CHSV(255,0,0);
}
}
void drawScreen()
{
if(screenIndex==0) complementEqualizer();
if(screenIndex==1) rainbowScreenWithClock();
if(screenIndex==2) blankScreen();
FastLED.show();
}
void getFreqs(void)
{
Audio.ReadFreq(FreqVal);//return 7 value of 7 bands pass filiter
//Frequency(Hz):63 160 400 1K 2.5K 6.25K 16K
//FreqVal[]: 0 1 2 3 4 5 6 f
for(uint8_t iter=0;iter<7;iter++)
{
castFreqVal[iter] = (uint16_t)FreqVal[iter];
if(castFreqVal[iter]<freqMinVals[iter]) freqMinVals[iter] = castFreqVal[iter];
if(castFreqVal[iter]>freqMaxVals[iter]) freqMaxVals[iter] = castFreqVal[iter];
currentBarVals[iter] = (uint8_t)map(castFreqVal[iter],freqMinVals[iter],freqMaxVals[iter],0,kMatrixHeight);
if(currentBarVals[iter]>displayedBarVals[iter]) displayedBarVals[iter]++;
else if(currentBarVals[iter]<displayedBarVals[iter]) displayedBarVals[iter]–;
}
}
void adjustFreqMinMax(void) //every few seconds decrease the max value and increase the min value. this means over time the range will adjust to the current music or situation. the timing of this task aims to cover 2 mins = 1023 adjustments
{
for(uint8_t x = 0;x<7;x++)
{
if(freqMinVals[x]<1023) freqMinVals[x]++;
if(freqMaxVals[x]>1) freqMaxVals[x]–;
}
}
// function that returns led index for XY position
uint16_t XY( uint8_t x, uint8_t y)
{
uint16_t i;
if( kMatrixSerpentineLayout == false) {
i = (y * kMatrixWidth) + x;
}
if( kMatrixSerpentineLayout == true) {
if( y & 0x01) {
// Odd rows run backwards
uint8_t reverseX = (kMatrixWidth – 1) – x;
i = (y * kMatrixWidth) + reverseX;
} else {
// Even rows run forwards
i = (y * kMatrixWidth) + x;
}
}
return i;
}
void paintRainbowBackground(uint8_t timeVal)
{
for (uint8_t x = 0; x < kMatrixWidth; x++)
{
for (uint8_t y = 0; y < kMatrixHeight; y++)
{
// The x * 20 and y * 10 terms determine the general
// direction and width of the rainbow, and the x * y term
// makes it curve a bit.
uint16_t p = (timeVal – x * 10 – y * 5 – x * y)%255;
//p=0;
leds[ XY( x, y) ] = CHSV((uint32_t)p, 255, 255);
}
}
}
void paintTime(uint8_t hour, uint8_t minute, uint8_t color, boolean rainbow, uint8_t rainbowTime, uint8_t xOffset, uint8_t yOffset)
{
uint8_t timeWidth = determineTimeWidth(hour, minute); //how many columns wide will the time end up being
uint8_t currentColumn = (kMatrixWidth – timeWidth)/2 + xOffset;
uint16_t p = 0;
uint8_t tempX = 0;
uint8_t tempY = 0;
uint8_t timeRowOffset = (kMatrixHeight – 5)/2 – yOffset;
if(hour/10==1)
{
for(uint8_t z = 0;z<numberIndicesLength[1];z++)
{
tempX = currentColumn + numberIndices[1][z][0] – 2; //-2 to offset the 1 properly
tempY = timeRowOffset + numberIndices[1][z][1];
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
}
currentColumn = currentColumn + 2;
}
if(hour%10==1)
{
for(uint8_t z = 0;z<numberIndicesLength[1];z++)
{
tempX = currentColumn + numberIndices[1][z][0] – 2; //-2 to offset the 1 properly
tempY = timeRowOffset + numberIndices[1][z][1];
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
}
currentColumn = currentColumn + 2;
}
else //paint normal, don't subtract 2 from x
{
for(uint8_t z = 0;z<numberIndicesLength[hour%10];z++)
{
tempX = currentColumn + numberIndices[hour%10][z][0];
tempY = timeRowOffset + numberIndices[hour%10][z][1];
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
}
currentColumn = currentColumn + 4;
}
//now add the colon
tempX = currentColumn;
tempY = timeRowOffset+1;
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
tempY = timeRowOffset+3;
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
currentColumn = currentColumn + 2;
if(minute/10==1)
{
for(uint8_t z = 0;z<numberIndicesLength[1];z++)
{
tempX = currentColumn + numberIndices[1][z][0] – 2; //-2 to offset the 1 properly
tempY = timeRowOffset + numberIndices[1][z][1];
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
}
currentColumn = currentColumn + 2;
}
else //paint normal, don't subtract 2 from x
{
for(uint8_t z = 0;z<numberIndicesLength[minute/10];z++)
{
tempX = currentColumn + numberIndices[minute/10][z][0];
tempY = timeRowOffset + numberIndices[minute/10][z][1];
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
}
currentColumn = currentColumn + 4;
}
if(minute%10==1)
{
for(uint8_t z = 0;z<numberIndicesLength[1];z++)
{
tempX = currentColumn + numberIndices[1][z][0] – 2; //-2 to offset the 1 properly
tempY = timeRowOffset + numberIndices[1][z][1];
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
}
currentColumn = currentColumn + 2;
}
else //paint normal, don't subtract 2 from x
{
for(uint8_t z = 0;z<numberIndicesLength[minute%10];z++)
{
tempX = currentColumn + numberIndices[minute%10][z][0];
tempY = timeRowOffset + numberIndices[minute%10][z][1];
if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 – tempX * tempY)%255;}
else p = color;
leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
}
}
}
view raw PixelClock.ino hosted with ❤ by GitHub