#include <Arduino.h>
#include <WiFi.h>
#include "time.h"
#include <MD_MAX72xx.h>
#include <SPI.h>
#include "Font_Data.h"
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW
#define MAX_DEVICES 4
#define CLK_PIN 18
#define DATA_PIN 23
#define CS_PIN 15
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
#define MYTZ "GMT0BST,M3.5.0/1,M10.5.0"
struct tm tm;
time_t now;
uint32_t timeStamp = 0, start = 0, lastNow = 0, nowTime = 0;
#define DELAYTIME 40 // in milliseconds, this is scroll anim frame delay
#define CHAR_SPACING 1 // pixels between characters
#define CHAR_COLS 8 // should match the fixed width character columns
#define ANIMATION_FRAME_DELAY 30 // in milliseconds, pushwheel frame delay
struct digitData {
uint8_t oldValue, newValue; // ASCII value for the character
uint8_t index; // animation progression index
uint32_t timeLastFrame; // time the last frame started animating
uint8_t charCols; // number of valid cols in the charMap
uint8_t charMap[CHAR_COLS]; // character font bitmap
};
const uint8_t ST_INIT = 0, ST_WAIT = 1, ST_ANIM = 2;
static uint8_t state = ST_INIT;
uint8_t count = 0;
uint8_t curCol = 0;
uint8_t prevSecond = 61;
uint8_t today = 0;
uint8_t hourNow, minuteNow, secondNow;
bool scroll = false;
bool showClock = true;
bool inTemp = true;
uint32_t startTime;
uint32_t scrTime;
bool f = true;
char dateStr [64];
uint8_t oi;
char ordInd[4][3] = { "th", "st", "nd", "rd" };
void updateDisplay(uint16_t numDigits, struct digitData *d)
// do the necessary to display current bitmap buffer to the LED display
{
curCol = 1;
mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
//mx.clear();
for (int8_t i = numDigits - 1; i >= 0; i--) {
if (i == 5 || i == 2) curCol += 2;//Skip the colons
else {
for (int8_t j = d[i].charCols - 1; j >= 0; j--) {
mx.setColumn(curCol++, d[i].charMap[j]);
}
curCol += CHAR_SPACING;
}
}
mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
}
boolean displayTime() {
const uint8_t DIGITS_SIZE = 8; //"HH:MM:SS"
static struct digitData digit[DIGITS_SIZE];
// finite state machine to control what we do
switch (state) {
case ST_INIT: // Initialize the display - done once only on first call
digit[7].oldValue = 20 + secondNow % 10;
digit[6].oldValue = 20 + secondNow / 10;
digit[5].oldValue = '!';//Borrowed '!' for single column space
digit[4].oldValue = 10 + minuteNow % 10;
digit[3].oldValue = 10 + minuteNow / 10;
digit[2].oldValue = '!';
digit[1].oldValue = 10 + hourNow % 10;
digit[0].oldValue = 10 + hourNow / 10;
// Display the starting number
for (int8_t i = DIGITS_SIZE - 1; i >= 0; i--) {
digit[i].charCols = mx.getChar(digit[i].oldValue, CHAR_COLS, digit[i].charMap);
}
updateDisplay(DIGITS_SIZE, digit);
// Now we wait for a change
state = ST_WAIT;
break;
case ST_WAIT: // Check for the values displayed for changes in them
digit[7].newValue = 20 + secondNow % 10;
digit[6].newValue = 20 + secondNow / 10;
digit[5].newValue = '!';
digit[4].newValue = 10 + minuteNow % 10;
digit[3].newValue = 10 + minuteNow / 10;
digit[2].newValue = '!';
digit[1].newValue = 10 + hourNow % 10;
digit[0].newValue = 10 + hourNow / 10;
for (uint8_t i = 0; i < 8; i++) {
if (digit[i].newValue != digit[i].oldValue) {
state = ST_ANIM;
digit[i].index = 0;
digit[i].timeLastFrame = 0;
}
}
if (state == ST_WAIT) break;
case ST_ANIM:
for (uint8_t i = 0; i < 8; i++) {
//update direction from hours to seconds
if ((digit[i].newValue != digit[i].oldValue) && (millis() - digit[i].timeLastFrame >= ANIMATION_FRAME_DELAY)) {
//if mismatch is detected then start the timer for the animation
uint8_t newChar[CHAR_COLS] = { 0 };
mx.getChar(digit[i].newValue, CHAR_COLS, newChar);
//Scroll down
for (uint8_t j = 0; j < digit[i].charCols; j++) {
newChar[j] = newChar[j] >> (COL_SIZE - 1 - digit[i].index);
digit[i].charMap[j] = digit[i].charMap[j] << 1;
digit[i].charMap[j] |= newChar[j];
}
/*
// scroll up
for (uint8_t j = 0; j < digit[i].charCols; j++) {
newChar[j] = newChar[j] << (COL_SIZE - 1 - digit[i].index);
digit[i].charMap[j] = digit[i].charMap[j] >> 1;
digit[i].charMap[j] |= newChar[j];
}
*/
// set new parameters for next animation and check if we are done
digit[i].index++;
digit[i].timeLastFrame = millis();
if (digit[i].index >= COL_SIZE)
digit[i].oldValue = digit[i].newValue; // done animating
}
}
updateDisplay(DIGITS_SIZE, digit); // show new display
// are we done animating?
{
boolean allDone = true;
for (uint8_t i = 0; allDone && (i < DIGITS_SIZE); i++) {
allDone = allDone && (digit[i].oldValue == digit[i].newValue);
}
if (allDone) state = ST_WAIT;
}
break;
default:
state = 0;
}
return (state == ST_WAIT); // animation has ended
}
void scrollText(const char *p)
{
uint8_t charWidth;
uint8_t cBuf[8]; // this should be ok for all built-in fonts
uint8_t tempWidth = 0;
while (*p != '\0')
{
charWidth = mx.getChar(*p++, sizeof(cBuf) / sizeof(cBuf[0]), cBuf);
tempWidth += charWidth + 1;
for (uint8_t i = 0; i <= charWidth; i++)
{
mx.transform(MD_MAX72XX::TSL);
mx.setColumn(0, (i < charWidth) ? cBuf[i] : 0);
delay(DELAYTIME);
}
}
if (*p == 0) { // On the fly col padding for variable temp width display (justify center)
uint8_t pad;
if (inTemp) {
pad = tempWidth - 43; //Column width of " Temp In "
inTemp = !inTemp;
}
else pad = tempWidth - 21; //Column width of "Out "
if (pad < 31) pad = (31 - pad) / 2;
else pad = 0;
tempWidth = 0;
if (pad) {
for (uint8_t i = 0; i < pad; i++)
{
mx.transform(MD_MAX72XX::TSL);
mx.setColumn(0, 0);
delay(DELAYTIME);
}
}
}
}
void scrollUp () {
for (uint8_t i = 0; i < 8; i ++) {
mx.transform(MD_MAX72XX::TSU);
delay(DELAYTIME);
}
}
void doDateStr() {
today = tm.tm_mday;
uint8_t oi = ((today + 90) % 100 - 10) % 10;
if (oi > 3) oi = 0;
char buf[32];
strftime(dateStr, sizeof(dateStr), "%A %d", &tm);
strcat(dateStr,ordInd[oi]);
strftime(buf, sizeof(buf), " %B %Y", &tm);
strcat(dateStr, buf);
//Serial.println(dateStr);
}
void wifiSetup() {
WiFi.begin("Wokwi-GUEST", "", 6);
//WiFi.begin(ssid, password);
Serial.print("\nConnecting");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("\nConnected, IP address: ");
Serial.println(WiFi.localIP());
}
/*
uint32_t sntp_set_sync_interval() {
return 6 * 60 * 60 * 1000UL; // 6 hours
}
*/
void setTime(int yr, int month, int mday, int hr, int minute, int sec, int isDst) {
struct tm tm;
tm.tm_year = yr - 1900; // Set date
tm.tm_mon = month - 1;
tm.tm_mday = mday;
tm.tm_hour = hr; // Set time
tm.tm_min = minute;
tm.tm_sec = sec;
tm.tm_isdst = isDst; // 1 or 0
time_t t = mktime(&tm);
Serial.printf("Setting time: %s", asctime(&tm));
struct timeval now = { .tv_sec = t };
settimeofday(&now, NULL);
}
void printLocalTime() {
if (!getLocalTime(&tm, 5000)) {
Serial.println("Failed to get Time!");
return;
}
if(today != tm.tm_mday) {
doDateStr();
}
time(&now); //Get a timestamp from ESP clock
localtime_r(&now, &tm); //Populate struct tm
Serial.print(dateStr);
Serial.println(&tm, " %H:%M:%S %Z");
}
void setup() {
Serial.begin(115200);
delay(1000);
start = millis();
wifiSetup();
Serial.println("\n[MD_MAX72XX Test & Demo]\n");
mx.begin();
mx.setFont(numeric7Se);
mx.control(MD_MAX72XX::INTENSITY, 0);
randomSeed(analogRead(A0));
// Set NTP servers
configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org");
Serial.println("Getting Time...");
printLocalTime();
Serial.println("Setup complete");
}
void loop() {
if (showClock) {
if (millis() - startTime >= 495) {
startTime = millis();
mx.setColumn(9, f ? 20 : 0);
mx.setColumn(21, f ? 20 : 0);
f = !f;
}
time(&now); //Get a timestamp from ESP clock
if (now != lastNow) {
startTime = millis();//Attempt to keep colons in sync
lastNow = now;
localtime_r(&now, &tm);
hourNow = tm.tm_hour;
minuteNow = tm.tm_min;
secondNow = tm.tm_sec;
count ++;
if (count == 45) {//Set clock 'display for' in seconds here
scroll = true;
showClock = false;
count = 0;
}
}
displayTime();
delay(1);
}
if (scroll) {
scrTime = millis();
mx.clear();
scrollText(dateStr);
inTemp = true;
char inStr[7];
float tempIn = 0.0;
tempIn = random(180, 220) / 10.0;
dtostrf(tempIn, 3, 1, inStr);
char tempin [20];
sprintf(tempin, " Temp In %s$", inStr);
scrollText(tempin);
delay(2000);
scrollUp();
char outStr[7];
float tempOut = 0.0;
tempOut = random(-120, 120) / 10.0; //Pseudo temp reading for demo only
dtostrf(tempOut, 3, 1, outStr);
char tempout [15];
sprintf(tempout, "Out %s$", outStr);
scrollText(tempout);
delay(2000);
scrollUp();
scroll = !scroll;
state = ST_INIT;//Ensures a new time refresh
uint16_t t = (millis() - scrTime) % 1000;
//Serial.print(F("t = "));
//Serial.println(t);
if (t < 495) f = 1; //We're in the first half of the second, next switch is colon on
else f = 0;
showClock = true;
}
}
/*
if (millis() - start >= 5000) {
printLocalTime();
start = millis();
}
for (uint8_t i = 0; i < 20; i++) {
delay(1000);
printLocalTime();
}
/*
setTime(2026, 3, 29, 0, 59, 50, 0);
for (uint8_t i = 0; i < 20; i++) {
delay(1000);
printLocalTime();
}
setTime(2026, 10, 25, 1, 59, 50, 1);
for (uint8_t i = 0; i < 20; i++) {
delay(1000);
printLocalTime();
}
*/