-
1Dot22Dot25
01/22/2026 at 20:39 • 0 commentsProject 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
01/20/2026 at 16:28 • 0 comments![]()
-
Ender Clock-Duino Sketch w/ DS3231
01/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..59bool 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);
} -
DS3231
01/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
01/12/2026 at 18:28 • 0 commentsIf screen is scrambled switch pin D13 and D11.
-RR
-
ST7920 Pin-Out
01/12/2026 at 18:25 • 0 comments![]()
Use EXP 3 Header!
-RR
-
Ender Clock-Duino Sketch w/o DS3231
01/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..59bool 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);
}
Rhea Rae

