/*
* Alarm clock using MAX7219 LED matrix (8x32), RTC DS3231, LDR and pushbuttons to set alarm time
* -> brief operation description
*
* -> git hub address
*
* Revision history:
* Feb/2022: Version 1
*
* Copyright (C) 2022 by Tiago Guedes
* Licensed under GNU GPL v3.0 (https://www.gnu.org/licenses/gpl.html)
*
*/
// Header files includes
//#include <ESP-WiFiSettings> // Arduino core for ESP8266 WiFi chip (https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi)
#include <WiFi.h>
#include <NTPClient.h> // NTPClient by Arduino (https://github.com/arduino-libraries/NTPClient)
#include <WiFiUdp.h> // Wifi Udp for NTP (https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/WiFiUdp.h)
#include <SPI.h> // Serial Peripheral Interface https://www.arduino.cc/en/reference/SPI
#include <RTClib.h> // Adafruit Real Time Clock library for Arduino https://adafruit.github.io/RTClib/html/index.html & https://github.com/adafruit/RTClib
#include "Quickstats.h" // Descriptive statistics for Arduino float arrays https://github.com/dndubins/QuickStats
#include <EasyButton.h> // Arduino library for debouncing momentary contact switches https://easybtn.earias.me/
/****************************************************************************************************************************************************
*
* DEBUG
*
****************************************************************************************************************************************************/
#define DEBUG 1
#define DEBUG_ANALOG 0
#define USE_MD_PAROLA 1
/****************************************************************************************************************************************************
*
* Conditional header file includes - at the end stick with MD Parola
*
****************************************************************************************************************************************************/
#if USE_MD_PAROLA
#include <MD_Parola.h> // Library for modular scrolling LED matrix text displays (https://github.com/MajicDesigns/MD_Parola)
#include <MD_MAX72xx.h> // MAX72xx LED Matrix Display Library (https://github.com/MajicDesigns/MD_MAX72XX)
#include "RetroFont.h" // User defined fonts (below)
#include "Font7Seg.h"
// (https://github.com/Cyb3rn0id/Orologio_Matrice/blob/main/arduino/orologio_matrice/myfont.h)
// (https://wokwi.com/arduino/projects/289186888566178317) (
// https://www.dotnetlovers.com/article/10247/how-to-make-a-digital-clock-using-arduino)
#else
#include <Adafruit_GFX.h> // Adafruit_GFX (https://github.com/adafruit/Adafruit-GFX-Library)
#include <Max72xxPanel.h> // Max72xxPanel.h based onAdafruit_GFX. (https://github.com/markruys/arduino-Max72xxPanel)
#endif
/****************************************************************************************************************************************************
*
* Constants
*
****************************************************************************************************************************************************/
// Wifi
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// NTP Configuration
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 0;
const int daylightOffset_sec = 3600;
/****************************************************************************************************************************************************
*
* External components
*
****************************************************************************************************************************************************/
// MAX7219 LED Matrix 8x32 (https://www.maximintegrated.com/en/products/power/display-power-control/MAX7219.html)
// MAX7219 -> ESP8266 12-E NodeMCU Kit
// Vcc -> 3v
// Gnd -> Gnd
// DIN -> D7 GPIO13 - MOSI
// CS -> D4 GPIO2 - TxD1
// CLK -> D5 GPIO14 - SCLK
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW
#define MAX_DEVICES_HOR 4
#define MAX_DEVICES_VERT 1
#define CLK_PIN 18
#define DATA_PIN 23
#define CS_PIN 5
#define MAX_MSG_SIZE 25
// LED
#define _LED_ON 22
// LDR pin and maximum readings
const int _LDR_PIN = 1;
const int _MAX_LDR_READINGS = 100;
// RTC maximum readings
const int _CLOCK_UPDATE_INTERVAL = 1000;
const int _TEMPERATURE_UPDATE_INTERVAL = 60000;
const int _MAX_TEMP_READINGS = 5;
// Pushbuttons pin. This is not the same for LIVE circuit
const int _SET_BUTTON = 2;
const int _CHANGE_BUTTON = 15;
const long _LONG_PRESS = 1000;
// Global variables
// NTP Client (https://www.ntppool.org/zone/@) and (https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv)
WiFiUDP ntpUDP;
NTPClient ntpTimeClient(ntpUDP,ntpServer);
#if USE_MD_PAROLA
// Hardware SPI (Serial Peripheral Interface) connection
MD_Parola ledDisplay = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES_HOR);
#else
// Max72xxPanel
Max72xxPanel matrix = Max72xxPanel(CS_PIN, MAX_DEVICES_HOR, MAX_DEVICES_VERT);
#endif
// Used by either MD Parola or Max72xxPanel
char currentTimeMsg[MAX_MSG_SIZE + 1] = "";
char alarmTime[MAX_MSG_SIZE + 1] = ""; // Remove this
// Used by either MD Parola or Max72xxPanel
// char bootMessage[] = "Starting Alarm Clock 1.0 ...";
// RTC DS1307
RTC_DS1307 realTimeClock;
uint8_t hours = 7, minutes = 00;
typedef struct {
char weekDay[4];
boolean alarmEnabled;
} weekDayAlarm;
weekDayAlarm alarmByDay[7] = {{"Su",false},{"Mo",true},{"Tu",true},{"We",true},{"Th",true},{"Fr",true},{"Sa",false}} ;
DateTime now;
// Array to store LDR readings for statistical calculation
float ldrReadings[_MAX_LDR_READINGS]; // LDR readings
int intLdrNumReadings = 0; // Counter for readings
// for analog LDR readings conversion (beween 0 and 15)
int mappedLDR = 0;
// Array to store RTC's temperature readings for statistical calculation
float fltCurrentRtcReading = 0;
float rtcReadings[5];
int intRtcNumReadings = 0;
//Button object creation
EasyButton btnSetButton(_SET_BUTTON);
EasyButton btnChangeButton(_CHANGE_BUTTON);
// Menu options
enum DisplayState {
DISPLAY_CLOCK,
SET_HOURS,
SET_MINUTES,
SET_DAYS,
SHOW_TEMP,
};
uint8_t colonSpace[] = { 4, 0, 0, 0, 0 }; // A space with 4 dots, to show in Display Clock the blink effect
uint8_t degC[] = { 6, 3, 3, 56, 68, 68, 68 }; // Degree symbol for temperature in °C
static DisplayState clockState = DISPLAY_CLOCK;
void setup (void) {
Serial.begin(115200);
// pinMode(_LDR_PIN,INPUT); // active in real circuitLDR to control led matrix intensity. Consider to change to another LDR module
// pinMode(_LED_ON,OUTPUT); // active in real circuit
pinMode(_SET_BUTTON, INPUT); // Set button as INPUT
pinMode(_CHANGE_BUTTON, INPUT); // Change button as INPUT
// Initialize buttons
btnSetButton.begin();
btnChangeButton.begin();
// Attach callback.
btnSetButton.onPressedFor(_LONG_PRESS, onPressedForDuration);
// Setup LED Matrix
#if USE_MD_PAROLA
ledDisplay.begin(1);
//ledDisplay.setInvert(false);
ledDisplay.setZone(0, MAX_DEVICES_HOR - 4, MAX_DEVICES_HOR - 1); // has to be like this
ledDisplay.setIntensity(0,0);
ledDisplay.setFont(0,myFont);
ledDisplay.displayZoneText(0,currentTimeMsg, PA_CENTER, 40, 0, PA_SCROLL_LEFT , PA_SCROLL_LEFT);
ledDisplay.displayReset(0);
ledDisplay.addChar('$', colonSpace); // add four dot space to the font definition in place of the '$' char
ledDisplay.addChar('&', degC); // add degree symbol to the font definition in place of the '&' char
#else
setupLedMatrix();
#endif
#if USE_MD_PAROLA
connect2Wifi();
connect2NtpServer();
#else
displayMessage(String("Wifi connecting to ") + String(ssid));
connect2Wifi();
displayMessage(String("Connecting to NTP Server ") + String(ntpServer));
connect2NtpServer();
#endif
// Start and adjust Real Time Clock
if (!realTimeClock.begin()){
// Repeat until Real Time Clock is ON
#if DEBUG
Serial.println("Couldn't find RTC");
Serial.flush();
abort();
while (1); // repeat until is on
#endif
}
syncTime();
}
void loop (void){
delay(10); // this speeds up the simulation
btnSetButton.read(); // read the buttons
btnChangeButton.read();
static uint32_t intLastTimeUpdate = 0; // Last "millis" of updating (ms)
static uint32_t intAlarmFlasher = 0; // Last "millis" of updating (ms)
static uint32_t intLastRtcUpdate = 0; // Last "millis" of updating (ms)
static uint8_t intDayOfWeekCounter = 0; // 0 is for Sunday
static bool flasherCol = false; // Flasher for mid dots
static bool flasherAlarm = false; // Flasher for alarm
ledDisplay.displayAnimate();
switch (clockState) {
case DISPLAY_CLOCK:
if ((millis() - intLastTimeUpdate) >= _CLOCK_UPDATE_INTERVAL/3){
// gets current time from RTC
intLastTimeUpdate = millis();
flasherAlarm = false;
getCurrentTime(currentTimeMsg,flasherCol,flasherAlarm);
flasherCol = !flasherCol;
}
if(btnSetButton.wasPressed()){
Serial.println("Button SET single press -> going to adjust Alarm - hours");
clockState = SET_HOURS;
} else if (btnChangeButton.wasPressed()) {
Serial.println("Button CHANGE single press -> Show temperature");
clockState = SHOW_TEMP;
}
break;
case SET_HOURS:
digitalWrite(_LED_ON, HIGH);
if ((millis() - intAlarmFlasher) >= _CLOCK_UPDATE_INTERVAL / 6){
// gets current time from RTC
intAlarmFlasher = millis();
flasherAlarm = true;
getCurrentTime(currentTimeMsg,flasherCol,flasherAlarm);
flasherCol = !flasherCol;
}
if(btnSetButton.wasPressed()){
Serial.println("Button SET single press -> going to adjust Alarm - minutes");
clockState = SET_MINUTES;
} else if (btnChangeButton.wasPressed()) {
Serial.println("Button CHANGE single press -> hours++");
(hours >= 23) ? (hours = 0) : (++hours);
}
break;
case SET_MINUTES:
if ((millis() - intAlarmFlasher) >= _CLOCK_UPDATE_INTERVAL / 6){
// gets current time from RTC
intAlarmFlasher = millis();
flasherAlarm = true;
getCurrentTime(currentTimeMsg,flasherCol,flasherAlarm);
flasherCol = !flasherCol;
}
if(btnSetButton.wasPressed()){
Serial.println("Button SET single press -> Exit alarm");
clockState = DISPLAY_CLOCK;
digitalWrite(_LED_ON, LOW);
} else if (btnChangeButton.wasPressed()) {
Serial.println("Button CHANGE single press -> minutes++");
(minutes >= 59) ? (minutes = 0) : (++minutes);
}
break;
case SET_DAYS:
if (intDayOfWeekCounter < 7) {
if ((millis() - intLastTimeUpdate) >= _CLOCK_UPDATE_INTERVAL/3){
setWeekdayAlarm(currentTimeMsg,alarmByDay[intDayOfWeekCounter].weekDay, alarmByDay[intDayOfWeekCounter].alarmEnabled,flasherCol);
flasherCol = !flasherCol;
intLastTimeUpdate = millis();
}
if (btnSetButton.wasPressed()){
Serial.print("Previous Day of Week: ");
Serial.println(intDayOfWeekCounter);
++intDayOfWeekCounter;
} else if (btnChangeButton.wasPressed()) {
Serial.println("Button CHANGE single press -> toggle day (alarm on / off)");
alarmByDay[intDayOfWeekCounter].alarmEnabled = !alarmByDay[intDayOfWeekCounter].alarmEnabled;
}
} else {
Serial.println("Button SET single press -> Exiting ""SET DAYS MODE""");
clockState = DISPLAY_CLOCK;
digitalWrite(_LED_ON, LOW);
intDayOfWeekCounter = 0;
}
break;
case SHOW_TEMP:
if ((millis() - intLastTimeUpdate) <= (_CLOCK_UPDATE_INTERVAL + 1500)) {
dtostrf(fltCurrentRtcReading, 3, 1, currentTimeMsg);
strcat(currentTimeMsg, "&");
} else {
// return to display clock
clockState = DISPLAY_CLOCK;
}
break;
}
if (intLdrNumReadings <= _MAX_LDR_READINGS - 1){
ldrReadings[intLdrNumReadings] = map(analogRead(_LDR_PIN),0,1024,0,15); // https://www.arduino.cc/reference/en/language/functions/math/map/
#if DEBUG_ANALOG
Serial.print("Analog reading #");
Serial.print(intLdrNumReadings);
Serial.print(" = ");
Serial.println(ldrReadings[intLdrNumReadings]);
#endif
intLdrNumReadings++;
} else{
mappedLDR = (int)mode(ldrReadings,intLdrNumReadings,0.00001);
#if DEBUG_ANALOG
Serial.println(mappedLDR);
#endif
intLdrNumReadings = 0;
}
if ((millis() - intLastRtcUpdate) >= _TEMPERATURE_UPDATE_INTERVAL && intRtcNumReadings < _MAX_TEMP_READINGS - 1){
// stores 1 RTC temperature reading every minute, calculates the median each 5 minutes
intLastRtcUpdate = millis();
// rtcReadings[intRtcNumReadings] = realTimeClock.getTemperature() -1 ;
#if DEBUG
Serial.print("intRtcNumReadings: ");
Serial.println(intRtcNumReadings);
Serial.print("Temperature: ");
Serial.println(rtcReadings[intRtcNumReadings]);
#endif
intRtcNumReadings++;
} else if ((millis() - intLastRtcUpdate) >= _TEMPERATURE_UPDATE_INTERVAL && intRtcNumReadings == _MAX_TEMP_READINGS - 1) {
// MAX_RTC_READINGS for temperature reached
intLastRtcUpdate = millis();
fltCurrentRtcReading = median(rtcReadings,intRtcNumReadings);
#if DEBUG
Serial.print("intRtcNumReadings: ");
Serial.println(intRtcNumReadings);
Serial.print("Temperature: ");
Serial.println(fltCurrentRtcReading);
#endif
intRtcNumReadings = 0;
}
#if USE_MD_PAROLA
ledDisplay.displayAnimate();
if (ledDisplay.getZoneStatus(0)){
#if DEBUG_ANALOG
Serial.println(mappedLDR);
#endif
ledDisplay.setIntensity(mappedLDR);
ledDisplay.setFont(0,myFont); // probably redundant
ledDisplay.setTextEffect(0,PA_PRINT, PA_NO_EFFECT);
ledDisplay.setPause(0,0);
ledDisplay.displayReset(0);
}
#else
// not using MD Parola here
matrix.setIntensity(mappedLDR); // Use a value between 0 and 15 for brightness
matrix.drawChar(2,0, currentTimeMsg[0], HIGH,LOW,1); // H
matrix.drawChar(8,0, currentTimeMsg[1], HIGH,LOW,1); // HH
matrix.drawChar(14,0,currentTimeMsg[2], HIGH,LOW,1); // HH:
matrix.drawChar(20,0,currentTimeMsg[3], HIGH,LOW,1); // HH:M
matrix.drawChar(26,0,currentTimeMsg[4], HIGH,LOW,1); // HH:MM
matrix.write(); // Send bitmap to display
#endif
}
#if !USE_MD_PAROLA
void setupLedMatrix(){
matrix.setRotation(0, 1); // The first display is position upside down
matrix.setRotation(1, 1); // The first display is position upside down
matrix.setRotation(2, 1); // The first display is position upside down
matrix.setRotation(3, 1); // The first display is position upside down
matrix.fillScreen(LOW);
matrix.write();
}
#endif
#if !USE_MD_PAROLA
void displayMessage(String message){
int wait = 70; // In milliseconds
int spacer = 1;
int width = 5 + spacer; // The font width is 5 pixels
int m;
for ( int i = 0 ; i < width * message.length() + matrix.width() - spacer; i++ ) {
//matrix.fillScreen(LOW);
int letter = i / width;
int x = (matrix.width() - 1) - i % width;
int y = (matrix.height() - 8) / 2; // center the text vertically
while ( x + width - spacer >= 0 && letter >= 0 ) {
if ( letter < message.length() ) {
matrix.drawChar(x, y, message[letter], HIGH, LOW, 1); // HIGH LOW means foreground ON, background off, reverse to invert the image
}
letter--;
x -= width;
}
matrix.write(); // Send bitmap to display
delay(wait/2);
}
}
#endif
void connect2Wifi(){
delay (1000);
#if DEBUG
Serial.print("Connecting to ");
Serial.print(ssid);
Serial.print(" ");
#endif
WiFi.begin(ssid, password);
while ( WiFi.status() != WL_CONNECTED ) {
delay ( 500 );
#if DEBUG
Serial.print ( "." );
#endif
}
#if DEBUG
Serial.println(" -> Connected!");
#endif
}
void disconnectWifi(){
// Disconnect wifi when not needed
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
// WiFi.forceSleepBegin();
}
void connect2NtpServer(){
// https://randomnerdtutorials.com/esp8266-nodemcu-date-time-ntp-client-server-arduino/
// Initialize NTP Client to get current time
ntpTimeClient.begin();
ntpTimeClient.setTimeOffset(gmtOffset_sec);
ntpTimeClient.update();
// 3 lines below seem redundant
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
setenv("TZ","WET0WEST,M3.5.0/1,M10.5.0",1);
tzset();
#if DEBUG
Serial.print("Time updated from NTP server. Current time: ");
Serial.println(ntpTimeClient.getFormattedTime());
#endif
}
void syncTime(){
unsigned long epochTime = ntpTimeClient.getEpochTime();
// Adjust Real Time Clock to epochTime
realTimeClock.adjust(DateTime(epochTime));
now = realTimeClock.now();
#if DEBUG
Serial.print("Current RTC DS1307 timestamp: ");
Serial.print(dow2str(now.dayOfTheWeek(), currentTimeMsg, MAX_MSG_SIZE));
Serial.print(", ");
Serial.print(now.day());
Serial.print(", ");
Serial.print(now.hour());
Serial.print(":");
Serial.println(now.minute());
#endif
}
// Code for reading clock time from based in https://github.com/MajicDesigns/MD_Parola/blob/main/examples/Parola_Zone_TimeMsg/Parola_Zone_TimeMsg.ino
void getCurrentTime(char *psz, bool f, bool alarm){
// this function is doing 2 things at the same
if (!alarm){
now = realTimeClock.now();
sprintf(psz, "%02d%c%02d", now.hour(), (f ? ':' : '$'), now.minute()); // it is not a $ sign, it is a colon space
} else {
switch (clockState) {
case SET_HOURS:
if (f){
sprintf(psz, "%02d%c%02d", hours,':',minutes);
} else {
sprintf(psz, "%c%c%c%02d",' ',' ',':',minutes);
}
break;
case SET_MINUTES:
if (f){
sprintf(psz, "%02d%c%02d", hours,':',minutes);
} else {
sprintf(psz, "%02d%c%c%c", hours,':',' ',' ');
}
break;
}
}
}
// Code for reading clock time from based in https://github.com/MajicDesigns/MD_Parola/blob/main/examples/Parola_Zone_TimeMsg/Parola_Zone_TimeMsg.ino
void setWeekdayAlarm(char *psz, char weekDay[], boolean alarmEnabled,bool flasher){
if (flasher){
sprintf(psz,"%s%s%s", weekDay, ":", (alarmEnabled? "on" : "off" ));
} else {
// comment the following line
sprintf(psz,"%s%s%c%c%c", weekDay, ":", (alarmEnabled? '$' : '$' ),(alarmEnabled? '$' : '$' ),(alarmEnabled? '\0': '$' ));
}
}
// This function converts a numeric value for week day to a text value
char *dow2str(uint8_t code, char *psz, uint8_t len) {
static const __FlashStringHelper* str[] =
{
F("Sunday"), F("Monday"), F("Tuesday"),
F("Wednesday"), F("Thursday"), F("Friday"),
F("Saturday")
};
strncpy_P(psz, (const char PROGMEM *)str[code], len);
psz[len] = '\0';
return (psz);
}
// Callback for long press.
void onPressedForDuration() {
Serial.println("Button has been pressed for the given duration!");
digitalWrite(_LED_ON, LOW);
// will only do something if current state is "DISPLAY_CLOCK"
clockState = SET_DAYS;
}