// Grains of sand falling in a hourglass.
//
// by Koepel, 26 May 2024, Public Domain.
// 
// I'm not entirely happy with the code,
// but at least I got the timing and the steps right.
//
// Calculate the ongoing time:
//   A ledstrip of 16 leds with 8 grains of sand.
//   The first grain falls 8 places.
//   The last grain falls 1 place.
//   1+2+3+4+5+6+7+8 = 36 steps.

#include <FastLED.h>

#define NUM_LEDS 16
#define DATA_PIN 6
CRGB leds[NUM_LEDS];

const CRGB idleColor = CRGB::Khaki;
const CRGB activeColor = CRGB::OrangeRed;
const CRGB finishedColor = CRGB::Plum;

const int startButtonPin = 12;
const int resetButtonPin = 11;

int grains_top;     // the number of grains of sand at the top
int grains_bottom;  // the number of grains of sand at the bottom
const int interval = 300;  // the delay for each step.

void setup() 
{
  Serial.begin(115200);
  Serial.println();
  Serial.println("⌛ Grains of sand falling in a hourglass ⌛");
  Serial.println("Press the start button.");

  pinMode(startButtonPin, INPUT_PULLUP);
  pinMode(resetButtonPin, INPUT_PULLUP);

  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
  //  FastLED.setBrightness(200); // Adjust the brightness to 200

  /*
  // Test the function showSand().
  Serial.print("Testing the showSand() function...");
  showSand(8,0,-1);
  delay(1000);
  showSand(0,8,-1);
  delay(1000);
  showSand(0,0,0);
  delay(1000);
  showSand(0,0,15);
  delay(1000);
  Serial.println(" done.");
  */
  
  // set initial values
  grains_top = 8;
  grains_bottom = 0;
  showSand(grains_top, grains_bottom, -1, idleColor);
}

void loop() 
{
  int start = digitalRead(startButtonPin);
  int reset = digitalRead(resetButtonPin);

  if(start == LOW and grains_top > 0)
  {
    Serial.print("The hourglass is running   : ");
    int steps = 0;
    int unsigned long t1 = millis();

    // There are 8 grains of sand that have to fall.
    for(int i=0; i<8; i++)
    {
      // The grain of sand is taken from index 7,
      // and then it starts to fall to index 8.
      // At the same moment, the number of grains in the top
      // is one grain lower.
      int falling = 8;

      // Reduce the grains of sand at the top.
      grains_top--;

      // let the grain fall.
      while(falling < NUM_LEDS - grains_bottom)
      {
        showSand( grains_top, grains_bottom, falling, activeColor);
        delay(interval);
        falling++;
        steps++;
        Serial.print("█"); // UTF-8 character "Full Block"
      }
      // One grain of sand was added to the bottom.
      grains_bottom++;
    }

    unsigned long t2 = millis();
    unsigned long elapsedMillis = t2 - t1;

    // Update the leds once more to show the new color
    showSand( grains_top, grains_bottom, -1, finishedColor);

    Serial.println();
    Serial.println("Theoretical number of steps: 36");
    Serial.print("Measured number of steps   : ");
    Serial.println(steps);
    Serial.print("Theoretical time           : ");
    Serial.print(interval * 36);
    Serial.println(" ms");
    Serial.print("Measured time              : ");
    Serial.print(elapsedMillis);
    Serial.println(" ms");
  }

  if(reset == LOW)
  {
    // set initial values
    grains_top = 8;
    grains_bottom = 0;
    showSand(grains_top, grains_bottom, -1, idleColor);
  }

  delay(10);
}

// Show the leds at the top, the leds at the bottom,
// and the falling led.
// The falling led can be -1 for no falling led.
// The led with index 0 is at the top.
// The led with index 15 is at the bottom.
// The sand barrier is between led 7 and 8.
void showSand(int top, int bottom, int fall, const CRGB color)
{
  // Start with all leds off
  FastLED.clear();
 
  // The leds at the top
  for (int i = 8-top; i < 8; i++) 
    leds[i] = color;

  // The leds at the bottom
  for (int i = NUM_LEDS-bottom; i<NUM_LEDS; i++) 
    leds[i] = color;
 
  // The falling led
  if(fall >= 0)
    leds[fall] = color;

  FastLED.show();
}
Hourglass