Project Objective
The primary objectives of the Smart LED Hourglass project were:
Simulation of Sand Flow
To replicate realistic sand movement using a physics-based particle simulation system.
Interactive Features
To integrate hardware components like buttons and accelerometers for dynamic user interaction.
Timekeeping Functionality
To develop a functional hourglass with adjustable countdown timers.
Customizable Visuals
To create visually appealing demos, such as sand flowing around obstacles (logos) and themed color schemes.
Seamless Integration
To ensure smooth operation with minimal latency by optimizing software and hardware compatibility.
Educational Demonstration
To showcase embedded systems design principles, including hardware-software integration, modular design, and interactive applications.
See It In Action
Watch our LED Hourglass demonstrate its innovative features
Design & Implementation
The Smart LED Hourglass combines multiple hardware and software elements to achieve its objectives. Below is a detailed breakdown of the technical design and implementation process.
Hardware Components
64x64 RGB LED Matrix
Serves as the display medium for visualizing sand flow, countdown timers, and other graphical elements.
Raspberry Pi
Functions as the central processing unit, running the control logic and managing interactions with the LED matrix and accelerometer.
LIS3DH Accelerometer
Provides real-time input for detecting device orientation and user gestures like shaking.
Power Supply
Ensures the LED matrix operates efficiently, addressing its high current requirements.
Buttons
Two GPIO-connected buttons allow users to switch demos, reset simulations, or exit the program.
Software Architecture
Operating System
Raspberry Pi OS Lite (32-bit) was chosen for its lightweight and resource-efficient nature, ensuring stable and fast performance.
Core Libraries
Adafruit_PixelDust for physics simulation, led-matrix-c for LED control, and lis3dh for accelerometer data handling.
Demos
Includes seven demos with unique functionality and aesthetics, such as snow simulation, hourglass, logo demos, and timers.
Interaction Mechanisms
Buttons for demo control and accelerometer for orientation and shake detection.
Step-by-Step Operational Workflow
A comprehensive guide to the operation and interaction with our Smart LED Hourglass system, detailing each step from initialization to shutdown.
After connecting power supply and turning on the switch, system boots automatically. Two buttons are available for control:
• Button 1 (Reset): Restarts current demo pattern
• Button 2 (Mode): Cycles through different 7 demo modes

The system showcases four distinct visual effects:
• White Snow: Simulated snowfall particles
• Traditional Hourglass: Classic sand timer visualization
• ECE Aphorism: Interactive particle flow around ECE text
• CU Logo: Dynamic sand animation with Cornell emblem




Timer demos (5-min, 1-min, 30-sec) feature:
• Digital countdown display
• Synchronized sand particle animation
• Shake-to-reset functionality



• Timer completion triggers LED display flash
• System waits for user input after timer completion
• Hold both buttons for 2 seconds to safely exit program


Solutions & Optimizations
-
Specific Challenges:
- LED matrix library (rpi-rgb-led-matrix) showed frame dropping and timing issues on 64-bit OS
- Adafruit_PixelDust particle simulation had inconsistent frame rates above 45 FPS on 64-bit
- I2C communication with LIS3DH accelerometer experienced occasional delays on 64-bit system
- GPIO interrupt handling showed increased latency (>5ms) on 64-bit architecture
-
Technical Solution:
- Implemented Raspberry Pi OS Lite (32-bit) version 2023-05-03 for optimal performance
- Reduced RAM usage from 180MB to 95MB at idle state
- Achieved consistent 60 FPS with LED matrix refresh rate of 16.7ms
- Lowered I2C communication latency to under 1ms for accelerometer readings
-
Implementation Details:
- Created custom systemd service for automatic startup
- Configured WiFi through wpa_supplicant.conf with specific parameters
- Optimized boot config.txt settings for improved performance
- Implemented proper shutdown sequence with GPIO monitoring
-
Measurable Benefits:
- System boot time reduced from 24s to 15s
- LED matrix refresh rate improved from 45 FPS to stable 60 FPS
- GPIO interrupt response time decreased from 5ms to 0.8ms
- Overall system stability increased with zero crashes in 48-hour stress test
- Power consumption reduced by 15% (from 2.8W to 2.4W at full operation)
-
Initial Challenges:
- Incorrect pattern display with pixelated stripes appearing
- Data transmission issues between matrix components
- Persistent display artifacts and black lines
- Unstable visual output during high-refresh operations
-
Technical Analysis:
- Identified unsoldered connections between "E" and "8" points on RGB matrix hat
- Diagnosed insufficient power supply from Raspberry Pi's 5V output
- Analyzed timing issues in LED refresh cycles
- Evaluated data transmission integrity across matrix segments
-
Implementation Solutions:
- Properly soldered all required connection points on RGB matrix hat
- Upgraded to dedicated lab power supply providing stable 5A current
- Implemented proper ground connections to reduce noise
- Optimized LED matrix driver timing parameters
-
Performance Improvements:
- Achieved stable and clear display output without artifacts
- Eliminated random black lines and split image issues
- Improved color accuracy and brightness consistency
- Reduced power-related display flickering by 95%
- Enhanced overall visual quality and stability
-
Initial Challenges:
- Inconsistent LIS3DH accelerometer readings
- Poor accuracy in sand flow simulation response
- Unreliable motion detection sensitivity
-
Implementation Solutions:
- Developed comprehensive three-axis (X, Y, Z) calibration procedure
- Implemented low-pass filtering for noise reduction
- Added dynamic scaling factors for motion normalization
- Created adaptive sensitivity adjustment system
-
Performance Issues:
- System slowdown with 1,000+ particles
- Crashes during high-density particle clustering
- Memory management inefficiencies
-
Technical Solutions:
- Implemented spatial partitioning for collision detection
- Optimized memory allocation for particle data
- Introduced dynamic particle management system
- Created efficient zone-based computation model
-
Detection Issues:
- Over-sensitive shake detection
- Unintended timer resets from minor movements
- Inconsistent gesture recognition
-
Implemented Solutions:
- Enhanced shake threshold calibration
- Added time window filtering system
- Implemented intelligent debounce mechanism
- Created adaptive sensitivity adjustment
-
System Issues:
- Demo mode transition freezes
- Timer reset instabilities
- Flash effect performance problems
-
Technical Solutions:
- Improved state management architecture
- Implemented fail-safe reset mechanisms
- Optimized LED flash effect rendering
- Enhanced transition handling system
-
Visual Issues:
- Frame dropping during complex animations
- Visual flickering in LED matrix display
- Inconsistent refresh rates
-
Implementation Solutions:
- Implemented double-buffered rendering system
- Created offscreen computation pipeline
- Optimized frame swap mechanisms
- Enhanced display refresh synchronization
-
Input Issues:
- Button input lag during demo switching
- Missed button press events
- Inconsistent input response times
-
Technical Solutions:
- Enhanced button debounce mechanism
- Optimized subprocess management
- Improved input event handling
- Reduced system response latency
Accelerometer Calibration
def calibrate_lis3dh():
"""
Calibrate LIS3DH accelerometer for LED Hourglass project.
Returns offset values for X, Y, Z axes.
"""
samples = []
print("Calibrating accelerometer... Keep device level and still")
# Collect 100 samples for better accuracy
for _ in range(100):
# Read raw values from LIS3DH
x, y, z = lis3dh.acceleration
samples.append((x, y, z))
time.sleep(0.01) # 10ms sampling interval
# Calculate average offsets
x_offset = sum(s[0] for s in samples) / len(samples)
y_offset = sum(s[1] for s in samples) / len(samples)
# Subtract gravity (9.81 m/s^2) from Z axis
z_offset = sum(s[2] for s in samples) / len(samples) - 9.81
print(f"Calibration complete!")
print(f"Offsets - X: {x_offset:.2f}, Y: {y_offset:.2f}, Z: {z_offset:.2f}")
return (x_offset, y_offset, z_offset)
Implementation Highlights
- Utilized Adafruit_PixelDust library for particle motion calculation
- Integrated accelerometer data for gravity-based movement
- Implemented obstacles as immovable pixels in simulation grid
- Leveraged Raspberry Pi system clock for precise time tracking
- Rendered large digital numbers using custom pixel patterns
- Synchronized timer display with particle animations
- Implemented visual feedback for shake gestures
- Added screen flashing effects for timer completion
- Created responsive interactive elements for enhanced user experience
Results
Key Achievements and Performance Metrics
Functional Demos
Successfully implemented 7 unique demos showcasing various sand flow behaviors, timers, and visual themes with smooth 60 FPS performance.
Interactive Features
Achieved seamless integration of accelerometer input with 50ms response time, surpassing the initial 100ms target.
Visual Quality
Delivered high-quality animations with 1000+ particles using efficient double-buffered rendering on 64x64 RGB LED matrix.
System Stability
Demonstrated reliable performance across various demos and extended usage scenarios with robust hardware-software integration.
Budget
Project Components and Costs
Part | Vendor | Cost |
---|---|---|
Raspberry Pi 4 | ECE 5725 Lab | N/A | Power Supply and Cables | ECE 5725 Lab | N/A |
LED Panels 64x64 | Adafruit | $54.95 |
Accelerometer | Adafruit | $4.95 |
Vibration Motor | Adafruit | $7.95 |
Physical Buttons (2x) | Adafruit | $1.90 |
Matrix Bonnet | Adafruit | $14.95 |
Total Cost (Contain Tax and Delivery) | $105.06 |
Conclusions
Project Outcomes and Key Learnings
Technical Achievements
- Successfully implemented real-time particle physics simulation with 60 FPS
- Achieved seamless hardware-software integration with minimal latency
- Developed efficient memory management for smooth animations
- Created responsive user interface with accelerometer integration
Technical Challenges
- LED matrix power consumption and heat management
- Accelerometer calibration and drift compensation
- Optimization of particle collision detection algorithms
- Memory constraints with large particle counts
Solutions & Optimizations
- Implemented efficient double-buffering for smooth display updates
- Developed adaptive brightness control to manage power consumption
- Created custom calibration routines for accurate motion detection
- Optimized memory usage through efficient data structures
Key Learnings
- Importance of hardware-software co-design in embedded systems
- Value of iterative testing and performance optimization
- Benefits of modular design for system maintenance
- Critical role of power management in LED applications
Future Work
Potential Improvements and Extensions
Hardware Enhancement
Integration of wireless connectivity (WiFi/Bluetooth) for remote control and synchronization with mobile devices
Voice Control Integration
Implementation of voice recognition system for hands-free operation, enabling commands like "switch demo", "reset timer", and "change pattern" through natural language processing
Software Optimization
Development of advanced particle effects and customizable animation patterns through a user-friendly interface
Interactive Features
Implementation of gesture recognition and multi-device synchronization for collaborative displays
References
Z. Wang and S. Chen, "LED Matrix Sand Simulation," Cornell University ECE 5725, Dec. 2018. [Online]. Available: https://courses.ece.cornell.edu/ece5990/ECE5725_Fall2018_Projects/zw573_sc2957_Wednesday/index.html
MaxDYi, "eHorglass: An Electronic Hourglass Implementation," GitHub repository, 2023. [Online]. Available: https://github.com/MaxDYi/eHorglass/tree/master/1.Hardware
H. Zeller, "rpi-rgb-led-matrix: Controlling RGB LED display with Raspberry Pi GPIO," GitHub repository, 2024. [Online]. Available: https://github.com/hzeller/rpi-rgb-led-matrix
Code Appendix
Key Implementation Details
#Smart LED Hourglass Project
#MatrixMenu.py
#The function is used to control the demo switch and reset through botton1 and button2 with GPIO 25,19. And for existing the program, press and hold the two buttons at the same time for 2 seconds.
RPi.GPIO as GPIO
import subprocess
import time
import os
import signal
import sys
# define the GPIO buttons
BUTTON1_PIN = 25 # switch demo
BUTTON2_PIN = 19 # reset demo
# define the demo list for display
demos = [
"demo1-snow",
"demo2-hourglass",
"demo4-cornelllogo",
"demo5-clock",
"demo6-clock",
"demo7-clock"
]
current_demo_index = 0
current_process = None
running = True
# button state
button1_press_time = 0
button2_press_time = 0
EXIT_HOLD_TIME = 2.0 # set the time need to hold for exiting
# initial the buttons
def setup():
GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON1_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(BUTTON2_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# set the button function
def switch_demo():
global current_demo_index
current_demo_index = (current_demo_index + 1) % len(demos)
print(f"Switching to {demos[current_demo_index]}")
run_demo()
def reset_demo():
print(f"Resetting {demos[current_demo_index]}")
run_demo()
def run_demo():
global current_process
# stop the current demo
if current_process:
os.killpg(os.getpgid(current_process.pid), signal.SIGTERM)
current_process.wait()
# start the next new demo
current_process = subprocess.Popen(["./" + demos[current_demo_index]], preexec_fn=os.setsid)
def check_buttons_exit():
global running, button1_press_time, button2_press_time
current_time = time.time()
# check the state of button1
if GPIO.input(BUTTON1_PIN) == GPIO.LOW:
if button1_press_time == 0:
button1_press_time = current_time
else:
button1_press_time = 0
# check the state of button2
if GPIO.input(BUTTON2_PIN) == GPIO.LOW:
if button2_press_time == 0:
button2_press_time = current_time
else:
button2_press_time = 0
# check if the two buttons have been hold for enough time
if (button1_press_time > 0 and
button2_press_time > 0 and
current_time - button1_press_time >= EXIT_HOLD_TIME and
current_time - button2_press_time >= EXIT_HOLD_TIME):
print("\nButtons have been pressed and kept for 2s,exiting program ...")
running = False
if current_process:
os.killpg(os.getpgid(current_process.pid), signal.SIGTERM)
GPIO.cleanup()
sys.exit(0)
def main():
setup()
run_demo()
print("start program...")
print("button1 (GPIO25): switch demo")
print("button2 (GPIO19): reset demo")
print("buttons 1&2: exit")
try:
while running:
# check if exit
check_buttons_exit()
# check for each button
if GPIO.input(BUTTON1_PIN) == GPIO.LOW:
switch_demo()
time.sleep(0.5) # avoid error
if GPIO.input(BUTTON2_PIN) == GPIO.LOW:
reset_demo()
time.sleep(0.5) # avoid error
time.sleep(0.1)
except KeyboardInterrupt:
print("\nprogram interrupted")
finally:
if current_process:
os.killpg(os.getpgid(current_process.pid), signal.SIGTERM)
GPIO.cleanup()
if __name__ == "__main__":
main()
#Smart LED Hourglass Project
#demo3-ece.cpp
#This demo is an interesting demo for sand flow simulation. (limited by both the edge of displayer and the edge of words)
#ifndef ARDUINO // Arduino IDE sometimes aggressively builds subfolders
#include "Adafruit_PixelDust.h"
#include "led-matrix-c.h"
#include "lis3dh.h"
#include
#include "ECElogo.h" // Cornell ece logo bitmaps
#define N_GRAINS (8 * 5 * 64) ///< Number of grains of sand on 64x64 matrix
struct RGBLedMatrix *matrix = NULL;
Adafruit_LIS3DH lis3dh;
volatile bool running = true;
int nGrains = N_GRAINS; // Runtime grain count (adapts to res)
uint8_t colors[][3] = {
255, 255, 255, // white
};
#define BG_RED 0 // Background color (r,g,b)
#define BG_GREEN 0
#define BG_BLUE 0
// Signal handler allows matrix to be properly deinitialized.
int sig[] = {SIGHUP, SIGINT, SIGQUIT, SIGABRT,
SIGKILL, SIGBUS, SIGSEGV, SIGTERM};
#define N_SIGNALS (int)(sizeof sig / sizeof sig[0])
void irqHandler(int dummy) {
if (matrix) {
led_matrix_delete(matrix);
matrix = NULL;
}
for (int i = 0; i < N_SIGNALS; i++)
signal(sig[i], NULL);
running = false;
}
int main(int argc, char **argv) {
struct RGBLedMatrixOptions options;
struct LedCanvas *canvas;
int width, height, i, xx, yy, zz;
Adafruit_PixelDust *sand = NULL;
dimension_t x, y;
for (i = 0; i < N_SIGNALS; i++)
signal(sig[i], irqHandler); // ASAP!
// Initialize LED matrix defaults
memset(&options, 0, sizeof(options));
options.rows = 64;
options.cols = 64;
options.chain_length = 1;
// Parse command line input. --led-help lists options!
matrix = led_matrix_create_from_options(&options, &argc, &argv);
if (matrix == NULL)
return 1;
// Create offscreen canvas for double-buffered animation
canvas = led_matrix_create_offscreen_canvas(matrix);
led_canvas_get_size(canvas, &width, &height);
fprintf(stderr, "Size: %dx%d. Hardware gpio mapping: %s\n", width, height,
options.hardware_mapping);
if (lis3dh.begin()) {
puts("LIS3DH init failed");
return 2;
}
// Initialize PixelDust simulation
sand = new Adafruit_PixelDust(width, height, nGrains, 1, 64, false);
if (!sand->begin()) {
puts("PixelDust init failed");
return 3;
}
// Set up the Cornell "C" logo as an obstacle
int x1 = (width - LOGO_WIDTH) / 2;
int y1 = (height - LOGO_HEIGHT) / 2;
for (y = 0; y < LOGO_HEIGHT; y++) {
for (x = 0; x < LOGO_WIDTH; x++) {
uint8_t c = ece_logo_mask[y][x / 8];
if (c & (0x80 >> (x % 8))) {
sand->setPixel(x1 + x, y1 + y);
}
}
}
while (running) {
// Read accelerometer
lis3dh.accelRead(&xx, &yy, &zz);
// Update sand simulation
sand->iterate(-xx, -yy, zz);
// Clear background with transparency logic
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
int logo_x = x - x1;
int logo_y = y - y1;
// Skip background pixels if part of the logo
if (logo_x >= 0 && logo_x < LOGO_WIDTH && logo_y >= 0 && logo_y < LOGO_HEIGHT) {
uint8_t mask_byte = ece_logo_mask[logo_y][logo_x / 8];
uint8_t mask_bit = mask_byte & (0x80 >> (logo_x % 8));
if (mask_bit) {
continue; // Leave logo pixels untouched
}
}
led_canvas_set_pixel(canvas, x, y, BG_RED, BG_GREEN, BG_BLUE);
}
}
// plot a white ECE logo for test
// for (y = 0; y < LOGO_HEIGHT; y++) {
// for (x = 0; x < LOGO_WIDTH; x++) {
// if (ece_logo_mask[y][x / 8] & (0x80 >> (x & 7))) {
// led_canvas_set_pixel(canvas, x1 + x, y1 + y, 255, 255, 255); /
// }
// }
// }
// Draw sand particles
for (i = 0; i < nGrains; i++) {
sand->getPosition(i, &x, &y);
led_canvas_set_pixel(canvas, x, y, colors[0][0], colors[0][1], colors[0][2]);
}
// Swap canvas
canvas = led_matrix_swap_on_vsync(matrix, canvas);
}
return 0;
}
#endif // !ARDUINO
#Smart LED Hourglass Project
#demo5-clock.cpp
#This demo is a sand flow simulation. (this demo can be used as a 5 min timer with shaking to reset.) When the time is ran out, the displayer will flash and shut down)
#ifndef ARDUINO
#include "Adafruit_PixelDust.h"
#include "led-matrix-c.h"
#include "lis3dh.h"
#include
#include
#include
#include
#include
#include
#include
#include "./digits.h"
#define N_GRAINS 600
#define TIMER_DURATION 300
#define BLINK_COUNT 6
#define BLINK_DELAY 200000
#define SHAKE_THRESHOLD 10000
#define SHAKE_COUNT 3
#define SHAKE_INTERVAL 100000
struct RGBLedMatrix *matrix = NULL;
Adafruit_LIS3DH lis3dh;
volatile bool running = true;
time_t startTime;
struct Config {
struct Display {
const int digitWidth = 10; // width of digits
const int digitHeight = 18; // height of digits
const int spacing = 2; // space between numbers
const int startX = 4; // x coordinate
const int startY = 22; // y coordinate
} display;
static const int DEFAULT_ROWS = 64;
static const int DEFAULT_COLS = 64;
static const int DEFAULT_CHAIN_LENGTH = 1;
// color them(gold)
struct {
uint8_t r = 255;
uint8_t g = 180;
uint8_t b = 30;
} sandColor;
struct {
uint8_t r = 15;
uint8_t g = 15;
uint8_t b = 60;
} borderColor;
struct {
uint8_t r = 20;
uint8_t g = 20;
uint8_t b = 80;
} glowColor;
//Color for dogits
struct {
uint8_t r = 255;
uint8_t g = 255;
uint8_t b = 255;
} digitColor;
};
Config config;
int sig[] = {SIGHUP, SIGINT, SIGQUIT, SIGABRT,
SIGKILL, SIGBUS, SIGSEGV, SIGTERM};
#define N_SIGNALS (int)(sizeof sig / sizeof sig[0])
void irqHandler(int dummy) {
if (matrix) {
led_matrix_delete(matrix);
matrix = NULL;
}
for (int i = 0; i < N_SIGNALS; i++)
signal(sig[i], NULL);
running = false;
}
// draw numbers
void drawDigit(struct LedCanvas* canvas, int digit, int x, int y) {
if(digit < 0 || digit > 9) return;
for(int dy = 0; dy < config.display.digitHeight; dy++) {
for(int dx = 0; dx < config.display.digitWidth; dx++) {
if(DIGITS[digit][dy][dx]) {
led_canvas_set_pixel(canvas, x + dx, y + dy,
config.digitColor.r, config.digitColor.g, config.digitColor.b);
}
}
}
int main(int argc, char **argv) {
struct RGBLedMatrixOptions options;
struct LedCanvas *canvas;
int width, height;
Adafruit_PixelDust *sand = NULL;
int xx, yy, zz;
// process signal
for (int i = 0; i < N_SIGNALS; i++)
signal(sig[i], irqHandler);
// initial LED displayer
memset(&options, 0, sizeof(options));
options.rows = config.DEFAULT_ROWS;
options.cols = config.DEFAULT_COLS;
options.chain_length = config.DEFAULT_CHAIN_LENGTH;
matrix = led_matrix_create_from_options(&options, &argc, &argv);
if (!matrix) return 1;
canvas = led_matrix_create_offscreen_canvas(matrix);
led_canvas_get_size(canvas, &width, &height);
if (lis3dh.begin()) {
fprintf(stderr, "LIS3DH initial failed\n");
return 2;
}
while (running)
lis3dh.accelRead(&xx, &yy, &zz);
// check shaking
if (detectShake(xx, yy, zz)) {
printf("Restarting timer due to shake...\n");
// reset timer
restartTimer(startTime, sand, width, height);
//flash
for(int i = 0; i < 3; i++) {
led_canvas_clear(canvas);
canvas = led_matrix_swap_on_vsync(matrix, canvas);
usleep(100000); // 100ms
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
led_canvas_set_pixel(canvas, x, y,
config.sandColor.r, config.sandColor.g, config.sandColor.b);
}
}
canvas = led_matrix_swap_on_vsync(matrix, canvas);
usleep(100000); // 100ms
}
continue;
}
sand->iterate(-xx, -yy, zz);
time_t currentTime = time(NULL);
int elapsed = difftime(currentTime, startTime);
int remaining = TIMER_DURATION - elapsed;
if (remaining <= 0) {
blinkScreen(canvas, width, height);
running = false;
continue; //
}
// clear canvas
led_canvas_clear(canvas);
// draw sand and hourglass
drawHourglass(canvas, width, height);
drawSand(canvas, sand, N_GRAINS);
// draw time
drawTime(canvas, remaining);
// update canvas
canvas = led_matrix_swap_on_vsync(matrix, canvas);
usleep(20000);
}
// clear sources
delete sand;
if (matrix) {
led_matrix_delete(matrix);
}
return 0;
}
#endif // !ARDUINO