SmartMatrix Digital Sand

Can anyone point me to Teensy 3.5 code using SmartMatrix to make the “Digital Sand” display as shown in nearly every Adafruit panel product? They seem to usually run a Raspberry Pi and there is similar code in the Adafruit PixelDust library (Snow.cpp) but don’t have the chops to convert that to Arduinoish code. I have most of the SmartMatrix library demo programs working so my system is good. This is the panel (and demo) I have.
https://www.adafruit.com/product/5362

This is from the Matrixportal demo Examples → Adafruit Protomatter → pixeldust (which uses Arduino libraries), and I’ve modified it slightly for my uses.

You will need to modify the Adafruit_Protomatter library for the Teensy, or replace its use with the SmartMatrix libraries. Unfortunately, there are some missing board specific dependencies for Teensy 3.2/3.5/3.6. It does build for the Teensy 4.1. Note, the pin assignments are for the Matrix Portal, and you will need to modify them for the Teensy that you use. Note, that you need to use a different Smart Matrix shield (and different pins) between the Teensy 3.x and Teensy 4.x processors.

Obviously, if you use a different accelerometer, you would need to modify that code as well.

/* ----------------------------------------------------------------------
"Pixel dust" Protomatter library example. As written, this is
SPECIFICALLY FOR THE ADAFRUIT MATRIXPORTAL M4 with 64x32 pixel matrix.
Change "HEIGHT" below for 64x64 matrix. Could also be adapted to other
Protomatter-capable boards with an attached LIS3DH accelerometer.

PLEASE SEE THE "simple" EXAMPLE FOR AN INTRODUCTORY SKETCH,
or "doublebuffer" for animation basics.

The original code was an Ardunio example for the Adafruit Matrix Portal.
	Examples -> Adafruit Protomatter -> pixeldust

I (Michael Meissner, arduino@the-meissners.org) have made several changes to
this file to add a non-interactive random mode, and to reformat some of the
code to meet my personal coding style.  If you include my changes, please at
least include the attribution of those changes.

------------------------------------------------------------------------- */

#include <Wire.h>			// For I2C communication
#include <Adafruit_LIS3DH.h>		// For accelerometer
#include <Adafruit_PixelDust.h>		// For sand simulation
#include <Adafruit_Protomatter.h>	// For RGB matrix
#include <Bounce2.h>			// For reading up/down buttons.

#define HEIGHT	32			// Matrix height (pixels) - SET TO 64 FOR 64x64 MATRIX!
#define WIDTH	64			// Matrix width (pixels)
#define MAX_FPS	45			// Maximum redraw rate, frames/second

#if HEIGHT == 64			// 64-pixel tall matrices have 5 address lines:
uint8_t addrPins[] = {17, 18, 19, 20, 21};
#else					// 32-pixel tall matrices have 4 address lines:
uint8_t addrPins[] = {17, 18, 19, 20};
#endif

// Remaining pins are the same for all matrix sizes. These values
// are for MatrixPortal M4. See "simple" example for other boards.
uint8_t		rgbPins[]		= {7, 8, 9, 10, 11, 12};

const uint8_t	clockPin		= 14;
const uint8_t	latchPin		= 15;
const uint8_t	oePin			= 16;

// Pins on the Matrix portal
const uint8_t	upPin			= 2;
const uint8_t	downPin			= 3;

// Pin to switch between using the accelerometer and using random.
const uint8_t	switchPin		= upPin;
const uint32_t	switchInterval		= 25;		// use a debounce interval of 25ms
Bounce		switchBounce;
bool		do_random		= true;

Adafruit_Protomatter matrix (WIDTH,				// Matrix width in pixels,
			     4,					// bit depth
			     1, rgbPins,			// # of matrix chains, array of 6 RGB pins
			     sizeof(addrPins), addrPins,	// # of address pins (height is inferred), array of pins
			     clockPin, latchPin, oePin, true);	// other matrix control pins.

Adafruit_LIS3DH accel = Adafruit_LIS3DH();

#define N_COLORS   8
#define BOX_HEIGHT 8
#define N_GRAINS (BOX_HEIGHT*N_COLORS*8)
uint16_t colors[N_COLORS];

Adafruit_PixelDust sand(WIDTH, HEIGHT, N_GRAINS, 1, 128, false);

uint32_t prevTime = 0; // Used for frames-per-second throttle

// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------

void err(int x) {
  uint8_t i;
  pinMode(LED_BUILTIN, OUTPUT);       // Using onboard LED
  for(i=1;;i++) {                     // Loop forever...
    digitalWrite(LED_BUILTIN, i & 1); // LED on/off blink to alert user
    delay(x);
  }
}

void setup(void) {
  Serial.begin (115200);
  while (!Serial && millis () < 3000UL) {
    delay (10);
  }

  // Attach bounce pin
  switchBounce.attach (switchPin, INPUT_PULLUP);
  switchBounce.interval (switchInterval);

  ProtomatterStatus status = matrix.begin();
  Serial.printf("Protomatter begin() status: %d\n", status);

  if (!sand.begin()) {
    Serial.println("Couldn't start sand");
    err(1000); // Slow blink = malloc error
  }

  if (!accel.begin(0x19)) {
    Serial.println("Couldn't find accelerometer");
    err(250);  // Fast bink = I2C error
  }
  accel.setRange(LIS3DH_RANGE_4_G);   // 2, 4, 8 or 16 G!

  //sand.randomize(); // Initialize random sand positions

  // Set up initial sand coordinates, in 8x8 blocks
  int n = 0;
  for(int i=0; i<N_COLORS; i++) {
    int xx = i * WIDTH / N_COLORS;
    int yy =  HEIGHT - BOX_HEIGHT;
    for(int y=0; y<BOX_HEIGHT; y++) {
      for(int x=0; x < WIDTH / N_COLORS; x++) {
        //Serial.printf("#%d -> (%d, %d)\n", n,  xx + x, yy + y);
        sand.setPosition(n++, xx + x, yy + y);
      }
    }
  }
  Serial.printf("%d total pixels\n", n);

  colors[0] = matrix.color565 (64,   64,  64); // Dark Gray
  colors[1] = matrix.color565 (120,  79,  23); // Brown
  colors[2] = matrix.color565 (228,   3,   3); // Red
  colors[3] = matrix.color565 (255, 140,   0); // Orange
  colors[4] = matrix.color565 (255, 237,   0); // Yellow
  colors[5] = matrix.color565 (  0, 128,  38); // Green
  colors[6] = matrix.color565 (  0,  77, 255); // Blue
  colors[7] = matrix.color565 (117,   7, 135); // Purple
}

// MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ----------------------------

void loop() {
  const  int	change	= 700;
  static int	counter	= change;
  static int	xx_i	= 0;
  static int	yy_i	= 0;
  static int	zz_i	= 0;
  double	xx;
  double	yy;
  double	zz;
  uint32_t	t;

  // Limit the animation frame rate to MAX_FPS.  Because the subsequent sand
  // calculations are non-deterministic (don't always take the same amount
  // of time, depending on their current states), this helps ensure that
  // things like gravity appear constant in the simulation.
  while(((t = micros()) - prevTime) < (1000000L / MAX_FPS))
    ;

  prevTime = t;

  // See if we pressed the button to switch between modes.
  switchBounce.update ();

  if (switchBounce.fell ()) {
    if (do_random) {
      Serial.println ("Switching to using the accelerometer");
      do_random = false;

    } else {
      Serial.println ("Switching to random mode");
      do_random = true;
      counter   = change;
    }
  }

  // Either do random mode or use the accelerometer.
  if (do_random) {
    if (counter++ > change) {
      counter = 0;
      int xx_j;
      int yy_j;
      int zz_j;
      int num = 0;
      do {
	xx_j = random (-4000, 4000);
	yy_j = random ( -400,  400);
	zz_j = random ( -400,  400);
      } while ((xx_i == xx_j) && (yy_i == yy_j) && (zz_i == zz_j) && (num++ < 5));

      xx_i = xx_j;
      yy_i = yy_j;
      zz_i = zz_j;

      Serial.printf ("xx = %5d, yy = %5d, zz = %5d\n", xx_i, yy_i, zz_i);
    }

    xx = xx_i;
    yy = yy_i;
    zz = zz_i;

  } else {

    // Read accelerometer...
    sensors_event_t event;
    accel.getEvent(&event);

    xx = event.acceleration.x * 1000;
    yy = event.acceleration.y * 1000;
    zz = event.acceleration.z * 1000;

    {
      long xx_l = (long) xx;
      long yy_l = (long) yy;
      long zz_l = (long) zz;
      long xx_f = (long) (((fabs (xx) - (fabs ((double) xx_l))) * 10.0) + 0.5);
      long yy_f = (long) (((fabs (yy) - (fabs ((double) yy_l))) * 10.0) + 0.5);
      long zz_f = (long) (((fabs (zz) - (fabs ((double) zz_l))) * 10.0) + 0.5);
      Serial.printf("xx = %8ld.%ld, yy = %8ld.%ld, zz = %8ld.%ld\n",
		    xx_l, xx_f,
		    yy_l, yy_f,
		    zz_l, zz_f);
    }
  }

  // Run one frame of the simulation
  sand.iterate (xx, yy, zz);

  //sand.iterate(-accel.y, accel.x, accel.z);

  // Update pixel data in LED driver
  dimension_t x, y;
  matrix.fillScreen(0x0);
  for(int i=0; i<N_GRAINS ; i++) {
    sand.getPosition(i, &x, &y);
    int n = i / ((WIDTH / N_COLORS) * BOX_HEIGHT); // Color index
    uint16_t flakeColor = colors[n];
    matrix.drawPixel(x, y, flakeColor);
    //Serial.printf("(%d, %d)\n", x, y);
  }
  matrix.show(); // Copy data to matrix buffers
}


// HISTORY
// $Log: Meissner_pixeldust_64x32.ino,v $
// Revision 1.13  2020/12/07 03:19:41  michaelmeissner
// Add message about changes.
//
// Revision 1.12  2020/12/07 01:52:23  michaelmeissner
// Iterate on random settings.
//
// Revision 1.11  2020/12/07 01:37:50  michaelmeissner
// Spacing.
//
// Revision 1.10  2020/12/05 23:21:49  michaelmeissner
// Bump up sizes for printing accel. information.
//
// Revision 1.9  2020/12/05 23:19:56  michaelmeissner
// Fix thinko in reporting accel. positions.
//
// Revision 1.8  2020/12/05 22:56:21  michaelmeissner
// Spacing.
//
// Revision 1.7  2020/12/05 22:55:09  michaelmeissner
// Comment parameters to the Adafruit_Protomatter setup.
//
// Revision 1.6  2020/12/05 22:46:46  michaelmeissner
// Spacing; Reset random counter when using button; Lower random change time to 700.
//
// Revision 1.5  2020/12/05 22:38:47  michaelmeissner
// Make things const; Spacing; Switch between random and accel. via the UP button.
//
// Revision 1.4  2020/12/05 22:22:29  michaelmeissner
// Spacing.
//
// Revision 1.3  2020/12/05 22:20:49  michaelmeissner
// Eliminate carriage returns.
//
// Revision 1.2  2020/12/05 22:12:33  michaelmeissner
// Spacing; switch to using random display instead of accelerometer.
//
// Revision 1.1  2020/12/05 20:56:46  michaelmeissner
// Initial version.
//

Thank you Michael. Using bits of your code and some from Overview | Shake Away 2021 with MatrixPortal | Adafruit Learning System and a lot of trial and error I got it working. Comments will be appreciated, I only pretend to be a programmer. This is a digital clock that melts into a sand display when you shake it a few times. Will be a cool Christmas present for some one. See Clock01 - YouTube
I am setting the “sand” to random colors. What I would like to do though, is randomize the pixel colors once, then maintain those colors for each pixel as it moves. I have not found a way to retrieve the pixel color from before the sand iteration. Apparently the way it works is the sand library only moves the pixel positions, It does not repaint the pixel in the buffer, that is left to the SmartMatrix library. I may have to make my own local rgb24 copy of the background buffer or maybe a local copy of the sand buffer or maybe both. I do not see a way to readpixel from the active matrix screen which I think would give me the “before” information.
The hardware for this sketch is Teensy 3.6, SmartMatrix adapter V4 (Adafruit #1902), LIS3DH accelerometer (Adafruit 2809) and 2mm LED panel (Adafruit 5362).

-------------------------------------------------------------------------
// SPDX-FileCopyrightText: 2020 Limor Fried for Adafrudit Industries
// https://learn.adafruit.com/matrixportal-shake-away-2020
// SPDX-License-Identifier: MIT
//
#include <MatrixHardware_Teensy3_ShieldV4.h>        // SmartLED Shield for Teensy 3 (V4)
#include <SmartMatrix.h>
#include <Adafruit_LIS3DH.h>                        // For accelerometer
#include <Adafruit_PixelDust.h>                     // For simulation
#include <TimeLib.h>

#define COLOR_DEPTH 24                  // Choose the color depth used for storing pixels in the layers: 24 or 48 (24 is good for most sketches - If the sketch uses type `rgb24` directly, COLOR_DEPTH must be 24)
const uint16_t kMatrixWidth = 64;       // Set to the width of your display, must be a multiple of 8
const uint16_t kMatrixHeight = 64;      // Set to the height of your display
const uint8_t kRefreshDepth = 36;       // Tradeoff of color quality vs refresh rate, max brightness, and RAM usage.  36 is typically good, drop down to 24 if you need to.  On Teensy, multiples of 3, up to 48: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48.  On ESP32: 24, 36, 48
const uint8_t kDmaBufferRows = 4;       // known working: 2-4, use 2 to save RAM, more to keep from dropping frames and automatically lowering refresh rate.  (This isn't used on ESP32, leave as default)
const uint8_t kPanelType = SM_PANELTYPE_HUB75_64ROW_MOD32SCAN;   // Choose the configuration that matches your panels.  See more details in MatrixCommonHub75.h and the docs: https://github.com/pixelmatix/SmartMatrix/wiki
const uint32_t kMatrixOptions = (SM_HUB75_OPTIONS_NONE);         // see docs for options: https://github.com/pixelmatix/SmartMatrix/wiki
const uint8_t kBackgroundLayerOptions = (SM_BACKGROUND_OPTIONS_NONE);
//const uint8_t kIndexedLayerOptions = (SM_INDEXED_OPTIONS_NONE);
//const uint8_t kScrollingLayerOptions = (SM_SCROLLING_OPTIONS_NONE);
const int displayCenter = (kMatrixHeight / 2);

SMARTMATRIX_ALLOCATE_BUFFERS(matrix, kMatrixWidth, kMatrixHeight, kRefreshDepth, kDmaBufferRows, kPanelType, kMatrixOptions);
SMARTMATRIX_ALLOCATE_BACKGROUND_LAYER(backgroundLayer, kMatrixWidth, kMatrixHeight, COLOR_DEPTH, kBackgroundLayerOptions);

const int defaultBrightness = (80 * 255) / 100;  // 80% brightness

const rgb24 Black = {0x00, 0x00, 0x00};
const rgb24 White = {0xFF, 0xFF, 0xFF};

#define SHAKE_ACCEL_G   2.9                                 // Force (in Gs) to trigger shake
#define SHAKE_ACCEL_MS2 (SHAKE_ACCEL_G * 9.8)               // Convert to m/s^2
#define SHAKE_ACCEL_SQ  (SHAKE_ACCEL_MS2 * SHAKE_ACCEL_MS2) // Avoid sqrt() in accel check
#define SHAKE_EVENTS    5                                   // Number of accel readings to trigger sand
#define SHAKE_PERIOD    3000                                // Period (in ms) when SHAKE_EVENTS must happen
#define SAND_TIME       30000                               // Time (in ms) to run simulation before restarting

#define MAX_FPS 60                      // Maximum redraw rate, frames/second
uint32_t prevTime = 0;                  // For frames-per-second throttle
uint16_t n_grains = 0;                  // Number of sand grains (counted on sand trigger)
Adafruit_PixelDust *sand;               // Sand object (allocated in setup())

Adafruit_LIS3DH accel = Adafruit_LIS3DH(); // Accelerometer
sensors_event_t event;                  // Accelerometer result

/////////////////////////////////////////////////////////////////
void setup(void) {

  // set the Time library to use Teensy 3.0's RTC
  setSyncProvider(getTeensy3Time);

  Serial.begin(115200);
  delay(100);                             // Wait for serial to start
  // Get clock time from the connected PC (if available)
  // Mandatory only if clock battery is not provided
  if (timeStatus() != timeSet) {
    Serial.println("Unable to sync with the RTC");
    Serial.println("Defaulting to stored clock");
  } else {
    Serial.println("RTC has set from PC system time");
  }

  // Setup SmartMatrix
  matrix.addLayer(&backgroundLayer);
  matrix.begin();
  matrix.setBrightness(defaultBrightness);
  Serial.println("Matrix begin()");
  backgroundLayer.setFont(gohufont11b);      // Font is 11 pixels high

  // PixelDust set up is deferred until sandSimulation()

  if  (!accel.begin(0x18)) {
    Serial.println("Couldn't Find Accelerometer");
    err(250);  // Fast blink = I2C error
  }
  Serial.println("Accelerometer OK");
  accel.setRange(LIS3DH_RANGE_8_G);


}
/////////////////////////////////////////////////////////////////
void loop() {
  char timeBuffer[10];             // Holds formatted time string
  int indent;                      // Left margin for prints to screen
  static int lastsec;              // To trigger a time update
  static int numGrains;            // # of pixels lit
  static bool notStirred = false;  // Shake counter entry flag
  time_t t;                        // Used to record current time in MS
  static time_t last_event_time;   // Calculated end of allowed shake period
  static int num_events;           // Counts shake events

  if (second() != lastsec) {       // Update the clock display once per second
    lastsec = second();
    backgroundLayer.fillScreen(Black);
    Serial.println(second());

    // Draw Clock to SmartMatrix using AM/PM 12 hour time
    //       Font position reference point is top left of first char
    //       Pixel zero is top left of display
    uint8_t Hour = hourFormat12();
    static char AMPM = 'A';
    if (isPM()) AMPM = 'P';

    // Draw time
    indent = 5;                    //  Left margin allow for 2 digit time
    if (Hour < 10) indent = 9;     //  Left margin allow for 3 digits
    sprintf(timeBuffer, "%d:%02d:%02d%c", Hour, minute(), second(), AMPM);
    backgroundLayer.drawString(indent, displayCenter - 18, White, timeBuffer);

    // Draw date
    indent = 4;                    //  Left margin allow for 2 digit month
    if (month() < 10) indent = 6;  //  Left margin allow for 3 digits month
    sprintf(timeBuffer, "%d/%02d/%02d", month(), day(), year());
    backgroundLayer.drawString(indent, displayCenter, White, timeBuffer);
  }

  // Look for shakes
  t = millis();                               // Current time
  if ((t > last_event_time)) {                // Timed out!
    notStirred = false;                       // Reset for another try
    //    num_events = 0;
  }

  accel.getEvent(&event);
  // Square the accelerometer readings (why?)
  // x**2 + y**2 + z**2
  float mag2 = event.acceleration.x * event.acceleration.x +
               event.acceleration.y * event.acceleration.y +
               event.acceleration.z * event.acceleration.z;

  if (mag2 >= SHAKE_ACCEL_SQ) {              // A Shake has been detected
    delay(5);                                // Debounce the accelerometer

    // notStirred is true if already counting shakes, false if first one
    if (notStirred) {                        // Count additional shakes
      if (++num_events >= SHAKE_EVENTS) {    // Enough shakes detected?

        // Cue the digital sand
        sandSimulation();

        num_events = 0;       // Reset for next shakes interval
      }
    } else {                // notStirred is false, initial shake detected
      notStirred = true;    // In a shake interval,flag to look for more
      num_events = 1;
      last_event_time = t + SHAKE_PERIOD; // Allowed interval for shakes
    }
  }                         // End of shake detect

  // OK to swap now
  backgroundLayer.swapBuffers();
}                           // End of Loop()


/* Function to create a random color pixel in rgb24 form */
rgb24 randomPixel() {
  uint8_t Red = random (255);
  uint8_t Grn = random (255);
  uint8_t Blu = random (255);
  return {Red, Grn, Blu};
}


/* function to retrieve time from Teensy RTC */
time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}


/*  code to process initial time sync messages from the serial port   */
#define TIME_HEADER  "T"   // Header tag for serial time sync message

unsigned long processSyncMessage() {
  unsigned long pctime = 0L;
  const unsigned long DEFAULT_TIME = 1357041600; // Jan 1 2013

  if (Serial.find(TIME_HEADER)) {
    pctime = Serial.parseInt();
    return pctime;
    if ( pctime < DEFAULT_TIME) { // check the value is a valid time (greater than Jan 1 2013)
      pctime = 0L; // return 0 to indicate that the time is not valid
    }
  }
  return pctime;
}


/* Error handler used by setup() */
void err(int x) {
  uint8_t i;
  pinMode(LED_BUILTIN, OUTPUT);       // Using onboard LED
  for (i = 1;; i++) {                 // Loop forever...
    digitalWrite(LED_BUILTIN, i & 1); // LED on/off blink to alert user
    delay(x);
  }
}


/* Run sand simulation for a few seconds
   At this point the background layer has not been swapped
*/
void sandSimulation() {
  int n_grains = 0;
  time_t t, elapsed;
  time_t prevTime, sandStartTime = millis();
  rgb24 thisPixel;
  int i, j = 0, b;

  // Count number of 'on' pixels (sand grains) in background buffer
  // This must be done *before* swapping buffers
  // as readPixel doesn't work in matrix layer.
  for (i = 0; i < kMatrixHeight; i++) {              // Row index (y coordinate)
    for (b = 0; b < kMatrixWidth; b++) {             // Column index (x coordinate)
      thisPixel = backgroundLayer.readPixel(i, b);

      if (thisPixel.red | thisPixel.green | thisPixel.blue) {
        n_grains++;
      }
    }
  }

  // Set initial sand pixel positions and draw initial matrix state
  // Allocate sand object based on matrix size and bitmap 'on' pixels
  sand = new Adafruit_PixelDust(kMatrixWidth, kMatrixHeight, n_grains, 1);
  if (!sand->begin()) {
    Serial.println("PixelDust init failed");
    return;
  }
  sand->clear();

  // Rescan the display and attach lit pixels to the sand grains
  for (i = 0; i < kMatrixHeight; i++) {              // Row index
    for (b = 0; b < kMatrixWidth; b++) {             // Column index
      thisPixel = backgroundLayer.readPixel(b, i);

      if (thisPixel.red | thisPixel.green | thisPixel.blue) {
        sand->setPosition(j++, b, i);
      }
    }
  }

  while (true) {                // Hijack loop()
    // Only run for specified time
    while ((elapsed = (millis() - sandStartTime)) < SAND_TIME) {

      // Limit the animation frame rate to MAX_FPS.
      while (((t = micros()) - prevTime) < (1000000L / MAX_FPS));
      prevTime = t;

      // Read accelerometer...
      sensors_event_t event;
      accel.getEvent(&event);

      // Run one frame of the simulation
      sand->iterate(event.acceleration.x * 1024, event.acceleration.y * 1024, event.acceleration.z * 1024);

      // Update pixel data in LED driver
      backgroundLayer.fillScreen(Black);
      dimension_t x, y;
      for (i = 0; i < n_grains ; i++) {
        sand->getPosition(i, &x, &y);

        // Would be nice to getPixel color here and reuse in the drawPixel
        // so individual grains don't shimmer
        //       backgroundLayer.drawPixel(x, y, Blue);  // works
        backgroundLayer.drawPixel(x, y, randomPixel());  // works
      }

      backgroundLayer.swapBuffers(false);

    }                                      // End of elapsed time test
    return;                                // Exit back to displaying clock
  }                                        // end of while true loop
}

I really like the effect, great job!

I find the transition from sand to the clock again to be abrupt. Some suggestions:

  • Simple: fade out the sand and fade in the clock
  • Advanced: make the clock pixels “sticky” so that sand that hits one of the clock pixels gets stuck there.
    • If the user isn’t actively moving sand when you want the clock to be shown, dump a load of sand from the top whatever doesn’t stick drops off the bottom

I do not see a way to readpixel from the active matrix screen

I may have to make my own local rgb24 copy of the background buffer or maybe a local copy of the sand buffer or maybe both

I’m not exactly sure what you need to do with an additional layer, but you can use multiple layers in SmartMatrix Library. The background layer is opaque, so if you’re adding multiple background layers, the last one you add will be the only one visible. You can create layers that aren’t refreshed to the display (by not calling addLayer(), but there are some things to look out for there. swapBuffers won’t work if the layer isn’t being refreshed. You can draw to a layer, get the pixel value, and clear the layer, so if you want to use a layer as a “scratchpad” for seeing where text is for example, you could do that. For text, using indexedLayer (see MatrixClock example) will only use one bit per pixels instead of 24 bits.

Happy to chat about this project more but I’m out of time now

Thank you Louis. I have the clock working as I hoped. The digital sand feature assigns random colors and maintains that color for each pixel as the sand plays out. I like this a lot better than continuously random generated colors. I did this by declaring an rgb24[4096] array which records the initial randomly selected color for each grain of sand. Then for each sand iteration I look up the color and paint the pixels. I made a nice Walnut box for it.

Short video at:

1 Like

Boy am I glad I posted this code here. Had a hard drive die Thursday and lost the working version. Was able to recreate from this post in a few hours.

Louis: I was able to get fade in/out working as you suggested. It looks nice.

Louis, could you have a look at this thread? I’m seeing a weird interaction with EEPROM writes and the clock used by Smart Matrix.

https://forum.pjrc.com/threads/70419-Teensy-3-6-clock-speed-after-EEPROM-write?p=307391#post307391

Another effort using the same hardware. Because the world needs another digital hourglass.
Parts: Adafruit 5362 display
Adafruit 2809 accelerometer
SmartMatrix V4 shield
PJRC Teensy 3.6

https://youtu.be/wedflls7OYo

/*
 * Hourglass simulation
 * Uses SmartMatrix 64x64 display, Teensy 3.6 by Jim Harvey https://wb8nbs.wordpress.com
 */

#include <MatrixHardware_Teensy3_ShieldV4.h> // SmartLED Shield for Teensy 3 (V4)
#include <SmartMatrix.h>
#include <Adafruit_PixelDust.h>          // For simulation
#include <Adafruit_LIS3DH.h>             // For accelerometer

#define COLOR_DEPTH 24                   // Choose the color depth used for storing pixels in the layers: 24 or 48 (24 is good for most sketches - If the sketch uses type `rgb24` directly, COLOR_DEPTH must be 24)
const uint16_t kMatrixWidth = 64;        // Set to the width of your display, must be a multiple of 8
const uint16_t kMatrixHeight = 64;       // Set to the height of your display
const uint8_t kRefreshDepth = 36;        // Tradeoff of color quality vs refresh rate, max brightness, and RAM usage.  36 is typically good, drop down to 24 if you need to.  On Teensy, multiples of 3, up to 48: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48.  On ESP32: 24, 36, 48
const uint8_t kDmaBufferRows = 4;        // known working: 2-4, use 2 to save RAM, more to keep from dropping frames and automatically lowering refresh rate.  (This isn't used on ESP32, leave as default)
const uint8_t kPanelType = SM_PANELTYPE_HUB75_64ROW_MOD32SCAN;   // Choose the configuration that matches your panels.  See more details in MatrixCommonHub75.h and the docs: https://github.com/pixelmatix/SmartMatrix/wiki
const uint32_t kMatrixOptions = (SM_HUB75_OPTIONS_NONE);         // see docs for options: https://github.com/pixelmatix/SmartMatrix/wiki
const uint8_t kBackgroundLayerOptions = (SM_BACKGROUND_OPTIONS_NONE);

const int displayCenter = (kMatrixHeight / 2);

const rgb24 Black = {0x00, 0x00, 0x00};
const rgb24 White = {0xFF, 0xFF, 0xFF};
const rgb24 Gray  = {0x64, 0x64, 0x64};
const rgb24 Red   = {0xFF, 0x00, 0x00};
const rgb24 Green = {0x00, 0xFF, 0x00};
const rgb24 Blue  = {0x50, 0x50, 0xFF};

SMARTMATRIX_ALLOCATE_BUFFERS(matrix, kMatrixWidth, kMatrixHeight, kRefreshDepth, kDmaBufferRows, kPanelType, kMatrixOptions);
SMARTMATRIX_ALLOCATE_BACKGROUND_LAYER(backgroundLayer, kMatrixWidth, kMatrixHeight, COLOR_DEPTH, kBackgroundLayerOptions);

float defaultBrightness = 100.0;
Adafruit_PixelDust *sand;                // Sand object (allocated in setup())

Adafruit_LIS3DH accel = Adafruit_LIS3DH(); // Accelerometer
sensors_event_t event;                    // Accelerometer result

void setup(void) {

  Serial.begin(115200);

  // Setup SmartMatrix
  matrix.addLayer(&backgroundLayer);
  matrix.begin();

  backgroundLayer.enableColorCorrection(true);
  matrix.setBrightness(180);

  if  (!accel.begin(0x18)) {
    Serial.println("Couldn't Find Accelerometer");
  }
  Serial.println("Accelerometer OK");
  accel.setRange(LIS3DH_RANGE_8_G);
}                                         // End setup()

void loop() {
  backgroundLayer.fillScreen(Black);      // Erase the screen
  backgroundLayer.swapBuffers(true);
  
// sand argument is delay to slow the grains. With Teensy set to 120 MHz
// 0 is about 30 seconds 
// 38 is about 2 minutes
// 88 is about 88 minutes
  sandSimulation(38 );

  Serial.println("resetting in loop()");
  delay (3000);
}                                         // end loop()


/* Run sand simulation
  Arg sets a delay between pixel frames to adjust the time to complete
*/
void sandSimulation(int procrastinate) {
  int n_grains = 0;
  int glassWidth = 40;                   // Pixels
  int glassHeight = 62;                  // Pixels
  int pixelBlack[20] {                   // Pixels to erase
    32, 33,
    32, 31,
    12,  1,
    13,  1,
    51,  1,
    52,  1,
    12, 62,
    13, 62,
    51, 62,
    52, 62
  };
  rgb24 thisPixel;                       // Temp variable 
  rgb24 grainColor[4096];                // Records initial color
  int i, j = 0, b;
  dimension_t x, y;

//  backgroundLayer.fillScreen(Black);   // Erase the screen
  backgroundLayer.fillTriangle(15, 3,  49, 3,  32, 30, Gray); // Fill the top triangle with gray sand

  // Outline the hour glass
  backgroundLayer.drawTriangle(12, 1,   52, 1,    32, 33, White);
  backgroundLayer.drawTriangle(12, 62,  52, 62,   32, 31, White);

  // Round off the sharp corners and create narrow center channel
  for (int i = 0; i < 20; i += 2) {
    backgroundLayer.drawPixel(pixelBlack[i], pixelBlack[i + 1], Black);
  }

  // Count number of 'on' pixels (marked Gray) in background buffer
  // This must be done *before* swapping buffers
  // as readPixel doesn't work in matrix layer.
  for (i = 0; i < kMatrixHeight; i++) {  // Row index (y coordinate)
    for (b = 0; b < kMatrixWidth; b++) { // Column index (x coordinate)

      thisPixel = backgroundLayer.readPixel(i, b);
      if (thisPixel.red == 100) {        // Is this Gray?
        thisPixel = randomPixel();       // Generate random color
        grainColor[n_grains] = thisPixel; // And remember that color
        n_grains++;                      // Count the grains
      }
    }
  }

  // Set initial sand pixel positions and draw initial matrix state
  // Allocate sand object based on matrix size and bitmap 'on' pixels
  sand = new Adafruit_PixelDust(kMatrixWidth, kMatrixHeight, n_grains, 1);

  if (!sand->begin()) {
    Serial.println("PixelDust init failed");
    return;
  }
  sand->clear();

  // Rescan the display and attach random colored pixels to the sand grains
  for (i = 1; i < (1 + glassHeight); i++) {    // Row index
    for (b = 12; b < (12 + glassWidth); b++) {  // Column index

      thisPixel = backgroundLayer.readPixel(b, i);

      if (thisPixel.red | thisPixel.green | thisPixel.blue) {  // is anything there?
        if (thisPixel.red == 0xFF) {     // Full White is outline
          sand->setPixel(b, i);          // Define outline pixels as obstacles
          // Must add blocking to the matrix else the outline leaks
          if ((b < 32)) sand->setPixel(b - 1, i); // Blocks diagonal pixels on left side
          if ((b > 32)) sand->setPixel(b + 1, i); // Blocks diagonal pixels on right side
        } else {
          backgroundLayer.drawPixel(b, i, grainColor[j]); // Not outline, Paint saved color
          sand->setPosition(j++, b, i);
        }
      }
    }
  }

  backgroundLayer.swapBuffers(true);     // Ready to go
  delay (500);                           // Pause to admire the colors

  while (true) {     // Main Spin loop
    
delay(procrastinate); // Limit the sand speed

    sensors_event_t event;               // Read accelerometer...
    accel.getEvent(&event);

    // Run one frame of the simulation
    sand->iterate(event.acceleration.x * 1024,
                  event.acceleration.y * 1024,
                  event.acceleration.z * 1024);

    // Update pixel data in LED driver
    // First,  erase the inside area
    for (i = 1; i < (1 + glassHeight); i++) {     // Row index
      for (b = 12; b < (12 + glassWidth); b++) {  // Column index
        
        thisPixel = backgroundLayer.readPixel(b, i);
        
        // If not black and not part of the outline --
        if ((thisPixel.red > 0) && (thisPixel.red < 255)) {
          backgroundLayer.drawPixel(b, i, Black);  // Erase the pixel
        }
      }
    }

// Then paint the repositioned pixels
    for (i = 0; i < n_grains ; i++) {
      sand->getPosition(i, &x, &y);
      // Recall that grain color for the new position
      backgroundLayer.drawPixel(x, y, grainColor[i]);
    }
    backgroundLayer.swapBuffers(false);
  }                                      // End of fade spin loop
  backgroundLayer.fillScreen(Black);     // Erase the screen
  backgroundLayer.swapBuffers(true);
  return;                                // Exit back to displaying clock
}

/* Function to create a random color pixel in rgb24 form
    + 3 ensures no pixels will be completely dark
    but will never be 255 - to distinguish between hour
    glass outline and pixels.
*/
rgb24 randomPixel() {
  uint8_t Red = random(220) + 34;
  uint8_t Grn = random(220) + 34;
  uint8_t Blu = random(220) + 34;
  return {Red, Grn, Blu};
}

I’m considering the clock project done after adding one final option - a digital “Magic 8 Ball”. Still having problems with clock speed after an EEPROM write but I will add a reset button to get around that. Just to recap these are the parts
Adafruit 5362 display
Adafruit 2809 accelerometer
SmartMatrix V4 shield
PJRC Teensy 3.6
I’m reluctant to spend a lot of time doing drawings as the Teensy 3.6 is obsolete and most of the connections are described in the SmartMatrix documents. Here is a series of YouTubes showing the clock’s evolution

[Clock01 - YouTube]
[Clock02 - YouTube]
[Clock04 - YouTube]
[Hour Glass - YouTube]
[Magic Eight Ball simulation - YouTube]

Thank you Louis for the library.

I documented the build on my wordpress page

Discussion of the clock itself
https://wb8nbs.wordpress.com/2022/07/14/a-clock-with-benefits/

Explanation of the Setup Menus
https://wb8nbs.wordpress.com/2022/07/15/sand-clock-settings/

Very nicely done.
The programmer in me only has 2 suggestions

  1. 24H time, AM/PM must die :slight_smile:
  2. 2022/08/14, programmer dates are ISO and sortable by having year first, this fixes the mess of english vs non english dates (month first or day first)

Thanks for the suggestions but this project will likely end up as a gift to some non-technical relative for whom 24 hour time is alien.