Close

Ender Clock-Duino v1.0 Sketch

A project log for Ender Clock-Duino

Arduino Clock

rhea-raeRhea Rae 19 hours ago0 Comments

#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);
}

Discussions