Components and supplies
1
Grove - Tilt Switch
1
Speaker, Micro
1
ESP32
1
470 Ohms Resistor (or multiple to add 470 Ohms)
1
16x16 RGB LED Matrix WS2812B
Tools and machines
1
Soldering kit
Apps and platforms
1
Arduino IDE
Project description
Code
Code
cpp
...
1/*ESP21 Hourglass on 16x16 Matrix WS2812b 2by mircemk, April 2025 3*/ 4 5#include <FastLED.h> 6 7#define LED_PIN 5 8#define NUM_LEDS 256 9#define BRIGHTNESS 64 10#define LED_TYPE WS2812B 11#define COLOR_ORDER GRB 12#define TILT_PIN 4 // D4 pin for tilt switch 13#define BUZZER_PIN 2 // Choose an available digital pin for the buzzer 14 15CRGB leds[NUM_LEDS]; 16 17// Colors 18CRGB BLACK = CRGB(0, 0, 0); 19CRGB MAGENTA = CRGB(255, 0, 255); 20CRGB YELLOW = CRGB(255, 255, 0); 21CRGB WHITE = CRGB(255, 255, 255); // Color for digits 22CRGB PALE_PURPLE = CRGB(0, 0, 0); // Very dim purple for outside dots 23CRGB PALE_RED = CRGB(7,15, 15); // Very dim red for inside dots 24 25// Animation timing 26const unsigned long PARTICLE_FALL_TIME = 2000; // 2 seconds per particle 27const int TOTAL_PARTICLES = 30; 28const unsigned long RESTART_DELAY = 60000; // 1 minute 29 30// Grid dimensions 31const int GRID_WIDTH = 16; 32const int GRID_HEIGHT = 16; 33 34// Digit display positions (7th row from top, 3 pixels from edges) 35const int LEFT_DIGIT_X = 0; // Changed from 3 to 0 (far left) 36const int RIGHT_DIGIT_X = 13; // Changed from 10 to 13 (far right) 37const int DIGIT_Y = 6; // Keep the same vertical position 38 39 40const int START_TONES[] = {300, 600, 900}; // Starting sequence frequencies 41const int TICK_TONE = 100; // Countdown tick frequency 42const int END_TONES[] = {900, 600, 300}; // Ending sequence frequencies 43const int START_END_TONE_DURATION = 200; // Duration for start/end tones in ms 44const int TICK_TONE_DURATION = 50; // Duration for tick tone in ms 45 46bool displayRotated = false; // Track if display is rotated 47unsigned long lastTiltCheck = 0; // Debouncing 48const unsigned long TILT_CHECK_DELAY = 50; // Check tilt every 50ms 49 50unsigned long lastSecond = 60; // Track last second for tone 51bool startTonesPlayed = false; // Track if start tones have been played 52bool endTonesPlayed = false; // Track if end tones have been played 53 54// Tracking variables 55unsigned long startTime = 0; 56unsigned long currentTime = 0; 57int particlesFallen = 0; 58bool animationComplete = false; 59 60// Use a 1D array to track sand (1=sand, 0=no sand) 61byte sandState[NUM_LEDS]; 62 63// Falling particle 64bool fallingParticle = false; 65uint8_t fallingParticleX = 0; 66uint8_t fallingParticleY = 0; 67unsigned long fallingStartTime = 0; 68 69// Convert x,y coordinates to LED index (assuming serpentine layout) 70uint16_t XY(uint8_t x, uint8_t y) { 71 uint16_t i; 72 73 if (displayRotated) { 74 // If rotated, flip both x and y coordinates 75 x = GRID_WIDTH - 1 - x; 76 y = GRID_HEIGHT - 1 - y; 77 } 78 79 if(y & 0x01) { // Odd rows run backwards 80 uint8_t reverseX = (GRID_WIDTH-1) - x; 81 i = (y * GRID_WIDTH) + reverseX; 82 } else { // Even rows run forwards 83 i = (y * GRID_WIDTH) + x; 84 } 85 86 return i; 87} 88 89 90 91// 5x3 Font Data for digits 0-9 (full 5x3 matrix design) 92const byte DIGITS[10][5][3] = { 93 { // 0 94 {1,1,1}, 95 {1,0,1}, 96 {1,0,1}, 97 {1,0,1}, 98 {1,1,1} 99 }, 100 { // 1 101 {0,1,0}, 102 {1,1,0}, 103 {0,1,0}, 104 {0,1,0}, 105 {1,1,1} 106 }, 107 { // 2 108 {1,1,1}, 109 {0,0,1}, 110 {1,1,1}, 111 {1,0,0}, 112 {1,1,1} 113 }, 114 { // 3 115 {1,1,1}, 116 {0,0,1}, 117 {1,1,1}, 118 {0,0,1}, 119 {1,1,1} 120 }, 121 { // 4 122 {1,0,1}, 123 {1,0,1}, 124 {1,1,1}, 125 {0,0,1}, 126 {0,0,1} 127 }, 128 { // 5 129 {1,1,1}, 130 {1,0,0}, 131 {1,1,1}, 132 {0,0,1}, 133 {1,1,1} 134 }, 135 { // 6 136 {1,1,1}, 137 {1,0,0}, 138 {1,1,1}, 139 {1,0,1}, 140 {1,1,1} 141 }, 142 { // 7 143 {1,1,1}, 144 {0,0,1}, 145 {0,1,0}, 146 {1,0,0}, 147 {1,0,0} 148 }, 149 { // 8 150 {1,1,1}, 151 {1,0,1}, 152 {1,1,1}, 153 {1,0,1}, 154 {1,1,1} 155 }, 156 { // 9 157 {1,1,1}, 158 {1,0,1}, 159 {1,1,1}, 160 {0,0,1}, 161 {1,1,1} 162 } 163}; 164 165// drawDigit function to handle 5x3 digits 166void drawDigit(int digit, int xPos, int yPos, CRGB color) { 167 for (int y = 0; y < 5; y++) { // Changed from 6 to 5 168 for (int x = 0; x < 3; x++) { 169 if (DIGITS[digit][y][x]) { 170 // Reverse the x-coordinate by drawing from right to left 171 leds[XY(xPos + (2 - x), yPos + y)] = color; 172 } 173 } 174 } 175} 176 177// Function to draw countdown number 178void drawCountdown(int seconds) { 179 int tens = seconds / 10; 180 int ones = seconds % 10; 181 182 // Draw tens digit on the right 183 drawDigit(tens, RIGHT_DIGIT_X, DIGIT_Y, WHITE); 184 185 // Draw ones digit on the left 186 drawDigit(ones, LEFT_DIGIT_X, DIGIT_Y, WHITE); 187} 188 189// Check if a position is within the hourglass container 190bool isInsideHourglass(uint8_t x, uint8_t y) { 191 // Top half 192 if (y <= 7) { 193 if (y == 0 && x >= 1 && x <= 14) return true; 194 if (y >= 1 && y <= 3 && x >= 2 && x <= 13) return true; 195 if (y == 4 && x >= 3 && x <= 12) return true; 196 if (y == 5 && x >= 4 && x <= 11) return true; 197 if (y == 6 && x >= 5 && x <= 10) return true; 198 if (y == 7 && x >= 6 && x <= 9) return true; 199 } 200 // Bottom half 201 else { 202 if (y == 8 && x >= 6 && x <= 9) return true; 203 if (y == 9 && x >= 5 && x <= 10) return true; 204 if (y == 10 && x >= 4 && x <= 11) return true; 205 if (y == 11 && x >= 3 && x <= 12) return true; 206 if (y >= 12 && y <= 14 && x >= 2 && x <= 13) return true; 207 if (y == 15 && x >= 1 && x <= 14) return true; 208 } 209 return false; 210} 211 212// Check if a position is part of the hourglass outline 213bool isHourglassOutline(uint8_t x, uint8_t y) { 214 // Top base (row 0) 215 if (y == 0 && x >= 1 && x <= 14) return true; 216 217 // Vertical walls - top half 218 if (y >= 1 && y <= 3 && (x == 2 || x == 13)) return true; 219 if (y == 4 && (x == 3 || x == 12)) return true; 220 if (y == 5 && (x == 4 || x == 11)) return true; 221 if (y == 6 && (x == 5 || x == 10)) return true; 222 if (y == 7 && (x == 6 || x == 9)) return true; 223 224 // Neck - only the sides, keeping the middle open 225 if (y == 7 && (x == 7 || x == 8)) return false; 226 if (y == 8 && (x == 7 || x == 8)) return false; 227 228 // Vertical walls - bottom half 229 if (y == 8 && (x == 6 || x == 9)) return true; 230 if (y == 9 && (x == 5 || x == 10)) return true; 231 if (y == 10 && (x == 4 || x == 11)) return true; 232 if (y == 11 && (x == 3 || x == 12)) return true; 233 if (y >= 12 && y <= 14 && (x == 2 || x == 13)) return true; 234 235 // Bottom base (row 15) 236 if (y == 15 && x >= 1 && x <= 14) return true; 237 238 return false; 239} 240 241// Check if a position is in the neck area 242bool isNeckPosition(uint8_t x, uint8_t y) { 243 return ((y == 7 || y == 8) && (x == 7 || x == 8)); 244} 245 246// Initialize the hourglass with sand particles 247// Initialize the hourglass with sand particles 248void initHourglass() { 249 // First, set the background colors instead of clearing to black 250 for (uint8_t y = 0; y < GRID_HEIGHT; y++) { 251 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 252 if (isInsideHourglass(x, y) && !isHourglassOutline(x, y)) { 253 // Inside hourglass - pale red background 254 sandState[XY(x, y)] = 0; // Initialize as empty 255 leds[XY(x, y)] = PALE_RED; 256 } else if (!isInsideHourglass(x, y)) { 257 // Outside hourglass - pale purple background 258 sandState[XY(x, y)] = 0; 259 leds[XY(x, y)] = PALE_PURPLE; 260 } else { 261 // Areas that will be outline 262 sandState[XY(x, y)] = 0; 263 leds[XY(x, y)] = BLACK; 264 } 265 } 266 } 267 268 int particleCount = 0; 269 270 // First add 2 particles in the upper neck area (y=7) 271 sandState[XY(7, 7)] = 1; // First neck particle 272 sandState[XY(8, 7)] = 1; // Second neck particle 273 particleCount = 2; 274 275 // Fill the remaining 28 particles in the top half 276 for (uint8_t y = 3; y <= 7; y++) { 277 for (uint8_t x = 0; x < GRID_WIDTH && particleCount < TOTAL_PARTICLES; x++) { 278 if (isInsideHourglass(x, y) && !isHourglassOutline(x, y) && 279 !(x == 7 && y == 7) && !(x == 8 && y == 7) && // Skip the neck positions we already filled 280 sandState[XY(x, y)] == 0) { 281 sandState[XY(x, y)] = 1; // Add sand 282 particleCount++; 283 } 284 } 285 } 286 287 // Draw initial state 288 for (uint8_t y = 0; y < GRID_HEIGHT; y++) { 289 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 290 if (sandState[XY(x, y)] == 1) { 291 leds[XY(x, y)] = YELLOW; // Draw sand particles 292 } else if (isHourglassOutline(x, y)) { 293 leds[XY(x, y)] = MAGENTA; // Draw outline 294 } 295 } 296 } 297 298 FastLED.show(); // Show the initial state 299 300 particlesFallen = 0; 301 animationComplete = false; 302} 303 304// Find a sand particle in the top container to drop 305bool findSandParticleToRemove(uint8_t* outX, uint8_t* outY) { 306 for (uint8_t y = 3; y <= 7; y++) { 307 int particleXPositions[GRID_WIDTH]; 308 int particleYPositions[GRID_WIDTH]; 309 int count = 0; 310 311 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 312 if (isInsideHourglass(x, y) && !isHourglassOutline(x, y) && sandState[XY(x, y)] == 1) { 313 particleXPositions[count] = x; 314 particleYPositions[count] = y; 315 count++; 316 } 317 } 318 319 if (count > 0) { 320 int randomIndex = random(count); 321 *outX = particleXPositions[randomIndex]; 322 *outY = particleYPositions[randomIndex]; 323 sandState[XY(*outX, *outY)] = 0; // Remove this particle 324 return true; 325 } 326 } 327 328 return false; 329} 330 331// Find a position in the bottom container 332bool findPositionInBottomContainer(uint8_t* outX, uint8_t* outY) { 333 for (uint8_t y = 15; y >= 9; y--) { 334 int availableSpots[GRID_WIDTH]; 335 int count = 0; 336 337 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 338 if (isInsideHourglass(x, y) && !isHourglassOutline(x, y) && sandState[XY(x, y)] == 0) { 339 availableSpots[count] = x; 340 count++; 341 } 342 } 343 344 if (count > 0) { 345 int randomIndex = random(count); 346 *outX = availableSpots[randomIndex]; 347 *outY = y; 348 return true; 349 } 350 } 351 352 return false; 353} 354 355// Start a new falling particle 356void startNewFallingParticle() { 357 uint8_t startX, startY; 358 359 360 if (!findSandParticleToRemove(&startX, &startY)) { 361 fallingParticle = false; 362 return; 363 } 364 365 fallingParticleX = startX; 366 fallingParticleY = startY; 367 fallingParticle = true; 368 fallingStartTime = millis(); 369 particlesFallen++; 370} 371 372// Update falling particle position 373void updateFallingParticle() { 374 if (!fallingParticle) return; 375 376 unsigned long elapsed = millis() - fallingStartTime; 377 378 if (elapsed >= PARTICLE_FALL_TIME) { 379 fallingParticle = false; 380 381 uint8_t endX, endY; 382 if (findPositionInBottomContainer(&endX, &endY)) { 383 sandState[XY(endX, endY)] = 1; 384 } 385 386 if (particlesFallen < TOTAL_PARTICLES) { 387 startNewFallingParticle(); 388 } else { 389 animationComplete = true; 390 } 391 return; 392 } 393 394 float progress = (float)elapsed / PARTICLE_FALL_TIME; 395 uint8_t targetX = (fallingParticleX < 8) ? 7 : 8; 396 397 if (progress < 0.5) { 398 float neckProgress = progress * 2; 399 fallingParticleX = fallingParticleX + (neckProgress * (targetX - fallingParticleX)); 400 fallingParticleY = fallingParticleY + (neckProgress * (7 - fallingParticleY)); 401 } else { 402 float bottomProgress = (progress - 0.5) * 2; 403 fallingParticleX = targetX; 404 uint8_t endY; 405 uint8_t endX; 406 findPositionInBottomContainer(&endX, &endY); 407 fallingParticleY = 8 + (bottomProgress * (endY - 8)); 408 } 409} 410 411 void playTone(int frequency, int duration) { 412 tone(BUZZER_PIN, frequency, duration); 413} 414 415 416void playStartSequence() { 417 for (int i = 0; i < 3; i++) { 418 playTone(START_TONES[i], START_END_TONE_DURATION); 419 delay(START_END_TONE_DURATION); 420 } 421 startTonesPlayed = true; 422} 423 424void playEndSequence() { 425 for (int i = 0; i < 3; i++) { 426 playTone(END_TONES[i], START_END_TONE_DURATION); 427 delay(START_END_TONE_DURATION); 428 } 429 endTonesPlayed = true; 430} 431 432void setup() { 433 delay(1000); 434 435 pinMode(BUZZER_PIN, OUTPUT); 436 pinMode(TILT_PIN, INPUT_PULLUP); 437 438 FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); 439 FastLED.setBrightness(BRIGHTNESS); 440 441 // Initial orientation check 442 displayRotated = !digitalRead(TILT_PIN); // Invert because of pull-up 443 444 randomSeed(analogRead(0)); 445 initHourglass(); 446 startTime = millis(); 447 startNewFallingParticle(); 448 startTonesPlayed = false; // Reset start tones flag 449 endTonesPlayed = false; // Reset end tones flag 450} 451 452 453void loop() { 454 currentTime = millis(); 455 456 // Check tilt switch with debouncing 457 if (currentTime - lastTiltCheck >= TILT_CHECK_DELAY) { 458 bool newRotation = !digitalRead(TILT_PIN); // Invert because of pull-up 459 if (newRotation != displayRotated) { 460 displayRotated = newRotation; 461 // Reset hourglass when flipped 462 initHourglass(); 463 startTime = currentTime; 464 particlesFallen = 0; 465 animationComplete = false; 466 startNewFallingParticle(); 467 } 468 lastTiltCheck = currentTime; 469 } 470 471 // Calculate remaining time 472 int remainingSeconds = 60; 473 if (currentTime > startTime) { 474 unsigned long elapsedTime = currentTime - startTime; 475 if (elapsedTime < RESTART_DELAY) { 476 remainingSeconds = (RESTART_DELAY - elapsedTime + 999) / 1000; 477 } else { 478 remainingSeconds = 0; 479 } 480 } 481 482 // Play start sequence if not yet played 483 if (!startTonesPlayed && remainingSeconds == 60) { 484 playStartSequence(); 485 } 486 487 // Play tick tone when second changes 488 if (remainingSeconds < lastSecond && remainingSeconds > 0) { 489 playTone(TICK_TONE, TICK_TONE_DURATION); 490 } 491 lastSecond = remainingSeconds; 492 493 // Play end sequence when countdown reaches zero 494 if (remainingSeconds == 0 && !endTonesPlayed && animationComplete) { 495 playEndSequence(); 496 } 497 498 // If animation is complete and time is up, show final state 499 if (animationComplete && (currentTime - startTime >= RESTART_DELAY)) { 500 // Draw background colors first 501 for (uint8_t y = 0; y < GRID_HEIGHT; y++) { 502 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 503 if (isInsideHourglass(x, y) && !isHourglassOutline(x, y)) { 504 leds[XY(x, y)] = PALE_RED; // Inside hourglass background 505 } else if (!isInsideHourglass(x, y)) { 506 leds[XY(x, y)] = PALE_PURPLE; // Outside hourglass background 507 } 508 } 509 } 510 511 // Draw final hourglass state 512 for (uint8_t y = 0; y < GRID_HEIGHT; y++) { 513 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 514 if (isHourglassOutline(x, y)) { 515 leds[XY(x, y)] = MAGENTA; 516 } 517 if (sandState[XY(x, y)] == 1) { 518 leds[XY(x, y)] = YELLOW; 519 } 520 } 521 } 522 523 // Draw final 00 524 drawCountdown(0); 525 526 FastLED.show(); 527 delay(50); 528 // return; 529 } 530 531 // Draw background colors first 532 for (uint8_t y = 0; y < GRID_HEIGHT; y++) { 533 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 534 if (isInsideHourglass(x, y) && !isHourglassOutline(x, y)) { 535 leds[XY(x, y)] = PALE_RED; // Inside hourglass background 536 } else if (!isInsideHourglass(x, y)) { 537 leds[XY(x, y)] = PALE_PURPLE; // Outside hourglass background 538 } 539 } 540 } 541 542 // Draw the hourglass outline 543 for (uint8_t y = 0; y < GRID_HEIGHT; y++) { 544 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 545 if (isHourglassOutline(x, y)) { 546 leds[XY(x, y)] = MAGENTA; 547 } 548 } 549 } 550 551 // Draw sand particles 552 for (uint8_t y = 0; y < GRID_HEIGHT; y++) { 553 for (uint8_t x = 0; x < GRID_WIDTH; x++) { 554 if (sandState[XY(x, y)] == 1) { 555 leds[XY(x, y)] = YELLOW; 556 } 557 } 558 } 559 560 // Update and draw falling particle 561 if (!animationComplete) { 562 updateFallingParticle(); 563 564 // Draw falling particle 565 if (fallingParticle) { 566 leds[XY(fallingParticleX, fallingParticleY)] = YELLOW; 567 } 568 } 569 570 // Draw countdown numbers 571 drawCountdown(remainingSeconds); 572 573 FastLED.show(); 574 delay(50); // Slow down animation to 20fps 575 // return; 576}
Documentation
Schematic
...
Schematic.jpg

Comments
Only logged in users can leave comments