#include <SPI.h>
#include <SD.h>
#include <PNGdec.h>
#include <Wire.h>
#include <RTClib.h>
#include <WiFi.h>
#include <TimeLib.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <U8g2_for_Adafruit_GFX.h>
#include "colors.h"
#include "MoonCalc.h"
#include <time.h>
#include <math.h>
// domyślna pozycja startowa (Caino), jeśli w NVRAM nie ma danych
// LAT = 45.613418;
// LON = 10.312439;
// szegokość geograficzna −90∘ do +90∘
double observer_lat = 45.6;
// długość geograficzna −180∘ do +180∘
double observer_lon = 10.3;
// wysokość n.p.m. w metrach
double observer_h_m = 385.0;
// nazwa miejsca
const char observer_place[] = "Caino";
const char *ssid = "Wokwi-GUEST";
const char *pass = "";
// czcionka U8g2 z polskimi znakami
//#define FONT1 u8g2_font_unifont_t_polish
//#define FONT1 u8g2_font_ncenR10_te // ok
#define FONT1 u8g2_font_unifont_te // ok
//#define FONT1 u8g2_font_missingplanet_t_all // 16x16 cyfrowe ok
//#define FONT1 u8g2_font_bitcasual_t_all // 12x15 fajna
//#define FONT1 u8g2_font_unifont_t_extended // 16x16 +
//#define FONT1 u8g2_font_8x13_te // 8x13
//#define FONT1 u8g2_font_9x15_te // 9x15
//#define FONT2 u8g2_font_unifont_t_weather
// SCLK, MOSI, MISO, DC, RST
#define TFT_MOSI 23 // esp:23 -> MOSI obu wyświetlaczy
#define TFT_SCLK 18 // esp:18 -> SCLK obu wyświetlaczy
#define TFT_MISO 19 // esp:19 -> MISO (i tak nieużywany, ale niech będzie)
#define TFT_DC 2 // esp:2 -> D/C obu wyświetlaczy
#define TFT_RST 4 // esp:4 -> RST obu wyświetlaczy
// ręczne przełączanie CS poza biblioteką;
#define DispText_CS 5
// wyświetlacz fazy Księżyca i względnego położenia Słońca/Księżyca/Ziemi
#define DispMoon_CS 16
// dwa osobne obiekty TFT – dla tekstu i dla Księżyca
Adafruit_ILI9341 tftText(DispText_CS, TFT_DC, TFT_RST);
Adafruit_ILI9341 tftMoon(DispMoon_CS, TFT_DC, TFT_RST);
// silnik czcionek U8g2 dla wyświetlacza tekstowego
U8G2_FOR_ADAFRUIT_GFX u8g2;
// osobny kontroler SPI dla karty SD (HSPI)
SPIClass spiSD(HSPI);
// klasa
MoonCalc moonCalc;
// połączenia SPI dla karty SD
#define MOSI 27
#define MISO 26
#define SCLK 14
#define SD_CS 15
// piny magistrali I2C
// domyślne piny
#define I2C_SDA 21
#define I2C_SCL 22
// old (warm) incandescent color, adjust as desired
#define TFT_COLOR1 0x6519 // 0xCD0C, 0x6519
// kolor ramek ustawień
uint16_t TFT_BOXES = TFT_COLOR1;//TFT_LTRED;
#define TFT_BLACK 0x0000
#define TFT_BLUE 0x001F
#define TFT_SKYBLUE 0x867D
#define TFT_DKGREEN 0x03E0
#define TFT_SILVER 0xC618
#define TFT_YELLOW 0xFFE0
#define TFT_DKYELLOW 0x8400
// obsługa obrazów PNG
PNG png;
// maksymalne wymiary obrazów PNG obracanych (tarcza oświetlonego Księżyca)
#define MAX_IMAGE_WIDTH 164
#define MAX_IMAGE_HEIGHT 164
// użycie wskaźników na wiersze do stworzenia bufora obrazu
uint16_t *originalImage[MAX_IMAGE_HEIGHT]; // 164 wskaźników × 4B = 656B
uint16_t *rotatedImage[MAX_IMAGE_HEIGHT]; // kolejne 656B
// informacje o obrazie
int16_t imageWidth = 0;
int16_t imageHeight = 0;
// biel jest traktowana jako kolor przezroczysty i nie jest rysowana na wyświetlaczu
uint16_t TRANSPARENT_COLOR = 0xFFFF;
// deklaracja funkcji ładującej konfigurację z karty SD
bool loadConfigFromSD();
// pierwszy przebieg po starcie
bool firstRun = true;
// WYGASZACZ EKRANÓW
bool dispActive = true;
unsigned long dispTimer = 0;
const unsigned long periodDisplay = 600000UL; // 10 min bezczynności
// zabezpieczenie przed wielokrotnym przeliczaniem, gdy seconds() nadal wynosi zero
bool MoonCalcDone = false;
// blokowanie ponownego ustawiania zegara sprzętowego w tej samej minucie (minute() == 0)
bool updatedHWclock = false;
// konfiguracja RTC
// faktycznie używany typ RTC to DS3231
RTC_DS3231 rtc;
// Zasady stref czasowych POSIX
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
{ "UTC", "GMT", "UTC", "UTC" }, // UTC
};
// 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];
// struktura lokalnej strefy czasowej
struct tm ltz;
// Zmienne do przechowywania danych astronomicznych
struct AstronomicalData {
// Dane Księżyca
double moonRA;
double moonDec;
double moonDistance;
double moonIllumination;
double moonAlt;
double moonAz;
// Dane Słońca
double sunRA;
double sunDec;
double sunDistance;
double sunAlt;
double sunAz;
// Kąty pozycyjne
double brightLimbAngle;
double poleAngle;
// Znaki zodiaku
String moonZodiac;
String sunZodiac;
};
AstronomicalData currentAstroData;
// struktura dla pozycji horyzontalnej
struct HorizontalPosition {
double altitude;
double azimuth;
};
// Zmienne faz księżyca oparte na MoonCalc
struct MoonPhaseEvent {
time_t timestamp;
int phaseType; // 0=nów, 1=I kwadra, 2=pełnia, 3=III kwadra
double distance; // odległość dla Super/Micro księżyca
};
MoonPhaseEvent moonPhases[240];
// Tablice stałych przeniesione na zewnątrz funkcji lub do sekcji globalnej/statycznej
// jeśli to możliwe, aby uniknąć wielokrotnej alokacji
static const double A[] = { 485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18, 17, 16, 14, 12, 12, 12, 9, 8 };
static const double B[] = { 324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58, 119.81, 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, 227.73, 15.45 };
static const double C[] = { 1934.136, 32964.467, 20.186, 445267.112, 45036.886, 22518.443, 65928.934, 3034.906, 9037.513, 33718.147, 150.678, 2281.226, 29929.562, 31555.956, 4443.417, 67555.328, 4562.452, 62894.029, 31436.921, 14577.848, 31931.756, 34777.259, 1222.114, 16859.074 };
// wyzwolanie obliczeń faz natychmiast podczas zmian ustawień, w normalnym trybie tylko raz na minutę
bool phaseNow = true;
// Faza 0–1199: 0 = nów, 300 = I kwadra, 600 = pełnia, 900 = III kwadra
int phaseRaw = 0;
// kąt oświetlonego brzegu tarczy
double limbAngle;
char limbAngleStr[28];
char limbAngleStrO[28];
// tekstowy zapis szerokości geograficznej obserwatora
char obsLat[20] = "Szer:-xxx.x";
char obsLatO[20] = "Szer:-xxx.x";
// tekstowy zapis długości geograficznej obserwatora
char obsLon[20] = "Dług:-xxx.x";
char obsLonO[20] = "Dług:-xxx.x";
// tekstowy zapis wysokości n.p.m. obserwatora
char obsNpm[20] = "npm:-xxx";
char obsNpmO[20] = "npm:-xxx";
// miejsce
char obsPlace[20];
char obsPlaceO[20];
// wysokość/azymut Księżyca i Słońca oraz odpowiadające im ciągi tekstowe
double moonAlt = 0;
char moonAltStr[20];
char moonAltStrO[20];
double moonAz = 0;
char moonAzStr[20];
char moonAzStrO[20];
double sunAlt = 0;
char sunAltStr[20];
char sunAltStrO[20];
double sunAz = 0;
char sunAzStr[20];
char sunAzStrO[20];
char tmp2str[64] = "***********************";
char tmp2strO[64] = "***********************";
// nazwa pliku z obrazem fazy Księżyca: 0.png do 39.png
char SDfilnam[20] = "xxxxxx.png";
char SDfilnamO[20] = "xxxxxx.png";
// Zmienne do przechowywania następnych faz
time_t NextMoonUTC[4];
int NextMoonInd[4];
time_t NextMoonUTCO = 0;
// 4 pory roku na rok, dla 5 lat
time_t seasonUTC[24];
time_t NextMoon[4];
// następny „czarny księżyc” (drugi nów w tym samym miesiącu)
time_t BkMoonM = 0;
// następny miesięczny niebieski Księżyc (druga pełnia w miesiącu)
time_t BlMoonM = 0;
// następny sezonowy nów księżyca w sezonie, jeśli sezon ma cztery nowie
time_t BkMoonS = 0;
// następny sezonowy niebieski Księżyc (trzecia pełnia z czterech w jednej porze roku)
time_t BlMoonS = 0;
// następny czerwony księżyc” (całkowitego zaćmienia Księżyca)
time_t RedMoon = 0;
// najbliższy Superksiężyc (pełnia blisko perygeum)
time_t SuperMoon = 0;
// najbliższy Mikroksiężyc (pełnia blisko apogeum)
time_t MicroMoon = 0;
// teksty czasu/daty dla niebieskiego/czarnego Księżyca oraz ich poprzednie wersje
char BkMoonMStr[40];
char BkMoonMStrO[40];
char BlMoonMStr[40];
char BlMoonMStrO[40];
char BkMoonSStr[40];
char BkMoonSStrO[40];
char BlMoonSStr[40];
char BlMoonSStrO[40];
char RedMoonStr[40];
char RedMoonStrO[40];
char SuperMoonStr[40];
char SuperMoonStrO[40];
char MicroMoonStr[40];
char MicroMoonStrO[40];
char moonIllumStr[30] = "";
char moonIllumStrO[30] = "";
char moonZodiacStr[32] = "";
char moonZodiacStrO[32] = "";
char sunZodiacStr[32] = "";
char sunZodiacStrO[32] = "";
void updateClock();
void updateTxtDisplay();
void displayMoonSunPos();
void LoadMoonFile(int idx);
void rotateImage(float angle);
void displayRotatedImage(int x, int y);
bool syncTimeFromNTP() {
Serial.println("Łączenie z WiFi w celu pobrania czasu NTP...");
WiFi.begin(ssid, pass);
unsigned long startAttempt = millis();
// czekamy max ~10s na WiFi
while (WiFi.status() != WL_CONNECTED && millis() - startAttempt < 10000) {
delay(200);
Serial.print(".");
}
Serial.println();
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Nie udało się połączyć z WiFi - brak synchronizacji NTP.");
return false;
}
Serial.println("Połączono z WiFi, pobieram czas z NTP...");
// 0,0 -> chcemy czysty UTC z NTP
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
time_t nowTs = 0;
int retry = 0;
// czekamy aż NTP zwróci sensowny timestamp (np. > 2024)
do {
nowTs = time(nullptr);
delay(500);
retry++;
} while ((nowTs < 1700000000) && retry < 20); // 1700000000 ~ 2023-11
if (nowTs < 1700000000) {
Serial.println("Nie udało się pobrać czasu z NTP.");
return false;
}
// mamy UTC z NTP
UTCsec = nowTs;
setTime(UTCsec); // TimeLib: now() = UTC
Serial.printf("NTP UTC: %04d-%02d-%02d %02d:%02d:%02d\n", year(), month(), day(), hour(), minute(), second());
// ustaw RTC DS3231 na ten sam UTC, żeby działał bez WiFi
rtc.adjust(DateTime(UTCsec));
// WiFi nie jest już potrzebne
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
return true;
}
// Funkcja do pobierania znaku zodiaku na podstawie rektascensji
// Poprawna funkcja do pobierania znaku zodiaku
String getZodiacSign(double declination, double rightAscension) {
// Prawidłowe obliczenie długości ekliptycznej
// Nachylenie ekliptyki (approx)
double ecliptic_obliquity = 23.4393;
// Konwersja współrzędnych równikowych na ekliptyczne
double ra_rad = rightAscension * 15.0 * M_PI / 180.0; // RA w stopniach, potem radiany
double dec_rad = declination * M_PI / 180.0;
double obl_rad = ecliptic_obliquity * M_PI / 180.0;
// Obliczenie długości ekliptycznej
double sin_lambda = sin(ra_rad) * cos(obl_rad) + tan(dec_rad) * sin(obl_rad);
double cos_lambda = cos(ra_rad);
double ecliptic_longitude_rad = atan2(sin_lambda, cos_lambda);
// Konwersja na stopnie i normalizacja
double ecliptic_longitude = ecliptic_longitude_rad * 180.0 / M_PI;
ecliptic_longitude = fmod(ecliptic_longitude, 360.0);
if (ecliptic_longitude < 0) ecliptic_longitude += 360.0;
// Debug
//Serial.print("Ecliptic Longitude: ");
//Serial.println(ecliptic_longitude, 2);
// Przydział znaków zodiaku (30° na znak)
if (ecliptic_longitude < 30) return "♈ Baran";
else if (ecliptic_longitude < 60) return "♉ Byk";
else if (ecliptic_longitude < 90) return "♊ Bliźnięta";
else if (ecliptic_longitude < 120) return "♋ Rak";
else if (ecliptic_longitude < 150) return "♌ Lew";
else if (ecliptic_longitude < 180) return "♍ Panna";
else if (ecliptic_longitude < 210) return "♎ Waga";
else if (ecliptic_longitude < 240) return "♏ Skorpion";
else if (ecliptic_longitude < 270) return "♐ Strzelec";
else if (ecliptic_longitude < 300) return "♑ Koziorożec";
else if (ecliptic_longitude < 330) return "♒ Wodnik";
else return "♓ Ryby";
}
// Oblicza Lokalny Czas Gwiazdowy (LST)
double calculateLST(double jd, double longitude) {
// Obliczenie Czasu Sideralnego Greenwich (GST)
double T = (jd - 2451545.0) / 36525.0; // Wiek juliański od J2000
double GMST = 280.46061837 + 360.98564736629 * (jd - 2451545.0) + 0.000387933 * T * T - T * T * T / 38710000.0;
// Normalizacja do zakresu 0-360 stopni
GMST = fmod(GMST, 360.0);
if (GMST < 0) GMST += 360.0;
// Lokalny Czas Sideralny (LST) = GST + Długość Geograficzna
double LST = GMST + longitude;
LST = fmod(LST, 360.0);
if (LST < 0) LST += 360.0;
return LST; // LST w stopniach
}
// Oblicza wysokość i azymut
HorizontalPosition calculateAltAz(double observerLat, double lst, double ra, double dec) {
// Konwersja RA z godzin na stopnie i normalizacja
double ra_deg = ra * 15.0;
// Kąt godzinny (Hour Angle, HA)
double HA = lst - ra_deg;
HA = fmod(HA, 360.0);
if (HA < 0) HA += 360.0;
// Konwersja na radiany
double lat_rad = observerLat * M_PI / 180.0;
double dec_rad = dec * M_PI / 180.0;
double ha_rad = HA * M_PI / 180.0;
// Obliczenie wysokości (Altitude, Alt)
double sin_alt = sin(dec_rad) * sin(lat_rad) + cos(dec_rad) * cos(lat_rad) * cos(ha_rad);
double alt_rad = asin(sin_alt);
// Obliczenie azymutu (Azimuth, Az)
double cos_az_num = sin(dec_rad) - sin_alt * sin(lat_rad);
double cos_az_den = cos(alt_rad) * cos(lat_rad);
double cos_az = cos_az_num / cos_az_den;
// Użycie atan2 do poprawnego określenia ćwiartki
double sin_A = -cos(dec_rad) * sin(ha_rad) / cos(alt_rad);
double azimuth = atan2(sin_A, cos_az) * 180.0 / M_PI;
// Normalizacja Azymutu do 0-360 stopni (N = 0)
azimuth = fmod(azimuth, 360.0);
if (azimuth < 0) azimuth += 360.0;
HorizontalPosition pos;
pos.altitude = alt_rad * 180.0 / M_PI;
pos.azimuth = azimuth;
return pos;
}
void setup() {
Serial.begin(115200);
Wire.begin(); // I2C
// inicjalizacja zegara RTC DS3231
if (!rtc.begin()) {
Serial.println("Błąd: nie mogę znaleźć RTC DS3231!");
while (1) {
delay(100);
}
}
// Najpierw spróbuj pobrać czas z NTP
bool ntpOK = syncTimeFromNTP();
if (!ntpOK) {
// Jeśli NTP nie działa, spróbuj użyć czasu z RTC
DateTime now_rtc = rtc.now();
// jeśli RTC jest "gówniany" (2000 rok) -> ustaw chociaż na czas kompilacji
if (now_rtc.year() < 2024 || now_rtc.year() > 2100) {
Serial.println("RTC ma zły czas - ustawiam na czas kompilacji (przy braku NTP)");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
now_rtc = rtc.now();
}
UTCsec = now_rtc.unixtime();
setTime(UTCsec);
Serial.printf("RTC UTC (fallback): %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());
}
// CET/CEST – Europa Środkowa (Polska, Włochy itd.)
setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
tzset();
// Masz już UTCsec i now() w UTC, więc tu przelicz swoje struktury:
convUnix(UTCsec);
// przygotuj stringi z czasem lokalnym do wyświetlania
GetTimeStr();
/*
// >>> 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();
// inicjalizacja magistrali SPI dla wyświetlaczy
SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI);
// inicjalizacja wyświetlacza tekstowego
tftText.begin();
tftText.setRotation(0);
tftText.fillScreen(TFT_BLACK);
tftText.drawRect(0, 0, tftText.width(), tftText.height(), TFT_BLUE);
// inicjalizacja wyświetlacza z księżycem
tftMoon.begin();
tftMoon.setRotation(0);
tftMoon.fillScreen(TFT_BLACK);
tftMoon.drawRect(0, 0, tftMoon.width(), tftMoon.height(), TFT_BLUE);
// inicjalizacja silnika U8g2 (polskie znaki) na wyświetlaczu tekstowym
u8g2.begin(tftText); // podłącz U8g2 do Adafruit_GFX
u8g2.setFont(FONT1); // u8g2_font_unifont_t_polish
//u8g2.setFont(FONT2); // u8g2_font_unifont_t_weather
u8g2.setFontMode(0); // tryb z tłem
u8g2.setForegroundColor(TFT_COLOR1);
u8g2.setBackgroundColor(TFT_BLACK);
// inicjalizacja karty SD na osobnej magistrali SPI (HSPI)
spiSD.begin(SCLK, MISO, MOSI, SD_CS);
if (!SD.begin(SD_CS, spiSD)) {
Serial.println("Inicjalizacja karty SD nie powiodła się!");
return;
}
dispActive = true;
// licznik wygaszacza
dispTimer = millis() + periodDisplay;
}
void loop() {
// Aktualizacja RTC raz na godzinę (gdy minuta == 0)
if (minute(now()) == 0 && !updatedHWclock) {
// pobiera czas z DS3231
//updateClock(); // ← ZAKOMENTONANE DLA WOKWI
updatedHWclock = true;
}
if (minute(now()) != 0) {
updatedHWclock = false;
}
/*
// 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;
}*/
// Wywołaj funkcję formatującą LT do wyświetlenia w każdym cyklu, aby ekran się odświeżał co sekundę, a nie tylko co 500ms
// Możesz umieścić to tutaj lub raz na sekundę w sekcji 'if (second(now()) == 0 ...)'
// Najlepiej wywoływać w każdym cyklu, ale formatować tylko raz na sekundę:
if (second(now()) != second(prevTime)) {
// Jeśli sekunda się zmieniła, przelicz i sformatuj LT
GetTimeStr();
// Musisz mieć globalną 'time_t prevTime;'
prevTime = now();
}
// Obliczenia astronomiczne + grafika Księżyca raz na godzinę
if (((minute(now()) == 0 && second(now()) == 0) && !MoonCalcDone) || firstRun) {
DateTime nowTime = rtc.now();
moonCalc.calculate(nowTime.year(), nowTime.month(), nowTime.day(), nowTime.hour(), nowTime.minute());
Serial.printf("🔄 LOOP MoonCalc refresh: %04d.%02d.%02d %02d:%02d:%02d\n", year(now()), month(now()), day(now()), hour(now()), minute(now()), second(now()));
updateAstronomicalData();
limbAngle = currentAstroData.brightLimbAngle;
computePhases(year(now()));
computeSeasons(year(now()));/*
FindBlackMoonM();
FindBlueMoonM();
FindBlackMoonS();
FindBlueMoonS();
FindRedMoon();
FindSuperMoon();
FindMicroMoon();*/
MoonCalcDone = true;
// aktualizacja części tekstowej
updateTxtDisplay();
phaseNow = false;
// przesunięcie phaseRaw o 15, żeby obraz fazy pokazywać od 15 „kroków” przed do 15 po dokładnym momencie fazy
// phaseRaw to liczba 0–1199: 0 = nów, 300 = I kwadra, 600 = pełnia, 900 = III kwadra
// przykład: pełnia wypada przy phaseRaw = 600, a obraz „pełnia” (#20) ma być wyświetlany od 585 do 615 (40 obrazków, 30 kroków phaseRaw = 1/40 z 1200)
phaseRaw += 15;
if (phaseRaw > 1199) {
// zawinięcie, jeśli wynik przekroczył 1199
phaseRaw -= 1200;
}
// indeks pliku 0–39
int filnum = map(phaseRaw, 0, 1199, 0, 40);
if (filnum > 39) {
// zachowanie zakresu indeksów
filnum = 0;
}
// sprawdzenie indeksu pliku PNG i ewentualne wczytanie nowego pliku z karty SD
Serial.printf("Free heap: %lu, max alloc: %lu\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
delay(5000);
LoadMoonFile(filnum);
Serial.printf("RAM po wczytaniu: %lu bajtów wolne\n", ESP.getFreeHeap());
// początkowy kąt obrotu obrazu = 90°
int tmp = 90;
if (filnum > 20) {
// obliczenia kąta brzegu dotyczą jasnej strony niezależnie od przybywania/ubywania,
// więc przy ubywającym Księżycu obrót musi być odwrócony
tmp = -90;
}
// tradycyjne grafiki faz zakładają, że oświetlona część przesuwa się z prawej na lewą (półkula północna)
// Obróć obraz o X stopni (kąt w float); pliki PNG mają jasny brzeg przy 90°
// obliczanie kąta zaczyna się od góry przeciwnie do ruchu wskazówek zegara,
// odwracamy to, aby łatwiej odczytać (zgodnie z ruchem wskazówek)
////rotateImage(360 - limbAngle - tmp);
// wyświetlenie obróconego obrazu na LCD w orientacji pionowej
int centerX = (tftMoon.width() - imageWidth) / 2;
int centerY = (tftMoon.height() - imageHeight) / 2;
// przesunięcie Księżyca o 65 pikseli w górę względem środka
centerY -= 65;
displayRotatedImage(centerX, centerY);
// oblicz wysokość i azymut Księżyca oraz Słońca
displayMoonSunPos();
firstRun = false;
}
if ((minute(now()) != 0 || second(now()) != 0) && MoonCalcDone) {
// reset flagi po odejściu od pełnej godziny
MoonCalcDone = false;
}
// Wygaszanie ekranu po 10 minutach bezczynności
if (dispActive && millis() > dispTimer) {
dispActive = false;
tftText.fillScreen(TFT_BLACK);
tftMoon.fillScreen(TFT_BLACK);
}
}
//
void calculateHorizontalCoordinates() {
// Pobierz datę juliańską z MoonCalc (jest już obliczona!)
double jd = moonCalc.getJulianDay();
// Oblicz Lokalny Czas Gwiazdowy (LST)
double lst = calculateLST(jd, observer_lon);
// Oblicz współrzędne horyzontalne dla Księżyca
HorizontalPosition moonPos = calculateAltAz(observer_lat, lst, currentAstroData.moonRA, currentAstroData.moonDec);
currentAstroData.moonAlt = moonPos.altitude;
currentAstroData.moonAz = moonPos.azimuth;
// Oblicz współrzędne horyzontalne dla Słońca
HorizontalPosition sunPos = calculateAltAz(observer_lat, lst, currentAstroData.sunRA, currentAstroData.sunDec);
currentAstroData.sunAlt = sunPos.altitude;
currentAstroData.sunAz = sunPos.azimuth;
}
// FUNKCJA AKTUALIZUJĄCA DANE ASTRONOMICZNE
void updateAstronomicalData() {
// Pobierz dane z MoonCalc
MoonCalc::MoonData moon = moonCalc.getMoonData();
MoonCalc::SunData sun = moonCalc.getSunData();
MoonCalc::PositionAngles angles = moonCalc.getPositionAngles();
// Zapisz podstawowe dane
currentAstroData.moonRA = moon.rightAscension;
currentAstroData.moonDec = moon.declination;
currentAstroData.moonDistance = moon.distance;
currentAstroData.moonIllumination = moon.illumination;
currentAstroData.sunRA = sun.rightAscension;
currentAstroData.sunDec = sun.declination;
currentAstroData.sunDistance = sun.distance;
currentAstroData.brightLimbAngle = angles.brightLimb;
currentAstroData.poleAngle = angles.pole;
// Oblicz znaki zodiaku
currentAstroData.moonZodiac = getZodiacSign(moon.declination, moon.rightAscension);
currentAstroData.sunZodiac = getZodiacSign(sun.declination, sun.rightAscension);
// OBLICZ WSPÓŁRZĘDNE HORYZONTALNE (używając sprawdzonych funkcji)
calculateHorizontalCoordinates();
}
// DEBUG: drukuj czas Unix jako UTC (nie ruszamy tego)
void printUnix(time_t unixTime) {
struct tm *tm_info = gmtime(&unixTime);
char buffer[30];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm_info);
Serial.print("Unix Time: ");
Serial.println(buffer);
}
// 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);
}
// 🌑 Znajdź najbliższy czarny księżyc (drugi nów w tym samym miesiącu)
void FindBlackMoonM() {
BkMoonM = 0;
// moon phase list new moon = 0. Iterujemy przez WSZYSTKIE fazy
for (int i = 4; i < 240; i++) {
// Sprawdzamy, czy bieżąca faza (i) i faza 4 kroki temu (i-4) to Nowie
if (moonPhases[i].phaseType == 0 && moonPhases[i - 4].phaseType == 0) {
DateTime prevDt = DateTime(moonPhases[i - 4].timestamp);
DateTime currDt = DateTime(moonPhases[i].timestamp);
// Warunek: Dwa nowie w tym samym miesiącu i roku
if (prevDt.month() == currDt.month() && prevDt.year() == currDt.year()) {
BkMoonM = moonPhases[i].timestamp;
if (BkMoonM > UTCsec) {
Serial.print(F("Black Moon (monthly) found: "));
time_t savedUTC = UTCsec;
UTCsec = BkMoonM;
GetTimeStr();
Serial.print(thisDate);
Serial.print(F(" "));
Serial.println(thisTime);
UTCsec = savedUTC;
GetTimeStr(); // Przywrócenie stanu
return;
}
}
}
}
Serial.println(F("Brak miesięcznego Black Moon w przyszłości"));
}
// 🔵 znajdź następny miesięczny niebieski Księżyc (druga pełnia w miesiącu)
void FindBlueMoonM() {
BlMoonM = 0;
// moon phase list full moon = 2. Iterujemy przez WSZYSTKIE fazy
for (int i = 4; i < 240; i++) {
// Sprawdzamy, czy bieżąca faza (i) i faza 4 kroki temu (i-4) to Pełnie
if (moonPhases[i].phaseType == 2 && moonPhases[i - 4].phaseType == 2) {
DateTime prevDt = DateTime(moonPhases[i - 4].timestamp);
DateTime currDt = DateTime(moonPhases[i].timestamp);
// Warunek: Dwie pełnie w tym samym miesiącu i roku
if (prevDt.month() == currDt.month() && prevDt.year() == currDt.year()) {
BlMoonM = moonPhases[i].timestamp;
if (BlMoonM > UTCsec) {
Serial.print(F("Blue Moon (monthly) found: "));
time_t savedUTC = UTCsec;
UTCsec = BlMoonM;
GetTimeStr();
Serial.print(thisDate);
Serial.print(F(" "));
Serial.println(thisTime);
UTCsec = savedUTC;
GetTimeStr(); // Przywrócenie stanu
return;
}
}
}
}
Serial.println(F("Brak miesięcznego Blue Moon w przyszłości"));
}
// 🌑 Oznacza trzeci nów księżyca w sezonie, jeśli sezon ma cztery nowie
void FindBlackMoonS() {
BkMoonS = 0;
// Przechodzimy przez 23 sezony
for (int j = 1; j < 24; j++) {
time_t seasonStart = seasonUTC[j - 1];
time_t seasonEnd = seasonUTC[j];
int newCount = 0;
int thirdNewIndex = -1;
// Iterujemy przez WSZYSTKIE fazy i szukamy Nowiów
for (int i = 0; i < 240; i++) {
if (moonPhases[i].phaseType == 0) { // Tylko Nowie
time_t newTime = moonPhases[i].timestamp;
// Sprawdzenie, czy Nów jest w bieżącym sezonie
if (newTime > seasonStart && newTime < seasonEnd) {
newCount++;
if (newCount == 3) {
// Zapamiętanie indeksu trzeciego Nowiu
thirdNewIndex = i;
}
}
}
}
// Jeśli są co najmniej cztery Nowie, używamy trzeciego
if (newCount >= 4 && thirdNewIndex >= 0) {
BkMoonS = moonPhases[thirdNewIndex].timestamp;
if (BkMoonS > UTCsec) {
Serial.print(F("Black Moon (seasonal, third new) found: "));
time_t savedUTC = UTCsec;
UTCsec = BkMoonS;
GetTimeStr();
Serial.print(thisDate);
Serial.print(F(" "));
Serial.print(thisTime);
UTCsec = savedUTC;
GetTimeStr(); // Przywrócenie stanu
Serial.print(F(" (new moons in season: "));
Serial.print(newCount);
Serial.println(")");
return;
}
}
}
Serial.println(F("Brak sezonowego Black Moon w przyszłości"));
}
// 🔵 znajdź następny sezonowy niebieski Księżyc (trzecia pełnia z czterech w tej samej porze roku)
void FindBlueMoonS() {
BlMoonS = 0;
// Przechodzimy przez 23 sezony (seasonUTC[1] do seasonUTC[23])
for (int j = 1; j < 24; j++) {
time_t seasonStart = seasonUTC[j - 1];
time_t seasonEnd = seasonUTC[j];
int fullCount = 0; // Licznik pełni w bieżącym sezonie
int thirdFullIndex = -1; // Indeks trzeciej pełni
// Iterujemy przez WSZYSTKIE fazy (0 do 239)
for (int i = 0; i < 240; i++) {
// Szukamy tylko Pełni (phaseType == 2)
if (moonPhases[i].phaseType == 2) {
time_t fullTime = moonPhases[i].timestamp;
// Sprawdzenie, czy Pełnia wypada w bieżącym sezonie
if (fullTime > seasonStart && fullTime < seasonEnd) {
fullCount++;
if (fullCount == 3) {
// Zapamiętanie indeksu trzeciej Pełni
thirdFullIndex = i;
}
}
}
}
// Jeśli są co najmniej cztery Pełnie, używamy trzeciej
if (fullCount >= 4 && thirdFullIndex >= 0) {
BlMoonS = moonPhases[thirdFullIndex].timestamp;
if (BlMoonS > UTCsec) {
Serial.print(F("Blue Moon (seasonal) found: "));
time_t savedUTC = UTCsec;
UTCsec = BlMoonS;
GetTimeStr();
Serial.print(thisDate);
Serial.print(F(" "));
Serial.print(thisTime);
UTCsec = savedUTC;
GetTimeStr(); // Przywrócenie stanu
Serial.print(F(" (full moons in season: "));
Serial.print(fullCount);
Serial.println(")");
return;
}
}
}
Serial.println(F("Brak sezonowego Blue Moon w przyszłości"));
}
// 🩸znajdź najbliższe w przyszłości całkowite zaćmienie Księżyca ("Czerwony księżyc")
void FindRedMoon() {
RedMoon = 0;
// Poprawny limit dla szerokości geograficznej (blisko płaszczyzny ekliptyki)
const double TOTAL_ECLIPSE_LAT_LIMIT = 0.5;
for (int i = 0; i < 240; i++) {
if (moonPhases[i].phaseType == 2) { // pełnia
time_t fullTime = moonPhases[i].timestamp;
if (fullTime <= UTCsec) continue;
// 1. Konwersja czasu Pełni na DateTime (wymaga RTCLib)
DateTime fullDateTime = DateTime(fullTime);
// 2. Używamy GLOBALNEGO obiektu moonCalc do obliczeń na ten moment
// To jest konieczne, by uzyskać librację dla tej konkretnej daty
moonCalc.calculate(fullDateTime.year(), fullDateTime.month(), fullDateTime.day(), fullDateTime.hour(), fullDateTime.minute());
// 3. Pobieramy Librację z globalnego obiektu
MoonCalc::Libration lib = moonCalc.getLibration();
double latAbs = fabs(lib.selenographicLatitude);
if (latAbs < TOTAL_ECLIPSE_LAT_LIMIT) {
RedMoon = fullTime;
Serial.print(F("Red Moon (eclipse candidate) found: "));
time_t savedUTC = UTCsec;
UTCsec = RedMoon;
GetTimeStr();
Serial.print(thisDate);
Serial.print(" ");
Serial.print(thisTime);
UTCsec = savedUTC;
GetTimeStr(); // Przywrócenie stanu
Serial.print(F(" (lat: "));
Serial.print(lib.selenographicLatitude, 2);
Serial.println(" deg)");
return;
}
}
}
Serial.println(F("Brak 'Czerwonego Księżyca' w przewidywanym zakresie faz."));
}
// Znajdź najbliższy w przyszłości Superksiężyc (pełnia przy małej odległości – blisko perygeum)
void FindSuperMoon() {
SuperMoon = 0;
// Limit dla Superksiężyca (Pełnia poniżej 57.0 R_Ziemi to typowy próg)
const double SUPERMOON_LIMIT = 57.0;
for (int i = 0; i < 240; i++) {
if (moonPhases[i].phaseType == 2) { // pełnia
time_t fullTime = moonPhases[i].timestamp;
if (fullTime <= UTCsec) continue;
// KOREKTA: Nie dzielimy, ponieważ distance jest już w R_Ziemi.
double dist_earth_radii = moonPhases[i].distance;
if (dist_earth_radii <= SUPERMOON_LIMIT) {
SuperMoon = fullTime;
// Wyświetlanie daty
Serial.print("Superksiężyc znaleziony: ");
time_t savedUTC = UTCsec;
UTCsec = SuperMoon;
GetTimeStr();
Serial.print(thisDate);
Serial.print(" ");
Serial.print(thisTime);
UTCsec = savedUTC;
GetTimeStr(); // Przywracamy czas główny
Serial.print(" dystans ~ ");
Serial.print(dist_earth_radii, 2);
Serial.println(" promieni Ziemi");
return;
}
}
}
Serial.println(F("Brak Superksiężyca w przewidywanym zakresie faz."));
}
// Znajdź najbliższy w przyszłości Mikroksiężyc (pełnia przy dużej odległości – blisko apogeum)
void FindMicroMoon() {
MicroMoon = 0;
// Limit 405 000 km odpowiada 63.5685 R_Ziemi
const double MICROMOON_LIMIT = 63.5685;
for (int i = 0; i < 240; i++) {
if (moonPhases[i].phaseType == 2 || moonPhases[i].phaseType == 0) {
time_t fullTime = moonPhases[i].timestamp;
if (fullTime <= UTCsec) continue;
// Nie dzielimy, ponieważ distance jest już w R_Ziemi
double dist_earth_radii = moonPhases[i].distance;
if (dist_earth_radii >= MICROMOON_LIMIT) {
MicroMoon = fullTime;
// Wyświetlanie daty
Serial.print("Mikroksiężyc znaleziony: ");
time_t savedUTC = UTCsec;
UTCsec = MicroMoon;
GetTimeStr();
Serial.print(thisDate);
Serial.print(" ");
Serial.print(thisTime);
UTCsec = savedUTC;
GetTimeStr(); // Przywracamy czas główny
Serial.print(" dystans ~ ");
Serial.print(dist_earth_radii, 2);
Serial.println(" promieni Ziemi");
return;
}
}
}
Serial.println(F("Brak Mikroksiężyca w przewidywanym zakresie faz."));
}
// Orbita Słońce/Księżyc na dolnej części ekranu Księżyca
void displayMoonSunPos() {
int centerX = tftMoon.width() / 2; // centerX = 120
int centerY = tftMoon.height() / 2; // centerY = 160
//Serial.printf("centerX = %d, centerY = %d\n", centerX, centerY);
// przesunięcie elipsy o 90 pikseli w dół od środka
centerY += 90;
// x,y,w,h,kolor – wymazanie dolnej połowy ekranu
tftMoon.fillRect(1, 180, 238, 139, TFT_BLACK);
// tftMoon.drawRect(1, 180, 238, 139, TFT_RED);
tftMoon.fillCircle(centerX, centerY, 8, TFT_DKGREEN);
// rysowanie orbity
tftMoon.drawEllipse(centerX, centerY, 90, 45, TFT_COLOR1);
// (N) wygenerowanie przerw w elipsie, aby narysować N/E/S/W
tftMoon.fillRect(centerX - 5, centerY - 50, 10, 12, TFT_BLACK); // (N)
tftMoon.fillRect(centerX + 87, centerY - 6, 10, 12, TFT_BLACK); // (E)
tftMoon.fillRect(centerX - 5, centerY + 40, 10, 12, TFT_BLACK); // (S)
tftMoon.fillRect(centerX - 93, centerY - 6, 10, 12, TFT_BLACK); // (W)
int obsOffset;
char compass[10];
if (observer_lat >= 0) {
// półkula północna – N u góry, E po prawej, S na dole, W po lewej
// SunMoonCalc: 0°=N, 90°=E, 180°=S, 270°=W → konwersja na nasz system
obsOffset = -90;
strcpy(compass, "NESW");
} else {
// półkula południowa – S u góry, W po prawej, N na dole, E po lewej
obsOffset = 90;
strcpy(compass, "SWNE");
}
tftMoon.setTextColor(TFT_BLUE, TFT_BLACK);
// N lub S
tftMoon.setCursor(centerX - 3, centerY - 49);
tftMoon.print(compass[0]);
// E lub W
tftMoon.setCursor(centerX + 88, centerY - 4);
tftMoon.print(compass[1]);
// S lub N
tftMoon.setCursor(centerX - 3, centerY + 42);
tftMoon.print(compass[2]);
// W lub E
tftMoon.setCursor(centerX - 91, centerY - 4);
tftMoon.print(compass[3]);
// położenie Księżyca wewnątrz elipsy
int MposX = centerX + 25 * cos(degToRad(currentAstroData.moonAz + obsOffset));
int MposY = centerY + 25 * sin(degToRad(currentAstroData.moonAz + obsOffset));
tftMoon.fillCircle(MposX, MposY, 4, TFT_SILVER);
// położenie Słońca na zewnątrz elipsy
int SposX = centerX + 105 * cos(degToRad(currentAstroData.sunAz + obsOffset));
int SposY = centerY + 60 * sin(degToRad(currentAstroData.sunAz + obsOffset));
tftMoon.fillCircle(SposX, SposY, 8, TFT_YELLOW);
// Oblicz wektor kierunku od Księżyca do Słońca
double dx = SposX - MposX;
double dy = SposY - MposY;
double length = sqrt(dx * dx + dy * dy);
// Normalizuj wektor
dx /= length;
dy /= length;
// Punkty początkowe i końcowe linii (na krawędziach kół)
int lineStartX = MposX + dx * 6; // 4 = promień Księżyca
int lineStartY = MposY + dy * 6;
int lineEndX = SposX - dx * 11; // 8 = promień Słońca
int lineEndY = SposY - dy * 11;
// linia łącząca Słońce i Księżyc - od krawędzi do krawędzi
tftMoon.drawLine(lineStartX, lineStartY, lineEndX, lineEndY, TFT_SKYBLUE);
// Serial.printf("Sun az=%.1f° alt=%.1f° | Moon az=%.1f° alt=%.1f°\n", sunAzDeg, sunAltDeg, moonAzDeg, moonAltDeg);
}
// Uproszczona funkcja obliczania dokładnego czasu fazy
time_t calculateExactPhaseTime(int year, int month, double phase) {
// To jest uproszczenie - w rzeczywistości powinno się użyć precyzyjnego algorytmu do znajdowania dokładnych momentów faz
int day = 1 + (int)(phase * 28);
return DateTime(year, month, day, 12, 0, 0).unixtime();
}
// Oblicz fazy księżyca używając MoonCalc
void computePhases(int cyear) {
// Poprawka 1: Usunięto zbędne int i = 0;
int k = getCycleEstimate(cyear, 0);
// Przejście przez 59 cykli faz, wypełniające 236 wpisów w moonPhases[240]
for (int i = -1; i < 58; i++) {
int index = (i + 1) * 4;
// Ustawianie timestamp i typu dla czterech faz
moonPhases[index].timestamp = getPhaseDate(k + i, 0);
moonPhases[index].phaseType = 0;
moonPhases[index + 1].timestamp = getPhaseDate(k + i, .25);
moonPhases[index + 1].phaseType = 1;
moonPhases[index + 2].timestamp = getPhaseDate(k + i, .5);
moonPhases[index + 2].phaseType = 2;
moonPhases[index + 3].timestamp = getPhaseDate(k + i, .75);
moonPhases[index + 3].phaseType = 3;
// Oblicz odległości używając MoonCalc (dla poprawnych jednostek R_Z)
for (int j = 0; j < 4; j++) {
DateTime dt = DateTime(moonPhases[index + j].timestamp);
moonCalc.calculate(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute());
// Zapisujemy odległość w R_Ziemi
moonPhases[index + j].distance = moonCalc.getMoonData().distance;
}
}
int i; // Zadeklaruj i tutaj dla dolnej pętli
// Przeglądanie listy faz Księżyca aż do znalezienia aktualnej „ćwiartki”
for (i = 0; i < 240; i++) {
if (moonPhases[i].timestamp > UTCsec) {
// Obliczanie bieżącego postępu w fazie
long iPhase = moonPhases[i].timestamp - moonPhases[i - 1].timestamp;
long uPhase = UTCsec - moonPhases[i - 1].timestamp;
// Mapowanie postępu na zakres 0-1199 (0-299 na ćwiartkę)
switch (moonPhases[i - 1].phaseType) {
case 0: phaseRaw = map(uPhase, 0, iPhase, 0, 299); break; // Nów
case 1: phaseRaw = map(uPhase, 0, iPhase, 300, 599); break; // I Kwadra
case 2: phaseRaw = map(uPhase, 0, iPhase, 600, 899); break; // Pełnia
case 3: phaseRaw = map(uPhase, 0, iPhase, 900, 1199); break; // III Kwadra
}
// zbierz kolejne cztery daty faz
for (int k = 0; k < 4; k++) {
// Poprawka 2: Zabezpieczenie przed przekroczeniem tablicy moonPhases[240]
if ((i + k) < 240) {
NextMoonUTC[k] = moonPhases[i + k].timestamp;
NextMoonInd[k] = moonPhases[i + k].phaseType;
} else {
// Wypełnij zerami, jeśli brak dalszych danych
NextMoonUTC[k] = 0;
NextMoonInd[k] = 0;
}
}
return;
}
}
}
// oblicz pory roku dla 5 lat, zaczynając od poprzedniego roku
void computeSeasons(int cyear) {
// zmienne, które muszą być resetowane/alokowane dla każdego roku
time_t UTCseason = 0;
char thisSeason[30];
// char FormatTD[] = "%4d-%02d-%02d - %02d:%02d:%02d"; // Użycie GetTimeStr jest lepsze
int idx = 0;
// start od poprzedniego roku
for (int Y = cyear - 1; Y < cyear + 5; Y++) {
// konwersja roku AD na tysiąclecia względem roku 2000
double M = (Y - 2000) / 1000.0;
// zmienne pomocnicze (muszą być alokowane wewnątrz, ale nie w wewnętrznej pętli i<4)
double T[4], W[4], L[4], S[4], JD[4];
double JDME[4];
// Wstępne przybliżenia
// równonoc marcowa – wartość bazowa
JDME[0] = 2451623.80984 + 365242.37404 * M + 0.05169 * pow(M, 2) - 0.00411 * pow(M, 3) - 0.00057 * pow(M, 4);
// przesilenie czerwcowe – wartość bazowa
JDME[1] = 2451716.56767 + 365241.62603 * M + 0.00325 * pow(M, 2) + 0.00888 * pow(M, 3) - 0.00030 * pow(M, 4);
// równonoc wrześniowa – wartość bazowa
JDME[2] = 2451810.21715 + 365242.01767 * M - 0.11575 * pow(M, 2) + 0.00337 * pow(M, 3) + 0.00078 * pow(M, 4);
// przesilenie grudniowe – wartość bazowa
JDME[3] = 2451900.05952 + 365242.74049 * M - 0.06223 * pow(M, 2) - 0.00823 * pow(M, 3) + 0.00032 * pow(M, 4);
// Pętla po 4 porach roku
for (int i = 0; i < 4; ++i) {
// wieki juliańskie od roku 2000 (equ/sol)
T[i] = (JDME[i] - 2451545.0) / 36525;
// stopnie dla anomalii średniej Słońca
W[i] = 35999.373 * T[i] - 2.47;
// Lambda (czynniki korygujące dla L)
L[i] = 1 + 0.0334 * cos(W[i] * PI / 180) + 0.0007 * cos(2 * W[i] * PI / 180);
S[i] = 0;
for (int j = 0; j < 24; j++) {
// wyliczenie perturbacji
S[i] += A[j] * cos((B[j] + C[j] * T[i]) * PI / 180); // Upewnij się, że PI jest zdefiniowane!
}
// ostateczny wynik w juliańskich dniach dynamicznych (JD)
JD[i] = JDME[i] + 0.00001 * S[i] / L[i];
// Konwersja JD na czas Unix (sekundy)
UTCseason = julianDateToUnix(JD[i]);
// Zapis do tablicy i zwiększenie indeksu
seasonUTC[idx] = UTCseason;
idx += 1;
}
}
}
// juliańska data dla epoki Unix
unsigned long julianDateToUnix(double julianDate) {
const double UNIX_EPOCH_JULIAN_DAY = 2440587.5;
// różnica w dniach, przeliczona na sekundy
time_t unixTime = (long)((julianDate - UNIX_EPOCH_JULIAN_DAY) * 86400);
return unixTime;
}
double mod360(int f) {
int t = f % 360;
if (t < 0)
t += 360;
return t;
}
double getCycleEstimate(int year, int month) {
// przybliżenie ułamka roku
double yearfrac = (month * 30 + 15) / 365.0;
double k = 12.3685 * ((year + yearfrac) - 2000); // 49.2
k = floor(k);
return k;
}
// z „Astronomical Algorithms” Meeusa – rozdział 49
unsigned long getPhaseDate(double cycle, double phase) {
double correction = 0;
double k = cycle + phase;
double toRad = PI / 180;
double T = k / 1236.85; //49.3
double JDE = 2451550.09766 + 29.530588861 * k + 0.00015437 * T * T - 0.000000150 * T * T * T + 0.00000000073 * T * T * T * T; //49.1
double E = 1 - 0.002516 * T - 0.0000074 * T * T; //47.6
double M = mod360(2.5534 + 29.10535670 * k - 0.0000014 * T * T - 0.00000011 * T * T * T) * toRad; //49.4
double Mp = mod360(201.5643 + 385.81693528 * k + 0.0107582 * T * T + 0.00001238 * T * T * T - 0.000000058 * T * T * T * T) * toRad; //49.5
double F = mod360(160.7108 + 390.67050284 * k - 0.0016118 * T * T - 0.00000227 * T * T * T + 0.000000011 * T * T * T * T) * toRad; //49.6
double Om = mod360(124.7746 - 1.56375588 * k + 0.0020672 * T * T + 0.00000215 * T * T * T) * toRad; //49.7
//P351-352
double A1 = mod360(299.77 + 0.107408 * k - 0.009173 * T * T) * toRad;
double A2 = mod360(251.88 + 0.016321 * k) * toRad;
double A3 = mod360(251.83 + 26.651886 * k) * toRad;
double A4 = mod360(349.42 + 36.412478 * k) * toRad;
double A5 = mod360(84.66 + 18.206239 * k) * toRad;
double A6 = mod360(141.74 + 53.303771 * k) * toRad;
double A7 = mod360(207.14 + 2.453732 * k) * toRad;
double A8 = mod360(154.84 + 7.306860 * k) * toRad;
double A9 = mod360(34.52 + 27.261239 * k) * toRad;
double A10 = mod360(207.19 + 0.121824 * k) * toRad;
double A11 = mod360(291.34 + 1.844379 * k) * toRad;
double A12 = mod360(161.72 + 24.198154 * k) * toRad;
double A13 = mod360(239.56 + 25.513099 * k) * toRad;
double A14 = mod360(331.55 + 3.592518 * k) * toRad;
if (phase == 0) {
correction = 0.00002 * sin(4 * Mp) + -0.00002 * sin(3 * Mp + M) + -0.00002 * sin(Mp - M - 2 * F) + 0.00003 * sin(Mp - M + 2 * F) + -0.00003 * sin(Mp + M + 2 * F) + 0.00003 * sin(2 * Mp + 2 * F) + 0.00003 * sin(Mp + M - 2 * F) + 0.00004 * sin(3 * M) + 0.00004 * sin(2 * Mp - 2 * F) + -0.00007 * sin(Mp + 2 * M) + -0.00017 * sin(Om) + -0.00024 * E * sin(2 * Mp - M) + 0.00038 * E * sin(M - 2 * F) + 0.00042 * E * sin(M + 2 * F) + -0.00042 * sin(3 * Mp) + 0.00056 * E * sin(2 * Mp + M) + -0.00057 * sin(Mp + 2 * F) + -0.00111 * sin(Mp - 2 * F) + 0.00208 * E * E * sin(2 * M) + -0.00514 * E * sin(Mp + M) + 0.00739 * E * sin(Mp - M) + 0.01039 * sin(2 * F) + 0.01608 * sin(2 * Mp) + 0.17241 * E * sin(M) + -0.40720 * sin(Mp);
} else if ((phase == 0.25) || (phase == 0.75)) {
correction = -0.00002 * sin(3 * Mp + M) + 0.00002 * sin(Mp - M + 2 * F) + 0.00002 * sin(2 * Mp - 2 * F) + 0.00003 * sin(3 * M) + 0.00003 * sin(Mp + M - 2 * F) + 0.00004 * sin(Mp - 2 * M) + -0.00004 * sin(Mp + M + 2 * F) + 0.00004 * sin(2 * Mp + 2 * F) + -0.00005 * sin(Mp - M - 2 * F) + -0.00017 * sin(Om) + 0.00027 * E * sin(2 * Mp + M) + -0.00028 * E * E * sin(Mp + 2 * M) + 0.00032 * E * sin(M - 2 * F) + 0.00032 * E * sin(M + 2 * F) + -0.00034 * E * sin(2 * Mp - M) + -0.00040 * sin(3 * Mp) + -0.00070 * sin(Mp + 2 * F) + -0.00180 * sin(Mp - 2 * F) + 0.00204 * E * E * sin(2 * M) + 0.00454 * E * sin(Mp - M) + 0.00804 * sin(2 * F) + 0.00862 * sin(2 * Mp) + -0.01183 * E * sin(Mp + M) + 0.17172 * E * sin(M) + -0.62801 * sin(Mp);
double W = 0.00306 - 0.00038 * E * cos(M) + 0.00026 * cos(Mp) - 0.00002 * cos(Mp - M) + 0.00002 * cos(Mp + M) + 0.00002 * cos(2 * F);
if (phase == 0.25) {
correction += W;
} else {
correction -= W;
}
} else if (phase == 0.5) {
correction = 0.00002 * sin(4 * Mp) + -0.00002 * sin(3 * Mp + M) + -0.00002 * sin(Mp - M - 2 * F) + 0.00003 * sin(Mp - M + 2 * F) + -0.00003 * sin(Mp + M + 2 * F) + 0.00003 * sin(2 * Mp + 2 * F) + 0.00003 * sin(Mp + M - 2 * F) + 0.00004 * sin(3 * M) + 0.00004 * sin(2 * Mp - 2 * F) + -0.00007 * sin(Mp + 2 * M) + -0.00017 * sin(Om) + -0.00024 * E * sin(2 * Mp - M) + 0.00038 * E * sin(M - 2 * F) + 0.00042 * E * sin(M + 2 * F) + -0.00042 * sin(3 * Mp) + 0.00056 * E * sin(2 * Mp + M) + -0.00057 * sin(Mp + 2 * F) + -0.00111 * sin(Mp - 2 * F) + 0.00209 * E * E * sin(2 * M) + -0.00514 * E * sin(Mp + M) + 0.00734 * E * sin(Mp - M) + 0.01043 * sin(2 * F) + 0.01614 * sin(2 * Mp) + 0.17302 * E * sin(M) + -0.40614 * sin(Mp);
}
JDE += correction;
// dodatkowe poprawki (Meeus, str. 252)
correction = 0.000325 * sin(A1) + 0.000165 * sin(A2) + 0.000164 * sin(A3) + 0.000126 * sin(A4) + 0.000110 * sin(A5) + 0.000062 * sin(A6) + 0.000060 * sin(A7) + 0.000056 * sin(A8) + 0.000047 * sin(A9) + 0.000042 * sin(A10) + 0.000040 * sin(A11) + 0.000037 * sin(A12) + 0.000035 * sin(A13) + 0.000023 * sin(A14);
JDE += correction;
// konwersja daty juliańskiej na czas epoki (Unix)
unsigned long UTCdate = julianDateToUnix(JDE);
return UTCdate;
}
// PNG – wczytanie, obrót, rysowanie: sprawdź, czy identyfikator pliku się zmienił i w razie potrzeby wczytaj nowy
void LoadMoonFile(int idx) {
// na wszelki wypadek – zwalnia poprzednie bufory PNG
png.close();
char stridx[11];
// konwersja idx z int na string (system dziesiętny)
itoa(idx, stridx, 10);
strcpy(SDfilnam, "/");
strcat(SDfilnam, stridx);
strcat(SDfilnam, ".png");
// sprawdzenie, czy potrzeba wczytać inny plik
if (strcmp(SDfilnamO, SDfilnam)) {
// zapamiętaj nazwę wczytanego pliku
strcpy(SDfilnamO, SDfilnam);
freeImageBuffers();
Serial.print("Ładowanie ");
Serial.println(SDfilnam);
// wczytanie pliku PNG z karty SD
if (loadPngFromSD(SDfilnam)) {
Serial.println("PNG załadowano pomyślnie");
} else {
Serial.println("Nie udało się załadować PNG");
}
}
}
// Obliczanie daty juliańskiej (Julian Date) na podstawie daty i czasu kalendarzowego
double julianDate(int year, int month, int day, int hour, int minute, int second) {
if (month <= 2) {
year -= 1;
month += 12;
}
int A = floor(year / 100.0);
int B = 2 - A + floor(A / 4.0);
double JD = floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + day + B - 1524.5;
JD += (hour + minute / 60.0 + second / 3600.0) / 24.0;
return JD;
}
// Funkcje pomocnicze do konwersji kątów
double degToRad(double degrees) {
return degrees * M_PI / 180.0;
}
double radToDeg(double radians) {
return radians * 180.0 / M_PI;
}
// Tekst – wyświetlacz tekstowy (U8g2 + polskie znaki) aktualizacja części tekstowej wyświetlacza
void updateTxtDisplay() {
GetTimeStr();
//Serial.printf("🔄 updateTxtDisplay MoonCalc refresh: %04d.%02d.%02d %02d:%02d:%02d\n", year(now()), month(now()), day(now()), hour(now()), minute(now()), second(now()));
int i = 0;
char tmpstr[20];
// aktualna lokalna data
updateText(thisDateO, thisDate, 2, 14);
strcpy(thisDateO, thisDate);
// aktualny lokalny czas
updateText(thisTimeO, thisTime, 91, 14);
strcpy(thisTimeO, thisTime);
// Dzień tygodnia (thisWk)
int16_t xRight1 = tftText.width() - 3 - u8g2.getUTF8Width(thisWk);
updateText(thisWkO, thisWk, xRight1, 14);
strcpy(thisWkO, thisWk);
updateText((char *)"", (char *)"Strefa:", 2, 28);
snprintf(TZstr, sizeof(TZstr), "%s", TZdata[TZselect][0]);
// nazwa strefy czasowej
updateText(TZstrO, TZstr, 91, 28);
strcpy(TZstrO, TZstr);
// Status DST (thisDst)
int16_t xRight2 = tftText.width() - 3 - u8g2.getUTF8Width(thisDst);
updateText(thisDstO, thisDst, xRight2, 28);
strcpy(thisDstO, thisDst);
// lokalizacja
updateText((char *)"", (char *)"Miejsce: ", 2, 42);
snprintf(obsPlace, sizeof(obsPlace), "%s", observer_place);
updateText(obsPlaceO, obsPlace, 91, 44);
strcpy(obsPlaceO, obsPlace);
// wysokość npm obserwatora
snprintf(obsNpm, sizeof(obsNpm), "npm: %.1fm", observer_h_m);
int16_t xRight3 = tftText.width() - 3 - u8g2.getUTF8Width(obsNpm);
updateText(obsNpmO, obsNpm, xRight3, 42);
strcpy(obsNpmO, obsNpm);
// szerokość geograficzna obserwatora
snprintf(obsLat, sizeof(obsLat), "szer: %.1f°", observer_lat);
updateText(obsLatO, obsLat, 2, 58);
strcpy(obsLatO, obsLat);
// długość geograficzna obserwatora
snprintf(obsLon, sizeof(obsLon), "dług: %.1f°", observer_lon);
updateText(obsLonO, obsLon, 121, 58);
strcpy(obsLonO, obsLon);
// stały napis, nic nie kasujemy
updateText((char *)"", (char *)"Wysokość Azymut Zodiak", 20, 77);
updateText((char *)"", (char *)"K:", 2, 91);
updateText((char *)"", (char *)"S:", 2, 105);
// wysokość Księżyca
snprintf(moonAltStr, sizeof(moonAltStr), "%4.1f°", currentAstroData.moonAlt);
updateText(moonAltStrO, moonAltStr, 30, 91);
strcpy(moonAltStrO, moonAltStr);
// azymut Księżyca
snprintf(moonAzStr, sizeof(moonAzStr), "%4.1f°", currentAstroData.moonAz);
updateText(moonAzStrO, moonAzStr, 100, 91);
strcpy(moonAzStrO, moonAzStr);
// zodiak Księżyca
snprintf(moonZodiacStr, sizeof(moonZodiacStr), "%s", currentAstroData.moonZodiac.c_str());
updateText(moonZodiacStrO, moonZodiacStr, 159, 91);
strcpy(moonZodiacStrO, moonZodiacStr);
// wysokość Słońca
snprintf(sunAltStr, sizeof(sunAltStr), "%4.1f°", currentAstroData.sunAlt);
updateText(sunAltStrO, sunAltStr, 30, 105);
strcpy(sunAltStrO, sunAltStr);
// azymut Słońca
snprintf(sunAzStr, sizeof(sunAzStr), "%4.1f°", currentAstroData.sunAz);
updateText(sunAzStrO, sunAzStr, 100, 105);
strcpy(sunAzStrO, sunAzStr);
// zodiak Słońca
snprintf(sunZodiacStr, sizeof(sunZodiacStr), "%s", currentAstroData.sunZodiac.c_str());
updateText(sunZodiacStrO, sunZodiacStr, 159, 105);
strcpy(sunZodiacStrO, sunZodiacStr);
// kąt rosnący przeciwnie do ruchu wskazówek zegara od północy
snprintf(limbAngleStr, sizeof(limbAngleStr), "Kąt oświetlenia: %3.1f°", currentAstroData.brightLimbAngle);
updateText(limbAngleStrO, limbAngleStr, 2, 124);
strcpy(limbAngleStrO, limbAngleStr);
// jasność księżyca
snprintf(moonIllumStr, sizeof(moonIllumStr), "Jasność: %3.1f%%", currentAstroData.moonIllumination * 100.0);
updateText(moonIllumStrO, moonIllumStr, 2, 138);
strcpy(moonIllumStrO, moonIllumStr);
// etykiety linii dla czterech kolejnych faz (nów, I kwadra, pełnia, III kwadra)
for (i = 0; i < 4; i++) {
// opis fazy po polsku na podstawie NextMoonInd[i]
switch (NextMoonInd[i]) {
case 0:
strcpy(tmpstr, " Nów:");
break;
case 1:
strcpy(tmpstr, " I kwadra:");
break;
case 2:
strcpy(tmpstr, " Pełnia:");
break;
case 3:
strcpy(tmpstr, " III kwadra:");
break;
}
// jeśli zmienił się znacznik czasu pierwszej fazy, skasuj całą tabelę „Next Moon”
if (NextMoonUTCO != NextMoonUTC[0]) {
// współrzędne x,y,w,h – wypełnienie prostokąta kolorem
tftText.fillRect(2, 158, 236, 54, TFT_BLACK);
// nagłówek rysujemy ręcznie, żółtą czcionką U8g2
u8g2.setForegroundColor(TFT_DKYELLOW);
u8g2.setCursor(29, 155);
u8g2.print("Następna faza Księżyca");
// wracamy do standardowego koloru tekstu dla reszty tabelki
u8g2.setForegroundColor(TFT_COLOR1);
// zapamiętanie znacznika czasu pierwszej fazy
NextMoonUTCO = NextMoonUTC[0];
}
// konwersja znacznika czasu na czytelną datę/godzinę kolejnych faz
convUnix(NextMoonUTC[i]);
//sprintf(tmp2str, "%s %s %s", tmpstr, thisDate, thisTime);
snprintf(tmp2str, sizeof(tmp2str), "%s %s %s", tmpstr, thisDate, thisTime);
// każda faza w osobnej linii
updateText(tmp2strO, tmp2str, 4, 169 + i * 14);
}
// następny czarny księżyc
UTCsec = BkMoonM; // Tymczasowo ustaw UTCsec
GetTimeStr(); // Użyj GetTimeStr() do konwersji
// następny miesięczny czarny Księżyc
convUnix(BkMoonM);
sprintf(BkMoonMStr, " Czarny M: %s %s", thisDate, thisTime);
updateText(BkMoonMStrO, BkMoonMStr, 4, 232);
strcpy(BkMoonMStrO, BkMoonMStr);
// następny miesięczny niebieski Księżyc
convUnix(BlMoonM);
sprintf(BlMoonMStr, "Niebieski M: %s %s", thisDate, thisTime);
updateText(BlMoonMStrO, BlMoonMStr, 4, 246);
strcpy(BlMoonMStrO, BlMoonMStr);
// następny sezonowy czarny Księżyc
convUnix(BkMoonS);
sprintf(BkMoonSStr, " Czarny S: %s %s", thisDate, thisTime);
updateText(BkMoonSStrO, BkMoonSStr, 4, 260);
strcpy(BkMoonSStrO, BkMoonSStr);
// następny sezonowy niebieski Księżyc
convUnix(BlMoonS);
sprintf(BlMoonSStr, "Niebieski S: %s %s", thisDate, thisTime);
updateText(BlMoonSStrO, BlMoonSStr, 4, 274);
strcpy(BlMoonSStrO, BlMoonSStr);
// następny czerwony księżyc
convUnix(RedMoon);
sprintf(RedMoonStr, " Czerwony: %s %s", thisDate, thisTime);
updateText(RedMoonStrO, RedMoonStr, 4, 288);
strcpy(RedMoonStrO, RedMoonStr);
// następny Superksiężyc
convUnix(SuperMoon);
sprintf(SuperMoonStr, " Super: %s %s", thisDate, thisTime);
updateText(SuperMoonStrO, SuperMoonStr, 4, 302);
strcpy(SuperMoonStrO, SuperMoonStr);
// następny Mikroksiężyc
convUnix(MicroMoon);
sprintf(MicroMoonStr, " Micro: %s %s", thisDate, thisTime);
updateText(MicroMoonStrO, MicroMoonStr, 4, 316);
strcpy(MicroMoonStrO, MicroMoonStr);
}
// funkcja rysująca tekst z wymazywaniem poprzedniej wersji
void updateText(char tberased[20], char tbwritten[20], int xpos, int ypos) {
// nadpisanie starego tekstu kolorem tła
if (tberased[0] != '\0') {
u8g2.setForegroundColor(TFT_BLACK);
u8g2.setBackgroundColor(TFT_BLACK);
u8g2.setCursor(xpos, ypos);
u8g2.print(tberased);
}
// ustawienie koloru tekstu pierwszego planu
u8g2.setForegroundColor(TFT_COLOR1);
u8g2.setBackgroundColor(TFT_BLACK);
u8g2.setCursor(xpos, ypos);
// tu już leci UTF-8 z polskimi znakami
u8g2.print(tbwritten);
}
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);
}
void updateRTC() {
setTime(UTCsec); // set HW clock
rtc.adjust(UTCsec); // update RTC
phaseNow = true; // trigger moon calc immediately, override once a minute calc interval
}
// 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");
}
}
// Funkcja rysująca linię PNG do bufora originalImage
int pngDraw(PNGDRAW * pDraw) {
uint16_t lineBuffer[MAX_IMAGE_WIDTH];
//png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff);
// Skopiuj tę linię do naszego bufora obrazu, jeśli dany wiersz istnieje
if (pDraw->y < imageHeight && originalImage[pDraw->y] != NULL) {
for (int x = 0; x < pDraw->iWidth && x < imageWidth; x++) {
originalImage[pDraw->y][x] = lineBuffer[x];
}
}
// 1 = kontynuuj dekodowanie (wymagane przez tę wersję PNGdec)
return 1;
}
// przydzielenie pamięci na oba bufory obrazów
bool allocateImageBuffers() {
// inicjalizacja wszystkich wskaźników jako NULL
for (int i = 0; i < MAX_IMAGE_HEIGHT; i++) {
originalImage[i] = NULL;
rotatedImage[i] = NULL;
}
// przydzielenie pamięci dla każdego wiersza
for (int i = 0; i < imageHeight; i++) {
originalImage[i] = (uint16_t *)malloc(imageWidth * sizeof(uint16_t));
rotatedImage[i] = (uint16_t *)malloc(imageWidth * sizeof(uint16_t));
if (!originalImage[i] || !rotatedImage[i]) {
// błąd alokacji pamięci – zwolnij dotychczasowe przydziały
for (int j = 0; j <= i; j++) {
if (originalImage[j]) free(originalImage[j]);
if (rotatedImage[j]) free(rotatedImage[j]);
originalImage[j] = NULL;
rotatedImage[j] = NULL;
}
return false;
}
// wstępne wypełnienie wiersza obrazu obróconego kolorem „przezroczystym”
for (int x = 0; x < imageWidth; x++) {
// używane, by nie nadpisywać tła, jeśli dany piksel nie został dotknięty przez przekształcenie
rotatedImage[i][x] = TRANSPARENT_COLOR;
}
}
return true;
}
bool loadPngFromSD(const char *filename) {
// otwarcie pliku
File pngFile = SD.open(filename, FILE_READ);
if (!pngFile) {
Serial.println("Nie udało się otworzyć pliku PNG");
return false;
}
// utworzenie bufora na dane pliku
size_t fileSize = pngFile.size();
uint8_t *pngBuffer = (uint8_t *)malloc(fileSize);
if (!pngBuffer) {
Serial.println("Za mało pamięci, aby załadować PNG");
pngFile.close();
return false;
}
// odczyt całego pliku do bufora
pngFile.read(pngBuffer, fileSize);
pngFile.close();
// dekodowanie PNG z bufora
int16_t rc = png.openRAM(pngBuffer, fileSize, pngDraw);
// domyślnie jest sporo większy, 8-16k często wystarczy
//png.setBufferSize(8192);
if (rc != PNG_SUCCESS) {
Serial.printf("Błąd dekodowania PNG: %d\n", rc);
free(pngBuffer);
return false;
}
// pobranie rozmiarów obrazu
imageWidth = png.getWidth();
imageHeight = png.getHeight();
// sprawdzenie, czy rozmiary obrazu nie są zbyt duże
if (imageWidth > MAX_IMAGE_WIDTH || imageHeight > MAX_IMAGE_HEIGHT) {
Serial.println("Obraz za duży do buforowania");
free(pngBuffer);
return false;
}
// przydzielenie pamięci na bufory obrazów
if (!allocateImageBuffers()) {
Serial.println("Nie udało się przydzielić pamięci dla buforów obrazu");
free(pngBuffer);
return false;
}
// dekodowanie PNG do naszego bufora
rc = png.decode(NULL, 0);
// zwolnienie bufora z danymi PNG
free(pngBuffer);
return (rc == PNG_SUCCESS);
}
// pobranie piksela z obrazu źródłowego
uint16_t getPixel(int x, int y) {
if (x >= 0 && x < imageWidth && y >= 0 && y < imageHeight && originalImage[y] != NULL) {
return originalImage[y][x];
}
// dla współrzędnych poza zakresem zwróć czarny
return TFT_BLACK;
}
// ustawienie piksela w obrazie obróconym
void setPixel(int x, int y, uint16_t color) {
if (x >= 0 && x < imageWidth && y >= 0 && y < imageHeight && rotatedImage[y] != NULL) {
rotatedImage[y][x] = color;
}
}
// obrót obrazu o dowolny kąt
void rotateImage(float angle) {
// konwersja kąta na radiany
float rad = angle * PI / 180.0;
float sinma = sin(rad);
float cosma = cos(rad);
// środek obrazu
float cx = imageWidth / 2.0;
float cy = imageHeight / 2.0;
// obrót z użyciem najbliższego sąsiada (szybkość ważniejsza niż jakość interpolacji)
for (int y = 0; y < imageHeight; y++) {
for (int x = 0; x < imageWidth; x++) {
// obliczenie współrzędnych źródłowych
float srcX = cosma * (x - cx) + sinma * (y - cy) + cx;
float srcY = -sinma * (x - cx) + cosma * (y - cy) + cy;
// sprawdzenie, czy współrzędne źródłowe mieszczą się w obrazie
if (srcX >= 0 && srcX < imageWidth && srcY >= 0 && srcY < imageHeight) {
// zaokrąglenie do najbliższego piksela
int sx = (int)(srcX + 0.5);
int sy = (int)(srcY + 0.5);
// skopiowanie piksela
setPixel(x, y, getPixel(sx, sy));
}
}
}
}
// wyświetlenie obróconego obrazu na TFT
void displayRotatedImage(int x, int y) {
for (int row = 0; row < imageHeight; row++) {
if (rotatedImage[row] != NULL) {
for (int col = 0; col < imageWidth; col++) {
uint16_t c = rotatedImage[row][col];
if (c != TRANSPARENT_COLOR) {
tftMoon.drawPixel(x + col, y + row, c);
}
}
}
}
}
// zwolnienie pamięci obu buforów obrazów, aby wczytać nowy
void freeImageBuffers() {
for (int i = 0; i < MAX_IMAGE_HEIGHT; i++) {
if (originalImage[i])
free(originalImage[i]);
if (rotatedImage[i])
free(rotatedImage[i]);
originalImage[i] = NULL;
rotatedImage[i] = NULL;
}
}
// Zwraca procent jasności (0..100) albo -1 jak coś nie tak
int getMoonIllumFromTable() {
// znajdź poprzedni i następny NÓW względem UTCsec
int prevNewIdx = -1;
int nextNewIdx = -1;
// nowe księżyce są gdzie phaseType == 0
for (int i = 0; i < 240; i++) {
if (moonPhases[i].phaseType == 0) { // nów
time_t t = moonPhases[i].timestamp;
if (t <= UTCsec) {
prevNewIdx = i; // ostatni nów <= teraz
} else {
nextNewIdx = i; // pierwszy nów > teraz
break;
}
}
}
if (prevNewIdx < 0 || nextNewIdx < 0) {
return -1;
}
double t0 = (double)moonPhases[prevNewIdx].timestamp;
double t1 = (double)moonPhases[nextNewIdx].timestamp;
double now = (double)UTCsec;
double age = now - t0; // ile czasu minęło od nowiu
double period = t1 - t0; // długość całego cyklu między nowiami
if (period <= 0.0) {
return -1;
}
double phase01 = age / period;
if (phase01 < 0.0) phase01 = 0.0;
if (phase01 > 1.0) phase01 = 1.0;
double frac = 0.5 * (1.0 - cos(2.0 * M_PI * phase01));
int illumPct = (int)round(frac * 100.0);
if (illumPct < 0) illumPct = 0;
if (illumPct > 100) illumPct = 100;
return illumPct;
}Loading
esp32-devkit-c-v4
esp32-devkit-c-v4