A visually and functionally effective beginner project that only required three components to build.
A hourglass, also known as an sand clock, is a device used to measure the passage of time. It consists of two glass bulbs connected by a narrow neck, through which sand flows from the upper bulb to the lower one.
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/311d11e6a81e9a0b330db3dac531bc3a.jpg)
The flow of sand is controlled so that it takes a set amount of time to completely empty the upper bulb. Hourglasses are often used as time management tools and decorative items. This time I will present you a very simple way to make a digital version of such a clock. This is another example in my collection of DIY unusual clocks which you can check out on my playlist. At first I tried to make the project with an Arduino Nano microcontroller, but it soon became clear to me that stronger performance was needed, so I used an ESP32 which is quite sufficient, even for a more complex project.
Honestly, my initial idea was to make a total simulation with the movement of sand according to the law of fluid motion using an IMU sensor, but at least so far I have not managed to completely realize that idea.
The device presented in this project is extremely simple to make and consists of only 3 components.
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/78bb9238ac2372a9e168d0c6f83b54af.jpg)
- ESP8266 microcontroller board
- SH1106 Oled display with resolution of 128x64 dots,
- and Tilt sensor
A single lithium cell is used to power the device.
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/428f1d17d1d4a310821102d2891f429a.jpg)
This project is sponsored by PCBWay. They has all the services you need to create your project at the best price, whether is a scool project, or complex professional project. On PCBWay you can share your experiences, or get inspiration for your next project. They also provide completed Surface mount SMT PCB assemblY service at a best price, and ISO9001 quality control. Visit pcbway.com for more services
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/fd1e11a0ae52ce9cf86703b69286f0fc.png)
The tilt sensor is essentially a switch that is in the open state when the legs are facing up, and in the closed state when it is rotated 180 degrees.
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/3fc035dc6b5851c70c8bbdfb227dcb76.jpg)
This component offers the simplest way to display both states of an hourglass while avoiding the use of additional sensors and libraries.
Now let's see how the device behaves in real conditions. Immediately after turning on the screen, the hourglass appears in the starting position and grains of sand randomly flow from the upper to the lower container. The time for which all the sand flows from the upper to the lower container is set in the code, and in this case it is set to exactly 1 minute. In the upper part above the hourglass, the percentage of time elapsed from the beginning of the flow to the moment of reading is displayed.
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/2d1c656655ba39656f4d5cd32f01be4d.jpg)
In this case, a certain animation can be observed in the upper and lower background as well as when the grains of sand flow in order for the hourglass to be as realistic as possible. When the sand runs out completely (that is in 60 seconds) we can turn the clock 180 degrees, and the countdown starts again. If we rotate the hourglass at any given moment, it starts counting down the time from the beginning.
Now a few words about the code. Namely, you can immediately see that it is not completely optimized, it is divided into several parts, but all in order to be easier to customize.
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/72e1753dfda4c99b59eac1cfe7c33532.jpg)
Very simply at the beginning of the code by changing the constants, you can change every parameter, starting from the size and shape of the glass container, the amount of sand, the duration and speed of the sand leakage, dome parameters, up to the intensity of the animation and the number of falling particles.
And finally, a short conclusion. A visually and functionally effective beginner project that only required three components to build, but at the same time highly customizable, so that we can almost unlimitedly change all phisical parameters according to our own idea of the way such a device should function. The assembly is mounted in a suitable box made of PVC board with a thickness of 3mm and covered with colored self-adhesive wallpaper.
![](https://dfimg.dfrobot.com/61deb84baa9508d63a41a133/community/6117b8d7e26518838154758725037cac.jpg)
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <algorithm> // Add this for std::min
// Define GPIO pin
#define GPIO_PIN 13
// Initialize U8G2 display - rotation will be set in setup
U8G2_SH1106_128X64_NONAME_F_HW_I2C display(U8G2_R1); // Default rotation
// Animation parameters
const uint8_t SAND_PARTICLES = 25;
const uint8_t ANIMATION_DELAY = 50;
const unsigned long HOURGLASS_DURATION = 60000; // 1 minute
const uint8_t NUM_FALLING_PARTICLES = 8;
const uint8_t PARTICLE_SPEED_MIN = 1;
const uint8_t PARTICLE_SPEED_MAX = 2;
// Hourglass dimensions
const uint8_t GLASS_WIDTH = 50;
const uint8_t GLASS_HEIGHT = 100;
const uint8_t GLASS_X = (64 - GLASS_WIDTH) / 2;
const uint8_t GLASS_Y = 14;
const uint8_t WALL_THICKNESS = 2;
const uint8_t TOP_THICKNESS = 5;
const uint8_t BASE_PROTRUSION = 2;
const uint8_t NECK_WIDTH = 2;
const uint8_t NECK_TOTAL = NECK_WIDTH + (WALL_THICKNESS * 2);
const uint8_t CURVE_STEPS = 15;
const uint8_t TOP_FILL_PERCENT = 60;
const uint8_t BOTTOM_FILL_PERCENT = 50;
const uint8_t DOME_MAX_HEIGHT = 15; // Maximum height of the initial dome
const uint8_t SPREAD_THRESHOLD = 8; // Height at which sand starts to spread more
const float DOME_CURVE_FACTOR = 0.7; // Controls dome roundness (0.5-1.0)
uint32_t topPixelCount = 0; // Using uint32_t for larger numbers
uint32_t bottomPixelCount = 0; // Using uint32_t for larger numbers
int calculateDomeHeight(int distanceFromCenter, int maxHeight) {
float normalizedDist = (float)distanceFromCenter / (GLASS_WIDTH / 2);
return maxHeight * (1 - pow(normalizedDist, DOME_CURVE_FACTOR));
}
// Structures for particles
struct Particle {
int8_t x;
int8_t y;
int8_t velocity;
bool active;
};
struct FallingParticle {
int8_t x;
int8_t y;
int8_t speed;
bool active;
};
// Global variables
Particle particles[SAND_PARTICLES];
FallingParticle fallingParticles[NUM_FALLING_PARTICLES];
unsigned long startTime;
bool isRunning = true;
uint8_t topFillPercent = TOP_FILL_PERCENT;
uint8_t bottomFillPercent = 0;
int16_t leftBoundary[GLASS_HEIGHT];
int16_t rightBoundary[GLASS_HEIGHT];
// Function declarations
void calculateBoundaries();
void initializeFallingParticles();
void bezierPoint(float t, int x0, int y0, int x1, int y1, int x2, int y2, float &outX, float &outY);
void drawTopBase(bool isTop);
void drawHourglass();
void updateFallingParticles();
void drawFallingParticles();
void updateSandLevels();
void drawTopSand();
void drawBottomSand();
void drawSand();
void checkGPIOAndRotation() {
static bool lastPinState = HIGH;
bool currentPinState = digitalRead(GPIO_PIN);
if (currentPinState != lastPinState) {
// Pin state changed
display.setDisplayRotation(currentPinState ? U8G2_R3 : U8G2_R1);
// Restart animation
startTime = millis();
topFillPercent = TOP_FILL_PERCENT;
bottomFillPercent = 0;
initializeFallingParticles();
lastPinState = currentPinState;
}
}
// Bezier curve calculation function
void bezierPoint(float t, int x0, int y0, int x1, int y1, int x2, int y2, float &outX, float &outY) {
float mt = 1 - t;
outX = mt * mt * x0 + 2 * mt * t * x1 + t * t * x2;
outY = mt * mt * y0 + 2 * mt * t * y1 + t * t * y2;
}
// Calculate the boundaries of the hourglass
void calculateBoundaries() {
// ... (No changes in this function)
int middleY = GLASS_Y + GLASS_HEIGHT / 2;
for (int y = 0; y < GLASS_HEIGHT; y++) {
float t;
float xL, yL, xR, yR;
if (y < GLASS_HEIGHT / 2) { // Top half
t = (float)(y) / (GLASS_HEIGHT / 2);
bezierPoint(t,
GLASS_X + WALL_THICKNESS - BASE_PROTRUSION, GLASS_Y,
GLASS_X + WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT / 3,
GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS, middleY,
xL, yL);
bezierPoint(t,
GLASS_X + GLASS_WIDTH - WALL_THICKNESS + BASE_PROTRUSION, GLASS_Y,
GLASS_X + GLASS_WIDTH - WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT / 3,
GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS, middleY,
xR, yR);
} else { // Bottom half
t = (float)(y - GLASS_HEIGHT / 2) / (GLASS_HEIGHT / 2);
bezierPoint(t,
GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS, middleY,
GLASS_X + WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3,
GLASS_X + WALL_THICKNESS - BASE_PROTRUSION, GLASS_Y + GLASS_HEIGHT,
xL, yL);
bezierPoint(t,
GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS, middleY,
GLASS_X + GLASS_WIDTH - WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3,
GLASS_X + GLASS_WIDTH - WALL_THICKNESS + BASE_PROTRUSION, GLASS_Y + GLASS_HEIGHT,
xR, yR);
}
leftBoundary[y] = round(xL) + 1;
rightBoundary[y] = round(xR) - 1;
}
}
// Draw top or bottom base of the hourglass
void drawTopBase(bool isTop) {
// ... (No changes in this function)
int yPos = isTop ? GLASS_Y : GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS;
int xExtension = 6; // Amount to extend beyond glass width on EACH side
// Original glass edges
int glassStartX = GLASS_X;
int glassEndX = GLASS_X + GLASS_WIDTH;
// Base edges (extending beyond glass)
int baseStartX = glassStartX - xExtension;
int baseEndX = glassEndX + xExtension;
// Draw main rectangle without corners
for (int x = baseStartX + 2; x <= baseEndX - 2; x++) {
display.drawPixel(x, yPos); // Top line
display.drawPixel(x, yPos + TOP_THICKNESS - 1); // Bottom line
}
// Draw vertical sides without top and bottom pixels
for (int y = yPos + 1; y < yPos + TOP_THICKNESS - 1; y++) {
display.drawPixel(baseStartX, y); // Left side
display.drawPixel(baseEndX, y); // Right side
}
// Draw rounded corners
// Top-left corner
display.drawPixel(baseStartX + 1, yPos);
display.drawPixel(baseStartX + 1, yPos + 1);
display.drawPixel(baseStartX, yPos + 1);
// Top-right corner
display.drawPixel(baseEndX - 1, yPos);
display.drawPixel(baseEndX - 1, yPos + 1);
display.drawPixel(baseEndX, yPos + 1);
// Bottom-left corner
display.drawPixel(baseStartX + 1, yPos + TOP_THICKNESS - 1);
display.drawPixel(baseStartX + 1, yPos + TOP_THICKNESS - 2);
display.drawPixel(baseStartX, yPos + TOP_THICKNESS - 2);
// Bottom-right corner
display.drawPixel(baseEndX - 1, yPos + TOP_THICKNESS - 1);
display.drawPixel(baseEndX - 1, yPos + TOP_THICKNESS - 2);
display.drawPixel(baseEndX, yPos + TOP_THICKNESS - 2);
// Fill the base
for (int x = baseStartX + 1; x < baseEndX; x++) {
for (int y = yPos + 1; y < yPos + TOP_THICKNESS - 1; y++) {
// display.drawPixel(x, y);
}
}
}
// Initialize falling particles
void initializeFallingParticles() {
for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
fallingParticles[i].active = false;
fallingParticles[i].x = 0;
fallingParticles[i].y = 0;
fallingParticles[i].speed = 0;
}
}
void updateFallingParticles() {
int middleY = GLASS_Y + GLASS_HEIGHT / 2;
int neckLeft = GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS + 1;
int neckWidth = NECK_WIDTH - 2;
int bottomLimit = GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS - (bottomFillPercent * GLASS_HEIGHT / 200);
// Activate new particles
for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
if (!fallingParticles[i].active && random(100) < 30 && topFillPercent > 0) {
fallingParticles[i].active = true;
fallingParticles[i].x = neckLeft + random(neckWidth);
fallingParticles[i].y = middleY;
fallingParticles[i].speed = random(PARTICLE_SPEED_MIN, PARTICLE_SPEED_MAX + 1);
}
}
// Update active particles
for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
if (fallingParticles[i].active) {
fallingParticles[i].y += fallingParticles[i].speed;
// Reduced horizontal movement chance
if (random(100) < 15) { // Reduced to 15%
fallingParticles[i].x += random(-1, 2);
// Keep within boundaries
int currentY = fallingParticles[i].y - GLASS_Y;
if (currentY >= 0 && currentY < GLASS_HEIGHT) {
fallingParticles[i].x = constrain(fallingParticles[i].x,
leftBoundary[currentY],
rightBoundary[currentY]);
}
}
// Deactivate if reached bottom fill level
if (fallingParticles[i].y >= bottomLimit) {
fallingParticles[i].active = false;
}
}
}
}
// Draw the falling particles
void drawFallingParticles() {
for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
if (fallingParticles[i].active) {
display.drawPixel(fallingParticles[i].x, fallingParticles[i].y);
}
}
}
// Update sand levels based on time
void updateSandLevels() {
unsigned long elapsedTime = millis() - startTime;
float progress = (float)elapsedTime / HOURGLASS_DURATION;
// Enhanced non-linear function for more realistic hourglass behavior
float topProgressFactor;
if (progress <= 1.0) {
// This formula creates three distinct phases:
// 1. Slow initial drop (wide part)
// 2. Accelerating middle section (curved part)
// 3. Fast final drop (neck part)
float x = progress;
// Cubic function with adjustable parameters
topProgressFactor = 0.3 * pow(x, 3) + 0.7 * x;
// Add small random variations for more natural look
float randomFactor = 1.0 + (random(-10, 11) / 1000.0); // ±1% variation
topProgressFactor *= randomFactor;
} else {
topProgressFactor = 1.0;
}
// Calculate new fill percentages
topFillPercent = TOP_FILL_PERCENT * (1.0 - topProgressFactor);
// Bottom chamber fills proportionally to top chamber's emptying
bottomFillPercent = BOTTOM_FILL_PERCENT * topProgressFactor;
// Constrain values
topFillPercent = constrain(topFillPercent, 0, TOP_FILL_PERCENT);
bottomFillPercent = constrain(bottomFillPercent, 0, BOTTOM_FILL_PERCENT);
}
// Draw the sand in both chambers
// Function to draw sand in top chamber
void drawTopSand() {
int middleY = GLASS_Y + GLASS_HEIGHT / 2;
if (topFillPercent > 0) {
int topHeight = (GLASS_HEIGHT / 2 - TOP_THICKNESS) * topFillPercent / 100;
int sandTop = middleY - topHeight;
for (int y = middleY - 1; y >= sandTop; y--) {
if (y >= GLASS_Y + TOP_THICKNESS) {
int leftX = leftBoundary[y - GLASS_Y];
int rightX = rightBoundary[y - GLASS_Y];
if (y == sandTop) {
// Slightly uneven surface at the top
for (int x = leftX; x <= rightX; x++) {
if (random(100) < 90) {
display.drawPixel(x, y);
topPixelCount++;
}
}
} else {
// Fill complete rows
for (int x = leftX; x <= rightX; x++) {
display.drawPixel(x, y);
topPixelCount++;
}
}
}
}
}
}
// Function to draw sand in bottom chamber
void drawBottomSand() {
int middleY = GLASS_Y + GLASS_HEIGHT / 2;
if (bottomFillPercent > 0) {
int sandBottom = GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS;
int maxFillHeight = (GLASS_HEIGHT / 2 - TOP_THICKNESS) * bottomFillPercent / 100;
int centerX = GLASS_X + GLASS_WIDTH / 2;
// Calculate current dome height based on fill percentage
int currentDomeHeight = std::min(maxFillHeight, (int)DOME_MAX_HEIGHT);
int spreadHeight = maxFillHeight - currentDomeHeight;
// Draw the main sand body (if any)
if (spreadHeight > 0) {
int flatSandTop = sandBottom - spreadHeight;
// Draw the flat accumulated sand
for (int y = sandBottom - 1; y >= flatSandTop; y--) {
if (y >= middleY) {
int leftX = leftBoundary[y - GLASS_Y];
int rightX = rightBoundary[y - GLASS_Y];
for (int x = leftX; x <= rightX; x++) {
display.drawPixel(x, y);
bottomPixelCount++;
}
}
}
// Adjust sandBottom for dome drawing
sandBottom = flatSandTop;
}
// Draw the dome shape with smoother top
for (int y = sandBottom; y >= sandBottom - currentDomeHeight; y--) {
if (y >= middleY) {
int leftX = leftBoundary[y - GLASS_Y];
int rightX = rightBoundary[y - GLASS_Y];
for (int x = leftX; x <= rightX; x++) {
int distFromCenter = abs(x - centerX);
int domeHeightAtDist = calculateDomeHeight(distFromCenter, currentDomeHeight);
if (sandBottom - y <= domeHeightAtDist) {
// Only add randomness at the very top edge of the dome
if (sandBottom - y == domeHeightAtDist) {
// Increased randomness at the dome's edge
if (random(100) < 70) { // 70% chance to skip pixel at the edge
continue;
}
}
display.drawPixel(x, y);
bottomPixelCount++;
}
}
}
}
}
}
// Add some randomness to the top surface
/* int topSurfaceY = sandBottom - currentDomeHeight;
if (topSurfaceY >= middleY) {
int leftX = leftBoundary[topSurfaceY - GLASS_Y];
int rightX = rightBoundary[topSurfaceY - GLASS_Y];
for (int x = leftX; x <= rightX; x++) {
if (random(100) < 20) {
display.drawPixel(x, topSurfaceY - 1);
bottomPixelCount++;
}
}
}
}
}
*/
// Main draw sand function that calls both chambers
void drawSand() {
topPixelCount = 0;
bottomPixelCount = 0;
drawTopSand();
drawBottomSand();
}
// Draw the hourglass frame - THIS WAS LIKELY MISSING OR INCOMPLETE
void drawHourglass() {
int middleY = GLASS_Y + GLASS_HEIGHT / 2;
// Draw the filled walls
for (int y = GLASS_Y + TOP_THICKNESS; y < GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS; y++) {
float t;
float xL1, yL1, xR1, yR1; // Inner curve points
float xL2, yL2, xR2, yR2; // Outer curve points
if (y < middleY) { // Top half
t = (float)(y - (GLASS_Y + TOP_THICKNESS)) / (GLASS_HEIGHT / 2 - TOP_THICKNESS);
// Inner curves
bezierPoint(t, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + TOP_THICKNESS, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS - 1, middleY, xL1, yL1);
bezierPoint(t, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + TOP_THICKNESS, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS + 1, middleY, xR1, yR1);
// Outer curves
bezierPoint(t, GLASS_X - BASE_PROTRUSION + 1, GLASS_Y + TOP_THICKNESS, GLASS_X + 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + 1, middleY, xL2, yL2);
bezierPoint(t, GLASS_X + GLASS_WIDTH + BASE_PROTRUSION - 1, GLASS_Y + TOP_THICKNESS, GLASS_X + GLASS_WIDTH - 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - 1, middleY, xR2, yR2);
} else { // Bottom half
t = (float)(y - middleY) / (GLASS_HEIGHT / 2 - TOP_THICKNESS);
// Inner curves
bezierPoint(t, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS - 1, middleY, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xL1, yL1);
bezierPoint(t, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS + 1, middleY, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xR1, yR1);
// Outer curves
bezierPoint(t, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + 1, middleY, GLASS_X + 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X - BASE_PROTRUSION + 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xL2, yL2);
bezierPoint(t, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - 1, middleY, GLASS_X + GLASS_WIDTH - 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH + BASE_PROTRUSION - 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xR2, yR2);
}
// Draw the walls
int xL2i = round(xL2);
int xR2i = round(xR2);
display.drawPixel(xL2i, y); // Left wall outer
display.drawPixel(xL2i + 1, y); // Left wall inner
display.drawPixel(xR2i, y); // Right wall outer
display.drawPixel(xR2i - 1, y); // Right wall inner
}
// Draw top and bottom bases
drawTopBase(true);
drawTopBase(false);
}
void setup() {
// Initialize GPIO13 as output and set it HIGH
pinMode(GPIO_PIN, OUTPUT);
digitalWrite(GPIO_PIN, HIGH);
// Initialize display with rotation based on GPIO state
if (digitalRead(GPIO_PIN)) {
display.setDisplayRotation(U8G2_R3);
} else {
display.setDisplayRotation(U8G2_R1);
}
// Initialize display
display.begin();
display.setFont(u8g2_font_6x10_tf);
// Calculate boundaries for the hourglass shape
calculateBoundaries();
// Initialize particles
initializeFallingParticles();
// Set start time
startTime = millis();
// Initialize random seed
randomSeed(os_random());
}
void loop() {
// Check GPIO state and handle rotation if needed
checkGPIOAndRotation();
// Calculate progress
unsigned long elapsedTime = millis() - startTime;
int progress = map(elapsedTime, 0, HOURGLASS_DURATION, 0, 100);
progress = constrain(progress, 0, 100);
// Begin drawing
display.clearBuffer();
// Draw progress percentage
char progressStr[5];
sprintf(progressStr, "%d%%", progress);
display.drawStr(23, 8, progressStr);
display.drawStr(1,127,"Sand Clock");
// Draw all hourglass elements
drawHourglass();
updateSandLevels();
drawSand();
updateFallingParticles();
drawFallingParticles();
// Send the buffer to the display
display.sendBuffer();
// Check if time's up
if (elapsedTime >= HOURGLASS_DURATION) {
// Instead of showing "Time's Up", just keep showing the final state
startTime = millis() - HOURGLASS_DURATION; // This keeps the progress at 100%
}
delay(ANIMATION_DELAY);
}
![licensBg](/images/license_bg.png)