#include <FastLED.h>
#include <avr/pgmspace.h>
// ================= HARDWARE CONFIGURATION =================
#define LED_PIN 12 // Data pin for LED matrix
#define COLOR_ORDER GRB // Color order (try RGB if colors wrong)
#define CHIPSET WS2812 // LED chip type
#define BRIGHTNESS 255 // Brightness (0-255)
// Matrix dimensions
const uint8_t matrixWidth = 32;
const uint8_t matrixHeight = 16;
#define NUM_LEDS (matrixWidth * matrixHeight)
CRGB leds[NUM_LEDS];
// ================= LAYOUT CONFIGURATION =================
#define SERPENTINE true // Set to false if rows are all same direction
// ================= 5x7 FONT DATA =================
const uint8_t font5x7[96][5] PROGMEM = {
{0,0,0,0,0}, // Space
{0,0,95,0,0}, // !
{0,7,0,7,0}, // "
{20,127,20,127,20}, // #
{36,42,127,42,18}, // $
{35,19,8,100,98}, // %
{54,73,86,32,80}, // &
{0,7,0,0,0}, // '
{0,28,34,65,0}, // (
{0,65,34,28,0}, // )
{20,8,62,8,20}, // *
{8,8,62,8,8}, // +
{0,160,96,0,0}, // ,
{8,8,8,8,8}, // -
{0,96,96,0,0}, // .
{32,16,8,4,2}, // /
{62,81,73,69,62}, // 0
{0,66,127,64,0}, // 1
{98,81,73,73,70}, // 2
{34,65,73,73,54}, // 3
{24,20,18,127,16}, // 4
{39,69,69,69,57}, // 5
{60,74,73,73,48}, // 6
{1,113,9,5,3}, // 7
{54,73,73,73,54}, // 8
{6,73,73,41,30}, // 9
{0,54,54,0,0}, // :
{0,182,102,0,0}, // ;
{8,20,34,65,0}, // <
{20,20,20,20,20}, // =
{0,65,34,20,8}, // >
{2,1,89,9,6}, // ?
{62,65,93,85,30}, // @
{126,17,17,17,126}, // A
{127,73,73,73,54}, // B
{62,65,65,65,34}, // C
{127,65,65,34,28}, // D
{127,73,73,73,65}, // E
{127,9,9,9,1}, // F
{62,65,73,73,122}, // G
{127,8,8,8,127}, // H
{0,65,127,65,0}, // I
{32,64,65,63,1}, // J
{127,8,20,34,65}, // K
{127,64,64,64,64}, // L
{127,2,12,2,127}, // M
{127,4,8,16,127}, // N
{62,65,65,65,62}, // O
{127,9,9,9,6}, // P
{62,65,97,33,94}, // Q
{127,9,25,41,70}, // R
{38,73,73,73,50}, // S
{1,1,127,1,1}, // T
{63,64,64,64,63}, // U
{31,32,64,32,31}, // V
{63,64,56,64,63}, // W
{99,20,8,20,99}, // X
{3,4,120,4,3}, // Y
{97,81,73,69,67} // Z
};
// ================= XY CONVERSION FUNCTION =================
uint16_t XY(uint8_t x, uint8_t y) {
if (x >= matrixWidth || y >= matrixHeight) return NUM_LEDS - 1;
if (SERPENTINE && (y & 1)) {
return (y * matrixWidth) + (matrixWidth - 1 - x);
} else {
return (y * matrixWidth) + x;
}
}
// ================= DRAW CHARACTER FUNCTION =================
void drawChar(int x, int y, char c, CRGB color) {
if (c < 32 || c > 127) return;
for (uint8_t col = 0; col < 5; col++) {
uint8_t line = pgm_read_byte(&font5x7[c - 32][col]);
for (uint8_t row = 0; row < 7; row++) {
if (line & (1 << row)) {
uint16_t ledIndex = XY(x + col, y + row);
if (ledIndex < NUM_LEDS) {
leds[ledIndex] = color;
}
}
}
}
}
// ================= DISPLAY NUMBER FUNCTION =================
void displayNumber(int num, CRGB color = CRGB::Red) {
FastLED.clear();
char buf[10];
sprintf(buf, "%d", num);
int textWidth = strlen(buf) * 6;
int startX = (matrixWidth - textWidth) / 2;
int startY = 4;
if (startX < 0) startX = 0;
for (uint16_t i = 0; buf[i] != '\0'; i++) {
drawChar(startX + i * 6, startY, buf[i], color);
}
FastLED.show();
}
// ================= DISPLAY TEXT FUNCTION =================
void displayText(const char* text, CRGB color = CRGB::Red, int scrollSpeed = 100) {
FastLED.clear();
int textLength = strlen(text);
int textWidth = textLength * 6;
// If text fits on screen, just center it
if (textWidth <= matrixWidth) {
int startX = (matrixWidth - textWidth) / 2;
int startY = 4;
for (uint16_t i = 0; text[i] != '\0'; i++) {
drawChar(startX + i * 6, startY, text[i], color);
}
FastLED.show();
} else {
// Text is too long, scroll it
scrollText(text, color, scrollSpeed);
}
}
// ================= SCROLLING TEXT FUNCTION =================
void scrollText(const char* text, CRGB color, int scrollSpeed) {
int textLength = strlen(text);
int totalWidth = textLength * 6;
// Scroll from right to left
for (int offset = matrixWidth; offset > -totalWidth; offset--) {
FastLED.clear();
for (int i = 0; i < textLength; i++) {
int charX = offset + (i * 6);
// Only draw if character is visible on screen
if (charX > -5 && charX < matrixWidth) {
for (uint8_t col = 0; col < 5; col++) {
uint8_t line = pgm_read_byte(&font5x7[text[i] - 32][col]);
for (uint8_t row = 0; row < 7; row++) {
if (line & (1 << row)) {
int x = charX + col;
int y = 4 + row;
if (x >= 0 && x < matrixWidth && y >= 0 && y < matrixHeight) {
leds[XY(x, y)] = color;
}
}
}
}
}
}
FastLED.show();
delay(scrollSpeed);
}
}
// ================= DISPLAY MULTI-LINE MESSAGE =================
void displayMultiLine(const char* line1, const char* line2, CRGB color = CRGB::Red, int duration = 3000) {
FastLED.clear();
// Center first line
int textWidth1 = strlen(line1) * 6;
int startX1 = (matrixWidth - textWidth1) / 2;
if (startX1 < 0) startX1 = 0;
// Center second line
int textWidth2 = strlen(line2) * 6;
int startX2 = (matrixWidth - textWidth2) / 2;
if (startX2 < 0) startX2 = 0;
// Draw first line (top)
for (uint16_t i = 0; line1[i] != '\0'; i++) {
drawChar(startX1 + i * 6, 1, line1[i], color);
}
// Draw second line (bottom)
for (uint16_t i = 0; line2[i] != '\0'; i++) {
drawChar(startX2 + i * 6, 8, line2[i], color);
}
FastLED.show();
delay(duration);
}
// ================= MESSAGE QUEUE =================
struct Message {
char text[50];
CRGB color;
int duration;
bool scroll;
};
#define MAX_MESSAGES 10
Message messageQueue[MAX_MESSAGES];
int queueStart = 0;
int queueEnd = 0;
int queueCount = 0;
void queueMessage(const char* text, CRGB color = CRGB::Red, int duration = 3000, bool scroll = false) {
if (queueCount < MAX_MESSAGES) {
strcpy(messageQueue[queueEnd].text, text);
messageQueue[queueEnd].color = color;
messageQueue[queueEnd].duration = duration;
messageQueue[queueEnd].scroll = scroll;
queueEnd = (queueEnd + 1) % MAX_MESSAGES;
queueCount++;
}
}
void processMessageQueue() {
if (queueCount > 0) {
Message msg = messageQueue[queueStart];
if (msg.scroll) {
scrollText(msg.text, msg.color, 100);
} else {
displayText(msg.text, msg.color);
delay(msg.duration);
}
FastLED.clear();
FastLED.show();
delay(500);
queueStart = (queueStart + 1) % MAX_MESSAGES;
queueCount--;
}
}
// ================= ANIMATION FUNCTIONS =================
void countdownCompleteAnimation() {
// Show "DONE" with animation
for (int i = 0; i < 3; i++) {
displayText("DONE", CRGB::Green);
delay(300);
FastLED.clear();
FastLED.show();
delay(200);
}
// Show celebration message
queueMessage("GOOD", CRGB::Yellow, 1000);
queueMessage("JOB!", CRGB::Yellow, 1000);
queueMessage("WELL", CRGB::Cyan, 1000);
queueMessage("DONE", CRGB::Cyan, 1000);
// Rainbow effect
for (int i = 0; i < 255; i+=5) {
fill_rainbow(leds, NUM_LEDS, i, 10);
FastLED.show();
delay(30);
}
FastLED.clear();
FastLED.show();
}
// ================= SERIAL VARIABLES =================
String serialBuffer = "";
int countdownValue = 0;
unsigned long lastUpdate = 0;
bool countdownActive = false;
bool countdownPaused = false;
unsigned long pauseTime = 0;
bool showingMessage = false;
unsigned long messageEndTime = 0;
// ================= PROCESS SERIAL COMMANDS =================
void processSerialCommand(String cmd) {
cmd.trim();
cmd.toUpperCase();
if (cmd == "HELP" || cmd == "?") {
Serial.println("\n=== Countdown & Message Commands ===");
Serial.println("NUMBER COMMANDS:");
Serial.println(" [number] - Start countdown (e.g., 60)");
Serial.println(" PAUSE - Pause countdown");
Serial.println(" RESUME - Resume countdown");
Serial.println(" STOP - Stop countdown");
Serial.println(" RESET - Reset to 0");
Serial.println("\nMESSAGE COMMANDS:");
Serial.println(" MSG [text] - Display message (e.g., MSG HELLO)");
Serial.println(" SCROLL [text] - Scrolling message");
Serial.println(" LINE1|[text] - Two-line message (e.g., LINE1|HELLO|WORLD)");
Serial.println(" COLOR [R,G,B] - Set color (e.g., COLOR 255,0,0)");
Serial.println(" CLEAR - Clear display");
Serial.println(" QUEUE - Show message queue");
Serial.println("\nOTHER:");
Serial.println(" TEST - Run display test");
Serial.println(" HELP - Show this menu");
Serial.println("========================\n");
}
else if (cmd.startsWith("MSG ")) {
String message = cmd.substring(4);
showingMessage = true;
messageEndTime = millis() + 3000;
displayText(message.c_str(), CRGB::Green);
Serial.print("Displaying message: ");
Serial.println(message);
}
else if (cmd.startsWith("SCROLL ")) {
String message = cmd.substring(7);
showingMessage = true;
messageEndTime = millis() + (strlen(message.c_str()) * 600); // Approximate scroll time
scrollText(message.c_str(), CRGB::Cyan, 100);
Serial.print("Scrolling message: ");
Serial.println(message);
}
else if (cmd.startsWith("LINE1|")) {
// Format: LINE1|first line|second line
int firstPipe = cmd.indexOf('|');
int secondPipe = cmd.indexOf('|', firstPipe + 1);
if (firstPipe > 0 && secondPipe > firstPipe) {
String line1 = cmd.substring(firstPipe + 1, secondPipe);
String line2 = cmd.substring(secondPipe + 1);
showingMessage = true;
messageEndTime = millis() + 4000;
displayMultiLine(line1.c_str(), line2.c_str(), CRGB::Yellow);
Serial.print("Two-line message: ");
Serial.print(line1);
Serial.print(" | ");
Serial.println(line2);
}
}
else if (cmd.startsWith("COLOR ")) {
// Parse RGB values
String rgbStr = cmd.substring(6);
int r = 255, g = 255, b = 255;
int firstComma = rgbStr.indexOf(',');
int secondComma = rgbStr.indexOf(',', firstComma + 1);
if (firstComma > 0 && secondComma > firstComma) {
r = rgbStr.substring(0, firstComma).toInt();
g = rgbStr.substring(firstComma + 1, secondComma).toInt();
b = rgbStr.substring(secondComma + 1).toInt();
// Set current color for future displays (you'd need to store this)
Serial.print("Color set to RGB(");
Serial.print(r);
Serial.print(",");
Serial.print(g);
Serial.print(",");
Serial.print(b);
Serial.println(")");
}
}
else if (cmd == "CLEAR") {
FastLED.clear();
FastLED.show();
Serial.println("Display cleared");
}
else if (cmd == "QUEUE") {
Serial.print("Messages in queue: ");
Serial.println(queueCount);
}
else if (cmd == "PAUSE") {
if (countdownActive && !countdownPaused) {
countdownPaused = true;
pauseTime = millis() - lastUpdate;
displayText("PAUSED", CRGB::Yellow);
Serial.println("Countdown paused");
}
}
else if (cmd == "RESUME") {
if (countdownActive && countdownPaused) {
countdownPaused = false;
lastUpdate = millis() - pauseTime;
displayNumber(countdownValue, CRGB::Red);
Serial.println("Countdown resumed");
}
}
else if (cmd == "STOP") {
countdownActive = false;
countdownPaused = false;
FastLED.clear();
FastLED.show();
Serial.println("Countdown stopped");
}
else if (cmd == "RESET") {
countdownActive = false;
countdownPaused = false;
countdownValue = 0;
displayNumber(0, CRGB::White);
Serial.println("Reset to 0");
}
else if (cmd == "TEST") {
Serial.println("Running display test...");
// Test all display modes
displayText("TEST", CRGB::Red);
delay(1000);
displayMultiLine("16X32", "MATRIX", CRGB::Green);
delay(2000);
scrollText("SCROLLING TEXT TEST", CRGB::Cyan, 80);
for (int i = 0; i <= 9; i++) {
displayNumber(i, CRGB::Yellow);
delay(300);
}
displayText("DONE", CRGB::Blue);
delay(1000);
}
else if (cmd.length() > 0) {
// Check if it's a number for countdown
boolean isValid = true;
for (int i = 0; i < cmd.length(); i++) {
if (!isDigit(cmd[i])) {
isValid = false;
break;
}
}
if (isValid) {
countdownValue = cmd.toInt();
if (countdownValue > 99999) countdownValue = 99999;
countdownActive = true;
countdownPaused = false;
lastUpdate = millis();
showingMessage = false;
displayNumber(countdownValue, CRGB::Red);
Serial.print("Countdown started: ");
Serial.println(countdownValue);
} else {
Serial.println("Unknown command. Type HELP for commands.");
}
}
}
// ================= SETUP =================
void setup() {
Serial.begin(9600);
Serial.println("\n=================================");
Serial.println("16x32 LED Matrix - Countdown & Messages");
Serial.println("=================================");
Serial.println("Type HELP for commands");
FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.clear();
FastLED.show();
// Show welcome message
displayMultiLine("WELCOME", "READY!", CRGB::Green, 2000);
Serial.println("System ready!");
}
// ================= MAIN LOOP =================
void loop() {
// Check for serial input
while (Serial.available()) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (serialBuffer.length() > 0) {
processSerialCommand(serialBuffer);
serialBuffer = "";
}
} else {
serialBuffer += c;
}
}
// Process message queue if not showing a temporary message
if (!showingMessage && queueCount > 0) {
processMessageQueue();
}
// Clear showing message flag after duration
if (showingMessage && millis() > messageEndTime) {
showingMessage = false;
FastLED.clear();
FastLED.show();
// If countdown was active, redisplay the number
if (countdownActive && !countdownPaused) {
displayNumber(countdownValue, CRGB::Red);
}
}
// Update countdown
if (countdownActive && !countdownPaused && !showingMessage) {
unsigned long now = millis();
if (now - lastUpdate >= 1000) {
countdownValue--;
lastUpdate = now;
if (countdownValue <= 0) {
countdownValue = 0;
countdownActive = false;
displayNumber(0, CRGB::Green);
Serial.println("COUNTDOWN FINISHED!");
countdownCompleteAnimation();
} else {
displayNumber(countdownValue, CRGB::Red);
if (countdownValue <= 10 && countdownValue > 0) {
if (countdownValue % 2 == 0) {
displayNumber(countdownValue, CRGB::Yellow);
}
}
Serial.println(countdownValue);
}
}
}
}
/*
MSG HELLO - Shows "HELLO" centered
SCROLL WELCOME! - Scrolls "WELCOME!"
LINE1|COUNT|DOWN - Shows "COUNT" on top, "DOWN" on bottom
COLOR 0,255,0 - Changes color to green
60 - Starts 60 second countdown
PAUSE - Pauses countdown
MSG BREAK TIME! - Shows message during countdown
RESUME - Countdown continues*/