/***************************************************
Analog and Digital Clock displaying time, date, dow 480 X 320
Uses Adafruit RTC 3231 real time clock, Adafruit 3.5" 320 x 480 TFT display in SPI mode HXD8357D PID: 2050 and
Rotary Encoder + Extras PID: 377 and Adafruit I2C QT Rotary Encoder with NeoPixel PID: 4991
Processor is Arduino Nano
June 3 2022 - Add non-colliding second hand
June 6 2022 - Lengthen 'tick' marks
****************************************************/
#include "Adafruit_GFX.h" // graphics libary to fonts, letters, shapes and lines
#include "Adafruit_HX8357.h" // hardware specific TFT library
#include <TimeLib.h> // date, time manipulation and structure
#include <RTClib.h> // real-time clock library
#include "Adafruit_seesaw.h" // used by rotary encoder i2c module
#define SS_SWITCH 24 // used to set up virtual interrupt pin - not a real GPIO
#define SEESAW_ADDR 0x36 // i2c address of encoder
Adafruit_seesaw ss;
int32_t encoder_position;
int encoder_delta = 0;
int encoder_base = 0;
int32_t new_position;
// These are 'flexible' lines that can be changed - Works on Nano
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST -1 // RST can be set to -1 if you tie it to Arduino's reset
// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC, TFT_RST);
// Arduino Nano
// SPI Mosi = 11, MISO = 12, Clock/SCK = 13
// SDA = A4 and SCL = A5
int MyYear; // Usual suspects for date and time
int MyMonth; //
int MyDay;
int MyHour; //
int MyMinute;
int MySecond;
int MyWeekDay;
const String DaysOfTheWeek[7]= {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; // text for days of the week
const int DOW_Pad[7] = {30,30,18,0,12,30,12}; // not an elegant way to do this, but centers each dow on the display
const String Months[12] = {"-Jan-", "-Feb-", "-Mar-", "-Apr-", "-May-", "-Jun-", "-Jul-", "-Aug-", "-Sep-", "-Oct-", "-Nov-", "-Dec-"}; // for dd-mon-yyyy display
#define sunday 0 // used for the daylight savings time routine
#define monday 1 //
#define tuesday 2 //
#define wednesday 3 //
#define thursday 4 // and instead we use the RTC3231 time and date for the DST logic, and must use 0-6
#define friday 5 // to match its 0-6 week for returned day of week
#define saturday 6
#define january 1 // months and days are used by DST routine among others
#define february 2
#define march 3
#define april 4
#define may 5
#define june 6
#define july 7
#define august 8
#define september 9
#define october 10
#define november 11
#define december 12
#define Screen_V 480
#define Screen_H 320
#define Circle_R Screen_H/2
#define cradians 57.29577951 // constant conversion from degrees to radians
#define dpm = 6; // constant conversion from minutes to degrees angle (e.g., 6 degrees*60 minutes or seconds = 360 degrees)
#define second_length 140 // Circle_R - 20 Length of each hand (though seconds is a 'dot')
#define minute_length 125 // Circle_R - 40
#define hour_length 80 // Circle_R - 80
#define tick_length 8 // pixels in from circle for 5 minute tick marks
float Last_sdangle; float sdangle; float hdangle; float mdangle; // hand 'angles' for second, hour and minute
int XS1; int YS1; int XM1; int YM1; int XH1; int YH1; // persistent second, minute and hour hand coordinates
int Persistent_Second = -1; // persistent second, etc., to eliminate un-needed updates
int Persistent_Day= -1;
int Persistent_Minute = -1;
int Persistent_Hour = -1;
String Persistent_HM_Display = "88:88"; //
RTC_DS3231 rtc; // structure for rtc DS3231 clock
unsigned long t_unix_date; // unix date for time adjustments
#define onehour 3600 // number of seconds in one hour for DST adjustments
bool Need_Setup = false; // assume clock is set, set 'true' when battery dead/lost power
/* debugging tools named after Digital Equipment Corporation's DDT (dynamic debugging technique)
where the mini-computer was invented and where I worked for several years
commented out to save space on the Nano (its up to 86% full!)
void DDTv(String st, int vt) { // Print descriptor and value
Serial.print(" ");
Serial.print(st);
Serial.print(" ");
Serial.print(vt);}
void DDTvl(String st, int vt) { // Print descriptor and value and new line
DDTv(st, vt);
Serial.println(" ");}
void DDTs(String st) { // Print string
Serial.print(st);}
void DDTsl(String st) { // Print string and new line
Serial.println(st);}
void DDTss(String st, String vt) { // Print descriptor and string value
Serial.print(st);
Serial.print(vt);}
void DDTssl(String st, String vt) { // Print descriptor and string value and new line
Serial.print(st);
Serial.println(vt);}
void DDTfl(String st, float fi) { // Print descriptor and floating point value and new line
Serial.print(" ");
Serial.print(st);
Serial.print(" ");
Serial.println(fi, 4);}
*/
void setup() {
Serial.begin(9600);
tft.begin(); // start display
tft.setRotation(0);
tft.setCursor(0, 0);
tft.setTextColor(HX8357_WHITE);
tft.setTextSize(3);
Serial.println("Starting Setup");
Serial.println("Looking for seesaw!"); // see-saw is adafruit library used for encoder
if (! ss.begin(SEESAW_ADDR) ) {
Serial.println("Couldn't find seesaw on default address");
while(1) delay(10);
}
Serial.println("seesaw started");
uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF);
if (version != 4991){
Serial.print("Wrong firmware loaded? ");
Serial.println(version);
while(1) delay(10);
}
Serial.println("Rotary Encoder Started");
// use a pin for the virtual encoder switch
ss.pinMode(SS_SWITCH, INPUT_PULLUP);
// get starting position
encoder_position = ss.getEncoderPosition(); // initialize encoder
Serial.println("Turning on interrupts");
delay(10);
ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1);
ss.enableEncoderInterrupt();
tft.fillScreen(HX8357_BLACK);
tft.println("Starting");
tft.println("Clock");
delay(1000);
if (! rtc.begin()) { // check that clock is there
Serial.println("No Clock");
tft.println("No Clock");
tft.println("Found??");
while (true) { // halt if no clock
delay(500);}
} else Serial.println("RTC Started");
if (rtc.lostPower()) { // if battery dead,
tft.println(" ");
tft.println("Clock");
tft.println("Battery Dead");
tft.println("& Lost Power");
tft.println(" ");
tft.println("Please Set");
tft.println("Date & Time");
Serial.println("RTC Battery Dead");
Need_Setup = true;
delay(5000); } // Then get time using rotary encoder
} // end of setup
void loop(void) {
if (!ss.digitalRead(SS_SWITCH) || Need_Setup) {Serial.println("Button Pushed");
Set_DateTime();} // If rotary encoder shaft (button) pressed, get set up
Load_DateTime(); // else, read the clock and
if (MySecond != Persistent_Second){ // if its the same second, do nothing
Set_Clock_Hands(); // else, set clock hands
Set_Digital_Display(); // set digital display
Persistent_Second = MySecond; } // and update persistent second
} // end of "loop"
void Load_DateTime(){ // routine to get time from the real time clock
DateTime now = rtc.now(); //do an initial load time from the RTC
MyYear= now.year();
MyMonth = now.month();
MyDay = now.day();
MyHour = now.hour();
MyMinute= now.minute();
MySecond = now.second();
MyWeekDay= now.dayOfTheWeek(); // RTC3231 returns 0-6 for day of week, not like unixdate return
if (DSTInEffect()){ // if we are in DST then
DateTime unow = rtc.now(); // Reload from RTC
t_unix_date = unow.unixtime()+onehour; //
MyYear = year(t_unix_date);
MyMonth = month(t_unix_date); // bump forward one hour
MyDay = day(t_unix_date);
MyHour = hour(t_unix_date); // really only want to bump hour plus one,
MyMinute = minute(t_unix_date);
MySecond = second(t_unix_date); // but that could trigger day, date cascade
MyWeekDay= (weekday(t_unix_date))-1;} // time.h library weekday returns a 1-7, not 0-6 so emulate RTC by subtracting one
/*
Serial.print("Today's Date and Time: "); // uncomment to display what you have
Serial.print(" ");
Serial.print(MyWeekDay);
Serial.print(" ");
Serial.print(MyMonth);
Serial.print("/");
Serial.print(MyDay);
Serial.print("/");
Serial.print(MyYear);
Serial.print(" ");
Serial.print(MyHour);
Serial.print(":");
if (MyMinute < 10) {Serial.print("0");}
Serial.print(MyMinute);
Serial.print(":");
if (MySecond < 10) {Serial.print("0");}
Serial.print(MySecond);
Serial.print(" ");
if (DSTInEffect()) {Serial.println("DST On");
} else {Serial.println("DST Off");}
*/
} // end of Load_DateTime
void Get_DateTime(){ // Gets the date and time using rotary encoder
// stepping through each of year, month, day, hour and minute (seconds are set to zero)
int MyDayUL = 31; // "usual" days in Month
encoder_base = 0;
ss.setEncoderPosition(0); // reset encoder position with button press
tft.fillScreen(HX8357_BLACK); // clear the screen
tft.setTextColor(HX8357_GREEN);
tft.setTextSize(4);
tft.setCursor(50, 350);
tft.println("Setup Mode");
delay(1000);
MyYear = 2022; // current year
MyYear = Get_Input("Year",MyYear,2022,2222); // Input, label, starting point, lower and upper bound
MyMonth = 1; // January,
MyMonth = Get_Input("Month",MyMonth,1,12);
MyDay = 1;
if ( (MyMonth == april) || (MyMonth == june) || (MyMonth == september) || (MyMonth == september) || (MyMonth == november) ) MyDayUL = 31;
if (MyMonth == february) MyDayUL = 28; // February but not Leap Year
if ( (MyMonth == february) && (MyYear%4 == 0) ) MyDayUL = 29; // Leap Year, and yes this is wrong for century years
MyDay = Get_Input("Day",MyDay,1,MyDayUL);
MyHour = 1; // starting point
MyHour = Get_Input("Hour",MyHour,0,23);
MyMinute = 1; // again start in the middle
MyMinute = Get_Input("Minute",MyMinute,0,59);
MySecond = 0; // when minute set, set seconds to zero
tft.fillScreen(HX8357_BLACK); // clear the screen
tft.setTextColor(HX8357_GREEN);
tft.setTextSize(4);
tft.setCursor(50, 350);
Need_Setup = false; // don't need set up any more
tft.println("Done!");
} // end of Get_DateTime
int Get_Input(String GLabel, int GStart, int GLower, int GUpper){ // routine to get all data input
Serial.println("Get Date Time Called");
tft.fillScreen(HX8357_BLACK); // clear the screen
tft.setTextColor(HX8357_GREEN);
tft.setTextSize(5);
tft.setCursor(50, 350);
tft.println(GLabel); // identify which item we are getting
tft.setTextColor(HX8357_YELLOW);
tft.setTextSize(4);
tft.setCursor(50, 400);
tft.println(GStart); // and initial value
while (true){ // infinite loop - 'return' is the exit
if (! ss.digitalRead(SS_SWITCH)) {return GStart; } // return updated value when button pushed again
new_position = (ss.getEncoderPosition()*-1); // encoder is counter-intuitive in +/- so adjust
if (encoder_position != new_position) {
encoder_delta = new_position - encoder_position;
encoder_base = encoder_base - encoder_delta;
// Serial.print("The change is ");
// Serial.println(encoder_delta);
tft.setTextColor(HX8357_BLACK); // black out (erase) old value
tft.setCursor(50, 400);
tft.println(GStart); // apply rotary encoder delta (-1 or +1) to base
GStart = GStart + encoder_delta;
if (GStart < GLower) GStart = GUpper; // ensure its between bounds
if (GStart > GUpper) GStart = GLower;
tft.setCursor(50, 400);
tft.setTextColor(HX8357_YELLOW);
tft.println(GStart); // and print new value
encoder_position = new_position; } // and save for next round
}
} // end of Get_Input
void Set_DateTime(){
Serial.println("Set Date Time Called");
Get_DateTime(); // either
rtc.adjust(DateTime(MyYear, MyMonth, MyDay, MyHour, MyMinute, MySecond)); // and set RTC
DateTime now = rtc.now(); //do an initial load time from the RTC for the DST checker
MyYear= now.year();
MyMonth = now.month();
MyDay = now.day();
MyHour = now.hour();
MyMinute= now.minute();
MySecond = now.second();
MyWeekDay= now.dayOfTheWeek();
if (DSTInEffect()) { // we adjust +1 hour in display during DST, so
DateTime unow = rtc.now(); // Reload from RTC
t_unix_date = unow.unixtime()-onehour; // and adjust BACK to std time
rtc.adjust(DateTime(t_unix_date));} // and re-set the time/date
delay(2000);
Setup_Clock();
} // end of Set_DateTime
bool DSTInEffect(){ // returns true if DST in effect
// by examining month,day and hour (first Sunday in March at 2am, 2nd Sunday in November at 2am)
int hhmm = (100*MyHour)+MyMinute; // quick way to check if its past 2:05 am on 'change day'
switch (MyMonth){
case april: // April thru October, we are in DST if its used
case may:
case june:
case july:
case august:
case september:
case october:
return true;
break;
case december: // December thru February always NOT in DST if used
case january:
case february:
return false;
break;
case march: // in March, we ARE in DST on 2nd Sunday
if (MyDay < 8) {return false;} // days 1 thru 7 can't be 2nd sunday
if (MyDay >14){return true;} // days 14 and beyond always WILL be DST
if (MyWeekDay == sunday){if (hhmm >= 205) {return true;} else return false; } else //Sunday AND after 2:05am then adjust
{if (MyDay >= (8+MyWeekDay)) {return true;} else {return false;}}
break;
case november: // in November, switch off of DST on first Sunday
if (MyDay > 7) {return false;} // Days 8 through end of month have to be non-DST
if (MyWeekDay == sunday){if (hhmm < 205) {return true;} else return false; } else //Sunday AND after 2:05am then adjust
{if (MyDay >= (1+MyWeekDay)) {return false;} else {return true;}}
break;
default: break;
return true;
} // end of case on month
return true;
} // end of DSTInEffect
void Setup_Clock(){ //set clock face (circle and tick marks)
int X1;
int Y1;
int X2;
int Y2;
float tdangle; // angle for tick marks at 5 minute intervals
XS1 = 0; // reset persistent second, minute and hour hand coordinates
YS1 = 0; // so initial 'blanking' doesn't cause problems
XM1 = 0;
YM1 = 0;
XH1 = 0;
YH1 = 0;
Persistent_Second = -1;
Persistent_Day= -1;
Persistent_Minute = -1;
Persistent_Hour = -1;
Persistent_HM_Display = " ";
tft.fillScreen(HX8357_BLACK);
tft.drawCircle(Circle_R, Circle_R, 159,HX8357_WHITE);
tft.drawCircle(Circle_R, Circle_R, 160,HX8357_WHITE);
for( int z=0; z < 360;z= z + 30 ){ //Begin at 0 and stop at 360, marking every 5 minutes (5 min x 6 degrees = +
tdangle=(float(z)/cradians); //Convert degrees to radians
int x1=(Circle_R+(sin(tdangle)*Circle_R));
int y1=(Circle_R-(cos(tdangle)*Circle_R));
int x2=(Circle_R+(sin(tdangle)*(Circle_R-tick_length)));
int y2=(Circle_R-(cos(tdangle)*(Circle_R-tick_length)));
tft.drawLine(x1,y1,x2,y2,HX8357_WHITE);
}
} // end of clock face setup
void Set_Clock_Hands(){
int X1; // target coordinates
int Y1;
Last_sdangle = sdangle; // angle of second hand we are about to erase
sdangle = (MySecond*6)/cradians; // seconds display
tft.fillCircle(XS1,YS1, 4,HX8357_BLACK); // erase the old 'dot'
tft.drawLine(Circle_R,Circle_R, XS1,YS1,HX8357_BLACK); // and the line
XS1 = (Circle_R+(sin(sdangle)*(second_length))); // calculate the new position
YS1 = (Circle_R-(cos(sdangle)*(second_length)));
tft.drawLine(Circle_R,Circle_R, XS1,YS1,HX8357_RED); //
tft.fillCircle(XS1,YS1, 4,HX8357_RED); // and draw new second 'dot'
if ( (MyMinute != Persistent_Minute) || (Last_sdangle == mdangle) || (sdangle == mdangle) ){ // minute changed or hands collide?
tft.drawLine(Circle_R,Circle_R, XM1,YM1,HX8357_BLACK); // clear old minute hand
tft.fillCircle(XM1,YM1,4,HX8357_BLACK); // and its tip
mdangle = (MyMinute*6)/cradians; // minute
X1 = (Circle_R+(sin(mdangle)*(minute_length))); // draw new minute hand and
Y1 = (Circle_R-(cos(mdangle)*(minute_length)));
tft.drawLine(Circle_R,Circle_R, X1,Y1,HX8357_GREEN);
tft.fillCircle(X1,Y1,4,HX8357_GREEN); //tip
XM1 = X1; // store what you drew to erase next time
YM1 = Y1; } // persistent minute will be updated by digital display - dont do it yet
if ( (MyMinute != Persistent_Minute) || (Last_sdangle == hdangle) || (sdangle == hdangle) ){ // minute changed or second hand collided?
tft.drawLine(Circle_R,Circle_R, XH1,YH1,HX8357_BLACK); // Black blanks out old hour hand and tip
tft.fillCircle(XH1,YH1, 4,HX8357_BLACK);
hdangle = (((MyHour%12)*30) + int((MyMinute/12)*6))/cradians; // hour jumps by 30 degrees + fractional minutes of the hour degrees
X1 = (Circle_R+(sin(hdangle)*(hour_length)));
Y1 = (Circle_R-(cos(hdangle)*(hour_length)));
tft.drawLine(Circle_R,Circle_R, X1,Y1,HX8357_YELLOW);
tft.fillCircle(X1,Y1, 4,HX8357_YELLOW);
XH1 = X1; // store for next time
YH1 = Y1;}
tft.fillCircle(Circle_R,Circle_R,5,HX8357_BLUE);
} // end Set_Clock_Hands
void Set_Digital_Display(){
int Display_P;
String Display_String;
if (MyDay != Persistent_Day) { // if its a new day then
Setup_Clock(); // clear whole display and then display outline of clock
Set_Clock_Hands(); // and clock
tft.setTextSize(3);
tft.setTextColor(HX8357_YELLOW);
if (MyDay < 10){tft.setCursor(77, Screen_H + 75);} else {tft.setCursor(65, Screen_H + 75);} // Center and ensure spacing for two digits for hour
Display_String = Display_String + MyDay;
Display_String = Display_String + Months[MyMonth-january];
Display_String = Display_String + MyYear;
tft.print(Display_String);
tft.setCursor(83+DOW_Pad[MyWeekDay-sunday], Screen_H + 115);
tft.setTextColor(HX8357_MAGENTA);
tft.print(DaysOfTheWeek[MyWeekDay-sunday]);
Persistent_Day = MyDay;}
if (MyMinute != Persistent_Minute) { // if minute has changed,
tft.setTextSize(5); // blank out HH:MM
tft.setCursor(89, Screen_H + 20);
tft.setTextColor(HX8357_BLACK);
tft.print(Persistent_HM_Display);
tft.setCursor(89, Screen_H + 20); // and refresh it
tft.setTextColor(HX8357_WHITE);
Display_P = MyHour%12; // Convert to 12 hour format
if (Display_P == 0) {Display_P = 12;} // and make midnite 12 too
if (Display_P < 10){Display_String = " ";} else {Display_String = "";} // ensure spacing for two digits for hour
Display_String = Display_String + Display_P;
Display_String = Display_String + ":";
if (MyMinute < 10){Display_String = Display_String + "0";} // always two digit minute
Display_String = Display_String + MyMinute;
//if (MyHour < 12) {Display_String = Display_String + " AM";} else {Display_String = Display_String + " PM";}
tft.print(Display_String);
Persistent_HM_Display = Display_String;
Persistent_Minute = MyMinute;}
} // end of Set Digital Display
Comments
Please log in or sign up to comment.