Close
0%
0%

Ender_Clock_Duino

Arduino Clock Rabbit Hole

Similar projects worth following
Hello World! This project is an Arduino-based clock that repurposes an Ender-3 Pro printer display and rotary encoder into a permanent, appliance-style timepiece. The ST7920 LCD is driven over hardware SPI using U8g2, and the encoder is handled through interrupts for responsive and accurate input. Timekeeping is provided by a DS3231 RTC module, ensuring long-term accuracy and retention across resets and power loss,while EEPROM is used to store user settings so configuration persists without reentry. The interface uses a 12-hour time format with a clear AM and PM indicator and provides audible feedback through a piezo buzzer using tone variation rather than simulated volume control. The system is designed to be stable, predictable, and capable of running unattended for extended periods, treating reclaimed hardware as functional hardware rather than obsolete part's.This is an evolving project.Now Includes weather from attached Arduino R4 Via wireless network in a master slave set config.

This project uses an Arduino Uno R3 board with an original Ender 3 Pro control panel LCD. The Arduino is driving the ST7920 LCD over hardware SPI and handling the integrated rotary encoder via interrupts for reliable input. Timekeeping is provided by a DS3231 RTC module to maintain accuracy across power loss, while user settings are stored in EEPROM so configuration persists without reentry. Audible feedback is implemented with a piezo buzzer using distinct tone patterns for interaction cues. 

Firmware developed collaboratively using AI-assisted code generation, with system architecture, behavior, integration, testing, and refinement by me :)

  • 1 × Elegoo Uno R3 Arduino R3 Clone
  • 1 × ST7920 Display Spare Stock Ender 3 LCD Display
  • 1 × Ender Clock-Duino Stand See links to download .stl file
  • 1 × Breadboard Electronic Components / Misc. Electronic Components
  • 1 × Assorted Jumpers

View all 6 components

  • R3_Master

    Rhea Rae02/21/2026 at 16:05 0 comments

    //====== R3_Master====
    #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 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;
    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;

    // ================= OUTDOOR WEATHER (R4 over I2C) =================
    static const uint8_t  R4_I2C_ADDR = 0x12;
    static const uint32_t OUT_POLL_PERIOD_MS = 10UL * 60UL * 1000UL; // 10 minutes
    static const uint32_t OUT_STALE_MS       = 40UL * 60UL * 1000UL; // stale after 40 minutes

    int16_t  out_temp_f_whole = INT16_MIN; // INT16_MIN = never got valid
    uint32_t out_last_good_ms = 0;
    uint32_t out_last_poll_ms = 0;

    // Debug: last I2C read error
    // 0=OK, 1=len, 2=crc, 3=flags(weatherOk=0)
    uint8_t  out_last_err = 0;
    uint32_t out_last_try_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;

    // IMPORTANT: enum defined BEFORE any function that references Sel
    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,...

    Read more »

  • R4_Slave

    Rhea Rae02/21/2026 at 16:03 0 comments

    // =====Code for the Arduino R4_Slave====

    #include <Arduino.h>
    #include <Wire.h>

    #include <WiFiS3.h>
    #include <WiFiSSLClient.h>
    #include <ArduinoHttpClient.h>

    // ================= USER SETTINGS =================
    static const char* WIFI_SSID = "wifi ssid";
    static const char* WIFI_PASS = "wifipassword";

    static const float LATITUDE  = 39.1518f;
    static const float LONGITUDE = -77.9822f;

    static const uint8_t I2C_ADDR = 0x12;

    static const uint32_t WEATHER_PERIOD_OK_MS   = 10UL * 60UL * 1000UL; // 10 min
    static const uint32_t WEATHER_PERIOD_FAIL_MS = 60UL * 1000UL;        // 60 sec base fail retry
    static const uint32_t FAIL_BACKOFF_MAX_MS    = 10UL * 60UL * 1000UL; // cap at 10 min

    static const char* WEATHER_HOST = "api.open-meteo.com";
    static const int   WEATHER_PORT = 443;

    static const uint16_t WIFI_CONNECT_WINDOW_MS = 3500;
    static const uint16_t WIFI_IP_WINDOW_MS      = 1200;
    static const uint16_t HTTP_TIMEOUT_MS        = 9000;

    static const uint32_t STALE_AFTER_MS         = 40UL * 60UL * 1000UL; // 40 min (matches R3)

    // ================= STATE =================
    static int16_t  g_tempF10   = 0;     // temp * 10 (F)
    static uint16_t g_wmoCode   = 0;     // weathercode
    static uint32_t g_lastOkMs  = 0;     // millis at last successful fetch

    static bool g_wifiOk        = false; // connected + real IP
    static bool g_haveWeather   = false; // ever fetched successfully since boot

    static uint32_t g_lastAttemptMs = 0;
    static uint32_t g_failBackoffMs = WEATHER_PERIOD_FAIL_MS;

    // Cached I2C packet (8 bytes)
    static volatile uint8_t g_pkt[8] = {0};

    // ================= LED =================
    static const uint32_t LED_FAST_MS = 120;
    static const uint32_t LED_SLOW_MS = 700;
    static uint32_t led_last_ms = 0;
    static bool led_state = false;

    static void led_update() {
      bool stale = false;
      if (g_haveWeather && g_lastOkMs != 0) stale = ((millis() - g_lastOkMs) > STALE_AFTER_MS);

      if (g_haveWeather && !stale) {
        digitalWrite(LED_BUILTIN, HIGH);
        return;
      }

      uint32_t now = millis();
      if (!g_wifiOk) {
        if (now - led_last_ms >= LED_FAST_MS) {
          led_last_ms = now;
          led_state = !led_state;
          digitalWrite(LED_BUILTIN, led_state ? HIGH : LOW);
        }
        return;
      }

      if (now - led_last_ms >= LED_SLOW_MS) {
        led_last_ms = now;
        led_state = !led_state;
        digitalWrite(LED_BUILTIN, led_state ? HIGH : LOW);
      }
    }

    // ================= UTILS =================
    static uint8_t crc_xor(const uint8_t* p, size_t n) {
      uint8_t c = 0;
      for (size_t i = 0; i < n; i++) c ^= p[i];
      return c;
    }

    static bool ip_is_nonzero(IPAddress ip) {
      return !(ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0);
    }

    static void wifi_ensure_connected() {
      if (WiFi.status() == WL_CONNECTED && ip_is_nonzero(WiFi.localIP())) {
        g_wifiOk = true;
        return;
      }

      g_wifiOk = false;

      Serial.println("WiFi: connecting...");
      WiFi.begin(WIFI_SSID, WIFI_PASS);

      uint32_t start = millis();
      while (millis() - start < WIFI_CONNECT_WINDOW_MS) {
        if (WiFi.status() == WL_CONNECTED) {
          uint32_t ipStart = millis();
          while (millis() - ipStart < WIFI_IP_WINDOW_MS) {
            IPAddress ip = WiFi.localIP();
            if (ip_is_nonzero(ip)) {
              g_wifiOk = true;
              Serial.print("WiFi: connected, IP=");
              Serial.println(ip);
              return;
            }
            delay(25);
          }
         ...

    Read more »

  • 1Dot22Dot25

    Rhea Rae01/22/2026 at 20:39 0 comments

    Project Log — Stardate 1Dot22Dot25

    R3 Master / R4 Slave configuration is operational. Primary system objectives have been met and baseline functionality is confirmed. Minor firmware anomalies remain and are currently under evaluation.

    Network connectivity is established; however, after extended operational periods the connection becomes intermittent and may disengage unexpectedly.

    System startup sequence is presently order-dependent. The R4 Slave must be powered prior to the R3 Master for proper clock output to render on the Ender-3 Pro display.

    Automatic external temperature updates are functional but exhibit inconsistent refresh behavior, indicating a possible synchronization or timing issue between devices.

    An alpha release of both firmware components will be published shortly to support collaborative review, testing, and external input.

    Overall system status remains stable. Continued observation and incremental firmware refinement are in progress.

    -RR

  • R4 Weather Slave

    Rhea Rae01/20/2026 at 16:28 0 comments

  • Ender Clock-Duino Sketch w/ DS3231

    Rhea Rae01/19/2026 at 19:53 0 comments

    #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...

    Read more »

  • DS3231

    Rhea Rae01/15/2026 at 17:05 0 comments

    This DS3231 RTC module includes a charge path intended for a rechargeable LIR2032, implemented using a small series resistor and a glass diode feeding the VBAT node. Since I am using a standard CR2032 (non-rechargeable), I disabled the charging path.

    To do this, I identified the resistor located directly next to the glass diode associated with the battery circuit (both resistors on this board are marked “102”). I lifted one leg of the resistor that feeds the diode, effectively opening the VCC → VBAT charging path while leaving the battery connected to the DS3231’s VBAT pin.

    After lifting the resistor, I installed a CR2032 coin cell, set the time, unplugged the clock for several minutes, and then re-applied power. The RTC retained the correct time, confirming that VBAT backup is functioning normally and that the coin cell is no longer being charged from VCC.

    This modification prevents unintended charging of a non-rechargeable coin cell while preserving full RTC backup operation.

    -RR

  • Scrambled

    Rhea Rae01/12/2026 at 18:28 0 comments

    If screen is scrambled switch pin D13 and D11.

    -RR

  • ST7920 Pin-Out

    Rhea Rae01/12/2026 at 18:25 0 comments

    Use EXP 3 Header!

    -RR

  • Ender Clock-Duino Sketch w/o DS3231

    Rhea Rae01/12/2026 at 18:19 0 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...

    Read more »

View all 9 project logs

  • 1
    Wiring Table Ender 3 Display to R3

    Wiring Table

    Ender EXP3 Pin Connect To
    5V5V
    GNDGND
    Chip SelectD10
    DataD11
    ClockD13
    Knob RotationD2
    Knob Rotation 2D3
    Knob ButtonD4
    BeeperD8
  • 2
    DS3231 Wiring Table to R3

    DS3231 RTC (I2C)

    RTC PinUno Pin
    VCC5V
    GNDGND
    SDAA4
    SCLA5
  • 3
    Outdoor Weather R4 Wiring Table

    Outdoor Weather R4 (I2C, address 0x12) Connects to R3

    Also uses I2C bus:

    R4 PinUno Pin
    VCC5V or 3.3V (depends on board)
    GNDGND
    SDAA4
    SCLA5

View all 3 instructions

Enjoy this project?

Share

Discussions

Does this project spark your interest?

Become a member to follow this project and never miss any updates