#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 "AstroCalc.h"
#include <math.h>
// KONFIGURACJA UŻYTKOWNIKA
const char *ssid = "Wokwi-GUEST"; // Tu wpisz swoje dane
const char *pass = "";
const char observer_place[] = "Caino";
double observer_lat = 45.613418;
double observer_lon = 10.312439;
double observer_h_m = 385.0;
// Interwały (ms)
const unsigned long CLOCK_UPDATE_INTERVAL = 1000; // Co 1s odświeżanie zegara
const unsigned long ASTRO_UPDATE_INTERVAL = 1800000; // Co 30 min pełne przeliczenie astro
const unsigned long SCREEN_TIMEOUT = 60000;//10UL * 60UL * 1000UL; // Czas bezczynności do wygaszenia ekranu (10 minut)
const unsigned long DEBOUNCE_DELAY = 50;
// DEFINICJE PINÓW (ESP32)
#define TFT_MOSI 11 // GPIO11
#define TFT_SCLK 12 // GPIO12
#define TFT_MISO 13 // GPIO13
#define TFT_DC 2 // może zostać 2 – zwykły GPIO
#define TFT_RST 4 // reset TFT
#define DispText_CS 5 // CS pierwszego wyświetlacza
#define DispMoon_CS 6 // CS drugiego wyświetlacza
// Karta SD (HSPI)
#define SD_MOSI 14 // GPIO14
#define SD_MISO 15 // GPIO15
#define SD_SCLK 16 // GPIO16
#define SD_CS 17 // GPIO17
// I2C
#define I2C_SDA 8 // GPIO8
#define I2C_SCL 9 // GPIO9
//
#define BTN_PIN 21 // przycisk z PULLUP, zwiera do GND
#define LED_R_PIN 38 // czerwony
#define LED_G_PIN 39 // zielony
#define LED_B_PIN 40 // niebieski
// OBIEKTY
Adafruit_ILI9341 tftText(DispText_CS, TFT_DC, TFT_RST);
Adafruit_ILI9341 tftMoon(DispMoon_CS, TFT_DC, TFT_RST);
//Adafruit_ILI9341 tftText = Adafruit_ILI9341(DispText_CS, TFT_DC, TFT_RST);
//Adafruit_ILI9341 tftMoon = Adafruit_ILI9341(DispMoon_CS, TFT_DC, TFT_RST);
U8G2_FOR_ADAFRUIT_GFX u8g2;
SPIClass spiSD(HSPI);
RTC_DS3231 rtc;
PNG png;
AstroCalc::AstroCalc astro(observer_lat, observer_lon);
// ZMIENNE GLOBALNE
AstroCalc::AstronomicalData currentAstroData;
AstroCalc::MoonPhaseEvent moonPhases[300];
time_t UTCsec = 0;
unsigned long lastClockUpdate = 0;
unsigned long lastAstroUpdate = 0;
unsigned long lastActivity = 0; // ostatnia aktywność (w tym wciśnięcie przycisku)
unsigned long lastDebounceTime = 0;
bool firstRun = true;
// Stan systemu
bool systemReady = false; // ustawiasz na true kiedy masz już sensowny czas i policzone dane
bool screenSaver = false; // czy wyświetlacze są „uśpione”
int lastBtnState = HIGH; // dla detekcji zbocza
// STAN SYSTEMU / EKRAN STARTOWY
bool timeSynced = false; // czy czas jest OK (RTC lub NTP)
bool astroReady = false; // czy policzono dane astro
// Bufory tekstowe (do wygaszania starych napisów)
char thisTime[10], thisTimeO[10];
char thisDate[20], thisDateO[20];
char thisWk[30], thisWkO[30];
char TZstr[15], TZstrO[15];
char thisDst[20], thisDstO[20];
char obsPlace[20], obsPlaceO[20];
char obsNpm[20], obsNpmO[20];
char obsLat[20], obsLatO[20];
char obsLon[20], obsLonO[20];
char tmp2str[64], tmp2strO[64];
// Bufory danych astro
char moonAltStr[20], moonAltStrO[20];
char moonAzStr[20], moonAzStrO[20];
char moonZodiacStr[32], moonZodiacStrO[32];
char sunAltStr[20], sunAltStrO[20];
char sunAzStr[20], sunAzStrO[20];
char sunZodiacStr[32], sunZodiacStrO[32];
char limbAngleStr[40], limbAngleStrO[40];
char moonIllumStr[40], moonIllumStrO[40];
// Specjalne księżyce - bufory
char BkMoonMStr[40], BkMoonMStrO[40];
char BlMoonMStr[40], BlMoonMStrO[40];
char BkMoonSStr[40], BkMoonSStrO[40];
char BlMoonSStr[40], BlMoonSStrO[40];
char RedMoonStr[40], RedMoonStrO[40];
char SuperMoonStr[40], SuperMoonStrO[40];
char MicroMoonStr[40], MicroMoonStrO[40];
//
char bootLine2[40] = "", bootLine2O[40] = "";
char bootLine3[40] = "", bootLine3O[40] = "";
// Zmienne czasowe dla zdarzeń
time_t BkMoonM = 0, BlMoonM = 0;
time_t BkMoonS = 0, BlMoonS = 0;
time_t RedMoon = 0, SuperMoon = 0, MicroMoon = 0;
time_t seasonUTC[24];
// Struktury do pór roku i faz
struct MoonPhaseEvent {
time_t timestamp;
int phaseType; // 0=nów, 1=I kw, 2=pełnia, 3=III kw
double distance; // R_Ziemi
};
time_t NextMoonUTC[4];
int NextMoonInd[4];
time_t NextMoonUTCO = 0;
int phaseRaw = 0; // 0-1199
// Kolory
#define TFT_COLOR1 0x6519
#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
#define TFT_RED 0xF800
// 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
int imageWidth = 0; // Będzie 164
int imageHeight = 0; // Będzie 164
uint16_t *originalImage[MAX_IMAGE_HEIGHT] = {NULL};
uint16_t *rotatedImage[MAX_IMAGE_HEIGHT] = {NULL};
// biel jest traktowana jako kolor przezroczysty i nie jest rysowana na wyświetlaczu
uint16_t TRANSPARENT_COLOR = 0xFFFF;
char SDfilnam[20] = "xxxxxx.png";
char SDfilnamO[20] = "xxxxxx.png";
// Strefy czasowe
const char * const TZdata[][4] = {
{ "Europe", "CET-1CEST,M3.5.0,M10.5.0/3", "CET", "CEST" },
{ "UTC", "GMT0", "UTC", "UTC" }
};
// mapowanie typu fazy 0..3 -> indeks w phaseNames[8]
static const uint8_t mainPhaseIdx[4] = { 0, 2, 4, 6 };
uint8_t TZselect = 0;
struct tm ltz; // czas lokalny
// DEKLARACJE FUNKCJI
void syncTimeLogic();
void updateAllAstroData();
void updateTxtDisplay();
void updateText(char* oldBuf, char* newBuf, int x, int y);
void GetTimeStr();
void convUnix(time_t ts);
void displayMoonSunPos();
void computePhases(int year);
void FindBlackMoonM();
void FindBlueMoonM();
void FindBlackMoonS();
void FindBlueMoonS();
void FindRedMoon();
void FindSuperMoon();
void FindMicroMoon();
bool loadPngFromSD(const char *filename);
void rotateImage(float angle);
void displayRotatedImage(int x, int y);
void freeImageBuffers();
void LoadMoonFile(int idx);
// Algorytmy Meeusa
double getCycleEstimate(int year, int month);
time_t getPhaseDate(double cycle, double phase);
unsigned long julianDateToUnix(double julianDate);
// Funkcja bezpiecznego sprawdzania timeoutu
bool isTimedOut(unsigned long lastTime, unsigned long timeout) {
unsigned long current = millis();
unsigned long elapsed;
if (current >= lastTime) {
// Normalny przypadek - bez przepełnienia
elapsed = current - lastTime;
} else {
// Przepełnienie - lastTime było przed przepełnieniem
elapsed = (ULONG_MAX - lastTime) + current;
}
return elapsed >= timeout;
}
// Ustawienie koloru przycisku RGB (0 lub 1)
void setLedColor(bool r, bool g, bool b) {
digitalWrite(LED_R_PIN, r ? HIGH : LOW);
digitalWrite(LED_G_PIN, g ? HIGH : LOW);
digitalWrite(LED_B_PIN, b ? HIGH : LOW);
}
// Wygaszenie wyświetlaczy + przejście w tryb „uśpiony”
void activateScreensaver() {
if (screenSaver)
return;
screenSaver = true;
// wygaszenie obu TFT
tftText.fillScreen(TFT_BLACK);
tftMoon.fillScreen(TFT_BLACK);
// Wyłącz wyświetlacz fizycznie
//tftText.writeCommand(ILI9341_DISPOFF);
//tftMoon.writeCommand(ILI9341_DISPOFF);
// LED: zielony = śpi / oszczędzanie
setLedColor(false, true, false);
Serial.println(">>> Wygaszacz AKTYWNY");
}
// Wybudzenie wyświetlaczy
void wakeFromScreensaver() {
if (!screenSaver)
return;
screenSaver = false;
Serial.printf("[WAKE] lastActivity ustawione na: %lu\n", lastActivity);
// Włącz wyświetlacz fizycznie
//tftText.writeCommand(ILI9341_DISPON);
//tftMoon.writeCommand(ILI9341_DISPON);
delay(100);
// pełne przerysowanie wszystkiego
updateAllAstroData();
updateTxtDisplay();
lastActivity = millis();
Serial.printf(">>> Wybudzenie zakończone. lastActivity=%lu\n", lastActivity);
// LED: niebieski = normalna praca
setLedColor(false, false, true);
}
// Rysuje podstawowe komunikaty startowe na ekranie tekstowym
void drawStartupScreenBase() {
tftText.fillScreen(TFT_BLACK);
tftText.drawRect(0, 0, tftText.width(), tftText.height(), TFT_BLUE);
u8g2.setForegroundColor(TFT_COLOR1);
tftText.setCursor(5, 5);
tftText.print("Start systemu AstroCalc");
// linie 2/3 inicjalnie puste – będą nadpisywane przez drawStartupStatus
bootLine2O[0] = '\0';
bootLine3O[0] = '\0';
}
// Aktualizuje tekst statusu po synchronizacji
void drawStartupStatus(const char* line2, const char* line3) {
strncpy(bootLine2, line2, sizeof(bootLine2));
bootLine2[sizeof(bootLine2) - 1] = '\0';
strncpy(bootLine3, line3, sizeof(bootLine3));
bootLine3[sizeof(bootLine3) - 1] = '\0';
updateText(bootLine2O, bootLine2, 5, 100);
strcpy(bootLine2O, bootLine2);
updateText(bootLine3O, bootLine3, 5, 120);
strcpy(bootLine3O, bootLine3);
}
// SETUP
void setup() {
Serial.begin(115200);
pinMode(BTN_PIN, INPUT_PULLUP); // przycisk zwiera do GND
pinMode(LED_R_PIN, OUTPUT);
pinMode(LED_G_PIN, OUTPUT);
pinMode(LED_B_PIN, OUTPUT);
// start: zasilanie / boot / synchronizacja -> czerwony
setLedColor(true, false, false);
delay(500);
Serial.println("\nSTART SYSTEMU AstroCalc v2.1");
Wire.begin(I2C_SDA, I2C_SCL);
if (!rtc.begin()) {
Serial.println("BŁĄD: Brak RTC DS3231!");
}
// 1TFT + SPI
SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI);
tftText.begin();
tftText.setRotation(0);
tftText.fillScreen(TFT_BLACK);
tftText.drawRect(0, 0, tftText.width(), tftText.height(), TFT_BLUE);
tftMoon.begin();
tftMoon.setRotation(0);
tftMoon.fillScreen(TFT_BLACK);
tftMoon.drawRect(0, 0, tftMoon.width(), tftMoon.height(), TFT_BLUE);
// U8g2
u8g2.begin(tftText);
u8g2.setFont(u8g2_font_unifont_te);
u8g2.setFontMode(0);
u8g2.setForegroundColor(TFT_COLOR1);
u8g2.setBackgroundColor(TFT_BLACK);
// Ekran startowy
drawStartupScreenBase();
// Synchronizacja czasu
syncTimeLogic(); // TYLKO RAZ
// Strefa czasowa dopiero po sync
setenv("TZ", TZdata[TZselect][1], 1);
tzset();
// SD
spiSD.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS, spiSD)) {
Serial.println("Błąd karty SD!");
}
// Astro
astro.setObserverHeight(observer_h_m);
// czas OK → czyścimy ekrany z komunikatów i startujemy normalny widok
if (timeSynced) {
// wyczyść oba ekrany z ekranu startowego
tftText.fillScreen(TFT_BLACK);
tftText.drawRect(0, 0, tftText.width(), tftText.height(), TFT_BLUE);
tftMoon.fillScreen(TFT_BLACK);
tftMoon.drawRect(0, 0, tftMoon.width(), tftMoon.height(), TFT_BLUE);
DateTime now = rtc.now();
UTCsec = now.unixtime();
updateAllAstroData(); // narysuje Księżyc + orbitę
updateTxtDisplay(); // od razu pierwsze dane tekstowe
astroReady = true;
systemReady = true;
screenSaver = false;
//lastActivity = millis();
// system działa normalnie -> niebieski
setLedColor(false, false, true);
} else {
// brak sensownego czasu
systemReady = false;
setLedColor(true, false, false);
}
lastActivity = millis();
lastBtnState = digitalRead(BTN_PIN);
}
// LOOP
void loop() {
unsigned long currentMillis = millis();
static unsigned long lastDebug = 0;
if (currentMillis - lastDebug > 5000) {
Serial.printf("[DEBUG] screenSaver: %d, lastActivity diff: %lu, systemReady: %d\n", screenSaver, currentMillis - lastActivity, systemReady);
lastDebug = currentMillis;
}
// OBSŁUGA PRZYCISKU (INPUT_PULLUP, więc wciśnięty = LOW)
int btnState = digitalRead(BTN_PIN);
if (lastBtnState == HIGH && btnState == LOW) {
Serial.println("[BTN] Przycisk wciśnięty!");
lastActivity = currentMillis;
if (screenSaver) {
wakeFromScreensaver();
//lastActivity = 0;
Serial.println(">>> Wybudź!");
}
}
lastBtnState = btnState;
// Jeśli śpimy, NIC już nie rysuj dalej:
if (screenSaver) {
// ewentualnie delikatne oszczędzanie mocy, mniejsze obciążenie
delay(50);
return;
Serial.println(">>> Śpię!");
}
// NORMALNE OPERACJE (zawsze resetują lastActivity)
bool wasActive = false;
// ZEGAR (co 1 sekunda)
if (currentMillis - lastClockUpdate >= CLOCK_UPDATE_INTERVAL) {
//lastActivity = currentMillis;
DateTime now = rtc.now();
UTCsec = now.unixtime();
updateTxtDisplay();
lastClockUpdate = currentMillis;
}
// ASTRONOMIA (co 30 min LUB przy pierwszym uruchomieniu)
if (currentMillis - lastAstroUpdate >= ASTRO_UPDATE_INTERVAL || firstRun) {
lastActivity = currentMillis;
Serial.println("🔄 Pełne odświeżanie Astro...");
updateAllAstroData();
lastAstroUpdate = currentMillis;
firstRun = false;
//Serial.printf("RAM wolne: %lu bajtów\n", ESP.getFreeHeap());
}
// sprawdź timeout - ale tylko jeśli nie było aktywności w tej iteracji
if (systemReady && !screenSaver) {
// Proste sprawdzenie - działa do 49 dni
if (currentMillis - lastActivity >= SCREEN_TIMEOUT) {
Serial.printf("[SCREEN] Aktywuję wygaszacz! Bezczynność: %lu ms\n", currentMillis - lastActivity);
activateScreensaver();
}
}
}
// wyświetlenie ORYGINALNEGO obrazu na TFT (bez rotacji)
void displayOriginalImage(int x, int y) {
//Serial.printf("MAX_IMAGE_HEIGHT=%d, sizeof(originalImage)=%d bajtów\n", MAX_IMAGE_HEIGHT, sizeof(originalImage)/sizeof(originalImage[0]));
// UWAGA: Używa globalnych: imageWidth, imageHeight, originalImage
for (int r = 0; r < imageHeight; r++) {
if (!originalImage[r]) continue;
for (int c = 0; c < imageWidth; c++) {
uint16_t col = originalImage[r][c];
// Nadal używamy TFT_BLACK jako klucza przezroczystości (tak jak w pngDraw)
if (col != TFT_BLACK) {
tftMoon.drawPixel(x + c, y + r, col);
}
}
}
}
// GŁÓWNA FUNKCJA AKTUALIZUJĄCA
void updateAllAstroData() {
///unsigned long startTime = millis();
// Obliczenia "TERAZ" AstroCalc
currentAstroData = astro.calculate(UTCsec);
// Obliczenia "PRZYSZŁOŚĆ" (Fazy, Black/Red Moon)
struct tm * ptm = gmtime(&UTCsec);
int currentYear = ptm->tm_year + 1900;
// Obliczamy fazy na dłuższy okres, aby znaleźć rzadkie zjawiska
astro.computePhases(currentYear, moonPhases, 300);
astro.computeSeasons(currentYear, seasonUTC, 24);
// oblicz sezony
//astro.computeSeasons(currentYear, seasonUTC, 24);
FindBlackMoonM();
FindBlueMoonM();
FindBlackMoonS();
FindBlueMoonS();
FindRedMoon();
FindSuperMoon();
FindMicroMoon();
// Grafika Księżyca (PNG)
// Szukamy aktualnej fazy w tablicy, aby ustawić phaseRaw (dla płynności animacji)
for (int i = 1; i < 300; i++) {
if (moonPhases[i].timestamp > UTCsec) {
long iPhase = moonPhases[i].timestamp - moonPhases[i - 1].timestamp;
long uPhase = UTCsec - moonPhases[i - 1].timestamp;
switch (moonPhases[i - 1].phaseType) {
case 0: phaseRaw = map(uPhase, 0, iPhase, 0, 299); break;
case 1: phaseRaw = map(uPhase, 0, iPhase, 300, 599); break;
case 2: phaseRaw = map(uPhase, 0, iPhase, 600, 899); break;
case 3: phaseRaw = map(uPhase, 0, iPhase, 900, 1199); break;
}
// Zapisujemy następne 4 fazy do tabelki
for (int k = 0; k < 4; k++) {
if (i + k < 300) {
NextMoonUTC[k] = moonPhases[i + k].timestamp;
NextMoonInd[k] = moonPhases[i + k].phaseType;
}
}
break;
}
}
int displayPhaseRaw = phaseRaw + 15;
if (displayPhaseRaw > 1199)
displayPhaseRaw -= 1200;
int filnum = map(displayPhaseRaw, 0, 1199, 0, 40);
if (filnum > 39)
filnum = 0;
LoadMoonFile(filnum);
// Kąt obrotu
double angle = currentAstroData.positionAngles.brightLimb;
int tmp = 90;
if (filnum > 20) tmp = -90;
int centerX = (tftMoon.width() - imageWidth) / 2;
int centerY = ((tftMoon.height() - imageHeight) / 2) - 75;
rotateImage(360.0 - angle - tmp);
displayRotatedImage(centerX, centerY);
//displayOriginalImage(centerX, centerY);
//tftMoon.drawRect(centerX - 1, centerY - 1, imageWidth + 2, imageHeight + 2, TFT_YELLOW);
// Rysowanie Orbity
displayMoonSunPos();
///unsigned long endTime = millis();
///Serial.printf("[PERF] updateAllAstroData() took: %lu ms\n", endTime - startTime);
}
// 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
// przesunięcie elipsy o 90 pikseli w dół od środka
centerY += 65;
// Czyścimy dolną część
tftMoon.fillRect(1, 175, 238, 144, TFT_BLACK);
//tftMoon.drawRect(1, 175, 238, 143, TFT_YELLOW);
// Ziemia (środek)
tftMoon.fillCircle(centerX, centerY, 7, TFT_DKGREEN);
tftMoon.drawEllipse(centerX, centerY, 90, 30, TFT_COLOR1);
// Znaczniki
tftMoon.setTextColor(TFT_BLUE, TFT_BLACK);
// Ekran: X rośnie w prawo. Y rośnie w dół.
// N (Az 0): X=0, Y=-R. (Góra)
// E (Az 90): X=R, Y=0. (Prawo)
// S (Az 180): X=0, Y=R. (Dół)
// W (Az 270): X=-R, Y=0. (Lewo)
// Rysowanie liter (Zakładamy Półkulę Północną jako domyślną orientację mapy)
tftMoon.setTextSize(1);
tftMoon.setCursor(centerX - 3, centerY - 30); tftMoon.print("N"); // Góra
tftMoon.setCursor(centerX + 85, centerY - 4); tftMoon.print("E"); // Prawo
tftMoon.setCursor(centerX - 3, centerY + 24); tftMoon.print("S"); // Dół
tftMoon.setCursor(centerX - 90, centerY - 4); tftMoon.print("W"); // Lewo
// Pobieramy azymuty (w radianach z biblioteki) AstroCalc zwraca: 0=N, PI/2=E, PI=S.
double m_az = currentAstroData.moonPosition.azimuth;
double s_az = currentAstroData.sunPosition.azimuth;
// Konwersja na współrzędne ekranowe:
// x = R * sin(az)
// y = -R * cos(az) (minus bo Y rośnie w dół, a cos(0)=1 czyli góra)
// Pozycja Księżyca (wewnątrz elipsy)
int MposX = centerX + (int)(25.0 * sin(m_az));
int MposY = centerY - (int)(15.0 * cos(m_az));
tftMoon.fillCircle(MposX, MposY, 3, TFT_SILVER);
// Pozycja Słońca (na zewnątrz)
int SposX = centerX + (int)(105.0 * sin(s_az));
int SposY = centerY - (int)(42.0 * cos(s_az)); // Spłaszczona elipsa
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);
// RZUT Z BOKU
int horizonY = centerY + 65; // linia horyzontu poniżej elipsy
int horizonX = 90; // pół długości linii horyzontu w px
// 0.142857f = 1° = ~0.143px (ok. ±70° -> ±10px, co daje 20px zakresu)
// 0.2142857f = 1° = ~0.214px (ok. ±70° -> ±15px, co daje 30px zakresu)
const float ALT_SCALE = 0.2142857f;
// Linia horyzontu i Ziemia (widok z boku)
tftMoon.drawLine(centerX - horizonX, horizonY, centerX + horizonX, horizonY, TFT_COLOR1);
tftMoon.fillCircle(centerX, horizonY, 7, TFT_DKGREEN);
// Wysokość w stopniach (z ograniczeniem)
float mAlt = constrain(currentAstroData.moonPosition.elevation * RAD_TO_DEG, -70, 70);
float sAlt = constrain(currentAstroData.sunPosition.elevation * RAD_TO_DEG, -70, 70);
// Pozycje Y (wyżej = większa wysokość)
int MsideY = horizonY - mAlt * ALT_SCALE;
int SsideY = horizonY - sAlt * ALT_SCALE;
// Księżyc i Słońce w rzucie bocznym
tftMoon.fillCircle(MposX, MsideY, 3, TFT_SILVER);
tftMoon.fillCircle(SposX, SsideY, 8, TFT_YELLOW);
}
// EKRAN TEKSTOWY
void updateTxtDisplay() {
///unsigned long startTime = millis();
GetTimeStr();
updateText(thisDateO, thisDate, 2, 14); strcpy(thisDateO, thisDate);
updateText(thisTimeO, thisTime, 91, 14); strcpy(thisTimeO, thisTime);
int16_t xRight = tftText.width() - 3 - u8g2.getUTF8Width(thisWk);
updateText(thisWkO, thisWk, xRight, 14); strcpy(thisWkO, thisWk);
updateText((char*)"", (char*)"Strefa:", 2, 28);
updateText(TZstrO, TZstr, 91, 28); strcpy(TZstrO, TZstr);
xRight = tftText.width() - 3 - u8g2.getUTF8Width(thisDst);
updateText(thisDstO, thisDst, xRight, 28); strcpy(thisDstO, thisDst);
updateText((char*)"", (char*)"Miejsce: ", 2, 42);
updateText(obsPlaceO, (char*)observer_place, 91, 42); strcpy(obsPlaceO, obsPlace);
snprintf(obsNpm, sizeof(obsNpm), "npm: %.0fm", observer_h_m);
xRight = tftText.width() - 3 - u8g2.getUTF8Width(obsNpm);
updateText(obsNpmO, obsNpm, xRight, 42); strcpy(obsNpmO, obsNpm);
snprintf(obsLat, sizeof(obsLat), "szer: %.1f°", observer_lat);
updateText(obsLatO, obsLat, 2, 58); strcpy(obsLatO, obsLat);
snprintf(obsLon, sizeof(obsLon), "dług: %.1f°", observer_lon);
updateText(obsLonO, obsLon, 121, 58); strcpy(obsLonO, obsLon);
updateText((char*)"", (char*)"Wysokość Azymut Zodiak", 20, 77);
// Wiersz Księżyca
updateText((char*)"", (char*)"K:", 2, 91);
snprintf(moonAltStr, sizeof(moonAltStr), "%5.1f°", currentAstroData.moonPosition.elevation * RAD_TO_DEG);
updateText(moonAltStrO, moonAltStr, 30, 91); strcpy(moonAltStrO, moonAltStr);
snprintf(moonAzStr, sizeof(moonAzStr), "%5.1f°", currentAstroData.moonPosition.azimuth * RAD_TO_DEG);
updateText(moonAzStrO, moonAzStr, 100, 91); strcpy(moonAzStrO, moonAzStr);
snprintf(moonZodiacStr, sizeof(moonZodiacStr), "%s", currentAstroData.moonData.zodiacMoonSign);
updateText(moonZodiacStrO, moonZodiacStr, 159, 91); strcpy(moonZodiacStrO, moonZodiacStr);
// Wiersz Słońca
updateText((char*)"", (char*)"S:", 2, 105);
snprintf(sunAltStr, sizeof(sunAltStr), "%5.1f°", currentAstroData.sunPosition.elevation * RAD_TO_DEG);
updateText(sunAltStrO, sunAltStr, 30, 105); strcpy(sunAltStrO, sunAltStr);
snprintf(sunAzStr, sizeof(sunAzStr), "%5.1f°", currentAstroData.sunPosition.azimuth * RAD_TO_DEG);
updateText(sunAzStrO, sunAzStr, 100, 105); strcpy(sunAzStrO, sunAzStr);
snprintf(sunZodiacStr, sizeof(sunZodiacStr), "%s", currentAstroData.sunPosition.zodiacSunSign);
updateText(sunZodiacStrO, sunZodiacStr, 159, 105); strcpy(sunZodiacStrO, sunZodiacStr);
snprintf(limbAngleStr, sizeof(limbAngleStr), "Kąt oświetlenia: %3.1f°", currentAstroData.positionAngles.brightLimb);
updateText(limbAngleStrO, limbAngleStr, 2, 124); strcpy(limbAngleStrO, limbAngleStr);
snprintf(moonIllumStr, sizeof(moonIllumStr), "Jasność: %3.1f%%", currentAstroData.moonPosition.illumination * 100.0);
updateText(moonIllumStrO, moonIllumStr, 2, 138); strcpy(moonIllumStrO, moonIllumStr);
// 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);
updateText((char *)"", (char *)"Następna faza Księżyca: ", 29, 155);
// zapamiętanie znacznika czasu pierwszej fazy
NextMoonUTCO = NextMoonUTC[0];
}
//char tmp2str[64];
for (int i = 0; i < 4; i++) {
// bierzemy nazwę z biblioteki
const char* phaseName = AstroCalc::AstroCalc::phaseNames[mainPhaseIdx[ NextMoonInd[i]]];
// ustawia thisDate / thisTime
convUnix(NextMoonUTC[i]);
// wyrównanie w kolumnie,
snprintf(tmp2str, sizeof(tmp2str), "%s: %s %s", phaseName, thisDate, thisTime);
// każda faza w osobnej linii
updateText(tmp2strO, tmp2str, 4, 169 + i * 14);
}
// Specjalne księżyce
convUnix(BkMoonM);
snprintf(BkMoonMStr, sizeof(BkMoonMStr), " Czarny M: %s %s", thisDate, thisTime);
updateText(BkMoonMStrO, BkMoonMStr, 4, 232); strcpy(BkMoonMStrO, BkMoonMStr);
convUnix(BlMoonM);
snprintf(BlMoonMStr, sizeof(BlMoonMStr), "Niebieski M: %s %s", thisDate, thisTime);
updateText(BlMoonMStrO, BlMoonMStr, 4, 246); strcpy(BlMoonMStrO, BlMoonMStr);
convUnix(BkMoonS);
snprintf(BkMoonSStr, sizeof(BkMoonSStr), " Czarny S: %s %s", thisDate, thisTime);
updateText(BkMoonSStrO, BkMoonSStr, 4, 260); strcpy(BkMoonSStrO, BkMoonSStr);
convUnix(BlMoonS);
snprintf(BlMoonSStr, sizeof(BlMoonSStr), "Niebieski S: %s %s", thisDate, thisTime);
updateText(BlMoonSStrO, BlMoonSStr, 4, 274); strcpy(BlMoonSStrO, BlMoonSStr);
convUnix(RedMoon);
snprintf(RedMoonStr, sizeof(RedMoonStr), " Czerwony: %s %s", thisDate, thisTime);
updateText(RedMoonStrO, RedMoonStr, 4, 288); strcpy(RedMoonStrO, RedMoonStr);
convUnix(SuperMoon);
snprintf(SuperMoonStr, sizeof(SuperMoonStr), " Super: %s %s", thisDate, thisTime);
updateText(SuperMoonStrO, SuperMoonStr, 4, 302); strcpy(SuperMoonStrO, SuperMoonStr);
convUnix(MicroMoon);
snprintf(MicroMoonStr, sizeof(MicroMoonStr), " Micro: %s %s", thisDate, thisTime);
updateText(MicroMoonStrO, MicroMoonStr, 4, 316); strcpy(MicroMoonStrO, MicroMoonStr);
///unsigned long endTime = millis();
///Serial.printf("[PERF] updateTxtDisplay() took: %lu ms\n", endTime - startTime);
}
void updateText(char* oldBuf, char* newBuf, int x, int y) {
if (strcmp(oldBuf, newBuf) != 0) {
u8g2.setForegroundColor(TFT_BLACK);
u8g2.setCursor(x, y);
u8g2.print(oldBuf);
u8g2.setForegroundColor(TFT_COLOR1);
u8g2.setCursor(x, y);
u8g2.print(newBuf);
}
}
// SYNCHRONIZACJA CZASU
void syncTimeLogic() {
bool needsSync = false;
DateTime nowRTC = rtc.now();
if (rtc.lostPower()) {
Serial.println("RTC: Utrata zasilania!");
needsSync = true;
} else {
if (nowRTC.year() < 2024) needsSync = true;
}
if (!needsSync) {
Serial.println("RTC ma prawidłowy czas - NTP pomijam");
timeSynced = true;
drawStartupStatus("RTC OK - pomijam NTP", "Przygotowuje dane astro...");
return;
}
// Tu już wiemy, że potrzebujemy NTP
Serial.println("Łączenie z WiFi...");
drawStartupStatus("Łączenie z WiFi...", "Synchronizacja czasu NTP...");
WiFi.begin(ssid, pass);
int cnt = 0;
while (WiFi.status() != WL_CONNECTED && cnt < 20) {
delay(250);
cnt++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi OK, pobieram czas NTP...");
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
time_t now;
int retry = 0;
while ((now = time(nullptr)) < 1700000000 && retry < 40) {
delay(250);
retry++;
}
if (now > 1700000000) {
rtc.adjust(DateTime(now));
Serial.println("RTC zsynchronizowany z NTP.");
timeSynced = true;
drawStartupStatus("WiFi/NTP OK", "Przygotowuje dane astro...");
} else {
Serial.println("NIE UDAŁO SIĘ POBRAĆ CZASU NTP!");
drawStartupStatus("Błąd NTP/RTC!", "Sprawdź połączenie.");
timeSynced = false;
}
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
} else {
Serial.println("Brak połączenia WiFi.");
drawStartupStatus("Błąd WiFi!", "Sprawdź sieć!");
timeSynced = false;
}
}
// Funkcje szukające (Black/Blue Moon itp.) - działają na tablicy moonPhases
void FindBlackMoonM() {
BkMoonM = 0;
for (int i = 4; i < 300; i++) {
if (moonPhases[i].timestamp > UTCsec && moonPhases[i].phaseType == 0 && moonPhases[i - 4].phaseType == 0) {
time_t t1 = moonPhases[i].timestamp;
time_t t2 = moonPhases[i - 4].timestamp;
struct tm tm1, tm2;
localtime_r(&t1, &tm1); localtime_r(&t2, &tm2);
if (tm1.tm_mon == tm2.tm_mon) { // Ten sam miesiąc
BkMoonM = t1;
return;
}
}
}
}
void FindBlueMoonM() {
BlMoonM = 0;
for (int i = 4; i < 300; i++) {
if (moonPhases[i].timestamp > UTCsec && moonPhases[i].phaseType == 2 && moonPhases[i - 4].phaseType == 2) {
time_t t1 = moonPhases[i].timestamp;
time_t t2 = moonPhases[i - 4].timestamp;
struct tm tm1, tm2;
localtime_r(&t1, &tm1); localtime_r(&t2, &tm2);
if (tm1.tm_mon == tm2.tm_mon) {
BlMoonM = t1;
return;
}
}
}
}
void FindSuperMoon() {
SuperMoon = 0;
const double LIMIT = 56.5;
for (int i = 0; i < 300; i++) {
if (moonPhases[i].phaseType == 2 && moonPhases[i].timestamp > UTCsec) {
if (moonPhases[i].distance <= LIMIT) {
SuperMoon = moonPhases[i].timestamp;
return;
}
}
}
}
void FindMicroMoon() {
MicroMoon = 0;
// Mikroksiężyc – pełnia przy dużej odległości (blisko apogeum)
// distance jest już w promieniach Ziemi (R_Z), bo liczy to computePhases z AstroCalc
// ~405 000 km / 6378 km
const double LIMIT = 63.57;
for (int i = 0; i < 300; i++) {
if (moonPhases[i].phaseType == 2 && moonPhases[i].timestamp > UTCsec) {
if (moonPhases[i].distance >= LIMIT) {
MicroMoon = moonPhases[i].timestamp;
return;
}
}
}
}
void FindRedMoon() {
RedMoon = 0;
for (int i = 0; i < 300; i++) {
if (moonPhases[i].phaseType == 2 && moonPhases[i].timestamp > UTCsec) {
AstroCalc::AstronomicalData d = astro.calculate(moonPhases[i].timestamp);
// Zaćmienie możliwe gdy lat < ~0.5 st.
if (abs(d.libration.selenographicLatitude) < 0.45) {
RedMoon = moonPhases[i].timestamp;
return;
}
}
}
}
// 🌑 Sezonowy Czarny Księżyc – trzeci nów w sezonie, jeśli sezon ma ≥ 4 nowie
void FindBlackMoonS() {
BkMoonS = 0;
// Przechodzimy po wszystkich sezonach (24 granice → 23 sezony)
for (int j = 1; j < 24; j++) {
time_t seasonStart = seasonUTC[j - 1];
time_t seasonEnd = seasonUTC[j];
int newCount = 0;
time_t thirdNew = 0;
// Szukamy nowiów w danym sezonie
for (int i = 0; i < 300; i++) {
// 0 = Nów
if (moonPhases[i].phaseType != 0)
continue;
time_t t = moonPhases[i].timestamp;
// poza sezonem
if (t <= seasonStart || t >= seasonEnd)
continue;
newCount++;
if (newCount == 3) {
// zapamiętujemy trzeci nów
thirdNew = t;
}
}
// Sezon „czarnoksiężycowy”: 4+ nowiów i trzeci jeszcze w przyszłości
if (newCount >= 4 && thirdNew > UTCsec) {
BkMoonS = thirdNew;
// pierwszy znaleziony w przyszłości → wychodzimy
return;
}
}
// jeśli nic nie znaleziono, BkMoonS zostaje = 0
}
// 🔵 Sezonowy Niebieski Księżyc – trzecia pełnia z ≥4 pełni w jednym sezonie
void FindBlueMoonS() {
BlMoonS = 0;
for (int j = 1; j < 24; j++) {
time_t seasonStart = seasonUTC[j - 1];
time_t seasonEnd = seasonUTC[j];
int fullCount = 0;
time_t thirdFull = 0;
for (int i = 0; i < 300; i++) {
if (moonPhases[i].phaseType != 2) continue; // 2 = Pełnia
time_t t = moonPhases[i].timestamp;
if (t <= seasonStart || t >= seasonEnd) continue;
fullCount++;
if (fullCount == 3) {
thirdFull = t; // zapamiętujemy trzecią pełnię
}
}
if (fullCount >= 4 && thirdFull > UTCsec) {
BlMoonS = thirdFull;
return;
}
}
}
// POMOCNICZE CZASOWE
void GetTimeStr() {
localtime_r(&UTCsec, <z);
snprintf(thisTime, sizeof(thisTime), "%02d:%02d", ltz.tm_hour, ltz.tm_min);
snprintf(thisDate, sizeof(thisDate), "%04d.%02d.%02d", ltz.tm_year + 1900, ltz.tm_mon + 1, ltz.tm_mday);
static const char *wdName[] = {"niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"};
snprintf(thisWk, sizeof(thisWk), "%s", wdName[ltz.tm_wday]);
if (ltz.tm_isdst) snprintf(thisDst, sizeof(thisDst), "DST:letni");
else snprintf(thisDst, sizeof(thisDst), "DST:zimowy");
if (ltz.tm_isdst) snprintf(TZstr, sizeof(TZstr), "%s", TZdata[TZselect][3]);
else snprintf(TZstr, sizeof(TZstr), "%s", TZdata[TZselect][2]);
}
//
void convUnix(time_t ts) {
if (ts == 0) {
strcpy(thisDate, "----.--.--");
strcpy(thisTime, "--:--");
return;
}
struct tm t;
localtime_r(&ts, &t);
snprintf(thisTime, sizeof(thisTime), "%02d:%02d", t.tm_hour, t.tm_min);
snprintf(thisDate, sizeof(thisDate), "%04d.%02d.%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
}
// OBSŁUGA PNG
// Funkcja rysująca linię PNG do bufora originalImage
int pngDraw(PNGDRAW * pDraw) {
uint16_t lineBuffer[MAX_IMAGE_WIDTH];
// Użycie TFT_BLACK jako koloru przezroczystości podczas dekodowania
png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_LITTLE_ENDIAN, TFT_BLACK);
if (pDraw->y < imageHeight && originalImage[pDraw->y] != NULL) {
memcpy(originalImage[pDraw->y], lineBuffer, pDraw->iWidth * 2);
}
return 1;
}
bool loadPngFromSD(const char *filename) {
Serial.printf("Ładuję: %s\n", filename);
File f = SD.open(filename, "r");
if (!f) {
Serial.println("Błąd otwarcia pliku!");
return false;
}
int sz = f.size();
uint8_t *buf = (uint8_t*)malloc(sz);
if (!buf) {
f.close();
return false;
}
f.read(buf, sz);
f.close();
int rc = png.openRAM(buf, sz, pngDraw);
if (rc == PNG_SUCCESS) {
imageWidth = png.getWidth();
imageHeight = png.getHeight();/*
Serial.printf("PNG: %dx%d, MAX_IMAGE_HEIGHT=%d\n", imageWidth, imageHeight, MAX_IMAGE_HEIGHT);
// DEBUG: sprawdź zakres
if (imageHeight > MAX_IMAGE_HEIGHT) {
Serial.printf("BŁĄD: imageHeight=%d > MAX=%d\n", imageHeight, MAX_IMAGE_HEIGHT);
}*/
// Alokacja
for (int i = 0; i < imageHeight; i++) {
if (i >= MAX_IMAGE_HEIGHT) {
Serial.printf("BŁĄD: i=%d >= MAX=%d\n", i, MAX_IMAGE_HEIGHT);
break;
}
originalImage[i] = (uint16_t*)malloc(imageWidth * 2);
rotatedImage[i] = (uint16_t*)malloc(MAX_IMAGE_WIDTH * 2);
if (!originalImage[i] || !rotatedImage[i]) {
Serial.printf("BŁĄD alokacji wiersza %d\n", i);
}
}
//Serial.printf("Zaalocowano %d wierszy\n", imageHeight);
png.decode(NULL, 0);
}
return rc == PNG_SUCCESS;
}
// obrót obrazu o dowolny kąt (KLUCZOWA POPRAWKA CENTROWANIA W BUFORZE)
void rotateImage(float angle) {
if (imageWidth == 0 || imageHeight == 0) return;
float rad = angle * M_PI / 180.0;
float s = sin(rad);
float c = cos(rad);
float cx = imageWidth / 2.0;
float cy = imageHeight / 2.0;
// Wyczyść bufor rotacji (na kolor przezroczysty)
for (int y = 0; y < MAX_IMAGE_HEIGHT; y++) {
if (rotatedImage[y]) {
for (int x = 0; x < MAX_IMAGE_WIDTH; x++) {
rotatedImage[y][x] = TFT_BLACK;
}
}
}
// REVERSE MAPPING — dla każdego piksela docelowego
for (int y = 0; y < imageHeight; y++) {
if (!rotatedImage[y]) continue;
for (int x = 0; x < imageWidth; x++) {
// Oblicz źródłowe współrzędne w oryginalnym obrazie
float sx = c * (x - cx) + s * (y - cy) + cx;
float sy = -s * (x - cx) + c * (y - cy) + cy;
int isx = (int)(sx + 0.5f);
int isy = (int)(sy + 0.5f);
// Jeśli w granicach — przepisz piksel
if (isx >= 0 && isx < imageWidth && isy >= 0 && isy < imageHeight &&
originalImage[isy]) {
uint16_t pix = originalImage[isy][isx];
if (pix != TFT_BLACK) { // nie kopiuj przezroczystego
rotatedImage[y][x] = pix;
}
}
}
}
}
// sprawdź, czy identyfikator pliku się zmienił i w razie potrzeby wczytaj nowy
void LoadMoonFile(int idx) {
png.close();
char stridx[10]; itoa(idx, stridx, 10);
strcpy(SDfilnam, "/");
strcat(SDfilnam, stridx);
strcat(SDfilnam, ".png");
if (strcmp(SDfilnamO, SDfilnam) != 0) {
strcpy(SDfilnamO, SDfilnam);
freeImageBuffers();
loadPngFromSD(SDfilnam);
} else if (originalImage[0] == NULL) {
loadPngFromSD(SDfilnam);
}
}
// wyświetlenie obróconego obrazu na TFT
void displayRotatedImage(int x, int y) {
for (int r = 0; r < MAX_IMAGE_HEIGHT; r++) {
if (!rotatedImage[r]) continue;
for (int c = 0; c < MAX_IMAGE_WIDTH; c++) {
uint16_t col = rotatedImage[r][c];
if (col != TFT_BLACK) {
tftMoon.drawPixel(x + c, y + r, col);
}
}
}
}
// zwolnienie pamięci (dla czystości)
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;
}
}
// Algorytmy Meeusa (NAPRAWIONE TYPY)
double getCycleEstimate(int year, int month) {
double yearfrac = (month * 30 + 15) / 365.0;
double k = 12.3685 * ((year + yearfrac) - 2000);
return floor(k);
}
unsigned long julianDateToUnix(double julianDate) {
// Używamy double do obliczeń, rzutowanie dopiero na końcu
return (unsigned long)((julianDate - 2440587.5) * 86400.0);
}
// funkcja zwracająca time_t zamiast unsigned long dla pewności
time_t getPhaseDate(double cycle, double phase) {
double k = cycle + phase;
double T = k / 1236.85;
double JDE = 2451550.09766 + 29.530588861 * k + 0.00015437 * T * T;
return (time_t)julianDateToUnix(JDE);
}Loading
esp32-s3-devkitc-1
esp32-s3-devkitc-1