/*----------------------------------------------
* R2D2 Clock
*
* Board: Node32s
*
* 2023-06-12 V5 jlb
* Added Alarm functionality
* Added Time format functionality
* Added code to flash colon every second
*--------------------------------------------*/
#include "WiFiManager.h" //https://github.com/tzapu/WiFiManager WiFi Configuration Magic
#include "ezTime.h" // Time Library - get your POSIX Timezone 'string' to replace TZ/GMT if needed, below, here: https://support.cyberdata.net/index.php?/Knowledgebase/Article/View/438/10/posix-timezone-strings
#include "SevenSegmentExtended.h"
#include "SevenSegmentTM1637.h"
#include "SevenSegmentFun.h" // For Text readouts
#include "EspSoftwareSerial.h"
#include "DFMiniMp3.h"
#include <EEPROM.h>
//========================USEFUL VARIABLES=============================
const char* ssid = "PUT_CLOCKS_OWN_WIFI_NAME_HERE"; // R2D2 Access point network name (to connect to R2D2, to change/add your WiFi details)
const char* password = "SET_A_PASSWORD"; // R2D2 Access point password
uint16_t notification_volume = 25; // Speaker volume
int Display_backlight = 45; // 4 Digit Display brightness when operating normally (outside of initialisation)
int BlueLED_brightness = 25; // Two blue LED's (when connected to ESP32 pin 14, instead of VCC)
const char* ntpServer = "pool.ntp.org"; // Your chosen TimeServer
//const String timezone_posix_string = "BST0GMT,M3.2.0/2:00:00,M11.1.0/2:00:00"; // Insert your own POSIX Timezone string here. See link above for info.
const String timezone_posix_string = "AEST-9AEDT,M3.2.0/2:00:00,M11.1.0/2:00:00"; //Sydney, Australia.
//=====================================================================
// Avoid changing below here...
// Call WiFi Manager
WiFiManager wifiManager;
// Call a Timezone
Timezone TZ;
#define CLK 25
#define DT 26
#define SW 27
#define RED_LED 17
#define WHITE_LED 16
#define BLUE_LED 14 // 14 corresponds to GPIO14
enum SETUP { SHOW_TIME, TIMER, TIME_FORMAT, ALARM_ONOFF, ALARM_HOUR, ALARM_MIN };
SETUP setupMode = SHOW_TIME;
enum SHOW {HOURS,MINUTES};
//Uncomment to reset EEPROM data
//#define RESET_EEPROM
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0DAD0BAD
typedef struct {
uint32_t magic;
bool timeFormat;
bool alarmOn;
int8_t alarmHour;
int8_t alarmMinute;
} EEPROM_DATA;
EEPROM_DATA EepromData; //Current EEPROM settings
bool set24 = false; //Time format being set
bool setAO = false; //Alarm on/off being set
int8_t setAH = 0; //Alarm Hour being set
int8_t setAM = 0; //Alarm Minute being set
bool alarmCancelled = false; //Set true if user cancelled alarm
bool colon = true; //Colon to flash every second
float counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir = "";
unsigned long lastButtonPress = 0;
// timer
int timer_secs = 0;
int timer_mins = 0;
float inc_red_led = 0;
// the number of the Blue LED's pin
const int BlueChannel = 1;
// setting PWM properties
const int freq = 5000;
const int resolution = 8;
class Mp3Notify
{
public:
static void PrintlnSourceAction(DfMp3_PlaySources source, const char* action)
{
if (source & DfMp3_PlaySources_Sd)
{
Serial.print("SD Card, ");
}
if (source & DfMp3_PlaySources_Usb)
{
Serial.print("USB Disk, ");
}
if (source & DfMp3_PlaySources_Flash)
{
Serial.print("Flash, ");
}
Serial.println(action);
}
static void OnError(uint16_t errorCode)
{
// see DfMp3_Error for code meaning
Serial.println();
Serial.print("Com Error ");
Serial.println(errorCode);
}
static void OnPlayFinished(DfMp3_PlaySources source, uint16_t track)
{
Serial.print("Play finished for #");
Serial.println(track);
}
static void OnPlaySourceOnline(DfMp3_PlaySources source)
{
PrintlnSourceAction(source, "online");
}
static void OnPlaySourceInserted(DfMp3_PlaySources source)
{
PrintlnSourceAction(source, "inserted");
}
static void OnPlaySourceRemoved(DfMp3_PlaySources source)
{
PrintlnSourceAction(source, "removed");
}
};
SevenSegmentExtended blue1(21, 22); // CLK, DIO
SevenSegmentFun words(21, 22); // CLK, DIO
SoftwareSerial secondarySerial(18, 19); // TX, RX
DFMiniMp3<SoftwareSerial, Mp3Notify> mp3(secondarySerial);
void configModeCallback (WiFiManager *myWiFiManager)
{
Serial.println("Entered WiFi Manager config mode..");
Serial.println(WiFi.softAPIP());
Serial.println(myWiFiManager->getConfigPortalSSID());
}
//flag for saving data
bool shouldSaveConfig = false;
//callback notifying us of the need to save config
void saveConfigCallback ()
{
Serial.println("Should save config");
shouldSaveConfig = true;
}
void setup()
{
words.setBacklight(80);
words.begin();
words.scrollingText("R2D2 START", 2);
pinMode(CLK, INPUT);
pinMode(DT, INPUT);
pinMode(SW, INPUT_PULLUP);
pinMode(RED_LED, OUTPUT);
pinMode(WHITE_LED, OUTPUT);
pinMode(BLUE_LED, OUTPUT);
// configure LED PWM functionalities
ledcSetup(0, 5000, 8);
// attach the channel to the GPIO to be controlled
ledcAttachPin(RED_LED, 0);
Serial.begin(115200);
//set custom ip for portal
wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 2, 1), IPAddress(192, 168, 2, 1), IPAddress(255, 255, 255, 0));
WiFiManagerParameter custom_text("<p>Welcome to R2D2: Please connect to WiFi using this portal</p>");
wifiManager.addParameter(&custom_text);
wifiManager.setAPCallback(configModeCallback);
//first parameter is name of access point, second is the password
wifiManager.autoConnect(ssid, password);
words.scrollingText("CONNECT TO R2D2 WIFI", 4);
wifiManager.setSaveConfigCallback(saveConfigCallback);
wifiManager.setConfigPortalTimeout(600); // Hold for 10 mins if unable to connect.
blue1.setBacklight(Display_backlight);
blue1.begin();
mp3.begin();
mp3.setVolume(notification_volume);
uint16_t count = mp3.getTotalTrackCount(DfMp3_PlaySource_Sd);
Serial.print("files ");
Serial.println(count);
ledcAttachPin(BLUE_LED, BlueChannel);
ledcWrite(BlueChannel, 200);
Serial.print("IP assigned by DHCP is ");
Serial.println(WiFi.localIP());
ledcWrite(BlueChannel, 0);
delay (2000);
ledcWrite(BlueChannel, 200);
Serial.println("\n Should be connected: Waiting for time sync");
waitForSync();
// Read the initial state of CLK
lastStateCLK = digitalRead(CLK);
TZ.setPosix(timezone_posix_string);
Serial.println("UTC: " + UTC.dateTime());
Serial.println("Local Time: " + TZ.dateTime("G") + ":" + TZ.dateTime("i"));
ledcWrite(BlueChannel, BlueLED_brightness);
//Read the alarm status
EEPROM.begin(sizeof(EEPROM_DATA));
readEepromData();
showTime();
}
//----------------------------------------------------------------------
// Main program loop
//----------------------------------------------------------------------
void loop()
{
if (minuteChanged())
{
colon = !colon;
showTime();
//Test if alarm gone off
Serial.println("Time " + String(TZ.hour()) + ":" + String(TZ.minute()));
Serial.println("alarmOn=" + String(EepromData.alarmOn) + ", alarmCancelled=" + String(alarmCancelled));
Serial.println("alarmTime=" + String(EepromData.alarmHour) + ":" + String(EepromData.alarmMinute));
if (alarmCancelled && (TZ.hour() != EepromData.alarmHour || TZ.minute() != EepromData.alarmMinute))
{
alarmCancelled = false;
}
else if (EepromData.alarmOn && !alarmCancelled && TZ.hour() == EepromData.alarmHour && TZ.minute() == EepromData.alarmMinute)
{
Serial.println("*** ALARM ON ****");
turnOnAlarm();
alarmCancelled = true;
}
}
else if (secondChanged())
{
colon = !colon;
showTime();
}
ledcWrite(0, inc_red_led);
digitalWrite(WHITE_LED, LOW);
//If we detect LOW signal, button is pressed
if (digitalRead(SW) == LOW)
{
//if 50ms have passed since last LOW pulse, it means that the
//button has been pressed, released and pressed again
if (millis() - lastButtonPress > 50)
{
Serial.println("Timer Button pressed!");
bool repeatLoop = true;
while (repeatLoop)
{
setupMode = (setupMode == ALARM_MIN) ? SHOW_TIME : (SETUP)((int)setupMode + 1);
switch (setupMode)
{
case SHOW_TIME:
showTime();
repeatLoop = false;
break;
case TIMER:
repeatLoop = setupTimer();
if (!repeatLoop)
{
setupMode = SHOW_TIME;
showTime();
}
break;
case TIME_FORMAT:
//Store settings so we can detect if they changed
set24 = EepromData.timeFormat;
setAO = EepromData.alarmOn;
setAH = EepromData.alarmHour;
setAM = EepromData.alarmMinute;
setTimeFormat();
break;
case ALARM_ONOFF:
setAlarmState(); break;
break;
case ALARM_HOUR:
setAlarmHour();
break;
case ALARM_MIN:
setAlarmMinute();
//Test if alarm status has changed
if (setAO != EepromData.alarmOn || setAH != EepromData.alarmHour || setAM != EepromData.alarmMinute || set24 != EepromData.timeFormat)
{
//Write changes back to EEPROM
EepromData.timeFormat = set24;
EepromData.alarmOn = setAO;
EepromData.alarmHour = setAH;
EepromData.alarmMinute = setAM;
writeEepromData();
}
break;
}
}
}
// Remember last button press event
lastButtonPress = millis();
}
inc_red_led = (inc_red_led <= 254) ? inc_red_led + 0.1 : 0;
delay(1);
}
//----------------------------------------------------------------------
// Display the current time
// 24hr format has leading zeros, 12hr format doesn't
//----------------------------------------------------------------------
void showTime()
{
if (EepromData.timeFormat)
{
showPartialTime(HOURS, TZ.dateTime("H").toInt(), true, true, colon);
}
else
{
showPartialTime(HOURS, TZ.dateTime("g").toInt(), false, true, colon);
}
showPartialTime(MINUTES, TZ.dateTime("i").toInt(), true, true, colon);
}
//---------------------------------------------------------------------
// Write two time digits to display
// s - SHOW constant
// num - (0 to 99)
// leadingZeros - true to have leading zeros
// on - true to show digit, false to show blank
// c - true to show colon
//---------------------------------------------------------------------
void showPartialTime(SHOW s, int num, bool leadingZeros, bool on, bool c)
{
num = max(min(num, 99), 0);
int b = (s == HOURS) ? 0 : 2;
for (int i = 0; i < 2; i++)
{
if (on && (num > 0 || i == 0 || leadingZeros))
{
blue1.setColonOn(c);
blue1.printRaw(blue1.encode((int16_t)(num % 10)), b + 1-i);
}
else
{
blue1.printRaw((uint8_t)TM1637_CHAR_SPACE, b + 1-i);
}
num = num / 10;
}
}
//----------------------------------------------------------------------
// Show/Change the time format
// - Rotary Encoder changes alarm state
//----------------------------------------------------------------------
void setTimeFormat()
{
Serial.println("In time format!");
showTimeFormat();
digitalWrite(RED_LED, HIGH);
digitalWrite(WHITE_LED, HIGH);
mp3.playMp3FolderTrack(2);
delay(500);
while (digitalRead(SW) == HIGH)
{
switch (readRotaryEncoder())
{
case 1: set24 = true; showTimeFormat(); break;
case -1: set24 = false; showTimeFormat(); break;
default: break;
}
delay(1);
}
Serial.println("Exit time format!");
}
//----------------------------------------------------------------------
// Show the current time format
//----------------------------------------------------------------------
void showTimeFormat()
{
blue1.printRaw(TM1637_CHAR_T, 0);
blue1.printRaw(TM1637_CHAR_f, 1);
if (set24)
{
blue1.printRaw(TM1637_CHAR_2, 2);
blue1.printRaw(TM1637_CHAR_4, 3);
}
else
{
blue1.printRaw(TM1637_CHAR_1, 2);
blue1.printRaw(TM1637_CHAR_2, 3);
}
}
//----------------------------------------------------------------------
// Show/Change alarm state
// - Rotary Encoder changes alarm state
//----------------------------------------------------------------------
void setAlarmState()
{
Serial.println("In alarm state!");
showAlarmState();
digitalWrite(RED_LED, HIGH);
digitalWrite(WHITE_LED, HIGH);
mp3.playMp3FolderTrack(2);
delay(500);
while (digitalRead(SW) == HIGH)
{
switch (readRotaryEncoder())
{
case 1: setAO = true; showAlarmState(); break;
case -1: setAO = false; showAlarmState(); break;
default: break;
}
delay(1);
}
Serial.println("Exit alarm State!");
}
//----------------------------------------------------------------------
// Show the current alarm state
//----------------------------------------------------------------------
void showAlarmState()
{
blue1.printRaw(TM1637_CHAR_A, 0);
blue1.printRaw((uint8_t)TM1637_CHAR_SPACE, 1);
blue1.printRaw(TM1637_CHAR_o, 2);
blue1.printRaw((setAO) ? TM1637_CHAR_n : TM1637_CHAR_f, 3);
}
//----------------------------------------------------------------------
// Show/Change alarm hours
//----------------------------------------------------------------------
void setAlarmHour()
{
Serial.println("In alarm hour!");
showAlarmHour();
digitalWrite(RED_LED, HIGH);
digitalWrite(WHITE_LED, HIGH);
mp3.playMp3FolderTrack(2);
delay(500);
while (digitalRead(SW) == HIGH)
{
switch (readRotaryEncoder())
{
case 1: setAH = (setAH + 1) % 24; showAlarmHour(); break;
case -1: setAH = (setAH + 23) % 24; showAlarmHour(); break;
default: break;
}
delay(1);
}
Serial.println("Exit alarm hour!");
}
//----------------------------------------------------------------------
// Show the current alarm hour
//----------------------------------------------------------------------
void showAlarmHour()
{
uint8_t buffer[2];
blue1.printRaw(TM1637_CHAR_A, 0);
blue1.printRaw(TM1637_CHAR_h, 1);
buffer[0] = blue1.encode(int16_t(setAH / 10));
buffer[1] = blue1.encode(int16_t(setAH % 10));
blue1.printRaw(buffer, 2, 2);
}
//----------------------------------------------------------------------
// Clock is in ALARM_MIN mode
//----------------------------------------------------------------------
void setAlarmMinute()
{
Serial.println("In alarm minute!");
showAlarmMinute();
digitalWrite(RED_LED, HIGH);
digitalWrite(WHITE_LED, HIGH);
mp3.playMp3FolderTrack(2);
delay(500);
while (digitalRead(SW) == HIGH)
{
switch (readRotaryEncoder())
{
case 1: setAM = (setAM + 1) % 60; showAlarmMinute(); break;
case -1: setAM = (setAM + 59) % 60; showAlarmMinute(); break;
default: break;
}
delay(1);
}
Serial.println("Exit alarm minute!");
}
//----------------------------------------------------------------------
// Show the current alarm hour
//----------------------------------------------------------------------
void showAlarmMinute()
{
uint8_t buffer[2];
blue1.printRaw(TM1637_CHAR_A, 0);
blue1.printRaw(TM1637_CHAR_n, 1);
buffer[0] = blue1.encode(int16_t(setAM / 10));
buffer[1] = blue1.encode(int16_t(setAM % 10));
blue1.printRaw(buffer, 2, 2);
}
//---------------------------------------------------------------
// Sound the alarm
// This will play for one minute or until the rotary switch is pressed
//---------------------------------------------------------------
void turnOnAlarm()
{
int counter = 0;
while (digitalRead(SW) == HIGH && !minuteChanged())
{
if (counter < 1)
{
playFolderTrackWithLights(1);
}
else if (counter < 5)
{
playFolderTrackWithLights(2);
}
else if (counter < 7)
{
playFolderTrackWithLights(4);
}
else if (counter < 8)
{
playFolderTrackWithLights(3);
}
else
{
playFolderTrackWithLights(5);
playFolderTrackWithLights(7);
}
counter++;
waitMilliseconds(200);
}
mp3.stop();
digitalWrite(WHITE_LED, LOW);
while (digitalRead(SW) == LOW)
{
//Wait until button released so it doesn't trigger setup
}
}
//----------------------------------------------------------------------
// Handle timer
// - Rotary Encoder changes seconds
// - Switch starts timer or changes mode if timer is at zero
// Returns true if timer not set
//----------------------------------------------------------------------
bool setupTimer()
{
Serial.println("In Timer State!");
blue1.printTime(88, 88, false);
digitalWrite(RED_LED, HIGH);
digitalWrite(WHITE_LED, HIGH);
mp3.playMp3FolderTrack(2);
delay(500);
while (digitalRead(SW) == HIGH)
{
counter += float(readRotaryEncoder() * 10);
if (counter < 0)
{
counter = 0;
}
timer_mins = counter / 60;
timer_secs = ((counter / 60) - timer_mins) * 60;
blue1.printTime(timer_mins, timer_secs, false);
delay(1);
}
bool timerNotUsed = (counter == 0);
if (counter > 0)
{
Countdown(counter);
}
Serial.println("Exit Timer State!");
return timerNotUsed;
}
//----------------------------------------------------------------------
// Handle timer
// - Countdown the time and play sound when done
//----------------------------------------------------------------------
void Countdown (float timer_counter)
{
mp3.playMp3FolderTrack(8);
delay(1000);
while (digitalRead(SW) == HIGH)
{
for (int i = 10 ; i > 0; i--)
{
timer_counter = timer_counter - 0.1;
timer_mins = timer_counter / 60;
timer_secs = ((timer_counter / 60) - timer_mins) * 60;
blue1.printTime(timer_mins, timer_secs, false);
delay(100);
digitalWrite(RED_LED, LOW);
}
if (timer_counter <= 0)
{
playFolderTrackWithLights(3);
playFolderTrackWithLights(5);
playFolderTrackWithLights(7);
break;
};
}
while (digitalRead(SW) == LOW)
{
//Wait until button released so it doesn't trigger setup
}
counter = 0;
}
//----------------------------------------------------------------------
// Play a MP3 track and flash the white light
//----------------------------------------------------------------------
void playFolderTrackWithLights(int track)
{
mp3.playMp3FolderTrack(track);
for ( int i = 0 ; i < 9 ; i++)
{
ledcWrite(0, 255);
waitMilliseconds(random(10, 150));
digitalWrite(WHITE_LED, HIGH);
waitMilliseconds(random(10, 150));
ledcWrite(0, 0);
waitMilliseconds(random(10, 150));
digitalWrite(WHITE_LED, LOW);
waitMilliseconds(random(10, 150));
}
}
//----------------------------------------------------------------------
// Read the rotary encoder
// - Returns 0, -1 or 1
//----------------------------------------------------------------------
int readRotaryEncoder()
{
int result = 0;
// Read the current state of CLK
currentStateCLK = digitalRead(CLK);
// If last and current state of CLK are different, then pulse occurred
// React to only 1 state change to avoid double count
if (currentStateCLK != lastStateCLK && currentStateCLK == 1)
{
// If the DT state is different than the CLK state then
// the encoder is rotating CCW so decrement
if (digitalRead(DT) != currentStateCLK)
{
result = -1;
currentDir = "CCW";
}
else
{
// Encoder is rotating CW so increment
result = 1;
currentDir = "CW";
}
Serial.print("Direction: ");
Serial.print(currentDir);
Serial.print(" | Counter: ");
Serial.println(counter);
}
// Remember last CLK state
lastStateCLK = currentStateCLK;
return result;
}
//----------------------------------------------------------------------
// Wait a number of milliseconds while updating MP3 player
// msWait - time to wait in mS
//----------------------------------------------------------------------
void waitMilliseconds(uint16_t msWait)
{
uint32_t start = millis();
while ((millis() - start) < msWait && digitalRead(SW) == HIGH)
{
// calling mp3.loop() periodically allows for notifications
// to be handled without interrupts
mp3.loop();
delay(1);
}
}
//---------------------------------------------------------------
// Write the EepromData structure to EEPROM
//---------------------------------------------------------------
void writeEepromData()
{
//This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
EEPROM.put(EEPROM_ADDRESS,EepromData);
EEPROM.commit();
}
//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
//---------------------------------------------------------------
void readEepromData()
{
//Eprom
EEPROM.get(EEPROM_ADDRESS,EepromData);
#ifndef RESET_EEPROM
if (EepromData.magic != EEPROM_MAGIC)
#endif
{
EepromData.magic = EEPROM_MAGIC;
EepromData.timeFormat = false;
EepromData.alarmOn = false;
EepromData.alarmHour = 6;
EepromData.alarmMinute = 0;
writeEepromData();
EEPROM.commit();
}
}
Comments