/*
Project: Arduino RTC Clock
Description: DSxxxx RTC module based clock.
The settings and modes can be set with a rotary encoder.
Date: 3.14.26
License: Beerware
Libraries:
https://github.com/arduino-libraries/LiquidCrystal
https://github.com/mathertel/RotaryEncoder
https://github.com/adafruit/RTClib
TO DO: Seems pretty good...
*/
#include <LiquidCrystal.h>
#include <RotaryEncoder.h>
#include <RTClib.h>
typedef enum { STATE_INIT,
STATE_DISPLAY,
STATE_DATE_FMT,
STATE_TIME_FMT,
STATE_SET_YEAR,
STATE_SET_MONTH,
STATE_SET_DATE,
STATE_SET_HOUR,
STATE_SET_MINUTE,
STATE_SET_SECOND,
STATE_CONFIRM,
STATE_SAVE
} states;
// set initial state
states state = STATE_INIT;
states oldState = state;
states nextState = STATE_DISPLAY;
const int RS = 12, EN = 11, D4 = 10, D5 = 9, D6 = 8, D7 = 7;
const int CK_PIN = 3;
const int DT_PIN = 2;
const int SW_PIN = 4;
const int NUM_DAYS[] = { // days per month, leap year adjusted below
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};
const char* MONTH_NAMES[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
const char* DOW[] = { // day of week labels
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
const unsigned long ONE_SEC = 1000;
bool isMDY = true;
bool isOldMDY = true;
bool is12Hour = true;
bool isOld12Hour = true;
bool doSave = false;
int year = 2026;
int month = 1;
int date = 1;
int hour = 12;
int minute = 0;
int second = 0;
int oldSwState = HIGH; // INPUT_PULLUP
int pos = 0;
int dir = 0;
unsigned long prevTime = 0;
LiquidCrystal lcd(RS, EN, D4, D5, D6, D7);
RotaryEncoder *encoder = nullptr;
RTC_DS1307 rtc;
void checkEncoder() {
int newPos = encoder->getPosition();
int newDir = (int)encoder->getDirection();
if (pos != newPos) {
//Serial.print("pos:");
//Serial.print(newPos);
//Serial.print(" dir:");
//Serial.println(newDir);
pos = newPos;
dir = newDir;
} else {
dir = 0;
}
}
void checkPosition() {
encoder->tick(); // calling tick() updates position and direction
}
void checkSwitch() {
int currSwState = digitalRead(SW_PIN); // read the switch
if (oldSwState != currSwState) { // if it changed
oldSwState = currSwState; // save the state
if (currSwState == LOW) { // if LOW was just pressed
//Serial.println("The switch was just pressed");
lcd.clear();
state = nextState;
} else { // if HIGH was just released
//Serial.println("The switch was just released");
}
delay(20); // debounce the switch
}
}
void confirmSave() {
lcd.setCursor(1, 0);
lcd.print("Save new time?");
if (dir != 0) {
doSave = !doSave;
}
lcd.setCursor(7, 1);
lcd.print(doSave ? "Yes" : "No ");
}
void saveTime() {
if (doSave) {
rtc.adjust(DateTime(year, month, date, hour, minute, second));
} else {
isMDY = isOldMDY;
is12Hour = isOld12Hour;
}
}
void setDateFormat() {
lcd.setCursor(2, 0);
lcd.print("Date format?");
if (dir != 0) {
isMDY = !isMDY;
}
lcd.setCursor(3, 1);
lcd.print(isMDY ? "MM/DD/YYYY" : "DD/MM/YYYY");
}
void setTimeFormat() {
lcd.setCursor(2, 0);
lcd.print("Time format?");
if (dir != 0) {
is12Hour = !is12Hour;
}
lcd.setCursor(6, 1);
lcd.print(is12Hour ? "12 H" : "24 H");
}
void setYear() {
lcd.setCursor(4, 0);
lcd.print("Set year");
if (dir != 0) {
year = year + dir;
}
lcd.setCursor(6, 1);
lcd.print(year);
}
void setMonth() {
lcd.setCursor(3, 0);
lcd.print("Set month");
if (dir != 0) {
month = month + dir;
if (month > 12) month = 1;
if (month < 1) month = 12;
lcd.setCursor(6, 1);
lcd.print(" ");
}
lcd.setCursor(6, 1);
lcd.print(MONTH_NAMES[month - 1]);
}
void setDate() {
int addDay = 0;
lcd.setCursor(4, 0);
lcd.print("Set date");
if (dir != 0) {
date = date + dir;
// leap year adjust
if (month == 2 && !(year % 4)) addDay = 1;
if (date > NUM_DAYS[month - 1] + addDay) date = 1;
if (date < 1) date = NUM_DAYS[month - 1] + addDay;
lcd.setCursor(6, 1);
lcd.print(" ");
}
lcd.setCursor(7, 1);
lcd.print(date);
}
void setHour() {
lcd.setCursor(4, 0);
lcd.print("Set hour");
if (dir != 0) {
hour = hour + dir;
if (hour > 23) hour = 0;
if (hour < 0) hour = 23;
lcd.setCursor(6, 1);
lcd.print(" ");
}
if (is12Hour) {
// show 12h
lcd.setCursor(6, 1);
if (hour == 0) {
lcd.print("12 AM");
} else if (hour == 12) {
lcd.print("12 PM");
} else if (hour > 12) {
lcd.print(hour - 12);
lcd.print(" PM");
} else {
lcd.print(hour);
lcd.print(" AM");
}
} else {
// show 24h
lcd.setCursor(7, 1);
lcd.print(hour);
}
}
void setMinute() {
lcd.setCursor(3, 0);
lcd.print("Set minute");
if (dir != 0) {
minute = minute + dir;
if (minute > 59) minute = 0;
if (minute < 0) minute = 59;
lcd.setCursor(6, 1);
lcd.print(" ");
}
lcd.setCursor(7, 1);
lcd.print(minute);
}
void setSecond() {
lcd.setCursor(3, 0);
lcd.print("Set second");
if (dir != 0) {
second = second + dir;
if (second > 59) second = 0;
if (second < 0) second = 59;
lcd.setCursor(6, 1);
lcd.print(" ");
}
lcd.setCursor(7, 1);
lcd.print(second);
}
void setState() {
if (state != oldState) {
oldState = state;
Serial.print("State: ");
Serial.println(state);
}
switch (state) {
case STATE_INIT:
showSplash();
state = STATE_DISPLAY;
break;
case STATE_DISPLAY:
showClock();
isOldMDY = isMDY;
isOld12Hour = is12Hour;
nextState = STATE_DATE_FMT;
break;
case STATE_DATE_FMT:
setDateFormat();
nextState = STATE_TIME_FMT;
break;
case STATE_TIME_FMT:
setTimeFormat();
nextState = STATE_SET_YEAR;
break;
case STATE_SET_YEAR:
setYear();
nextState = STATE_SET_MONTH;
break;
case STATE_SET_MONTH:
setMonth();
nextState = STATE_SET_DATE;
break;
case STATE_SET_DATE:
setDate();
nextState = STATE_SET_HOUR;
break;
case STATE_SET_HOUR:
setHour();
nextState = STATE_SET_MINUTE;
break;
case STATE_SET_MINUTE:
setMinute();
nextState = STATE_SET_SECOND;
break;
case STATE_SET_SECOND:
setSecond();
nextState = STATE_CONFIRM;
break;
case STATE_CONFIRM:
confirmSave();
nextState = STATE_SAVE;
break;
case STATE_SAVE:
saveTime();
state = STATE_DISPLAY;
break;
default:
Serial.println("A bad thing happened...");
break;
}
}
void showClock() {
char dateBuffer[16];
char timeBuffer[16];
if (millis() - prevTime >= ONE_SEC) {
prevTime = millis();
DateTime now = rtc.now();
if (is12Hour) {
snprintf(timeBuffer, 16, "%2d:%02d:%02d %s",
now.twelveHour(),
now.minute(),
now.second(),
now.isPM() ? "PM" : "AM"
);
lcd.setCursor(2, 0);
} else {
snprintf(timeBuffer, 16, "%2d:%02d:%02d",
now.hour(),
now.minute(),
now.second()
);
lcd.setCursor(4, 0);
}
lcd.print(timeBuffer);
//Serial.print(timeBuffer);
//Serial.print("\t");
if (isMDY) {
snprintf(dateBuffer, 16, "%s %2d/%2d/%4d",
DOW[now.dayOfTheWeek()],
now.month(),
now.day(),
now.year()
);
} else {
snprintf(dateBuffer, 16, "%s %2d/%2d/%4d",
DOW[now.dayOfTheWeek()],
now.day(),
now.month(),
now.year()
);
}
lcd.setCursor(1, 1);
lcd.print(dateBuffer);
//Serial.println(dateBuffer);
}
}
void showSplash() {
lcd.setCursor(1, 0);
lcd.print("Arduino Clock");
lcd.setCursor(5, 1);
lcd.print("V1.00");
delay(2000);
lcd.clear();
}
void setup() {
Serial.begin(115200);
lcd.begin(16, 2);
if (! rtc.begin()) {
Serial.println("Couldn't find RTC");
Serial.flush();
while (true);
}
// setup the rotary encoder functionality
// use FOUR3 mode when CK_PIN, DT_PIN signals are always HIGH in latch position.
encoder = new RotaryEncoder(CK_PIN, DT_PIN, RotaryEncoder::LatchMode::FOUR3);
attachInterrupt(digitalPinToInterrupt(CK_PIN), checkPosition, CHANGE);
attachInterrupt(digitalPinToInterrupt(DT_PIN), checkPosition, CHANGE);
pinMode(SW_PIN, INPUT_PULLUP);
//if (rtc.lostPower()) { // DS3231, this is better (as is the RTC)
if (!rtc.isrunning()) { // DS1307, what Wokwi has
nextState = STATE_DATE_FMT;
} else {
nextState = STATE_DISPLAY;
}
}
void loop() {
checkEncoder();
checkSwitch();
setState();
}