-
R3_Master
02/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 minutesint16_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..59bool 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, 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
temp_f_cached = (tc * 9.0f / 5.0f) + 32.0f; // Fahrenheit
last_temp_read_ms = now_ms;
}
}// ================= OUTDOOR I2C READ =================
static uint8_t pkt_crc_xor(const uint8_t* p, uint8_t n) {
uint8_t c = 0;
for (uint8_t i = 0; i < n; i++) c ^= p[i];
return c;
}static bool read_outdoor_from_r4() {
uint8_t b[8];
uint8_t got = 0;// record last try time
out_last_try_ms = millis();Wire.requestFrom((int)R4_I2C_ADDR, 8);
while (Wire.available() && got < 8) b[got++] = (uint8_t)Wire.read();
while (Wire.available()) (void)Wire.read();if (got != 8) { out_last_err = 1; return false; }
if (pkt_crc_xor(b, 7) != b[7]) { out_last_err = 2; return false; }uint8_t flags = b[6];
if ((flags & (1 << 1)) == 0) { out_last_err = 3; return false; } // weatherOkint16_t tempF10 = (int16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8));
int16_t whole = (int16_t)((tempF10 >= 0) ? (tempF10 + 5) / 10 : (tempF10 - 5) / 10);out_temp_f_whole = whole;
out_last_good_ms = millis();
out_last_err = 0;
return true;
}static void outdoor_update_if_due() {
uint32_t now = millis();
if (out_last_poll_ms != 0 && (now - out_last_poll_ms) < OUT_POLL_PERIOD_MS) return;
out_last_poll_ms = now;// Retry a few times so a single bad moment doesn't waste a whole poll window.
for (uint8_t i = 0; i < 3; i++) {
if (read_outdoor_from_r4()) return;
delay(15);
}
}// ================= TEMP FORMAT =================
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);
}// New behavior:
// - OUT NA only if we've never gotten a valid temp.
// - If stale, show last value with a '?' to indicate it's old.
static void format_temp_out_integer(char *out, size_t n, int16_t tfWhole, bool stale) {
if (tfWhole == INT16_MIN) { snprintf(out, n, "OUT NA"); return; }
if (stale) { snprintf(out, n, "OUT %dF?", (int)tfWhole); return; }
snprintf(out, n, "OUT %dF", (int)tfWhole);
}// ================= 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();
Wire.setClock(100000);rtc.begin();
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();out_temp_f_whole = INT16_MIN;
out_last_good_ms = 0;
out_last_poll_ms = 0;
out_last_err = 0;
out_last_try_ms = 0;// Poll once at boot so OUT shows quickly
outdoor_update_if_due();
}// ================= LOOP =================
void loop() {
rtc_read_cached();
rtc_temp_read_cached();
outdoor_update_if_due();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 inbuf[14];
format_temp_in_integer(inbuf, sizeof(inbuf), temp_f_cached);bool out_stale = (out_last_good_ms == 0) || (millis() - out_last_good_ms > OUT_STALE_MS);
char outbuf[14];
format_temp_out_integer(outbuf, sizeof(outbuf), out_temp_f_whole, out_stale);char footer[32];
// ===== Draw =====
u8g2.firstPage();
do {
// Header title
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 16, set_mode ? "SET" : "Ender Clock-Duino");// OUT upper-left, IN upper-right
u8g2.setFont(u8g2_font_4x6_tf);
u8g2.drawStr(0, 7, outbuf);uint8_t in_w = u8g2.getStrWidth(inbuf);
u8g2.drawStr(128 - in_w, 7, inbuf);// AM/PM ONLY in 12-hour mode
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);
}
}// Tiny debug indicator bottom-right, only if error present
if (out_last_err != 0) {
char ebuf[8];
snprintf(ebuf, sizeof(ebuf), "E%u", (unsigned)out_last_err);
uint8_t ew = u8g2.getStrWidth(ebuf);
u8g2.setFont(u8g2_font_4x6_tf);
u8g2.drawStr(128 - ew, 63, ebuf);
}} while (u8g2.nextPage());
delay(10);
} -
R4_Slave
02/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 minstatic 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 fetchstatic bool g_wifiOk = false; // connected + real IP
static bool g_haveWeather = false; // ever fetched successfully since bootstatic 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);
}
Serial.print("WiFi: connected but no IP, IP=");
Serial.println(WiFi.localIP());
g_wifiOk = false;
return;
}
delay(50);
}Serial.println("WiFi: connect timeout");
g_wifiOk = false;
}// Parse helpers
static bool parse_float_after_key(const String &s, int startAt, const char* key, float &out) {
int idx = s.indexOf(key, startAt);
if (idx < 0) return false;
idx += (int)strlen(key);
while (idx < (int)s.length() && s[idx] == ' ') idx++;
int end = idx;
while (end < (int)s.length()) {
char c = s[end];
if ((c >= '0' && c <= '9') || c == '-' || c == '.') end++;
else break;
}
if (end <= idx) return false;
out = s.substring(idx, end).toFloat();
return true;
}static bool parse_int_after_key(const String &s, int startAt, const char* key, int &out) {
int idx = s.indexOf(key, startAt);
if (idx < 0) return false;
idx += (int)strlen(key);
while (idx < (int)s.length() && s[idx] == ' ') idx++;
int end = idx;
while (end < (int)s.length()) {
char c = s[end];
if (c >= '0' && c <= '9') end++;
else break;
}
if (end <= idx) return false;
out = s.substring(idx, end).toInt();
return true;
}static bool fetch_weather_once(int16_t &tempF10_out, uint16_t &code_out) {
if (!g_wifiOk) return false;// DNS sanity check (only costs a tiny bit)
IPAddress hostIp;
int dnsOk = WiFi.hostByName(WEATHER_HOST, hostIp);
Serial.print("DNS ");
Serial.print(WEATHER_HOST);
Serial.print(" ok=");
Serial.print(dnsOk);
Serial.print(" ip=");
Serial.println(hostIp);
if (dnsOk == 0 || !ip_is_nonzero(hostIp)) return false;WiFiSSLClient ssl;
HttpClient client(ssl, WEATHER_HOST, WEATHER_PORT);
client.setHttpResponseTimeout(HTTP_TIMEOUT_MS);char path[240];
snprintf(
path, sizeof(path),
"/v1/forecast?latitude=%.4f&longitude=%.4f¤t_weather=true&temperature_unit=fahrenheit&timezone=auto",
(double)LATITUDE, (double)LONGITUDE
);Serial.print("HTTP GET ");
Serial.println(path);client.get(path);
int status = client.responseStatusCode();
Serial.print("HTTP status: ");
Serial.println(status);String body = client.responseBody();
Serial.print("Body len: ");
Serial.println(body.length());
Serial.print("Body head: ");
Serial.println(body.substring(0, 140));if (status != 200) return false;
if (body.length() < 50) return false;int cw = body.indexOf("\"current_weather\"");
if (cw < 0) {
Serial.println("No current_weather object");
return false;
}float tF = NAN;
int weathercode = -1;bool okT = parse_float_after_key(body, cw, "\"temperature\":", tF);
bool okC = parse_int_after_key(body, cw, "\"weathercode\":", weathercode);if (!okT || !okC || isnan(tF) || weathercode < 0) return false;
tempF10_out = (int16_t)lroundf(tF * 10.0f);
if (weathercode > 65535) weathercode = 65535;
code_out = (uint16_t)weathercode;return true;
}static void rebuild_packet_cache() {
uint32_t lastOk = g_lastOkMs;uint32_t ageMs = (lastOk == 0) ? 0xFFFFFFFFUL : (millis() - lastOk);
uint32_t ageSec32 = (ageMs == 0xFFFFFFFFUL) ? 65535UL : (ageMs / 1000UL);
if (ageSec32 > 65535UL) ageSec32 = 65535UL;
uint16_t ageSec = (uint16_t)ageSec32;bool stale = false;
if (g_haveWeather && lastOk != 0) stale = ((millis() - lastOk) > STALE_AFTER_MS);uint8_t pkt[8];
int16_t t = g_tempF10;
uint16_t c = g_wmoCode;pkt[0] = (uint8_t)(t & 0xFF);
pkt[1] = (uint8_t)((t >> 8) & 0xFF);pkt[2] = (uint8_t)(c & 0xFF);
pkt[3] = (uint8_t)((c >> 8) & 0xFF);pkt[4] = (uint8_t)(ageSec & 0xFF);
pkt[5] = (uint8_t)((ageSec >> 8) & 0xFF);// bit0 = wifiOk, bit1 = haveWeather, bit2 = stale
uint8_t flags = 0;
if (g_wifiOk) flags |= (1 << 0);
if (g_haveWeather) flags |= (1 << 1);
if (stale) flags |= (1 << 2);
pkt[6] = flags;pkt[7] = crc_xor(pkt, 7);
noInterrupts();
for (uint8_t i = 0; i < 8; i++) g_pkt[i] = pkt[i];
interrupts();
}static void weather_update_if_due() {
uint32_t now = millis();uint32_t period = g_haveWeather ? WEATHER_PERIOD_OK_MS : g_failBackoffMs;
if (g_lastAttemptMs != 0 && (now - g_lastAttemptMs) < period) return;
g_lastAttemptMs = now;wifi_ensure_connected();
if (!g_wifiOk) {
Serial.println("Weather: skip (no wifi)");
// backoff grows when failing before first success
if (!g_haveWeather) {
g_failBackoffMs = min(g_failBackoffMs * 2UL, FAIL_BACKOFF_MAX_MS);
Serial.print("Backoff now ");
Serial.print(g_failBackoffMs / 1000UL);
Serial.println("s");
}
rebuild_packet_cache();
return;
}int16_t newTempF10 = 0;
uint16_t newCode = 0;bool ok = fetch_weather_once(newTempF10, newCode);
if (ok) {
g_tempF10 = newTempF10;
g_wmoCode = newCode;
g_lastOkMs = now;
g_haveWeather = true;
g_failBackoffMs = WEATHER_PERIOD_FAIL_MS; // reset backoff
Serial.println("Weather: OK");
} else {
Serial.println("Weather: FAIL (keeping last good if any)");
if (!g_haveWeather) {
g_failBackoffMs = min(g_failBackoffMs * 2UL, FAIL_BACKOFF_MAX_MS);
Serial.print("Backoff now ");
Serial.print(g_failBackoffMs / 1000UL);
Serial.println("s");
}
}rebuild_packet_cache();
}// I2C request: send cached 8-byte packet (FAST)
static void onI2CRequest() {
Wire.write((const uint8_t*)g_pkt, 8);
}void setup() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);Serial.begin(115200);
delay(300);
Serial.println("R4 Weather Slave starting...");Wire.begin(I2C_ADDR);
Wire.onRequest(onI2CRequest);g_tempF10 = 0;
g_wmoCode = 0;
g_lastOkMs = 0;
g_wifiOk = false;
g_haveWeather = false;
g_lastAttemptMs = 0;
g_failBackoffMs = WEATHER_PERIOD_FAIL_MS;rebuild_packet_cache();
weather_update_if_due();
}void loop() {
weather_update_if_due();
led_update();
delay(10);
} -
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

