Pedro Martin
Published © GPL3+

The Circle of Life (a digitless, pixeldust clock)

Measuring time using rhythms, pulses and gravity with an animated 32x32 RGB LED matrix. A clock as object of contemplation.

AdvancedFull instructions provided312

Things used in this project

Hardware components

Adafruit Matrix Portal for RGB Matrices
×1
Adafruit 32x32 RGB LED Matrix Panel - 4mm Pitch
×1

Software apps and online services

Arduino IDE 2.0

Story

Read more

Code

designTwo_v3.ino

C/C++
#include "declarations.h"
#include "setup.h"
#include "clockDisplay.h"
#include "inputSerial.h"

void loop() {
  receiveData();  //READ Serial Input to set clock
  if (newData == true) parseData();
  while (((t = micros()) - prevTime) < (1000000L / maxFPS))
    ;  //Ensures FPS. Results in FPS evaluations (program loops) per second
  prevTime = t;
  if (millis() - prevSecond > 1000) {  //Update clock display once per second, else run pixelDust simulation
    prevSecond = millis();
    miniPulseDone = false;
    TOD[S]++;
    if (TOD[S] > 59) {  //60 seconds (0..59) have elapsed therefore New MIN
      TOD[M]++;
      clearSMH(S, 0, grains[S]);
      if (TOD[M] > 59) {  //60 minutes (00..59) have elapsed therefore New HR
        TOD[H]++;
        clearSMH(M, grains[S], grains[S] + grains[M]);
        if (TOD[H] > 11) {  //12 hours (00.11) have elapsed
          if (AMPM == 'P')  //changed from 11PM to 0 hours (AM) or from 12PM (noon) to 1PM
            clearSMH(H, grains[S] + grains[M], grains[S] + grains[M] + grains[H]);
          else {  //changed from 11AM to noon (M) so no clearing/incr currRow necessary: Because it won't go into addSMH, must draw topGrain for HOUR=12 here
            updateAMPM('P');
            drawHOUR();
          }
        } else
          addSMH(H, grains[S] + grains[M], grains[S] + grains[M] + grains[H]);
      } else
        addSMH(M, grains[S], grains[S] + grains[M]);
    } else
      addSMH(S, 0, grains[S]);
  } else {  //still within the same second, therefore run PixelDust simulation
    if (millis() - prevSecond > 250 && !miniPulseDone)
      miniPulse();  //mini pulse after Seconds pulse has "hit the ground"
    accel.getEvent(&event);
    xx = event.acceleration.x * 1000;
    yy = event.acceleration.y * 1000;
    zz = event.acceleration.z * 1000;
    pulse.iterate(xx, yy, zz);  //relocate grains based on accel & gravity = xx,yy,zz that is, run pixeldust simulation w/o drawing the pixels yet
  }
  drawSimulation();
}

declarations.h

C/C++
#include <stdint.h>
//#include <Wire.h>
#include <Adafruit_LIS3DH.h>
#include <Adafruit_PixelDust.h>
#include <Adafruit_Protomatter.h>
uint8_t addrPins[] = { 17, 18, 19, 20 };
uint8_t rgbPins[] = { 7, 8, 9, 10, 11, 12 };
uint8_t clockPin = 14;
uint8_t latchPin = 15;
uint8_t oePin = 16;
// https://learn.adafruit.com/adafruit-matrixportal-m4/arduino-libraries
// https://learn.adafruit.com/adafruit-protomatter-rgb-matrix-library/arduino-library
Adafruit_Protomatter matrix(32, 6, 1, rgbPins, sizeof(addrPins), addrPins, clockPin, latchPin, oePin, true);
Adafruit_LIS3DH accel = Adafruit_LIS3DH();

const uint8_t rowHeight[3] = { 1, 3, 4 };  //options min=2,3 hr=3,4
const uint8_t smhSize[3] = { 1, 2, 3 };    //options min=1,2 hr=2,3
const uint8_t grains[3] = { 60, 40, 30 };  //pixel count (grains) for s.m.h., 0 indexed when counted, min=20, max=60
                                           //so grains[0..59] are for S, grains[60..99] are for M, grains [100..129] for H
const uint8_t S = 0;
const uint8_t M = 1;
const uint8_t H = 2;
const uint8_t clkWidth[3] = { 10, 10, 11 };  //total width in pixels for s.m.h
const uint8_t startCol[3] = { 22, 12, 1 };   //starting X position for seconds, minutes, hours
uint8_t currRow[3] = { 0, 0, 0 };            //current row receivng new grains for s.m.h,  0 indexed when counted
uint16_t clkColor[3][6];                     //six color intensitites for s.m.h.
uint16_t basePM, baseAM, crawlPM, crawlAM;   //colors for AMPM
uint8_t TOD[3] = { 0, 0, 0 };                //time of day in s.m.h: Holds currently running s.m.h vs Display matrix shows the elapsed s.m.h
                                             //so displayed[s.m.h] = TOD[s.m.h] + 1, meaning s.m=60 is never displayed
char AMPM = 'P';                             // midnight is 0 hours

// https://learn.adafruit.com/adafruit-matrixportal-m4/arduino-pixel-dust-demo
// https://github.com/adafruit/Adafruit_PixelDust
Adafruit_PixelDust pulse(32, 32, grains[S] + grains[M] + grains[H], 1, 128, false);  //100 = jumpiness, false = less computation & more loony
const uint8_t maxFPS = 60;

double xx, yy, zz;
sensors_event_t event;

dimension_t x, y;
uint32_t prevTime = 0;
uint32_t prevSecond, t;
int i, j, k, c, r;  // for-loop counters

const byte inputChars = 32;
char receivedChars[inputChars];
boolean newData = false;
boolean miniPulseDone = false;

inputSerial.h

C/C++
void receiveData() {
  static boolean recvInProgress = false;
  static byte ndx = 0;
  char startMarker = '<';
  char endMarker = '>';
  char rc;
  while (Serial.available() > 0 && newData == false) {
    rc = Serial.read();
    if (recvInProgress == true) 
      if (rc != endMarker) {
        receivedChars[ndx] = rc;
        ndx++;
        if (ndx >= inputChars) ndx = inputChars - 1;
      } else {
        receivedChars[ndx] = '\0';  // terminate the string
        recvInProgress = false;
        ndx = 0;
        newData = true;
      }
    else 
      if (rc == startMarker) recvInProgress = true;
  }
}

void parseData() {
  char tempChars[inputChars];
  int v1, v2, v3;
  newData = false;
  strcpy(tempChars, receivedChars);
  char* strtokIndx;
  strtokIndx = strtok(tempChars, ",");
  v1 = atoi(strtokIndx);  //input how many hours
  strtokIndx = strtok(NULL, ",");
  v2 = atoi(strtokIndx);  //input how many minutes
  strtokIndx = strtok(NULL, ",");
  v3 = atoi(strtokIndx);  //input how many seconds
  if (v1 > 11) AMPM = 'P';
  else AMPM = 'A';
  if (v1 > 12) v1 -= 12;
  TOD[H] = v1;
  TOD[M] = v2;
  TOD[S] = v3;
  updateAMPM('N');
  for (k = S; k <= M; k++) currRow[k] = int((TOD[k] - 1) / clkWidth[k]);
  currRow[H] = int((TOD[H] - 1) / 3);
  Serial.printf("hrs:%d mins:%d secs:%d", TOD[H], TOD[M], TOD[S]);
  Serial.println();
  for (k = S; k <= H; k++) 
    for (int i = 0; i < TOD[k]; i++) 
      if (k == M) 
        for (j = 0; j < smhSize[M]; j++)
          matrix.drawPixel(startCol[k] + (i % clkWidth[k]), (i / clkWidth[k]) * rowHeight[M] + j, clkColor[k][currRow[k] - i / clkWidth[k]]);
        else 
          if (k == S)
            matrix.drawPixel(startCol[k] + (i % clkWidth[k]), i / clkWidth[k], clkColor[k][currRow[k] - i / clkWidth[k]]);
          else 
            for (c = 0; c < smhSize[H]; c++)
              for (r = 0; r < smhSize[H]; r++)
                matrix.drawPixel(4 * (i + 1) - int(i / 3) * 12 - 3 + c, int(i / 3) * rowHeight[H] + r, clkColor[k][currRow[k] - int(i / 3)]);
  matrix.show();
  delay(1000);
}

clockDisplay.h

C/C++
void drawHOUR() {
  for (c=0;c<smhSize[H];c++)
    for (r=0;r<smhSize[H];r++)          
      matrix.drawPixel(4*TOD[H]-int((TOD[H]-1)/3)*12-3+c, int((TOD[H]-1)/3)*rowHeight[H]+r, clkColor[H][currRow[H]-int((TOD[H]-1)/3)]);
      // to understand these parameters, see excel docs
}

void updateAMPM(char newVal) {
  if (newVal != 'N') 
    AMPM = newVal;
  byte hour24 = (AMPM=='A')*TOD[H] + (AMPM=='P')*(TOD[H]+12) -12*(TOD[H]==12);
  for (i=0; i<32; i++)
    matrix.drawPixel(0,i,basePM*(AMPM=='P') + baseAM*(AMPM=='A'));
  for (i=0; i<8; i++)
    matrix.drawPixel(0,hour24 + i,crawlPM*(AMPM=='P') + crawlAM*(AMPM=='A')); 
}

void clearSMH(uint8_t smh, int firstPixel, int lastPixel) { 
  for (i=firstPixel; i<lastPixel; i++) { // Clear pixelDust & place grains[smh] on top grains/clkWidth rows (+4 if hour=13) as pixelDust for drop sim
    pulse.getPosition(i, &x, &y); 
    pulse.clearPixel(x, y); 
    pulse.setPosition(i, startCol[smh] + ((i - firstPixel) % clkWidth[smh]), (TOD[H]==13)*rowHeight[H] + ((i - firstPixel) / clkWidth[smh]));
  }   
  currRow[smh] = 0; // restart currRow
  if (smh != H) {
    TOD[smh] = 0; // restart s.m at 0
  }else { // for H, clearSMH is called only if TOD[H]>11
    if (TOD[smh] == 12) { //time changed from 23:59:59 (11PM) to 0 hours (12AM)
      updateAMPM('A');
      TOD[smh] = 0;
    }else { //time changed from 12:59:59 (PM) to 1PM so must draw HOUR=1 topGrain here because it didn't go thru addSMH
      TOD[smh] = 1;
      matrix.fillRect(startCol[H], 0, clkWidth[H], 3, 0x0); //erase hours 2 & 3 from currRow = 0 
      drawHOUR();
    }
  }
}

void addSMH(uint8_t smh, int firstPixel, int lastPixel) {
  bool newRow = false; //drop to the next row
  if (TOD[smh] != 1) //to filter out MODULUS=1 when TOD[]=1: drop new row only if not 1st s.m.h
    if ((smh == H && TOD[smh] % 3 == 1) || (smh != H && TOD[smh] % clkWidth[smh] == 1)) { //if MOD=1, TOD[smh] is first pixel on new row
      currRow[smh]++;
      newRow = true;
    }
  switch (smh) {   // add a Top Grain and recolor previous (existing) rows if newRow
    case S: matrix.drawPixel(startCol[smh] + ((TOD[smh] - 1) % clkWidth[smh]), currRow[smh], clkColor[smh][0]);
            if (newRow)
              for (i=currRow[smh]-1; i>=0; i--) 
                for (j=startCol[smh]; j<startCol[smh] + clkWidth[smh] + 1; j++) 
                  matrix.drawPixel(j, i, clkColor[smh][currRow[smh] - i]); 
            break;
    case M: for (k=0; k<smhSize[M]; k++)
              matrix.drawPixel(startCol[smh] + ((TOD[smh] - 1) % clkWidth[smh]), currRow[smh] * rowHeight[M] + k, clkColor[smh][0]);
            if (newRow)
              for (k=0; k<smhSize[M]; k++)
                for (i=currRow[smh]-1; i>=0; i--) 
                  for (j=startCol[smh]; j<startCol[smh] + clkWidth[smh] + 1; j++) 
                    matrix.drawPixel(j, i * rowHeight[M] + k, clkColor[smh][currRow[smh] - i]);
            break;
    case H: drawHOUR();
            if (newRow)
              for (i=currRow[smh]-1; i>=0; i--) 
                for (j=1;j<=9;j+=4)
                  for (c=0;c<smhSize[H];c++)
                    for (r=0;r<smhSize[H];r++)         
                      matrix.drawPixel(j+c, rowHeight[H]*i+r, clkColor[H][currRow[H]-i]);  
            updateAMPM('N'); //update AMPM crawler
            break;
  }
  for (i = firstPixel;  i < lastPixel; i++)  { //clear sim pixels & place new explosion below currRow
    pulse.getPosition(i, &x, &y); 
    pulse.clearPixel(x,y); 
    while (!pulse.setPosition(i, random(startCol[smh],startCol[smh]+clkWidth[smh]), random((currRow[smh]+1)*rowHeight[smh],32))); }
}

void miniPulse() {
  miniPulseDone = true;
  for (i=(grains[S] + grains[M]) * 0.85; i<grains[S] + grains[M]; i++)  {  //animate the last 15% the mins grains
    pulse.getPosition(i, &x, &y); 
    pulse.clearPixel(x, y);  
    while (!pulse.setPosition(i, random(startCol[M], startCol[M] + clkWidth[M]), random(20, 32)));    
  }
  for (i=(grains[S] + grains[M] + grains[H]) * 0.85; i<grains[S] + grains[M] + grains[H]; i++)  {  //animate the last 15% of the hrs grains
    pulse.getPosition(i, &x, &y); 
    pulse.clearPixel(x, y);  
    while (!pulse.setPosition(i, random(startCol[H], startCol[H] + clkWidth[H]), random(20, 32)));  
  }
}

void drawSimulation() {
  for (k=S; k<=H; k++)  //clear area of all s.m.h below currRow or from 0 when new s.m.h starts
    matrix.fillRect(startCol[k], currRow[k]*rowHeight[k] + (TOD[k]>0)*(k + 1), clkWidth[k], 32-(currRow[k]*rowHeight[k] + (TOD[k]>0)*(k + 1)),0x0);
  for (k=S; k<=M; k++)  //redraw border pixels
    for (i=33-(grains[k+1]/10); i<32; i++) 
      matrix.drawPixel(startCol[k] - 1, i, clkColor[k+1][0x0]);  
  for (i=0; i<grains[S] + grains[M] + grains[H]; i++) {  //draw simulation s.m.h grains in their (latest) position
    pulse.getPosition(i, &x, &y);
    matrix.drawPixel(x, y, clkColor[(i>=grains[S]) + (i>=grains[S]+grains[M])][0x0]);
  }
  matrix.show(); 
}

setup.h

C/C++
void setColors() {
  for (i = 5; i >= 0; i--) {
    clkColor[S][i] = matrix.color565(0, 5 + 10 * (5 - i), 50 + 40 * (5 - i));
    clkColor[M][i] = matrix.color565(20 + 40 * (5 - i), 0, 0);
  }
  clkColor[H][5] = matrix.color565(100, 0, 100);
  clkColor[H][4] = matrix.color565(0, 100, 0);

  clkColor[H][0] = matrix.color565(20, 180, 20);
  clkColor[H][1] = matrix.color565(10, 100, 10);
  clkColor[H][2] = matrix.color565(5, 60, 5);
  clkColor[H][3] = matrix.color565(0, 20, 0);
  basePM = matrix.color565(20, 0, 20);
  crawlPM = matrix.color565(110, 0, 100);
  baseAM = matrix.color565(90, 20, 0);
  crawlAM = matrix.color565(150, 80, 0);
}

void err(int x) {
  pinMode(LED_BUILTIN, OUTPUT);
  switch (x) {
    case 100: Serial.println("Error in matrix:"); // Very Fast blink = Matrix error
      break;
    case 1000: Serial.println("Error in pixelDust"); // Slow blink = malloc error
      break;
    case 400: Serial.println("Error in accelerometer"); // Fast blink = I2C error
      break;
  }
  for (i = 1;; i++) {
    digitalWrite(LED_BUILTIN, i & 1);
    delay(x);
  }
}

void setup() {
  Serial.begin(115200);
  //while (!Serial) delay(10);
  //ProtomatterStatus status = matrix.begin();
  //if (status != 0) err(100);
  if (!matrix.begin()) err(100);
  if (!pulse.begin()) err(1000); 
  if (!accel.begin(0x19)) err(400);                              
  accel.setRange(LIS3DH_RANGE_4_G);  // 2, 4, 8 or 16 G
  setColors();
  int l = 0;
  for (k = S; k <= H; k++) { //iterate s.m.h
    for (j = 0; j < 32; j++) 
      pulse.setPixel(startCol[k] - 1, j);  //boundaries
    for (i = grains[S] * (k > S) + grains[M] * (k > M); i < grains[k] + grains[S] * (k > S) + grains[M] * (k > M); i++) {  //create & draw first s.m.h explosions
      while (!pulse.setPosition(i, random(startCol[k], startCol[k] + clkWidth[k]), random(1, 32)))
        ;  //(from row 1, not row 0, to row 31)
      pulse.getPosition(i, &x, &y);
      matrix.drawPixel(x, y, clkColor[k][0x0]);
    }
  }
  matrix.show();
}

Credits

Pedro Martin

Pedro Martin

8 projects • 15 followers

Comments