#include <Arduino.h>
#include <U8g2lib.h>
#include <EEPROM.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; // uint32_t (4 bytes)
const int EEPROM_CHIME_ADDR = EEPROM_TIME_ADDR + 4; // uint8_t
const int EEPROM_VOL_ADDR = EEPROM_CHIME_ADDR + 1; // uint8_t
const uint8_t EEPROM_SIG = 0xA5; // marker that EEPROM has valid data
// ================= BUZZER =================
const uint8_t PIN_BUZZER = 8;
// Chime settings (editable, saved)
bool chime_enabled = true;
uint8_t chime_volume_percent = 18; // editable 0..100 (0 = silent)
// ================= CLOCK CORE =================
uint32_t clock_sec = 0;
uint32_t last_tick_ms = 0;
// ================= SET MODE FIELDS =================
bool set_mode = false;
uint8_t edit_hour12 = 12; // 1..12
uint8_t edit_min = 0; // 0..59
uint8_t edit_sec = 0; // 0..59 (frozen in SET)
bool edit_pm = false; // false=AM, true=PM
bool edit_chime_enabled = true;
uint8_t edit_volume_percent = 18;
enum Sel : uint8_t { SEL_HOUR, SEL_MIN, SEL_AMPM, SEL_CHIME, SEL_VOL, SEL_TEST };
Sel sel = SEL_HOUR;
uint32_t saved_banner_until_ms = 0;
// Track last hour we chimed so we only do it once per hour
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 uint32_t wrap_day(uint32_t sec) { return sec % 86400UL; }
static void sec_to_hms(uint32_t sec, uint8_t &h24, uint8_t &m, uint8_t &s) {
sec = wrap_day(sec);
h24 = sec / 3600UL;
m = (sec / 60UL) % 60UL;
s = sec % 60UL;
}
static void h24_to_edit(uint8_t h24, uint8_t m, uint8_t s,
uint8_t &h12, bool &pm, uint8_t &emin, uint8_t &esec) {
pm = (h24 >= 12);
uint8_t t = h24 % 12;
if (t == 0) t = 12;
h12 = t;
emin = m;
esec = s;
}
static uint8_t edit_to_h24(uint8_t h12, bool pm) {
uint8_t h = h12 % 12;
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 = h24 % 12;
if (h12 == 0) h12 = 12;
snprintf(out, n, "%02u:%02u:%02u", h12, m, s);
}
static const char* ampm_str(bool pm) { return pm ? "PM" : "AM"; }
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_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_chime(int steps) { if (steps != 0) edit_chime_enabled = !edit_chime_enabled; }
static void adjust_vol(int steps) {
int v = (int)edit_volume_percent + steps;
if (v < 0) v = 0;
if (v > 100) v = 100;
edit_volume_percent = (uint8_t)v;
}
// ================= BUZZER HELPERS =================
static void softBeep(uint16_t freq, uint16_t durationMs, uint8_t volumePercent) {
if (volumePercent == 0) return;
uint16_t volCurve = (uint16_t)volumePercent * (uint16_t)volumePercent / 100;
if (volCurve == 0) volCurve = 1;
const uint8_t windowMs = 20;
uint8_t onTime = map(volCurve, 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 volumePercent) {
if (volumePercent == 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, volumePercent);
}
noTone(PIN_BUZZER);
}
static void frogRibbet(uint8_t volumePercent) {
softChirpDown(2000, 950, 110, volumePercent);
delay(170);
softChirpDown(1850, 850, 120, volumePercent);
noTone(PIN_BUZZER);
}
static void hourlyChime(uint8_t h24) { (void)h24; frogRibbet(chime_volume_percent); }
// ================= EEPROM LOAD/SAVE =================
static void load_from_eeprom() {
uint8_t sig = EEPROM.read(EEPROM_SIG_ADDR);
if (sig != EEPROM_SIG) {
clock_sec = 0;
chime_enabled = true;
chime_volume_percent = 18;
return;
}
uint32_t stored = 0;
EEPROM.get(EEPROM_TIME_ADDR, stored);
if (stored >= 86400UL) stored = 0;
clock_sec = stored;
uint8_t ce = EEPROM.read(EEPROM_CHIME_ADDR);
chime_enabled = (ce != 0);
uint8_t vol = EEPROM.read(EEPROM_VOL_ADDR);
if (vol > 100) vol = 18;
chime_volume_percent = vol;
}
static void save_all_to_eeprom(uint32_t sec, bool ce, uint8_t vol) {
EEPROM.update(EEPROM_SIG_ADDR, EEPROM_SIG);
EEPROM.put(EEPROM_TIME_ADDR, sec);
EEPROM.update(EEPROM_CHIME_ADDR, ce ? 1 : 0);
EEPROM.update(EEPROM_VOL_ADDR, vol);
}
// ================= 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);
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);
load_from_eeprom();
last_tick_ms = millis();
}
// ================= LOOP =================
void loop() {
uint32_t now_ms = millis();
if (!set_mode) {
uint32_t delta = now_ms - last_tick_ms;
if (delta >= 1000UL) {
uint32_t add = delta / 1000UL;
clock_sec = wrap_day(clock_sec + add);
last_tick_ms += add * 1000UL;
}
} else {
last_tick_ms = now_ms;
}
// 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;
uint8_t h24, m, s;
sec_to_hms(clock_sec, h24, m, s);
h24_to_edit(h24, m, s, edit_hour12, edit_pm, edit_min, edit_sec);
edit_chime_enabled = chime_enabled;
edit_volume_percent = chime_volume_percent;
} else {
set_mode = false;
uint8_t h24 = edit_to_h24(edit_hour12, edit_pm);
clock_sec = wrap_day((uint32_t)h24 * 3600UL + (uint32_t)edit_min * 60UL + (uint32_t)edit_sec);
chime_enabled = edit_chime_enabled;
chime_volume_percent = edit_volume_percent;
save_all_to_eeprom(clock_sec, chime_enabled, chime_volume_percent);
saved_banner_until_ms = millis() + 1200;
uint8_t th, tm, ts;
sec_to_hms(clock_sec, th, tm, ts);
last_chimed_h24 = th;
}
}
// In SET:
if (set_mode && sp) {
if (sel == SEL_TEST) {
frogRibbet(edit_volume_percent);
} else {
if (sel == SEL_HOUR) sel = SEL_MIN;
else if (sel == SEL_MIN) sel = SEL_AMPM;
else if (sel == SEL_AMPM) sel = SEL_CHIME;
else if (sel == SEL_CHIME) sel = SEL_VOL;
else if (sel == SEL_VOL) sel = SEL_TEST;
else sel = SEL_HOUR;
}
}
// Rotate in SET changes selection
if (set_mode && steps != 0) {
if (sel == SEL_HOUR) adjust_hour12(steps);
else if (sel == SEL_MIN) adjust_min(steps);
else if (sel == SEL_AMPM) adjust_ampm(steps);
else if (sel == SEL_CHIME) adjust_chime(steps);
else if (sel == SEL_VOL) adjust_vol(steps);
}
// Display time
uint8_t dh24, dm, ds;
bool dpm;
if (set_mode) {
dh24 = edit_to_h24(edit_hour12, edit_pm);
dm = edit_min;
ds = edit_sec;
dpm = edit_pm;
} else {
sec_to_hms(clock_sec, dh24, dm, ds);
dpm = (dh24 >= 12);
}
// Hourly chime
if (!set_mode && chime_enabled && chime_volume_percent > 0) {
if (dm == 0 && ds == 0 && (int8_t)dh24 != last_chimed_h24) {
last_chimed_h24 = (int8_t)dh24;
hourlyChime(dh24);
}
}
char tbuf[16];
format_time_12(tbuf, sizeof(tbuf), dh24, dm, ds);
const char* ap = ampm_str(dpm);
char footer[32];
// Draw
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_6x12_tf);
// CHANGE: Title text
u8g2.drawStr(0, 12, set_mode ? "SET" : "Ender Clock-Duino");
uint8_t w = u8g2.getStrWidth(ap);
u8g2.drawStr(128 - w, 12, ap);
u8g2.drawHLine(0, 14, 128);
u8g2.setFont(u8g2_font_logisoso24_tf);
u8g2.drawStr(0, 50, tbuf);
u8g2.setFont(u8g2_font_5x8_tf);
if (set_mode) {
if (sel == SEL_HOUR) u8g2.drawHLine(2, 52, 26);
else if (sel == SEL_MIN) u8g2.drawHLine(34, 52, 26);
else if (sel == SEL_AMPM) u8g2.drawHLine(128 - w, 13, 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_CHIME) snprintf(footer, sizeof(footer), "CHIME:%s ROT=TOG", edit_chime_enabled ? "ON" : "OFF");
else if (sel == SEL_VOL) snprintf(footer, sizeof(footer), "TONE:%u ROT=CHG", edit_volume_percent);
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 CH:%s T:%u",
chime_enabled ? "ON" : "OFF", chime_volume_percent);
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.