#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_ST7796S.h>
#include <U8g2_for_Adafruit_GFX.h>
#include "AstroCalc.h"
#include <math.h>
#include <TinyGPSPlus.h>
#include <HardwareSerial.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// KONFIGURACJA UŻYTKOWNIKA
const char *ssid = "Wokwi-GUEST";
const char *pass = "";
#define GEONAMES_USER ""
char observer_place[32] = "";
double observer_lat = NAN;
double observer_lon = NAN;
double observer_alt = NAN;
// 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 = 40000;//10UL * 60UL * 1000UL; // Czas bezczynności do wygaszenia ekranu (10 minut)
const unsigned long DEBOUNCE_DELAY = 50;
// DEFINICJE PINÓW (ESP32)
//ESP32-S3 GPIO 11 → MOSI → TFT MOSI + SD_MOSI
//ESP32-S3 GPIO 12 → SCK → TFT SCK + SD_SCK
//ESP32-S3 GPIO 13 → MISO → TFT MISO + SD_MISO
#define TFT_MOSI 11 // GPIO11
#define TFT_SCLK 12 // GPIO12
#define TFT_MISO 13 // GPIO13
#define TFT_DC 7 // może zostać 2 – zwykły GPIO
#define TFT_RST 4 // reset TFT
#define TFT_CS1 5 // CS pierwszego wyświetlacza
#define TFT_CS2 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 10 // GPIO10
// I2C
#define I2C_SDA 8 // GPIO8
#define I2C_SCL 9 // GPIO9
//
#define BTN_PIN 21 // przycisk z PULLUP, zwiera do GND
#define LED_R 38 // czerwony
#define LED_G 39 // zielony
#define LED_B 40 // niebieski
// GPS (UART1)
#define RXD2 17
#define TXD2 18
#define GPS_BAUD 9600
// OBIEKTY
Adafruit_ST7796S tftText(TFT_CS1, TFT_DC, TFT_RST);
Adafruit_ST7796S tftMoon(TFT_CS2, TFT_DC, TFT_RST);
U8G2_FOR_ADAFRUIT_GFX u8g2;
SPIClass spiSD(HSPI);
// GPS
HardwareSerial gpsSerial(2);
TinyGPSPlus gps;
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; // 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
// Stan GPS
long rawTzOffsetSec = 3600; // stały GMT (Europe/Rome = +1h)
long tzOffsetSec = 3600; // finalny (GMT + DST)
bool isDST = false; // flaga DST
char tzBuffer[64];
// 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 tzName[20], tzNameO[20];
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 0xCD0C
#define TFT_COLOR2 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
#define TFT_ORANGE 0xFC00
// biel jest traktowana jako kolor przezroczysty i nie jest rysowana na wyświetlaczu
uint16_t TRANSPARENT_COLOR = 0xFFFF;
// 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;
int imageHeight = 0;
uint16_t *originalImage[MAX_IMAGE_HEIGHT] = {NULL};
uint16_t *rotatedImage[MAX_IMAGE_HEIGHT] = {NULL};
char SDfilnam[20] = "xxxxxx.png";
char SDfilnamO[20] = "xxxxxx.png";
// Strefy czasowe
struct tm ltz;
// mapowanie typu fazy 0..3 -> indeks w phaseNames[8]
static const uint8_t mainPhaseIdx[4] = { 0, 2, 4, 6 };
// DEKLARACJE FUNKCJI
String getLocationAndTimeZone(double lat, double lon, char *tzName, size_t tzBuffer);
void updateAstroData();
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);
// init
void fullInitSequence() {
drawStartupStatus("Inicjalizacja", "Uruchamianie modułów...");
bool wifiOK = false;
bool gpsOK = false;
// 1️⃣ WiFi
///Serial.println("Łączenie z WiFi...");
WiFi.begin(ssid, pass);
int cnt = 0;
while (WiFi.status() != WL_CONNECTED && cnt < 30) {
delay(250);
///Serial.print(".");
cnt++;
}
///Serial.println();
if (WiFi.status() == WL_CONNECTED) {
wifiOK = true;
///Serial.println("WiFi OK");
drawStartupStatus("WiFi OK", "Czekam na GPS...");
} else {
drawStartupStatus("WiFi ERROR", "Tryb offline");
}
// 2️⃣ GPS
unsigned long startGPS = millis();
while (millis() - startGPS < 8000) {
while (gpsSerial.available()) {
char c = gpsSerial.read();
if (gps.encode(c) && gps.location.isValid()) {
observer_lat = gps.location.lat();
observer_lon = gps.location.lng();
observer_alt = gps.altitude.isValid() ? gps.altitude.meters() : 0;
gpsOK = true;
break;
}
}
if (gpsOK) break;
}
if (!gpsOK) {
observer_lat = 45.613418;
observer_lon = 10.312439;
observer_alt = 385.0;
drawStartupStatus("GPS brak fixa", "Pozycja domyślna");
} else {
drawStartupStatus("GPS Fix OK", "");
}
///Serial.printf("[GPS] %.6f %.6f %.1f m\n", observer_lat, observer_lon, observer_alt);
astro.setLocation(observer_lat, observer_lon);
// 3️⃣ Miasto + strefa + offset
/////String tzName = "UTC";
if (wifiOK) {
drawStartupStatus("Lokalizacja", "Pobieram dane...");
String city = getLocationAndTimeZone(observer_lat, observer_lon, tzName, sizeof(tzBuffer));
city.toCharArray(observer_place, sizeof(observer_place));
} else {
strncpy(observer_place, "Offline", sizeof(observer_place));
tzOffsetSec = 0;
}
///Serial.printf("[LOC] %s | %s | offset %ld\n", observer_place, tzName, tzOffsetSec);
// 4️⃣ NTP → RTC (UTC)
bool needsSync = rtc.lostPower() || rtc.now().year() < 2024;
if (needsSync && wifiOK) {
drawStartupStatus("Czas", "Synchronizacja NTP...");
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
time_t utcNow = 0;
int retry = 0;
while ((utcNow = time(nullptr)) < 1700000000 && retry < 20) {
delay(500);
retry++;
}
if (utcNow > 1700000000) {
rtc.adjust(DateTime(utcNow));
UTCsec = utcNow;
timeSynced = true;
drawStartupStatus("Czas OK", "UTC zapisany w RTC");
} else {
timeSynced = false;
drawStartupStatus("Błąd NTP", "Brak synchronizacji");
}
} else {
// RTC już ma poprawny UTC
UTCsec = rtc.now().unixtime();
timeSynced = true;
}
// DST + OFFSET (ZAWSZE PO UTC!)
if (timeSynced) {
// reset do czystego GMT (ważne!)
tzOffsetSec = rawTzOffsetSec; // np. 3600 dla Europe/Rome
isDST = false;
struct tm utc_tm;
gmtime_r(&UTCsec, &utc_tm);
int month = utc_tm.tm_mon + 1; // 1–12
int day = utc_tm.tm_mday;
// uproszczone DST Europa (wystarczające)
if (month > 3 && month < 10) {
isDST = true;
} else if (month == 3 && day >= 25) {
isDST = true;
} else if (month == 10 && day < 25) {
isDST = true;
}
if (isDST) {
tzOffsetSec += 3600;
}
///Serial.printf("[DST] %s | raw=%ld final=%ld\n", isDST ? "LATO" : "ZIMA", rawTzOffsetSec, tzOffsetSec);
}
// 5️⃣ WiFi OFF
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
// 6️⃣ Podsumowanie
const char* status = (gpsOK && timeSynced) ? "Wszystkie dane poprawne" : "Nie wszystkie dane pobrano!";
drawStartupStatus("Dane gotowe!", status);
delay(2000);
}
// 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, r ? HIGH : LOW);
digitalWrite(LED_G, g ? HIGH : LOW);
digitalWrite(LED_B, 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;
lastActivity = millis();
Serial.printf("[wakeFromScreensaver] lastActivity ustawione na: %lu\n", lastActivity);
// Włącz wyświetlacz fizycznie
///tftText.writeCommand(ILI9341_DISPON);
///tftMoon.writeCommand(ILI9341_DISPON);
tftText.drawRect(0, 0, tftText.width(), tftText.height(), TFT_BLUE);
tftMoon.drawRect(0, 0, tftMoon.width(), tftMoon.height(), TFT_BLUE);
delay(100);
// Wymuś pełne odświeżenie Astro w następnym przebiegu pętli
thisTimeO[0] = 0; thisDateO[0] = 0; thisWkO[0] = 0;
TZstrO[0] = 0; thisDstO[0] = 0; obsPlaceO[0] = 0;
obsNpmO[0] = 0; obsLatO[0] = 0; obsLonO[0] = 0;
moonAltStrO[0] = 0; moonAzStrO[0] = 0; moonZodiacStrO[0] = 0;
sunAltStrO[0] = 0; sunAzStrO[0] = 0; sunZodiacStrO[0] = 0;
limbAngleStrO[0] = 0; moonIllumStrO[0] = 0; tzNameO[0] = 0;
BkMoonMStrO[0] = 0; BlMoonMStrO[0] = 0; BkMoonSStrO[0] = 0;
BlMoonSStrO[0] = 0; RedMoonStrO[0] = 0; SuperMoonStrO[0] = 0;
// żeby nagłówek „Następna faza...” też się odświeżył
MicroMoonStrO[0] = 0; NextMoonUTCO = 0;
// pełne przerysowanie wszystkiego
updateAstroData();
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_COLOR2);
tftText.setCursor(10, 10);
tftText.print("Start systemu AstroCalc");
// linie 2/3 inicjalnie puste – 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, 130);
strcpy(bootLine3O, bootLine3);
}
// SETUP
void setup() {
Serial.begin(115200);
pinMode(TFT_CS1, OUTPUT);
pinMode(TFT_CS2, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP); // przycisk zwiera do GND
pinMode(LED_R, OUTPUT);
pinMode(LED_G, OUTPUT);
pinMode(LED_B, 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!");
}
// GPS UART
gpsSerial.begin(GPS_BAUD, SERIAL_8N1, RXD2, TXD2);
Serial.println(F("[GPS] UART started"));
// 1TFT + SPI
SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI);
tftText.init(320, 480, 0, 0, ST7796S_BGR);
tftMoon.init(320, 480, 0, 0, ST7796S_BGR);
tftText.setRotation(0);
tftMoon.setRotation(0);
tftText.fillScreen(TFT_BLACK);
tftMoon.fillScreen(TFT_BLACK);
tftText.drawRect(0, 0, tftText.width(), tftText.height(), TFT_RED);
tftMoon.drawRect(0, 0, tftMoon.width(), tftMoon.height(), TFT_RED);
// U8g2
u8g2.begin(tftText);
u8g2.setFont(u8g2_font_t0_22_me); // u8g2_font_unifont_te
/*
u8g2_font_helvB14_te
u8g2_font_helvR18_te
u8g2_font_lubB14_te ok
u8g2_font_t0_22_me ok stały
*/
u8g2.setFontMode(0);
u8g2.setForegroundColor(TFT_COLOR2);
u8g2.setBackgroundColor(TFT_BLACK);
// Ekran startowy
drawStartupScreenBase();
// Synchronizacja czasu
fullInitSequence();
// 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_lat);
// czas OK → czyścimy ekrany z komunikatów i startujemy normalny widok
if (timeSynced) {
// wyczyść oba ekrany z ekranu startowego
tftText.fillScreen(TFT_BLACK);
tftMoon.fillScreen(TFT_BLACK);
tftText.drawRect(0, 0, tftText.width(), tftText.height(), TFT_BLUE);
tftMoon.drawRect(0, 0, tftMoon.width(), tftMoon.height(), TFT_BLUE);
DateTime now = rtc.now();
UTCsec = now.unixtime();
GetTimeStr();
// Debug: co RTC ma zapisane
/*
Serial.print("[DEBUG] RTC (UTC): ");
Serial.print(now.year()); Serial.print("-");
Serial.print(now.month()); Serial.print("-");
Serial.print(now.day()); Serial.print(" ");
Serial.print(now.hour()); Serial.print(":");
Serial.print(now.minute()); Serial.print(":");
Serial.println(now.second());*/
updateAstroData(); // narysuje Księżyc + orbitę
updateTxtDisplay(); // od razu pierwsze dane tekstowe
astroReady = true;
systemReady = true;
screenSaver = false;
// 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() {
//fullInitSequence();
unsigned long currentMillis = millis();
static unsigned long lastDebug = 0;
if (currentMillis - lastDebug > 5000) {
Serial.printf("[DEBUG] screenSaver: %d, currentMillis: %lu, lastActivity: %lu, lastActivity diff: %lu, systemReady: %d\n", screenSaver, currentMillis, lastActivity, 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)
// 1800000
if (currentMillis - lastAstroUpdate >= ASTRO_UPDATE_INTERVAL || firstRun) {
lastActivity = currentMillis;
Serial.println("🔄 Pełne odświeżanie Astro...");
updateAstroData();
updateTxtDisplay();
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 updateAstroData() {
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 księżyce
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) - 150; // H 240-164=76
rotateImage(360.0 - angle - tmp);
displayRotatedImage(centerX, centerY);
///tftMoon.drawRect(centerX - 1, centerY - 1, imageWidth + 2, imageHeight + 2, TFT_YELLOW);
// Rysowanie Orbity
displayMoonSunPos();
}
// Orbita Słońce/Księżyc na dolnej części ekranu Księżyca
void displayMoonSunPos() {
int centerX = tftMoon.width() / 2; // centerX = 160
int centerY = tftMoon.height() / 2; // centerY = 240
// przesunięcie elipsy o 90 pikseli w dół od środka
centerY += 40;
// Czyścimy dolną część
//tftMoon.fillRect(5, 190, 310, 285, TFT_RED);
//tftMoon.drawRect(1, 180, 318, 300, TFT_YELLOW);
//tftMoon.drawLine(1, 175, 318, 175, TFT_RED);
// Ziemia (środek)
tftMoon.fillCircle(centerX, centerY, 8, TFT_DKGREEN);
tftMoon.drawEllipse(centerX, centerY, 120, 50, TFT_COLOR2);
// 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(2);
tftMoon.setCursor(centerX - 4, centerY - 56); tftMoon.print("N"); // Góra
tftMoon.setCursor(centerX + 115, centerY - 6); tftMoon.print("E"); // Prawo
tftMoon.setCursor(centerX - 4, centerY + 44); tftMoon.print("S"); // Dół
tftMoon.setCursor(centerX - 124, centerY - 6); 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;
// 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, 4, TFT_SILVER);
// Pozycja Słońca (na zewnątrz)
int SposX = centerX + (int)(145.0 * sin(s_az));
int SposY = centerY - (int)(75.0 * cos(s_az)); // Spłaszczona elipsa
tftMoon.fillCircle(SposX, SposY, 10, 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 * 7; // 4 = promień Księżyca
int lineStartY = MposY + dy * 7;
int lineEndX = SposX - dx * 12; // 10 = promień Słońca
int lineEndY = SposY - dy * 12;
// 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 + 140; // linia horyzontu poniżej elipsy
int horizonX = 120; // 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)
// 0.250000f = 1° = ~0.250px (ok. ±70° -> ±17.5px, co daje 35px zakresu)
// 0.285714f = 1° = ~0.286px (ok. ±70° -> ±20px, co daje 40px zakresu)
// 0.357142f = 1° = ~0.357px (ok. ±70° -> ±25px, co daje 50px zakresu)
// 0.428571f = 1° = ~0.429px (ok. ±70° -> ±30px, co daje 60px zakresu)
const float ALT_SCALE = 0.428571f;
// Linia horyzontu i Ziemia (widok z boku)
tftMoon.drawLine(centerX - horizonX, horizonY, centerX + horizonX, horizonY, TFT_COLOR2);
tftMoon.fillCircle(centerX, horizonY, 8, 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;
// Maksymalna wysokość (90°):
/*int Y_at_S_90 = horizonY - 90.0f * ALT_SCALE;
// Minimalna wysokość (-90°):
int Y_at_N_90 = horizonY - (-90.0f) * ALT_SCALE;
tftMoon.fillCircle(centerX, Y_at_S_90, 10, TFT_ORANGE);
tftMoon.fillCircle(centerX, Y_at_N_90, 10, TFT_ORANGE);*/
// Księżyc i Słońce w rzucie bocznym
tftMoon.fillCircle(MposX, MsideY, 4, TFT_SILVER);
tftMoon.fillCircle(SposX, SsideY, 10, TFT_YELLOW);
}
// EKRAN TEKSTOWY
void updateTxtDisplay() {
GetTimeStr();
updateText(thisDateO, thisDate, 2, 20); strcpy(thisDateO, thisDate);
updateText(thisTimeO, thisTime, 120, 20); strcpy(thisTimeO, thisTime);
int16_t xRight = tftText.width() - 3 - u8g2.getUTF8Width(thisWk);
updateText(thisWkO, thisWk, xRight, 20); strcpy(thisWkO, thisWk);
steadyText("Strefa:", 2, 42);
updateText((char*)"", (char*)"", 2, 42);
//updateText(TZstrO, TZstr, 83, 42); strcpy(TZstrO, TZstr);
updateText(tzNameO, tzName, 83, 42); strcpy(tzNameO, tzName);
steadyText("DST:", 260, 42);
updateText((char*)"", (char*)"", 2, 42);
xRight = tftText.width() - 3 - u8g2.getUTF8Width(thisDst);
updateText(thisDstO, thisDst, xRight, 42); strcpy(thisDstO, thisDst);
steadyText("Miejsce:", 2, 64);
updateText((char*)"", (char*)"", 2, 64);
updateText(obsPlaceO, (char*)observer_place, 91, 64); strcpy(obsPlaceO, obsPlace);
int labelWidth = u8g2.getUTF8Width("npm:");
if (isnan(observer_alt))
snprintf(obsNpm, sizeof(obsNpm), "");
else
snprintf(obsNpm, sizeof(obsNpm), "%.0fm", observer_alt);
int xRightValue = tftText.width() - 3 - u8g2.getUTF8Width(obsNpm);
int labelX = xRightValue - labelWidth;
steadyText("npm:", labelX, 64);
updateText(obsNpmO, obsNpm, xRightValue, 64); strcpy(obsNpmO, obsNpm);
steadyText("szer:", 2, 86);
snprintf(obsLat, sizeof(obsLat), "%.1f°", observer_lat);
updateText(obsLatO, obsLat, 60, 86); strcpy(obsLatO, obsLat);
steadyText("dług:", 160, 86);
snprintf(obsLon, sizeof(obsLon), "%.1f°", observer_lon);
updateText(obsLonO, obsLon, 218, 86); strcpy(obsLonO, obsLon);
steadyText("Wysokość Azymut Zodiak", 20, 110);
// Wiersz Księżyca
steadyText("K:", 2, 132);
snprintf(moonAltStr, sizeof(moonAltStr), "%5.1f°", currentAstroData.moonPosition.elevation * RAD_TO_DEG);
updateText(moonAltStrO, moonAltStr, 30, 132); strcpy(moonAltStrO, moonAltStr);
snprintf(moonAzStr, sizeof(moonAzStr), "%5.1f°", currentAstroData.moonPosition.azimuth * RAD_TO_DEG);
updateText(moonAzStrO, moonAzStr, 120, 132); strcpy(moonAzStrO, moonAzStr);
snprintf(moonZodiacStr, sizeof(moonZodiacStr), "%s", currentAstroData.moonData.zodiacMoonSign);
updateText(moonZodiacStrO, moonZodiacStr, 200, 132); strcpy(moonZodiacStrO, moonZodiacStr);
// Wiersz Słońca
steadyText("S:", 2, 154);
snprintf(sunAltStr, sizeof(sunAltStr), "%5.1f°", currentAstroData.sunPosition.elevation * RAD_TO_DEG);
updateText(sunAltStrO, sunAltStr, 30, 154); strcpy(sunAltStrO, sunAltStr);
snprintf(sunAzStr, sizeof(sunAzStr), "%5.1f°", currentAstroData.sunPosition.azimuth * RAD_TO_DEG);
updateText(sunAzStrO, sunAzStr, 120, 154); strcpy(sunAzStrO, sunAzStr);
snprintf(sunZodiacStr, sizeof(sunZodiacStr), "%s", currentAstroData.sunPosition.zodiacSunSign);
updateText(sunZodiacStrO, sunZodiacStr, 200, 154); strcpy(sunZodiacStrO, sunZodiacStr);
steadyText("Kąt oświetlenia:", 2, 180);
snprintf(limbAngleStr, sizeof(limbAngleStr), "%3.1f°", currentAstroData.positionAngles.brightLimb);
updateText(limbAngleStrO, limbAngleStr, 180, 180); strcpy(limbAngleStrO, limbAngleStr);
steadyText("Jasność:", 2, 202);
snprintf(moonIllumStr, sizeof(moonIllumStr), "%3.1f%%", currentAstroData.moonPosition.illumination * 100.0);
updateText(moonIllumStrO, moonIllumStr, 180, 202); 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, 231, 316, 86, TFT_BLACK);
steadyText("Następna faza Księżyca:", 29, 226);
// 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, 8, 248 + i * 22);
}
// Specjalne księżyce
convUnix(BkMoonM);
steadyText(" Czarny M:", 5, 342);
snprintf(BkMoonMStr, sizeof(BkMoonMStr), "%s %s", thisDate, thisTime);
updateText(BkMoonMStrO, BkMoonMStr, 140, 342); strcpy(BkMoonMStrO, BkMoonMStr);
convUnix(BlMoonM);
steadyText("Niebieski M:", 5, 364);
snprintf(BlMoonMStr, sizeof(BlMoonMStr), "%s %s", thisDate, thisTime);
updateText(BlMoonMStrO, BlMoonMStr, 140, 364); strcpy(BlMoonMStrO, BlMoonMStr);
convUnix(BkMoonS);
steadyText(" Czarny S:", 5, 386);
snprintf(BkMoonSStr, sizeof(BkMoonSStr), "%s %s", thisDate, thisTime);
updateText(BkMoonSStrO, BkMoonSStr, 140, 386); strcpy(BkMoonSStrO, BkMoonSStr);
convUnix(BlMoonS);
steadyText("Niebieski S:", 5, 408);
snprintf(BlMoonSStr, sizeof(BlMoonSStr), "%s %s", thisDate, thisTime);
updateText(BlMoonSStrO, BlMoonSStr, 140, 408); strcpy(BlMoonSStrO, BlMoonSStr);
convUnix(RedMoon);
steadyText(" Czerwony:", 5, 430);
snprintf(RedMoonStr, sizeof(RedMoonStr), "%s %s", thisDate, thisTime);
updateText(RedMoonStrO, RedMoonStr, 140, 430); strcpy(RedMoonStrO, RedMoonStr);
convUnix(SuperMoon);
steadyText(" Super:", 5, 452);
snprintf(SuperMoonStr, sizeof(SuperMoonStr), "%s %s", thisDate, thisTime);
updateText(SuperMoonStrO, SuperMoonStr, 140, 452); strcpy(SuperMoonStrO, SuperMoonStr);
convUnix(MicroMoon);
steadyText(" Mikro:", 5, 474);
snprintf(MicroMoonStr, sizeof(MicroMoonStr), "%s %s", thisDate, thisTime);
updateText(MicroMoonStrO, MicroMoonStr, 140, 474); strcpy(MicroMoonStrO, MicroMoonStr);
///unsigned long endTime = millis();
///Serial.printf("[PERF] updateTxtDisplay() took: %lu ms\n", endTime - startTime);
}
void steadyText(const char* text, int x, int y) {
u8g2.setForegroundColor(TFT_COLOR1);
u8g2.setCursor(x, y);
u8g2.print(text);
}
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_COLOR2);
u8g2.setCursor(x, y);
u8g2.print(newBuf);
}
}
// 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;
}
}
}
// 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) {
// double do obliczeń, rzutowanie dopiero na końcu
return (unsigned long)((julianDate - 2440587.5) * 86400.0);
}
// POMOCNICZE CZASOWE
void GetTimeStr() {
///Serial.printf("[GetTimeStr ENTER] tzOffsetSec = %ld at %lu ms\n", tzOffsetSec, millis());
if (tzOffsetSec == 0) {
///Serial.printf("[ERROR] tzOffsetSec is ZERO! Should be 7200\n");
// Spróbuj odczytać ponownie
delay(10);
///Serial.printf("[ERROR RETRY] tzOffsetSec = %ld\n", tzOffsetSec);
}
// Zabezpieczenie przed zmianą w trakcie wykonywania (jeśli zmienna jest modyfikowana w przerwaniu)
noInterrupts();
time_t currentUTC = UTCsec;
long currentOffset = tzOffsetSec;
interrupts();
// Debug: sprawdź wartości przed obliczeniami
///Serial.printf("[DEBUG GetTimeStr] UTC=%ld, offset=%ld\n", currentUTC, currentOffset);
// Sprawdź czy offset ma rozsądną wartość (między -12 a +14 godzin)
if (currentOffset < -43200 || currentOffset > 50400) {
///Serial.printf("[ERROR] Invalid tzOffsetSec: %ld (should be ±14 hours)\n", currentOffset);
// Jeśli offset wygląda jak timestamp (UTC + offset), spróbuj wyliczyć prawdziwy offset
if (currentOffset > 1000000000) {
long calculatedOffset = currentOffset - currentUTC;
if (calculatedOffset >= -43200 && calculatedOffset <= 50400) {
///Serial.printf("[FIX] Recalculated offset: %ld -> %ld\n", currentOffset, calculatedOffset);
currentOffset = calculatedOffset;
// Napraw globalną zmienną
tzOffsetSec = calculatedOffset;
}
}
}
time_t localSec = currentUTC + currentOffset;
struct tm t;
gmtime_r(&localSec, &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);
static const char *wdName[] = {"niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"};
snprintf(thisWk, sizeof(thisWk), "%s", wdName[t.tm_wday]);
// DST: Z / L
snprintf(thisDst, sizeof(thisDst), "%c", isDST ? 'L' : 'Z');
// DEBUG - tylko co 10 sekund
static unsigned long last = 0;
if (millis() - last > 10000) {
// Ponownie zabezpiecz odczyt
noInterrupts();
time_t debugUTC = UTCsec;
long debugOffset = tzOffsetSec;
interrupts();
time_t debugLocal = debugUTC + debugOffset;
///Serial.printf("[TIME] UTC=%ld local=%ld offset=%ld\n", debugUTC, debugLocal, debugOffset);
// Dodatkowe informacje debug
struct tm utc_tm;
gmtime_r(&debugUTC, &utc_tm);
struct tm local_tm;
gmtime_r(&debugLocal, &local_tm);
/*
Serial.printf("[TIME DETAILS] UTC: %04d-%02d-%02d %02d:%02d:%02d, Local: %04d-%02d-%02d %02d:%02d:%02d\n",
utc_tm.tm_year + 1900, utc_tm.tm_mon + 1, utc_tm.tm_mday,
utc_tm.tm_hour, utc_tm.tm_min, utc_tm.tm_sec,
local_tm.tm_year + 1900, local_tm.tm_mon + 1, local_tm.tm_mday,
local_tm.tm_hour, local_tm.tm_min, local_tm.tm_sec);*/
last = millis();
}
}
//
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);
}
String getLocationAndTimeZone(double lat, double lon, char *tzName, size_t tzBuffer) {
if (WiFi.status() != WL_CONNECTED) {
strncpy(tzName, "UTC", tzBuffer);
tzName[tzBuffer - 1] = '\0';
tzOffsetSec = 0;
isDST = false;
return "Offline";
}
HTTPClient http;
String cityName = "---";
// OSM – miasto
String urlOSM = "https://nominatim.openstreetmap.org/reverse?format=json&lat=" + String(lat, 6) + "&lon=" + String(lon, 6);
http.begin(urlOSM);
http.addHeader("User-Agent", "ESP32-MoonTracker");
http.setTimeout(10000);
if (http.GET() == 200) {
DynamicJsonDocument doc(4096);
deserializeJson(doc, http.getString());
const char* city = doc["address"]["city"]
| doc["address"]["town"]
| doc["address"]["village"]
| doc["address"]["municipality"];
if (city) cityName = city;
}
http.end();
delay(1000);
// GeoNames – strefa
String urlTZ = "http://api.geonames.org/timezoneJSON?lat=" + String(lat, 6) + "&lng=" + String(lon, 6) + "&username=" + String(GEONAMES_USER);
HTTPClient httpTZ;
httpTZ.begin(urlTZ);
httpTZ.addHeader("User-Agent", "ESP32-AstroCalc");
if (httpTZ.GET() == 200) {
DynamicJsonDocument docTZ(1024);
deserializeJson(docTZ, httpTZ.getString());
const char* tz = docTZ["timezoneId"] | "UTC";
strncpy(tzName, tz, tzBuffer);
tzName[tzBuffer - 1] = '\0';
float rawOffset = docTZ["rawOffset"] | 0.0;
tzOffsetSec = (long)(rawOffset * 3600);
isDST = false; // NA RAZIE – liczymy później
///Serial.printf("[TZ] %s | rawOffset=%.1f (%ld s)\n", tzName, rawOffset, tzOffsetSec);
}
httpTZ.end();
return cityName;
}MODUŁ MP1584EN:
INPUT + (5V) ────┬───[100µF]─── GND ← BLISKO modułu!
└───[100nF]─── GND
OUTPUT + (3.3V) ─┬───[220µF]─── GND ← BARDZO BLISKO!
├───[100nF]─── GND
└───[10µF]──── GND
ESP32:
3V3 pin ────[100nF]─── GND (blisko pinu!)
ST7796S (każdy wyświetlacz):
VCC ────[10µF]─── GND
VCC ────[100nF]── GND
NEO-8M (GPS):
VCC ────[10µF]─── GND
VCC ────[100nF]── GND
DS3231 (zegar):
VCC ────[100nF]── GND (wystarczy mały)
microSD:
VDD ────[10µF]─── GND
VDD ────[100nF]── GND
100nF (ceramiczne) = "Gaszenie szumów wysokoczęstotliwościowych"
10-220µF (elektrolityczne) = "Zapas energii"
a) Przy OUTPUT MP1584EN:
- 220µF/6.3V elektrolit (niski ESR)
- 100nF ceramiczny X7R
b) Przy każdym wyświetlaczu (dodatkowo):
- 47µF/6.3V elektrolit równolegle do istniejących
- Szczególnie ważne przy max brightness
c) Przy GPS NEO-8M:
- 10µF tantal + 100nF ceramiczny
- Blisko goldpinów VCC/GND
d) Przy microSD (na wyświetlaczu):
- 22µF/6.3V elektrolit
- 100nF ceramiczny przy VDD