#include <ESP32RotaryEncoder.h>
#include <TimeLib.h>
#include <SPI.h>
#include <Wire.h>
#include <RTClib.h>
#define ENCODER_CLK 32
#define ENCODER_DT 33
#define ENCODER_K 25
// domyślne piny
#define I2C_SDA 21
#define I2C_SCL 22
// faktycznie używany typ RTC to DS3231
RTC_DS3231 rtc;
// biblioteka obsługuje też przycisk, ale na ESP-C3 powoduje panic na rdzeniu 0 z powodu timeoutu w obsłudze przerwania
RotaryEncoder rotaryEncoder(ENCODER_CLK, ENCODER_DT);
// zapamiętanie poprzedniego stanu przycisku
bool SelBtnOld = false;
// znacznik czasu zwolnienia przycisku
unsigned long BtnRelease = 0;
// skrócony czas odświeżania (ms) podczas ekranu ustawień, żeby ograniczyć migotanie
unsigned long displayRefresh = 5000;//500
// następny zaplanowany czas odświeżenia (millis)
unsigned long displayRefreshNext = 0;
boolean blinking = true;
// znacznik czasu w ms
unsigned long previousBlinking = 0;
// pół sekundy włącz/wyłącz = częstotliwość 1 Hz
const unsigned long periodBlinking = 5000; //500
void SelClick();
void countUp();
void countDn();
// zmienne menu
byte menu = 0;
int temp = 0;
// stary (ciepły) kolor żarówki, można zmienić według uznania
#define TFT_COLOR1 0x6519
#define TFT_BLACK 0x0000
#define TFT_RED 0xF800
// kolor ramek ustawień
uint16_t TFT_BOXES = TFT_COLOR1;
const uint8_t TZcolumns = 4;
const char *const TZdata[][TZcolumns] = {
{ "Europe", "CET-1CEST,M3.5.0/2:00:00,M10.5.0/2:00:00", "CET", "CEST" }, // Central European Time, Central European Summer Time
{ "UK", "GMT0BST,M3.5.0/2:00:00,M10.5.0/2:00:00", "GMT", "BST" }, // UK GMT, British Summer Time
{ "UTC", "GMT", "UTC", "UTC" }, // UTC
{ "Eastern", "EST5EDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "EST", "EDT" },
{ "Central", "CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "CST", "CDT" },
{ "Mountain", "MST7MDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "MST", "MDT" },
{ "Arizona", "MST7", "MST", "MST" },
{ "Pacific", "PST8PDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "PST", "PDT" },
{ "Alaska", "ASKT9AKDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "AKST", "AKDT" },
{ "Hawaii", "HST10", "HST", "HST" },
};
// liczba wierszy w tablicy TZdata
const uint8_t TZrows = sizeof(TZdata) / sizeof(TZdata[0]);
// domyślna strefa czasowa: Europe
uint8_t TZselect = 0;
// wyświetlana nazwa strefy
char TZstr[] = "xxxxxxxxxxxx";
// poprzednia wartość, żeby skasować stary tekst przed wpisaniem nowego
char TZstrO[] = "xxxxxxxxxxxx";
// aktualny czas UTC w formie epoki (sekundy od 1.1.1970)
time_t UTCsec;
time_t prevTime = 0;
// format wyświetlania czasu/dat/strefy
char thisTime[10] = "00:00"; // miejsce na "23:59"
// poprzednie wyświetlane ciągi czasu/dat
char thisTimeO[10] = "00:00";
char thisDate[20] = "2000.00.00 ";
char thisDateO[20] = "2000.00.00 ";
char thisWk[16]; // np. "Poniedziałek"
char thisWkO[16];
char thisDst[12]; // np. "DST=Z" / "DST=L" / ""
char thisDstO[12];
char TZmode[10]; // "czas zimowy (CET)" / "czas letni (CEST)"
char TZmodeO[10];
char outDate[16];
char outTime[8];
// wyzwolanie obliczeń faz natychmiast podczas zmian ustawień, w normalnym trybie tylko raz na minutę
bool phaseNow = true;
time_t NextMoonUTCO = 0;
// szegokość geograficzna −90∘ do +90∘
double observer_lat = 45.6;
// długość geograficzna −180∘ do +180∘
double observer_lon = 10.3;
// struktura lokalnej strefy czasowej
struct tm ltz;
void updateClock();
void setup() {
Serial.begin(115200);
pinMode(ENCODER_CLK, INPUT);
pinMode(ENCODER_DT, INPUT);
pinMode(ENCODER_K, INPUT_PULLUP);
Wire.begin(); // I2C
// inicjalizacja zegara RTC DS3231
if (!rtc.begin()) {
Serial.println("Błąd: nie mogę znaleźć RTC DS3231!");
while (1) {
delay(100);
}
}
// >>> NOWA SEKCJA TYMCZASOWA DLA WOKWI <<<
DateTime now_check = rtc.now();
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// >>> KONIEC SEKCJI HACKUJĄCEJ WOKWI <<<
DateTime now_rtc = rtc.now();
Serial.printf("DEBUG: Raw RTC Time (Expected UTC): %04d-%02d-%02d %02d:%02d:%02d\n", now_rtc.year(), now_rtc.month(), now_rtc.day(), now_rtc.hour(), now_rtc.minute(), now_rtc.second());
// Uaktualnij TimeLib z RTC + policz UTCsec
updateClock();
// Aktualizuj wyświetlane stringi
GetTimeStr();
// USTAW RTC NA UTC
////setenv("TZ", "UTC", 1);
////tzset();
// TZ na lokalną tylko do wyświetlania
setenv("TZ", TZdata[TZselect][1], 1);
tzset();
// Pierwsze wyliczenie lokalnego thisDate / thisTime
convUnix(UTCsec);
// Jeśli RTC stracił zasilanie, ustaw na UTC
if (rtc.lostPower()) {
Serial.println("RTC stracił zasilanie - ustawiam UTC");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}/*
if (rtc.lostPower()) {
Serial.println("RTC stracił zasilanie - ustawiam UTC");
// Tutaj też używamy UTC do ustawienia RTC
setenv("TZ", "UTC", 1);
tzset();
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
setenv("TZ", TZdata[TZselect][1], 1);
tzset();
}*/
UTCsec = DateTime(year(now()), month(now()), day(now()), hour(now()), minute(now()), second(now())).unixtime();
// konfiguracja enkodera
if (digitalRead(ENCODER_K) == LOW) {
Serial.println("BTN LOW");
}
// enkoder nie ma rezystorów podciągających (w przeciwieństwie do wersji HAS_PULLUP na płytce modułowej)
rotaryEncoder.setEncoderType(EncoderType::FLOATING);
//rotaryEncoder.setEncoderType(EncoderType::HAS_PULLUP);
// Parametry: dolny_limit, górny_limit, false = bez zapętlania; odczyty: 0 = krok w lewo, 1 = krok w prawo
rotaryEncoder.setBoundaries(0, 1, false);
// wskaźnik na funkcję obsługi
rotaryEncoder.onTurned(RotaryEnc);
// brak potrzeby dodatkowej obsługi enkodera w loop()
rotaryEncoder.begin(true);
// inicjalizacja magistrali SPI dla wyświetlaczy
}
int lastClk = HIGH;
void loop() {
// debug
//debugButton();
/*
bool btn = digitalRead(ENCODER_K);
// wciśnięcie przycisku (z debounce po puszczeniu)
if (btn == LOW && SelBtnOld == false && millis() - BtnRelease > 50) {
SelClick();
//Serial.println("SelClick w loop");
}
// zwolnienie przycisku -> przygotowanie na kolejne kliknięcie
if (btn == HIGH && SelBtnOld == true) {
SelBtnOld = false;
BtnRelease = millis();
}
*/
if (digitalRead(ENCODER_K) == LOW && SelBtnOld == false && millis() - BtnRelease > 50) {
// wciśnięcie przycisku – przejście do obsługi po zwolnieniu, z debounce przy puszczeniu
SelClick();
}
// reset stanu przycisku, jeśli aktualnie nie wciśnięty, a poprzednio był
if (digitalRead(ENCODER_K) == HIGH && SelBtnOld == true) {
SelBtnOld = false;
// zapamiętanie czasu zwolnienia przycisku i debounce 50 ms
BtnRelease = millis();
}
// pobranie czasu z RTC i aktualizacja zegara programowego raz na godzinę
////if ((minute(now()) == 0 && updatedHWclock == false)) {
// ustawienie zegara programowego
//updateClock();
// zablokowanie ponownej aktualizacji w tej samej minucie (minute = 0)
////updatedHWclock = true;
////}
if (minute(now()) != 0) {
// zniesienie blokady aktualizacji zegara
//updatedHWclock = false;
}
// timer 500 ms dla selektora ustawień, aktualizacji UTCsec i migającej ramki
// ta część pętli wykonywana jest co 500 ms
if (millis() - previousBlinking >= periodBlinking) {
// zapamiętanie czasu ostatniego „mignięcia”
previousBlinking = millis();
// przełączanie wskaźnika ustawień (migającej ramki)
blinking = !blinking;
// okresowe uaktualnianie UTCsec z zegara programowego
/////UTCsec = DateTime(year(now()), month(now()), day(now()), hour(now()), minute(now()), second(now())).unixtime();
// natychmiastowa aktualizacja tekstu podczas zmian ustawień
if (menu != 0) {
// miganie ramki ustawień
if (blinking) {
TFT_BOXES = TFT_COLOR1;
} else {
TFT_BOXES = TFT_BLACK;
}
//updateTxtDisplay();
}
}
//debugButton();
}
// czas lokalny -> thisDate / thisTime: używa localtime_r oraz aktualnego TZ
void convUnix(time_t locUTC) {
// Wywołanie localtime_r UŻYJE GLOBALNEJ ZMIENNEJ ŚRODOWISKOWEJ TZ
// (ustawionej na CET-1CEST,M3.5.0/2:00:00,M10.5.0/2:00:00)
// i wypełni GLOBALNĄ strukturę ltz z Czasem Lokalnym
//localtime_r(&locUTC, <z);
// Zmienna locUTC to w naszym przypadku UTCsec, a wynik w ltz to CZAS LOKALNY
///sprintf(thisTime, "%02d:%02d", ltz.tm_hour, ltz.tm_min);
///sprintf(thisDate, "%04d.%02d.%02d", ltz.tm_year + 1900, ltz.tm_mon + 1, ltz.tm_mday);
}
// czas trwania kliknięcia używany do wejścia w tryb ustawień
void SelClick() {
//Serial.println("SelClick() WYWOŁANE");
// zapamiętanie aktualnego stanu przycisku
SelBtnOld = true;
// uniknięcie zmian, gdy wyświetlacz dopiero się włączył
/*
if (!dispActive) {
return;
}
*/
// zapamiętanie czasu pierwszego naciśnięcia
unsigned long firstPress = millis();
while (digitalRead(ENCODER_K) == LOW) {
// długie naciśnięcie (>600 ms)
if (millis() - firstPress > 600) {
switch (menu) {
case 0:
// ustawienia daty/czasu
menu = 11;
break;
default:
// nieużywane – powrót do ekranu głównego
menu = 0;
break;
}
Serial.print("DŁUGI klik, menu = ");
Serial.println(menu);
return;
}
}
switch (menu) {
case 11:
menu = 12;
// wyczyszczenie ramki „Miesiąc”
// tftText.drawRect(2, 247, 30, 18, TFT_RED);//TFT_BLACK
break;
case 12:
menu = 13;
// „Dzień”
// tftText.drawRect(34, 247, 30, 18, TFT_RED);
break;
case 13:
menu = 14;
// „Rok”
// tftText.drawRect(66, 247, 30, 18, TFT_RED);
break;
case 14:
menu = 15;
// „Godzina”
// tftText.drawRect(112, 247, 30, 18, TFT_RED);
break;
case 15:
menu = 16;
// „Minuta”
// tftText.drawRect(144, 247, 30, 18, TFT_RED);
break;
case 16:
menu = 17;
// „Strefa czasowa”
// tftText.drawRect(112, 267, 110, 18, TFT_RED);
break;
case 17:
menu = 18;
// „Szerokość”
// tftText.drawRect(2, 302, 110, 18, TFT_RED);
break;
case 18:
menu = 0;
// „Długość”
// tftText.drawRect(112, 302, 110, 18, TFT_RED);
// koniec menu – zapis ustawień
//write_NVRAM();
break;
default:
break;
}
Serial.print("KRÓTKI klik, menu = ");
Serial.println(menu);
}
// enkoder na panelu frontowym
void RotaryEnc(long EncPos) {
Serial.print("ENC = ");
Serial.println(EncPos);
// odświeżenie timera wygaszania wyświetlacza
//dispTimer = millis() + periodDisplay;
// uniknięcie zmian, gdy wyświetlacz dopiero się włączył
/*
if (!dispActive) {
return;
}
*/
if (EncPos == 0) {
countDn();
} else {
countUp();
}
}
// funkcja wywoływana przy obrocie w prawo
void countUp() {
switch (menu) {
case 11:
// dodaj jeden miesiąc
temp = month(UTCsec) + 1;
if (temp > 12) {
temp = 1;
}
UTCsec = DateTime(year(UTCsec), temp, day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
// UTCsec = mktime(&);
break;
case 12:
// dodaj jeden dzień
UTCsec += 86400;
break;
case 13:
// dodaj jeden rok
temp = year(UTCsec) + 1;
if (temp > 2099) {
temp = 2099;
}
UTCsec = DateTime(temp, month(UTCsec), day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
break;
case 14:
// dodaj jedną godzinę
UTCsec += 3600;
break;
case 15:
// dodaj jedną minutę
UTCsec += 60;
break;
case 16:
// następna strefa czasowa
TZselect += 1;
if (TZselect >= TZrows) {
TZselect = 0;
}
// ustaw strefę czasową na nowo wybraną
setenv("TZ", TZdata[TZselect][1], 1);
// uaktywnienie strefy
tzset();
// flaga wymuszająca odświeżenie tabeli kolejnych faz Księżyca
NextMoonUTCO = 0UL;
break;
case 17:
// dodaj 1 stopień szerokości geograficznej
observer_lat += 1;
if (observer_lat > 90) {
observer_lat -= 180;
}
break;
case 18:
// dodaj 1 stopień długości geograficznej
observer_lon += 1;
if (observer_lon > 180) {
observer_lon -= 360;
}
break;
default:
// brak zmian, jeśli nie jesteśmy w trybie ustawień (żeby uniknąć przypadkowej zmiany RTC)
return;
break;
}
// aktualizacja RTC tylko przy zmianie ustawień
updateRTC();
}
// funkcja wywoływana przy obrocie w lewo
void countDn() {
switch (menu) {
case 11:
// cofnięcie o jeden miesiąc
temp = month(UTCsec) - 1;
if (temp <= 0) {
temp = 12;
}
UTCsec = DateTime(year(UTCsec), temp, day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
break;
case 12:
// cofnięcie o jeden dzień
UTCsec -= 86400;
break;
case 13:
// cofnięcie o jeden rok
temp = year(UTCsec) - 1;
if (temp < 2000) {
temp = 2000;
}
UTCsec = DateTime(temp, month(UTCsec), day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
break;
case 14:
// cofnięcie o jedną godzinę
UTCsec -= 3600;
break;
case 15:
// cofnięcie o jedną minutę
UTCsec -= 60;
break;
case 16:
// poprzednia strefa czasowa
TZselect -= 1;
if (TZselect >= TZrows) {
TZselect = TZrows - 1;
}
// ustawienie strefy czasowej na nowo wybraną
setenv("TZ", TZdata[TZselect][1], 1);
// uaktywnienie strefy
tzset();
// flaga wymuszająca odświeżenie tabeli kolejnych faz Księżyca
NextMoonUTCO = 0UL;
break;
case 17:
// zmniejszenie szerokości geograficznej o 1 stopień
observer_lat -= 1;
if (observer_lat < -90) {
observer_lat += 180; // zawinięcie
}
break;
case 18:
// zmniejszenie długości geograficznej o 1 stopień
observer_lon -= 1;
if (observer_lon < -180) {
observer_lon += 360; // zawinięcie
}
break;
default:
// brak zmian, jeśli nie jesteśmy w trybie ustawień (żeby uniknąć przypadkowej zmiany RTC)
return;
break;
}
// aktualizacja RTC tylko przy zmianie ustawień
updateRTC();
}
void setTimeFromCompileTime() {
const char *date = __DATE__; // np. "Nov 30 2025"
const char *timeStr = __TIME__; // np. "13:42:05"
char monthStr[4];
int day, year, hour, minute, second;
// rozbijamy "__DATE__"
sscanf(date, "%3s %d %d", monthStr, &day, &year);
// rozbijamy "__TIME__"
sscanf(timeStr, "%d:%d:%d", &hour, &minute, &second);
int month = 0;
if (!strcmp(monthStr, "Jan")) month = 1;
else if (!strcmp(monthStr, "Feb")) month = 2;
else if (!strcmp(monthStr, "Mar")) month = 3;
else if (!strcmp(monthStr, "Apr")) month = 4;
else if (!strcmp(monthStr, "May")) month = 5;
else if (!strcmp(monthStr, "Jun")) month = 6;
else if (!strcmp(monthStr, "Jul")) month = 7;
else if (!strcmp(monthStr, "Aug")) month = 8;
else if (!strcmp(monthStr, "Sep")) month = 9;
else if (!strcmp(monthStr, "Oct")) month = 10;
else if (!strcmp(monthStr, "Nov")) month = 11;
else if (!strcmp(monthStr, "Dec")) month = 12;
// ustawiamy zegar programowy TimeLib
setTime(hour, minute, second, day, month, year);
// i od razu zapisujemy epoch
UTCsec = now();
}
void updateRTC() {
setTime(UTCsec); // set HW clock
rtc.adjust(UTCsec); // update RTC
phaseNow = true; // trigger moon calc immediately, override once a minute calc interval
}
void updateClock() {
// Get epoch time from RTC. Ta funkcja jest KLUCZOWA - pobiera CZAS UTC.
UTCsec = rtc.now().unixtime();
// set HW clock (ustawia czas systemowy TimeLib na CZAS UTC, co jest poprawną praktyką)
setTime(UTCsec);
}
// pobranie lokalnego czasu na podstawie UTCsec i wpisanie do thisTime / thisDate
void GetTimeStr() {
// UWAGA: Konwersja na czas lokalny (LT) musi być wykonana na początku, aby struktura ltz zawierała prawidłowe wartości do formatowania
// 1. Zabezpieczenie przed błędnym UTCsec. Użyj aktualniejszej daty granicznej
// 1704067200 to 1 stycznia 2024 00:00:00 UTC
if (UTCsec < 1704067200) {
//Serial.println("⚠️ Błędny UTCsec, używam RTC direct (FALLBACK)");
DateTime now = rtc.now();
// Fallback: Formatowanie bezpośrednio z RTC (BEZ KOREKTY STREFY/DST!)
snprintf(thisTime, sizeof(thisTime), "%02d:%02d", now.hour(), now.minute());
snprintf(thisDate, sizeof(thisDate), "%04d.%02d.%02d", now.year(), now.month(), now.day());
snprintf(TZstr, sizeof(TZstr), "BŁĄD"); // Ustaw stan błędu dla TZ
return;
}
// 2. KLUCZOWY KROK: Konwersja UTC na Czas Lokalny (LT)
// Funkcja localtime_r używa globalnej konfiguracji TZ do konwersji czasu UTCsec i wypełnienia GLOBALNEJ struktury 'ltz' czasem lokalnym
localtime_r(&UTCsec, <z);
//Serial.print("🔍 GetTimeStr - UTCsec: ");
//Serial.println(UTCsec);
// 3. FORMATOWANIE CZASU LOKALNEGO Z ltz (LT)
// CZAS – tylko HH:MM
snprintf(thisTime, sizeof(thisTime), "%02d:%02d", ltz.tm_hour, ltz.tm_min);
// DATA
snprintf(thisDate, sizeof(thisDate), "%04d.%02d.%02d", ltz.tm_year + 1900, ltz.tm_mon + 1, ltz.tm_mday);
// dzień tygodnia
static const char *wdName[] = {"niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"};
int wday = ltz.tm_wday;
if (wday < 0 || wday > 6) wday = 0;
snprintf(thisWk, sizeof(thisWk), "%s", wdName[wday]);
// Czas letni/zimowy
const char *dstStr = "?";
if (ltz.tm_isdst == 1) {
dstStr = "letni"; // letni
} else if (ltz.tm_isdst == 0) {
dstStr = "zimowy"; // zimowy
}
snprintf(thisDst, sizeof(thisDst), "DST:%s", dstStr);
// Nazwa strefy czasowej (np. CET lub CEST)
if (TZselect < TZrows) {
// Indeks [3] to nazwa letnia (CEST), [2] to nazwa zimowa (CET)
if (ltz.tm_isdst == 1) {
snprintf(TZstr, sizeof(TZstr), "%s", TZdata[TZselect][3]);
} else {
snprintf(TZstr, sizeof(TZstr), "%s", TZdata[TZselect][2]);
}
} else {
snprintf(TZstr, sizeof(TZstr), "UTC");
}
}
void debugButton() {
static unsigned long lastDebug = 0;
static bool lastState = HIGH;
bool currentState = digitalRead(ENCODER_K);
if(currentState != lastState || millis() - lastDebug > 2000) {
Serial.printf("🔘 Przycisk: %s (LOW=%s)\n", currentState ? "HIGH" : "LOW", currentState ? "NIE" : "TAK");
lastState = currentState;
lastDebug = millis();
}
}