Currently highest number is 9 which is more than enough for me, but should be no problem to either have it show something like 9+ for more or even a scrolling number once you need more than 1 digit⦠Or if you really have many you could use one led per task which would get you up to 64. That would let you encode each tasks status as a color as well 
Code
/*
* ESP32-S3 RGB Matrix Beeminder Status Display (Time-Based Escalation)
* Monitors Beeminder goals and displays escalating status via 8x8 RGB LED matrix
* Escalates brightness during day, then blinking frequency in evening
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <time.h>
#include <FastLED.h>
// Configuration - MODIFY THESE VALUES
const char* WIFI_SSID = "";
const char* WIFI_PASSWORD = "";
const char* BEEMINDER_USERNAME = "";
const char* BEEMINDER_AUTH_TOKEN = "";
// NTP Server for time synchronization
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 1*60*60; // Adjust for your timezone (0 = UTC)
const int daylightOffset_sec = 1*60*60; // Adjust for daylight saving
// Time-based escalation settings
const int BRIGHTNESS_START_HOUR = 7; // 7 AM - start brightness escalation
const int ESCALATION_START_HOUR = 19; // 7 PM - start blinking escalation
const int MAX_ESCALATION_HOUR = 24; // Midnight - maximum blinking
const int SLEEP_START = 0;
const int SLEEP_END = 7;
// Brightness settings
const int MIN_BRIGHTNESS = 1;
const int MAX_BRIGHTNESS = 10;
// RGB Matrix configuration for Waveshare ESP32-S3-Matrix board
#define LED_PIN 14 // ESP32-S3-Matrix uses GPIO14 for the built-in 8x8 RGB matrix
#define NUM_LEDS 64 // 8x8 = 64 LEDs
#define LED_TYPE WS2812B // Board uses WS2812B RGB LEDs
#define COLOR_ORDER RGB // Changed from GRB to RGB - board has different color order
CRGB leds[NUM_LEDS];
// Timing constants
const unsigned long CHECK_INTERVAL = 1 * 60 * 1000; // 20 minutes in milliseconds
// Global variables
unsigned long lastBeeminderCheck = 0;
unsigned long lastAnimation = 0;
int urgentGoals = 0;
int displayMode = 0; // 0=off, 1=solid_number, 2=blinking_number
unsigned long animationInterval = 1000;
int animationStep = 0;
int currentBrightness = MIN_BRIGHTNESS;
// Color definitions
CRGB COLOR_OFF = CRGB::Black;
CRGB COLOR_URGENT = CRGB::Red;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Starting ESP32-S3 RGB Matrix Beeminder Status Display...");
// Initialize FastLED
FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
FastLED.setBrightness(MIN_BRIGHTNESS);
// Test the matrix
testMatrix();
// Connect to WiFi
connectToWiFi();
// Initialize time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("Getting time from NTP server...");
delay(2000); // Give time to sync
// Initial Beeminder check
checkBeeminderGoals();
Serial.println("Setup complete!");
}
void loop() {
unsigned long currentTime = millis();
// Check Beeminder goals periodically
if (currentTime - lastBeeminderCheck >= CHECK_INTERVAL) {
if (WiFi.status() == WL_CONNECTED) {
checkBeeminderGoals();
} else {
Serial.println("WiFi disconnected, attempting reconnection...");
connectToWiFi();
}
lastBeeminderCheck = currentTime;
}
// Handle LED animations
handleMatrixDisplay();
// Small delay to prevent excessive CPU usage
delay(50);
}
void testMatrix() {
Serial.println("Testing RGB Matrix...");
// Fill with red
fill_solid(leds, NUM_LEDS, CRGB::Red);
FastLED.show();
delay(500);
// Fill with green
fill_solid(leds, NUM_LEDS, CRGB::Green);
FastLED.show();
delay(500);
// Fill with blue
fill_solid(leds, NUM_LEDS, CRGB::Blue);
FastLED.show();
delay(500);
// Clear
FastLED.clear();
FastLED.show();
Serial.println("Matrix test complete!");
}
void connectToWiFi() {
Serial.println("Connecting to WiFi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.println("WiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("");
Serial.println("WiFi connection failed!");
}
}
void checkBeeminderGoals() {
Serial.println("Checking Beeminder goals...");
HTTPClient http;
String url = String("https://www.beeminder.com/api/v1/users/") +
BEEMINDER_USERNAME + "/goals.json?auth_token=" + BEEMINDER_AUTH_TOKEN;
http.begin(url);
http.addHeader("User-Agent", "ESP32-S3-RGB-Matrix-Beeminder/1.0");
int httpResponseCode = http.GET();
if (httpResponseCode == 200) {
String payload = http.getString();
parseBeeminderResponse(payload);
} else {
Serial.print("HTTP Error: ");
Serial.println(httpResponseCode);
Serial.println("Response: " + http.getString());
// Keep display off on error
setDisplayMode(0);
}
http.end();
}
void parseBeeminderResponse(String jsonResponse) {
DynamicJsonDocument doc(8192);
DeserializationError error = deserializeJson(doc, jsonResponse);
if (error) {
Serial.print("JSON parsing failed: ");
Serial.println(error.c_str());
setDisplayMode(0); // Turn off on error
return;
}
urgentGoals = 0;
int totalGoals = 0;
// Get current time using time library
time_t now;
time(&now);
for (JsonObject goal : doc.as<JsonArray>()) {
String slug = goal["slug"];
long losedate = goal["losedate"];
bool queued = goal["queued"];
totalGoals++;
// Skip queued goals
if (queued) {
continue;
}
// Check if goal is urgent (derails within 24 hours)
long timeUntilDerail = losedate - now;
if (timeUntilDerail <= 86400) { // 24 hours in seconds
urgentGoals++;
Serial.print("URGENT: ");
Serial.print(slug);
Serial.print(" derails in ");
Serial.print(timeUntilDerail / 3600);
Serial.println(" hours");
}
}
Serial.print("Total goals: ");
Serial.println(totalGoals);
Serial.print("Urgent goals: ");
Serial.println(urgentGoals);
// Set display pattern based on urgency and time
updateDisplayBasedOnTimeAndUrgency();
}
void updateDisplayBasedOnTimeAndUrgency() {
if (urgentGoals == 0) {
setDisplayMode(0); // Off - no urgent goals
return;
}
// Get current hour
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
setDisplayMode(1); // Default to solid display if time unavailable
return;
}
int currentHour = timeinfo.tm_hour;
int currentMinute = timeinfo.tm_min;
Serial.print("Current time: ");
Serial.print(currentHour);
Serial.print(":");
Serial.println(currentMinute);
if (SLEEP_START <= currentHour && currentHour < SLEEP_END) {
setDisplayMode(0);
Serial.println("Sleep time - display off");
return;
}
if (currentHour >= BRIGHTNESS_START_HOUR && currentHour < ESCALATION_START_HOUR) {
// Brightness escalation period (7 AM to 7 PM)
calculateBrightness(currentHour, currentMinute);
setDisplayMode(1); // Solid number display
animationInterval = 0; // No animation, just solid display
Serial.print("Brightness escalation period - brightness: ");
Serial.println(currentBrightness);
} else if (currentHour >= ESCALATION_START_HOUR) {
// Blinking escalation period (7 PM to midnight)
currentBrightness = MAX_BRIGHTNESS; // Max brightness during blinking period
FastLED.setBrightness(currentBrightness);
setDisplayMode(2); // Blinking number display
calculateBlinkingSpeed(currentHour, currentMinute);
Serial.print("Blinking escalation period - interval: ");
Serial.print(animationInterval);
Serial.println("ms");
} else {
// Between midnight and 7 AM - display off
setDisplayMode(0);
Serial.println("Night time - display off");
}
}
void calculateBrightness(int hour, int minute) {
// Calculate time progress from BRIGHTNESS_START_HOUR to ESCALATION_START_HOUR
float totalMinutes = (ESCALATION_START_HOUR - BRIGHTNESS_START_HOUR) * 60.0;
float currentMinutes = (hour - BRIGHTNESS_START_HOUR) * 60.0 + minute;
float progress = currentMinutes / totalMinutes;
if (progress > 1.0) progress = 1.0;
if (progress < 0.0) progress = 0.0;
// Exponential brightness increase: y = MIN + (MAX-MIN) * progress^2
currentBrightness = MIN_BRIGHTNESS + (MAX_BRIGHTNESS - MIN_BRIGHTNESS) * progress * progress;
if (currentBrightness > MAX_BRIGHTNESS) currentBrightness = MAX_BRIGHTNESS;
if (currentBrightness < MIN_BRIGHTNESS) currentBrightness = MIN_BRIGHTNESS;
FastLED.setBrightness(currentBrightness);
}
void calculateBlinkingSpeed(int hour, int minute) {
// Calculate time progress from ESCALATION_START_HOUR to MAX_ESCALATION_HOUR
float totalMinutes = (MAX_ESCALATION_HOUR - ESCALATION_START_HOUR) * 60.0;
float currentMinutes = (hour - ESCALATION_START_HOUR) * 60.0 + minute;
float progress = currentMinutes / totalMinutes;
if (progress > 1.0) progress = 1.0;
if (progress < 0.0) progress = 0.0;
// Linear increase in blinking frequency
// Start with slow blink (2 seconds), end with fast blink (200ms)
int maxInterval = 2000; // 2 seconds
int minInterval = 200; // 200ms
animationInterval = maxInterval - (maxInterval - minInterval) * progress;
}
void setDisplayMode(int mode) {
displayMode = mode;
animationStep = 0; // Reset animation
Serial.print("Display mode set to: ");
switch (mode) {
case 0: Serial.println("Off"); break;
case 1: Serial.println("Solid number"); break;
case 2: Serial.println("Blinking number"); break;
}
}
void handleMatrixDisplay() {
unsigned long currentTime = millis();
// Handle solid display (no animation timing needed)
if (displayMode == 1) {
displaySolidNumber();
FastLED.show();
return;
}
// Handle animated displays
if (displayMode == 2 && currentTime - lastAnimation >= animationInterval) {
lastAnimation = currentTime;
displayBlinkingNumber();
FastLED.show();
return;
}
// Handle off state
if (displayMode == 0) {
displayOff();
FastLED.show();
return;
}
}
void displayOff() {
FastLED.clear();
}
void displaySolidNumber() {
FastLED.clear();
if (urgentGoals == 0) {
return;
}
int numberToShow = min(urgentGoals, 9); // Cap at 9
displayNumber(numberToShow, COLOR_URGENT);
}
void displayBlinkingNumber() {
if (animationStep % 2 == 0) {
// Show number
displaySolidNumber();
} else {
// Hide number
FastLED.clear();
}
animationStep++;
}
// Utility function to get LED index from row/col
int getIndex(int row, int col) {
if (row < 0 || row >= 8 || col < 0 || col >= 8) {
return -1; // Invalid position
}
return row * 8 + col;
}
// Display a number (0-9) on the 8x8 matrix
void displayNumber(int number, CRGB color) {
// 8x8 digit patterns - each number is defined as a 2D array
// 1 = LED on, 0 = LED off
bool patterns[10][8][8] = {
// 0
{
{0,0,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,1,0,0,0,0,1,0},
{0,1,0,0,0,0,1,0},
{0,1,0,0,0,0,1,0},
{0,1,0,0,0,0,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,0,0}
},
// 1
{
{0,0,0,1,1,0,0,0},
{0,0,1,1,1,0,0,0},
{0,0,0,1,1,0,0,0},
{0,0,0,1,1,0,0,0},
{0,0,0,1,1,0,0,0},
{0,0,0,1,1,0,0,0},
{0,0,0,1,1,0,0,0},
{0,1,1,1,1,1,1,0}
},
// 2
{
{0,0,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,0,0,0,0,1,1,0},
{0,0,0,0,1,1,0,0},
{0,0,0,1,1,0,0,0},
{0,0,1,1,0,0,0,0},
{0,1,1,0,0,0,0,0},
{0,1,1,1,1,1,1,0}
},
// 3
{
{0,0,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,0,0,0,0,1,1,0},
{0,0,0,1,1,1,0,0},
{0,0,0,0,0,1,1,0},
{0,0,0,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,0,0}
},
// 4
{
{0,0,0,0,1,1,0,0},
{0,0,0,1,1,1,0,0},
{0,0,1,1,1,1,0,0},
{0,1,1,0,1,1,0,0},
{0,1,1,1,1,1,1,0},
{0,0,0,0,1,1,0,0},
{0,0,0,0,1,1,0,0},
{0,0,0,0,1,1,0,0}
},
// 5
{
{0,1,1,1,1,1,1,0},
{0,1,1,0,0,0,0,0},
{0,1,1,0,0,0,0,0},
{0,1,1,1,1,1,0,0},
{0,0,0,0,0,1,1,0},
{0,0,0,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,0,0}
},
// 6
{
{0,0,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,0,0,0},
{0,1,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,0,0}
},
// 7
{
{0,1,1,1,1,1,1,0},
{0,0,0,0,0,1,1,0},
{0,0,0,0,1,1,0,0},
{0,0,0,0,1,1,0,0},
{0,0,0,1,1,0,0,0},
{0,0,0,1,1,0,0,0},
{0,0,1,1,0,0,0,0},
{0,0,1,1,0,0,0,0}
},
// 8
{
{0,0,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,0,0}
},
// 9
{
{0,0,1,1,1,1,0,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,1,0},
{0,0,0,0,0,1,1,0},
{0,1,1,0,0,1,1,0},
{0,0,1,1,1,1,0,0}
}
};
// Display the pattern
if (number >= 0 && number <= 9) {
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
if (patterns[number][row][col] == 1) {
int index = getIndex(row, col);
if (index >= 0) {
leds[index] = color;
}
}
}
}
}
}