#include <Arduino.h>
#include <U8g2lib.h>
#include <EEPROM.h>
#include <Wire.h>
#include <RTClib.h>
#include <math.h>
// ================= LCD =================
const uint8_t PIN_LCD_CS = 10; // CS/RS
U8G2_ST7920_128X64_1_HW_SPI u8g2(U8G2_R0, PIN_LCD_CS, U8X8_PIN_NONE);
// ================= ENCODER =================
const uint8_t PIN_ENC_A = 2;
const uint8_t PIN_ENC_B = 3;
const uint8_t PIN_ENC_BTN = 4;
// If your encoder changes 2 numbers per click, set this to 2.
// If it changes 1 per click, leave at 4.
const int8_t TRANSITIONS_PER_DETENT = 4;
// ================= UI TIMING =================
const uint16_t LONG_MS = 700;
const uint16_t DEBOUNCE_MS = 25;
// ================= EEPROM =================
const int EEPROM_SIG_ADDR = 0;
const int EEPROM_TIME_ADDR = 1; // (legacy; not used now, kept for spacing)
const int EEPROM_CHIME_ADDR = EEPROM_TIME_ADDR + 4; // uint8_t
const int EEPROM_TONE_ADDR = EEPROM_CHIME_ADDR + 1; // uint8_t
const int EEPROM_FMT_ADDR = EEPROM_TONE_ADDR + 1; // uint8_t (0=12h,1=24h)
const uint8_t EEPROM_SIG = 0xA5;
// ================= BUZZER =================
const uint8_t PIN_BUZZER = 8;
// Settings (editable, saved)
bool chime_enabled = true;
uint8_t tone_amount = 18; // 0..100 (0 = silent)
bool fmt_24h = false; // false=12h, true=24h
// ================= RTC =================
RTC_DS3231 rtc;
// Throttle RTC reads so we don't spam I2C
DateTime rtc_cached;
uint32_t last_rtc_read_ms = 0;
// Temperature cache (DS3231 internal sensor)
float temp_f_cached = NAN;
uint32_t last_temp_read_ms = 0;
static const uint32_t TEMP_READ_PERIOD_MS = 5000;
// ================= SET MODE FIELDS =================
bool set_mode = false;
// Editing fields
uint8_t edit_hour12 = 12; // 1..12
bool edit_pm = false;
uint8_t edit_hour24 = 0; // 0..23
uint8_t edit_min = 0; // 0..59
bool edit_chime_enabled = true;
uint8_t edit_tone_amount = 18;
bool edit_fmt_24h = false;
enum Sel : uint8_t { SEL_HOUR, SEL_MIN, SEL_AMPM, SEL_FMT, SEL_CHIME, SEL_TONE, SEL_TEST };
Sel sel = SEL_HOUR;
uint32_t saved_banner_until_ms = 0;
int8_t last_chimed_h24 = -1;
// ================= ENCODER ISR =================
volatile int16_t enc_delta = 0;
volatile uint8_t enc_prev = 0;
static const int8_t ENC_TAB[16] = {
0, -1, 1, 0,
1, 0, 0, -1,
-1, 0, 0, 1,
0, 1, -1, 0
};
void enc_isr() {
uint8_t a = digitalRead(PIN_ENC_A) ? 1 : 0;
uint8_t b = digitalRead(PIN_ENC_B) ? 1 : 0;
uint8_t cur = (a << 1) | b;
uint8_t idx = (enc_prev << 2) | cur;
enc_prev = cur;
enc_delta += ENC_TAB[idx];
}
// ================= BUTTON =================
bool btn_last_raw = true;
bool btn_stable = true;
uint32_t btn_change_ms = 0;
uint32_t btn_down_ms = 0;
bool long_fired = false;
static void poll_button(bool &short_press, bool &long_press) {
short_press = false;
long_press = false;
bool raw = digitalRead(PIN_ENC_BTN); // HIGH idle, LOW pressed
uint32_t now = millis();
if (raw != btn_last_raw) {
btn_last_raw = raw;
btn_change_ms = now;
}
if ((now - btn_change_ms) > DEBOUNCE_MS) {
if (btn_stable != raw) {
btn_stable = raw;
if (btn_stable == LOW) {
btn_down_ms = now;
long_fired = false;
} else {
if (!long_fired) short_press = true;
}
}
}
if (btn_stable == LOW && !long_fired && (now - btn_down_ms) >= LONG_MS) {
long_fired = true;
long_press = true;
}
}
// ================= HELPERS =================
static const char* ampm_str(bool pm) { return pm ? "PM" : "AM"; }
static void h24_to_12(uint8_t h24, uint8_t &h12, bool &pm) {
pm = (h24 >= 12);
uint8_t t = h24 % 12;
if (t == 0) t = 12;
h12 = t;
}
static uint8_t h12_to_24(uint8_t h12, bool pm) {
uint8_t h = h12 % 12; // 12 -> 0
if (pm) h += 12;
return h;
}
static void format_time_12(char *out, size_t n, uint8_t h24, uint8_t m, uint8_t s) {
uint8_t h12; bool pm;
h24_to_12(h24, h12, pm);
snprintf(out, n, "%02u:%02u:%02u", h12, m, s);
}
static void format_time_24(char *out, size_t n, uint8_t h24, uint8_t m, uint8_t s) {
snprintf(out, n, "%02u:%02u:%02u", h24, m, s);
}
// ================= SETTINGS EEPROM =================
static void load_settings_from_eeprom() {
uint8_t sig = EEPROM.read(EEPROM_SIG_ADDR);
if (sig != EEPROM_SIG) {
chime_enabled = true;
tone_amount = 18;
fmt_24h = false;
return;
}
chime_enabled = (EEPROM.read(EEPROM_CHIME_ADDR) != 0);
uint8_t t = EEPROM.read(EEPROM_TONE_ADDR);
if (t > 100) t = 18;
tone_amount = t;
uint8_t f = EEPROM.read(EEPROM_FMT_ADDR);
fmt_24h = (f != 0);
}
static void save_settings_to_eeprom(bool ce, uint8_t tone, bool f24) {
EEPROM.update(EEPROM_SIG_ADDR, EEPROM_SIG);
EEPROM.update(EEPROM_CHIME_ADDR, ce ? 1 : 0);
EEPROM.update(EEPROM_TONE_ADDR, tone);
EEPROM.update(EEPROM_FMT_ADDR, f24 ? 1 : 0);
}
// ================= BUZZER =================
static void softBeep(uint16_t freq, uint16_t durationMs, uint8_t amount) {
if (amount == 0) return;
uint16_t curve = (uint16_t)amount * (uint16_t)amount / 100;
if (curve == 0) curve = 1;
const uint8_t windowMs = 20;
uint8_t onTime = map(curve, 1, 100, 1, windowMs);
uint8_t offTime = windowMs - onTime;
uint32_t start = millis();
while (millis() - start < durationMs) {
tone(PIN_BUZZER, freq);
delay(onTime);
noTone(PIN_BUZZER);
if (offTime) delay(offTime);
}
}
static void softChirpDown(uint16_t fStart, uint16_t fEnd, uint16_t msTotal, uint8_t amount) {
if (amount == 0) return;
const uint8_t steps = 14;
for (uint8_t i = 0; i < steps; i++) {
uint16_t f = (uint16_t)(fStart + (int32_t)(fEnd - fStart) * (int32_t)i / (int32_t)(steps - 1));
uint16_t d = msTotal / steps;
softBeep(f, d, amount);
}
noTone(PIN_BUZZER);
}
static void frogRibbet(uint8_t amount) {
softChirpDown(2000, 950, 110, amount);
delay(170);
softChirpDown(1850, 850, 120, amount);
noTone(PIN_BUZZER);
}
// ================= RTC =================
static void rtc_read_cached() {
uint32_t now_ms = millis();
if (now_ms - last_rtc_read_ms >= 200) {
rtc_cached = rtc.now();
last_rtc_read_ms = now_ms;
}
}
static void rtc_temp_read_cached() {
uint32_t now_ms = millis();
if (isnan(temp_f_cached) || (now_ms - last_temp_read_ms >= TEMP_READ_PERIOD_MS)) {
float tc = rtc.getTemperature(); // Celsius from DS3231
temp_f_cached = (tc * 9.0f / 5.0f) + 32.0f; // Fahrenheit
last_temp_read_ms = now_ms;
}
}
// Integer-only temp label for AVR-safe formatting: "IN 73F"
static void format_temp_in_integer(char *out, size_t n, float tf) {
if (isnan(tf)) { snprintf(out, n, "IN NA"); return; }
int whole = (int)(tf + (tf >= 0 ? 0.5f : -0.5f));
snprintf(out, n, "IN %dF", whole);
}
// ================= UI ADJUSTERS =================
static void adjust_hour12(int steps) {
while (steps > 0) {
if (edit_hour12 == 11) { edit_hour12 = 12; edit_pm = !edit_pm; }
else if (edit_hour12 == 12) { edit_hour12 = 1; }
else { edit_hour12++; }
steps--;
}
while (steps < 0) {
if (edit_hour12 == 12) { edit_hour12 = 11; edit_pm = !edit_pm; }
else if (edit_hour12 == 1) { edit_hour12 = 12; }
else { edit_hour12--; }
steps++;
}
}
static void adjust_hour24(int steps) {
int v = (int)edit_hour24 + steps;
while (v < 0) v += 24;
while (v > 23) v -= 24;
edit_hour24 = (uint8_t)v;
}
static void adjust_min(int steps) {
int v = (int)edit_min + steps;
while (v < 0) v += 60;
while (v > 59) v -= 60;
edit_min = (uint8_t)v;
}
static void adjust_ampm(int steps) { if (steps != 0) edit_pm = !edit_pm; }
static void adjust_fmt(int steps) { if (steps != 0) edit_fmt_24h = !edit_fmt_24h; }
static void adjust_chime(int steps) { if (steps != 0) edit_chime_enabled = !edit_chime_enabled; }
static void adjust_tone(int steps) {
int v = (int)edit_tone_amount + steps;
if (v < 0) v = 0;
if (v > 100) v = 100;
edit_tone_amount = (uint8_t)v;
}
// Cycle selection; skip AMPM when in 24h
static Sel next_sel(Sel cur, bool use24) {
switch (cur) {
case SEL_HOUR: return SEL_MIN;
case SEL_MIN: return use24 ? SEL_FMT : SEL_AMPM;
case SEL_AMPM: return SEL_FMT;
case SEL_FMT: return SEL_CHIME;
case SEL_CHIME: return SEL_TONE;
case SEL_TONE: return SEL_TEST;
case SEL_TEST: return SEL_HOUR;
default: return SEL_HOUR;
}
}
// ================= SETUP =================
void setup() {
pinMode(PIN_ENC_A, INPUT_PULLUP);
pinMode(PIN_ENC_B, INPUT_PULLUP);
pinMode(PIN_ENC_BTN, INPUT_PULLUP);
pinMode(PIN_BUZZER, OUTPUT);
noTone(PIN_BUZZER);
Wire.begin();
rtc.begin();
// No coin cell -> likely lostPower after every power cycle.
if (rtc.lostPower()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
load_settings_from_eeprom();
u8g2.begin();
u8g2.setContrast(180);
u8g2.setBusClock(400000);
enc_prev = ((digitalRead(PIN_ENC_A) ? 1 : 0) << 1) | (digitalRead(PIN_ENC_B) ? 1 : 0);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_A), enc_isr, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_B), enc_isr, CHANGE);
rtc_cached = rtc.now();
last_rtc_read_ms = millis();
temp_f_cached = NAN;
last_temp_read_ms = 0;
rtc_temp_read_cached();
}
// ================= LOOP =================
void loop() {
rtc_read_cached();
rtc_temp_read_cached();
uint8_t h24 = rtc_cached.hour();
uint8_t m = rtc_cached.minute();
uint8_t s = rtc_cached.second();
bool pm_now = (h24 >= 12);
// Encoder -> steps
static int8_t accum = 0;
int16_t d;
noInterrupts();
d = enc_delta;
enc_delta = 0;
interrupts();
accum += d;
int steps = 0;
while (accum >= TRANSITIONS_PER_DETENT) { steps++; accum -= TRANSITIONS_PER_DETENT; }
while (accum <= -TRANSITIONS_PER_DETENT) { steps--; accum += TRANSITIONS_PER_DETENT; }
// Button
bool sp, lp;
poll_button(sp, lp);
// Long press toggles SET mode
if (lp) {
if (!set_mode) {
set_mode = true;
sel = SEL_HOUR;
edit_chime_enabled = chime_enabled;
edit_tone_amount = tone_amount;
edit_fmt_24h = fmt_24h;
if (edit_fmt_24h) edit_hour24 = h24;
else h24_to_12(h24, edit_hour12, edit_pm);
edit_min = m;
} else {
set_mode = false;
chime_enabled = edit_chime_enabled;
tone_amount = edit_tone_amount;
fmt_24h = edit_fmt_24h;
save_settings_to_eeprom(chime_enabled, tone_amount, fmt_24h);
uint8_t new_h24 = fmt_24h ? edit_hour24 : h12_to_24(edit_hour12, edit_pm);
DateTime dtn(rtc_cached.year(), rtc_cached.month(), rtc_cached.day(), new_h24, edit_min, 0);
rtc.adjust(dtn);
rtc_cached = rtc.now();
last_rtc_read_ms = millis();
saved_banner_until_ms = millis() + 1200;
last_chimed_h24 = (int8_t)rtc_cached.hour();
}
}
// Short press in SET
if (set_mode && sp) {
if (sel == SEL_TEST) {
frogRibbet(edit_tone_amount);
} else {
sel = next_sel(sel, edit_fmt_24h);
}
}
// Rotate in SET
if (set_mode && steps != 0) {
if (sel == SEL_HOUR) {
if (edit_fmt_24h) adjust_hour24(steps);
else adjust_hour12(steps);
} else if (sel == SEL_MIN) {
adjust_min(steps);
} else if (sel == SEL_AMPM) {
adjust_ampm(steps);
} else if (sel == SEL_FMT) {
bool oldFmt = edit_fmt_24h;
adjust_fmt(steps);
if (oldFmt != edit_fmt_24h) {
if (edit_fmt_24h) edit_hour24 = h12_to_24(edit_hour12, edit_pm);
else h24_to_12(edit_hour24, edit_hour12, edit_pm);
}
} else if (sel == SEL_CHIME) {
adjust_chime(steps);
} else if (sel == SEL_TONE) {
adjust_tone(steps);
}
}
// Hourly chime
if (!set_mode && chime_enabled && tone_amount > 0) {
if (m == 0 && s == 0 && (int8_t)h24 != last_chimed_h24) {
last_chimed_h24 = (int8_t)h24;
frogRibbet(tone_amount);
}
}
// Display fields
uint8_t disp_h24 = h24;
uint8_t disp_m = m;
uint8_t disp_s = s;
bool disp_pm = pm_now;
bool active24 = set_mode ? edit_fmt_24h : fmt_24h;
if (set_mode) {
disp_m = edit_min;
disp_s = 0;
if (edit_fmt_24h) {
disp_h24 = edit_hour24;
disp_pm = (disp_h24 >= 12);
} else {
disp_h24 = h12_to_24(edit_hour12, edit_pm);
disp_pm = edit_pm;
}
}
char tbuf[16];
if (active24) format_time_24(tbuf, sizeof(tbuf), disp_h24, disp_m, disp_s);
else format_time_12(tbuf, sizeof(tbuf), disp_h24, disp_m, disp_s);
// Indoor temp string (integer only)
char tempbuf[14];
format_temp_in_integer(tempbuf, sizeof(tempbuf), temp_f_cached);
char footer[32];
// ===== Draw =====
u8g2.firstPage();
do {
// Left header title (already shifted down)
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 14, set_mode ? "SET" : "Ender Clock-Duino");
// Temp in top-right above AM/PM
u8g2.setFont(u8g2_font_4x6_tf);
uint8_t tw = u8g2.getStrWidth(tempbuf);
u8g2.drawStr(128 - tw, 7, tempbuf);
// AM/PM ONLY in 12-hour mode (shifted down)
uint8_t ap_w = 0;
if (!active24) {
u8g2.setFont(u8g2_font_6x12_tf);
const char* ap = ampm_str(disp_pm);
ap_w = u8g2.getStrWidth(ap);
u8g2.drawStr(128 - ap_w, 16, ap);
}
// Divider
u8g2.drawHLine(0, 18, 128);
// Main time
u8g2.setFont(u8g2_font_logisoso24_tf);
u8g2.drawStr(0, 52, tbuf);
// Footer
u8g2.setFont(u8g2_font_5x8_tf);
if (set_mode) {
// Underlines for selected fields
if (sel == SEL_HOUR) u8g2.drawHLine(2, 54, 26);
else if (sel == SEL_MIN) u8g2.drawHLine(34, 54, 26);
else if (sel == SEL_AMPM && !active24) u8g2.drawHLine(128 - ap_w, 17, ap_w);
if (sel == SEL_HOUR) snprintf(footer, sizeof(footer), "HOUR ROT=CHG CLK=NEXT");
else if (sel == SEL_MIN) snprintf(footer, sizeof(footer), "MIN ROT=CHG CLK=NEXT");
else if (sel == SEL_AMPM) snprintf(footer, sizeof(footer), "AMPM ROT=TOG CLK=NEXT");
else if (sel == SEL_FMT) snprintf(footer, sizeof(footer), "FMT:%s ROT=TOG", edit_fmt_24h ? "24" : "12");
else if (sel == SEL_CHIME) snprintf(footer, sizeof(footer), "CHIME:%s ROT=TOG", edit_chime_enabled ? "ON" : "OFF");
else if (sel == SEL_TONE) snprintf(footer, sizeof(footer), "TONE:%u ROT=CHG", edit_tone_amount);
else snprintf(footer, sizeof(footer), "TEST CLICK=RIBBIT");
u8g2.drawStr(0, 62, footer);
} else {
if ((int32_t)(millis() - saved_banner_until_ms) < 0) {
u8g2.drawStr(0, 62, "SAVED");
} else {
snprintf(footer, sizeof(footer), "HOLD=SET %s CH:%s T:%u",
fmt_24h ? "24H" : "12H",
chime_enabled ? "ON" : "OFF",
tone_amount);
u8g2.drawStr(0, 62, footer);
}
}
} while (u8g2.nextPage());
delay(10);
}
Rhea Rae
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.