Close

Ender Clock-Duino Sketch w/o DS3231

A project log for Ender_Clock_Duino

Arduino Clock Rabbit Hole

rhea-raeRhea Rae 01/12/2026 at 18:190 Comments

#include <Arduino.h>
#include <U8g2lib.h>
#include <EEPROM.h>
#include <Wire.h>
#include <RTClib.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;

// ================= 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;
  }
}

// ================= 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 -> will likely report lostPower after every power cycle.
  // Auto-set to compile time so it's at least reasonable.
  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();
}

// ================= LOOP =================
void loop() {
  rtc_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);

  char footer[32];

  // ===== Draw =====
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_6x12_tf);
    u8g2.drawStr(0, 12, set_mode ? "SET" : "Ender Clock-Duino");

    // AM/PM ONLY in 12-hour mode
    uint8_t ap_w = 0;
    if (!active24) {
      const char* ap = ampm_str(disp_pm);
      ap_w = u8g2.getStrWidth(ap);
      u8g2.drawStr(128 - ap_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) {
      // Underlines
      if (sel == SEL_HOUR) u8g2.drawHLine(2, 52, 26);
      else if (sel == SEL_MIN) u8g2.drawHLine(34, 52, 26);
      else if (sel == SEL_AMPM && !active24) u8g2.drawHLine(128 - ap_w, 13, 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);
}

Discussions