#include <SPI.h>
#include <SD.h>
#include <PNGdec.h>
#include <Wire.h>
#include <RTClib.h>
#include <ESP32RotaryEncoder.h>
#include <TimeLib.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <U8g2_for_Adafruit_GFX.h>
#include "colors.h"
#include "SunCalc.h"
// czcionka U8g2 z polskimi znakami
#define FONT1 u8g2_font_unifont_t_polish
// piny TFT (jak w poprzednim User_Setup od TFT_eSPI)
// board-esp32-s3-devkitc-1
// 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);
// 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
// piny enkodera
// pin A enkodera (zamień piny, jeśli kierunek obrotu jest odwrócony, albo użyj knob->invertDirection(); w setup)
#define RotA 32
// pin B enkodera
#define RotB 33
// przycisk enkodera
#define BtnPin 25
// biblioteka obsługuje też przycisk, ale na ESP-C3 powoduje panic na rdzeniu 0 z powodu timeoutu w obsłudze przerwania
RotaryEncoder rotaryEncoder(RotA, RotB);
// zapamiętanie poprzedniego stanu przycisku
bool SelBtnOld = false;
// znacznik czasu zwolnienia przycisku
unsigned long BtnRelease = 0;
// skrócony czas odświeżania (ms) podczas ekranu ustawień, żeby ograniczyć migotanie
unsigned long displayRefresh = 5000;//500
// następny zaplanowany czas odświeżenia (millis)
unsigned long displayRefreshNext = 0;
// stary (ciepły) kolor żarówki, można zmienić według uznania
#define TFT_COLOR1 0x6519
// kolor ramek ustawień
uint16_t TFT_BOXES = TFT_COLOR1;
// 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();
// zmienne migania dla ekranów ustawień
boolean blinking = true;
// znacznik czasu w ms
unsigned long previousBlinking = 0;
// pół sekundy włącz/wyłącz = częstotliwość 1 Hz
const unsigned long periodBlinking = 5000; //500
// konfiguracja RTC
// faktycznie używany typ RTC to DS3231
RTC_DS3231 RTC;
// domyślna pozycja startowa (Caino), jeśli w NVRAM nie ma danych
// LAT = 45.613418;
// LON = 10.312439;
// szegokość geograficzna
double observer_lat = 45.6;
// długość geograficzna
double observer_lon = 10.3;
// Zasady stref czasowych POSIX
// definicje stref wzięte stąd: https://support.cyberdata.net/portal/en/kb/articles/010d63c0cfce3676151e1f2d5442e311
// nie wszystkie wpisy z tej strony są poprawne, część wymagała poprawek (zamiana czasu standardowego z letnim, błędne weekendy itp.)
// być może lepiej korzystać z tej mapy na Wikipedii https://upload.wikimedia.org/wikipedia/commons/9/94/Timezones2008_UTC-9_gray.png
// Format strefy czasowej POSIX: Mm.n.d/time
// Mm: miesiąc (1-12).
// n: numer tygodnia (1-5) (5 oznacza ostatni tydzień).
// d: dzień tygodnia (0 = niedziela, 6 = sobota).
// time: czas w formacie 24-godzinnym (np. 2:00:00, może być skrócony).
const uint8_t TZcolumns = 4;
const char *const TZdata[][TZcolumns] = {
{ "Europe", "CET-1CEST,M3.5.0/2:00:00,M10.5.0/2:00:00", "CET", "CEST" }, // Central European Time, Central European Summer Time
{ "UK", "GMT0BST,M3.5.0/2:00:00,M10.5.0/2:00:00", "GMT", "BST" }, // UK GMT, British Summer Time
{ "UTC", "GMT", "UTC", "UTC" }, // UTC
{ "Eastern", "EST5EDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "EST", "EDT" },
{ "Central", "CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "CST", "CDT" },
{ "Mountain", "MST7MDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "MST", "MDT" },
{ "Arizona", "MST7", "MST", "MST" },
{ "Pacific", "PST8PDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "PST", "PDT" },
{ "Alaska", "ASKT9AKDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "AKST", "AKDT" },
{ "Hawaii", "HST10", "HST", "HST" },
};
// liczba wierszy w tablicy TZdata
const uint8_t TZrows = sizeof(TZdata) / sizeof(TZdata[0]);
// domyślna strefa czasowa: Europe
uint8_t TZselect = 0;
// wyświetlana nazwa strefy
char TZstr[] = "xxxxxxxxxxxx";
// poprzednia wartość, żeby skasować stary tekst przed wpisaniem nowego
char TZstrO[] = "xxxxxxxxxxxx";
// aktualny czas UTC w formie epoki (sekundy od 1.1.1970)
time_t UTCsec;
// format wyświetlania czasu/dat/strefy
char thisTime[16] = "00:00 xxxx"; // miejsce na "23:59 CEST" + '\
// poprzednie wyświetlane ciągi czasu/dat
char thisTimeO[16] = "00:00 xxxx";
char thisDate[12] = "2000.00.00 ";
char thisDateO[12] = "2000.00.00 ";
// struktura lokalnej strefy czasowej
tm ltz;
// zmienne menu
byte menu = 0;
// zmienna pomocnicza ogólnego przeznaczenia
int temp = 0;
// zmienne związane z fazami Księżyca
// wyzwalanie obliczeń faz natychmiast podczas zmian ustawień, w normalnym trybie tylko raz na minutę
bool phaseNow = true;
// 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;
// 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[24];
char limbAngleStrO[24];
// 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";
// wysokość/azymut Księżyca i Słońca oraz odpowiadające im ciągi tekstowe
double moonAlt = 0;
char moonAltStr[] = "xxxxxxxxxx";
char moonAltStrO[] = "xxxxxxxxxx";
double moonAz = 0;
char moonAzStr[] = "xxxxxxxxxx";
char moonAzStrO[] = "xxxxxxxxxx";
double sunAlt = 0;
char sunAltStr[] = "xxxxxxxxxx";
char sunAltStrO[] = "xxxxxxxxxx";
double sunAz = 0;
char sunAzStr[] = "xxxxxxxxxx";
char sunAzStrO[] = "xxxxxxxxxx";
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";
time_t NextMoonUTCO = 0;
// fazy Księżyca (nów, I kwadra, pełnia, III kwadra) na ok. 5 lat
time_t moonPhaseUTC[240];
int moonPhaseInd[240];
// 4 pory roku na rok, dla 5 lat
time_t seasonUTC[24];
time_t NextMoonUTC[4];
int NextMoonInd[4];
// następny „czarny księżyc” (drugi nów w tym samym miesiącu)
time_t BkMoon = 0;
// następny miesięczny niebieski Księżyc (druga pełnia w miesiącu)
time_t BlMoonM = 0;
// następny sezonowy niebieski Księżyc (trzecia pełnia z czterech w jednej porze roku)
time_t BlMoonS = 0;
// teksty czasu/daty dla niebieskiego/czarnego Księżyca oraz ich poprzednie wersje
char BkMoonStr[40];
char BkMoonStrO[40];
char BlMoonMStr[40];
char BlMoonMStrO[40];
char BlMoonSStr[40];
char BlMoonSStrO[40];
void updateClock();
void updateTxtDisplay();
void displayMoonSunPos();
void LoadMoonFile(int idx);
void rotateImage(float angle);
void displayRotatedImage(int x, int y);
void SelClick();
void countUp();
void countDn();
void setup() {
Serial.begin(115200);
// 1. NAJPIERW bufor obrazów, zanim cokolwiek innego zacznie żreć RAM
if (!allocateImageBuffers()) {
Serial.println("KRYTYCZNY: brak pamięci na bufory obrazów!");
while (1) delay(100); // dalej nie ma sensu
}
Wire.begin(); // I2C
// inicjalizacja zegara RTC DS3231
if (!RTC.begin()) {
Serial.println("Błąd: nie mogę znaleźć RTC DS3231!");
while (1) {
delay(100);
}
}
// jeśli DS3231 stracił zasilanie – ustaw czas na czas kompilacji
if (RTC.lostPower()) {
Serial.println("RTC stracił zasilanie - ustawiam czas na czas kompilacji.");
RTC.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
// ustawienie zegara programowego na podstawie RTC
updateClock(); // ustawia UTCsec z RTC
// SPRAWDŹ CZAS Z RTC ZANIM UŻYJESZ TimeLib
DateTime now = RTC.now();
// Serial.printf("RTC time: %04d-%02d-%02d %02d:%02d:%02d\n", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
if (now.year() < 2000 || now.year() > 2099) {
Serial.println("RTC ma nieprawidłowy rok - ustawiam na 2000");
RTC.adjust(DateTime(2000, 1, 1, 0, 0, 0));
updateClock();
}
// ustawienie strefy czasowej na wcześniej zdefiniowaną
setenv("TZ", TZdata[TZselect][1], 1);
// uaktywnienie nowej strefy
tzset();
// wygeneruj łańcuchy czasu/dat
GetTimeStr();
// konfiguracja enkodera
// enkoder nie ma rezystorów podciągających (w przeciwieństwie do wersji HAS_PULLUP na płytce modułowej)
rotaryEncoder.setEncoderType(EncoderType::FLOATING);
// Parametry: dolny_limit, górny_limit, false = bez zapętlania; odczyty: 0 = krok w lewo, 1 = krok w prawo
rotaryEncoder.setBoundaries(0, 1, false);
// wskaźnik na funkcję obsługi
rotaryEncoder.onTurned(RotaryEnc);
// brak potrzeby dodatkowej obsługi enkodera w loop()
rotaryEncoder.begin(true);
// przycisk obsługujemy osobno, poza biblioteką enkodera
pinMode(BtnPin, INPUT_PULLUP);
// 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.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;
}
}
void loop() {
if (digitalRead(BtnPin) == LOW && SelBtnOld == false && millis() - BtnRelease > 50) {
// wciśnięcie przycisku – przejście do obsługi po zwolnieniu, z debounce przy puszczeniu
SelClick();
}
// reset stanu przycisku, jeśli aktualnie nie wciśnięty, a poprzednio był
if (digitalRead(BtnPin) == HIGH && SelBtnOld == true) {
SelBtnOld = false;
// zapamiętanie czasu zwolnienia przycisku i debounce 50 ms
BtnRelease = millis();
}
// pobranie czasu z RTC i aktualizacja zegara programowego raz na godzinę
if ((minute(now()) == 0 && updatedHWclock == false)) {
// ustawienie zegara programowego
updateClock();
// zablokowanie ponownej aktualizacji w tej samej minucie (minute = 0)
updatedHWclock = true;
}
if (minute(now()) != 0) {
// zniesienie blokady aktualizacji zegara
updatedHWclock = false;
}
// timer 500 ms dla selektora ustawień, aktualizacji UTCsec i migającej ramki
// ta część pętli wykonywana jest co 500 ms
if (millis() - previousBlinking >= periodBlinking) {
// zapamiętanie czasu ostatniego „mignięcia”
previousBlinking = millis();
// przełączanie wskaźnika ustawień (migającej ramki)
blinking = !blinking;
// okresowe uaktualnianie UTCsec z zegara programowego
UTCsec = DateTime(year(now()), month(now()), day(now()), hour(now()), minute(now()), second(now())).unixtime();
// natychmiastowa aktualizacja tekstu podczas zmian ustawień
if (menu != 0) {
// miganie ramki ustawień
if (blinking) {
TFT_BOXES = TFT_COLOR1;
} else {
TFT_BOXES = TFT_BLACK;
}
updateTxtDisplay();
}
}
// oblicz fazy Księżyca tylko raz na minutę; podczas zmian ustawień wymuś natychmiast, ustawiając phaseNow = true
if ((second(now()) == 0 && MoonCalcDone == false) || phaseNow) {
// oblicz daty nowiu, I kwadry, pełni i III kwadry dla 60 miesięcy
computePhases(year(now()));
// oblicz pory roku dla 4 lat
computeSeasons(year(now()));
// znajdź następny czarny księżyc (drugi nów w miesiącu)
FindBlackMoon();
// znajdź następny miesięczny niebieski Księżyc (druga pełnia w miesiącu)
FindTFT_BLUEMoonM();
// znajdź następny sezonowy niebieski Księżyc (trzecia pełnia z czterech w tej samej porze roku)
FindTFT_BLUEMoonS();
// oblicz kąt oświetlonego brzegu Księżyca
limbAngle = calcMoonIlluminatedLimbAngle(UTCsec, observer_lat, observer_lon);
Serial.print("Kąt: ");
Serial.print(limbAngle);
Serial.println("°"); // użyta czcionka ma znak stopnia
MoonCalcDone = true;
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
LoadMoonFile(filnum);
// 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();
// aktualizacja części tekstowej
updateTxtDisplay();
}
if (second(now()) != 0 && MoonCalcDone) {
// flaga wymuszająca ponowne przeliczenie w następnej minucie
MoonCalcDone = false;
}
}
// Astronomia – Black/Blue Moon
// zaznacz czarny księżyc, jeśli dwa nowie wypadają w jednym miesiącu
void FindBlackMoon() {
// lista faz: nów = 0, zaczynamy od drugiego nowiu w liście
for (int i = 4; i < 240; i += 4) {
Serial.print("month/moonPhaseUTC: ");
Serial.print(month(moonPhaseUTC[i]));
Serial.print(" ");
Serial.print(month(moonPhaseUTC[i - 4]));
Serial.print(" ");
Serial.print(month(moonPhaseUTC[i]) - month(moonPhaseUTC[i - 4]));
Serial.println(" ");
printUnix(moonPhaseUTC[i]);
if (month(moonPhaseUTC[i]) == month(moonPhaseUTC[i - 4])) {
BkMoon = moonPhaseUTC[i];
Serial.print("BkMoon:");
Serial.print(BkMoon);
Serial.print(" UTCsec:");
Serial.print(UTCsec);
Serial.print(" i:");
Serial.print(i);
Serial.print(" Dif:");
Serial.println(BkMoon - UTCsec);
if (BkMoon > UTCsec) {
// wyjście z pętli, jeśli data jest w przyszłości
return;
}
}
}
}
// zaznacz miesięczny niebieski Księżyc, jeśli dwie pełnie wypadają w jednym miesiącu
void FindTFT_BLUEMoonM() {
// lista faz: pełnia = 2, zaczynamy od drugiej pełni w liście
for (int i = 6; i < 240; i += 4) {
if (month(moonPhaseUTC[i]) == month(moonPhaseUTC[i - 4])) {
BlMoonM = moonPhaseUTC[i];
if (BlMoonM > UTCsec) {
// wyjście z pętli, jeśli data jest w przyszłości
return;
}
}
}
}
// zaznacz trzecią pełnię w porze roku, jeśli w sezonie są cztery pełnie
void FindTFT_BLUEMoonS() {
// licznik pełni w danej porze roku
int k = 0;
// indeks trzeciej pełni w obrębie sezonu
int l = 0;
// przejście po kolejnych sezonach
for (int j = 1; j < 24; j++) {
for (int i = 2; i < 240; i += 4) {
// sprawdzenie, ile wpisów pełni (Fmoon[]) wypada w danym sezonie
if (moonPhaseUTC[i] > seasonUTC[j - 1] && moonPhaseUTC[i] < seasonUTC[j]) {
k++;
if (k == 3) {
// zapamiętanie trzeciej pełni
l = i;
}
}
}
// jeżeli w sezonie są cztery pełnie, trzecia jest sezonowym niebieskim Księżycem
if (k > 3) {
BlMoonS = moonPhaseUTC[l];
if (BlMoonS > UTCsec) {
// wyjście z pętli, jeśli data jest w przyszłości
return;
}
}
k = 0;
}
}
// pobierz czas lokalny na podstawie strefy, czasu standardowego/letniego i wpisz do 'thisTime' oraz 'thisDate'
void convUnix(time_t locUTC) {
// konwersja UTC na czas lokalny (struktura ltz)
localtime_r(&locUTC, <z);
// Debug: sprawdź wartości
//Serial.printf("DEBUG: tm_year=%d, tm_mon=%d, tm_mday=%d, tm_hour=%d, tm_min=%d\n", ltz.tm_year, ltz.tm_mon, ltz.tm_mday, ltz.tm_hour, ltz.tm_min);
sprintf(thisTime, "%02d:%02d", ltz.tm_hour, ltz.tm_min);
// Upewnij się że rok jest prawidłowy
if (ltz.tm_year < 0) {
// Błąd konwersji czasu - użyj zapasowej daty
sprintf(thisDate, "2024.%02d.%02d", ltz.tm_mon + 1, ltz.tm_mday);
} else {
sprintf(thisDate, "%04d.%02d.%02d", ltz.tm_year + 1900, ltz.tm_mon + 1, ltz.tm_mday);
}
}
// 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, 100, 50, TFT_COLOR1);
// (N) wygenerowanie przerw w elipsie, aby narysować N/E/S/W
tftMoon.fillRect(centerX - 5, centerY - 55, 10, 12, TFT_BLACK); // (N)
tftMoon.fillRect(centerX + 95, centerY - 6, 10, 12, TFT_BLACK); // (E)
tftMoon.fillRect(centerX - 5, centerY + 45, 10, 12, TFT_BLACK); // (S)
tftMoon.fillRect(centerX - 102, centerY - 6, 10, 12, TFT_BLACK); // (W)
int obsOffset;
char compass[5];
double moonAzAdj, sunAzAdj;
// Użyj biblioteki SunCalc do obliczenia pozycji Księżyca i Słońca
SunCalc::MoonPosition moonPos = SunCalc::getMoonPosition(UTCsec, observer_lat, observer_lon);
SunCalc::SunPosition sunPos = SunCalc::getSunPosition(UTCsec, observer_lat, observer_lon);
if (observer_lat >= 0) {
// półkula północna – N u góry, E po prawej, S na dole, W po lewej
// SunCalc: 0°=N, 90°=E, 180°=S, 270°=W → konwersja na nasz system
obsOffset = -90;
strcpy(compass, "NESW");
// Dla półkuli północnej: obracamy azymut o 180 stopni
moonAzAdj = fmod(moonPos.azimuth * 180.0 / M_PI + 180.0, 360.0);
sunAzAdj = fmod(sunPos.azimuth * 180.0 / M_PI + 180.0, 360.0);
} else {
// półkula południowa – S u góry, W po prawej, N na dole, E po lewej
obsOffset = 90;
strcpy(compass, "SWNE");
// Dla półkuli południowej: używamy azymutu bez zmian
moonAzAdj = moonPos.azimuth * 180.0 / M_PI;
sunAzAdj = sunPos.azimuth * 180.0 / M_PI;
}
tftMoon.setTextColor(TFT_BLUE, TFT_BLACK);
// N lub S
tftMoon.setCursor(centerX - 3, centerY - 54);
tftMoon.print(compass[0]);
// E lub W
tftMoon.setCursor(centerX + 98, centerY - 4);
tftMoon.print(compass[1]);
// S lub N
tftMoon.setCursor(centerX - 3, centerY + 48);
tftMoon.print(compass[2]);
// W lub E
tftMoon.setCursor(centerX - 101, centerY - 4);
tftMoon.print(compass[3]);
// Konwersja wysokości z radianów na stopnie
moonAlt = moonPos.altitude * 180.0 / M_PI;
sunAlt = sunPos.altitude * 180.0 / M_PI;
// Debug: wypisz wartości do monitora szeregowego
// Serial.printf("Obserwator: lat=%.1f° (półkula %s)\n", observer_lat, (observer_lat >= 0) ? "PN" : "PD");
// Serial.printf("Słońce - Az: %.1f° -> %.1f°, Alt: %.1f°\n", sunPos.azimuth * 180.0 / M_PI, sunAzAdj, sunAlt);
// Serial.printf("Księżyc - Az: %.1f° -> %.1f°, Alt: %.1f°\n", moonPos.azimuth * 180.0 / M_PI, moonAzAdj, moonAlt);
// położenie Księżyca wewnątrz elipsy
int MposX = centerX + 80 * cos(degToRad(moonAzAdj + obsOffset));
int MposY = centerY + 30 * sin(degToRad(moonAzAdj + obsOffset));
tftMoon.fillCircle(MposX, MposY, 4, TFT_SILVER);
// położenie Słońca na zewnątrz elipsy
int SposX = centerX + 115 * cos(degToRad(sunAzAdj + obsOffset));
int SposY = centerY + 60 * sin(degToRad(sunAzAdj + 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);
// Aktualizuj globalne zmienne dla wyświetlania tekstowego
moonAz = moonAzAdj;
sunAz = sunAzAdj;
}
// Fazy Księżyca / pory roku
void computePhases(int cyear) {
int i = 0;
// znajdź pierwszy cykl dla danego roku
int k = getCycleEstimate(cyear, 0);
float PhaseL = 0;
// przejście przez poprzedni i kolejne 57 cykli
for (int i = -1; i < 58; i++) {
moonPhaseUTC[(i + 1) * 4] = getPhaseDate(k + i, 0);
// 0 = nów
moonPhaseInd[(i + 1) * 4] = 0;
moonPhaseUTC[(i + 1) * 4 + 1] = getPhaseDate(k + i, .25);
// 1 = I kwadra
moonPhaseInd[(i + 1) * 4 + 1] = 1;
moonPhaseUTC[(i + 1) * 4 + 2] = getPhaseDate(k + i, .5);
// 2 = pełnia
moonPhaseInd[(i + 1) * 4 + 2] = 2;
moonPhaseUTC[(i + 1) * 4 + 3] = getPhaseDate(k + i, .75);
// 3 = III kwadra
moonPhaseInd[(i + 1) * 4 + 3] = 3;
}
// przeglądanie listy faz Księżyca aż do znalezienia aktualnej „ćwiartki”
for (i = 0; i < 240; i++) {
if (moonPhaseUTC[i] > UTCsec) {
// długość aktualnej ćwiartki fazy Księżyca
long iPhase = moonPhaseUTC[i] - moonPhaseUTC[i - 1];
// jak daleko jesteśmy w bieżącej ćwiartce
long uPhase = UTCsec - moonPhaseUTC[i - 1];
// wskazuje, czy jesteśmy po nowiu, I kwadrze, pełni czy III kwadrze
switch (moonPhaseInd[i - 1]) {
case 0:
// po nowiu
phaseRaw = map(uPhase, 0, iPhase, 0, 299);
break;
case 1:
// po I kwadrze
phaseRaw = map(uPhase, 0, iPhase, 300, 599);
break;
case 2:
// po pełni
phaseRaw = map(uPhase, 0, iPhase, 600, 899);
break;
case 3:
// po III kwadrze
phaseRaw = map(uPhase, 0, iPhase, 900, 1199);
break;
}
// zbierz kolejne cztery daty faz
for (k = 0; k < 4; k++) {
NextMoonUTC[k] = moonPhaseUTC[i + k];
NextMoonInd[k] = moonPhaseInd[i + k];
}
// wyjście – nie ma potrzeby przeglądać dalszych wpisów
return;
}
}
}
// oblicz pory roku dla 5 lat, zaczynając od poprzedniego roku
void computeSeasons(int cyear) {
// czas Unix
time_t UTCseason = 0;
// bufor tekstowy do wyświetlania
char thisSeason[] = "0000-00-00 - 00:00:00";
// format wyjściowy
char FormatTD[] = "%4d-%02d-%02d - %02d:%02d:%02d";
// indeks w tablicy seasonUTC[]
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
double T[4], W[4], L[4], S[4], JD[4];
// stałe perturbacyjne (24 elementy)
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 };
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 };
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 };
// wstępne przybliżenia
double JDME[4];
// 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);
for (int i = 0; i < 4; ++i) {
// wieki juliańskie od roku 2000 (equ/sol)
T[i] = (JDME[i] - 2451545.0) / 36525;
// stopnie
W[i] = 35999.373 * T[i] - 2.47;
// Lambda
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);
}
// ostateczny wynik w juliańskich dniach dynamicznych
JD[i] = JDME[i] + 0.00001 * S[i] / L[i];
UTCseason = julianDateToUnix(JD[i]);
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 / Księżyc – wczytanie, obrót, rysowanie
// sprawdź, czy identyfikator pliku się zmienił i w razie potrzeby wczytaj nowy
void LoadMoonFile(int idx) {
char stridx[] = "xxxxxx";
// 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");
}
}
}
// oblicz kąt oświetlonego brzegu tarczy na podstawie szer./dł. geograficznej obserwatora
double calcMoonIlluminatedLimbAngle(time_t time, double observer_lat, double observer_lon) {
// Użyj biblioteki SunCalc do obliczenia iluminacji Księżyca
SunCalc::MoonIllumination moonIllum = SunCalc::getMoonIllumination(time);
// Kąt oświetlonego brzegu (limb angle) to kąt jasnej krawędzi zwrócony przez bibliotekę
// Konwersja z radianów na stopnie
double brightLimbAngle = moonIllum.angle * 180.0 / M_PI;
// Normalizacja do zakresu 0-360 stopni
brightLimbAngle = fmod(brightLimbAngle, 360.0);
if (brightLimbAngle < 0.0) {
brightLimbAngle += 360.0;
}
return brightLimbAngle;
}
// Tekst – wyświetlacz tekstowy (U8g2 + polskie znaki)
// aktualizacja części tekstowej wyświetlacza
void updateTxtDisplay() {
GetTimeStr();
int i = 0;
char tmpstr[] = "**********";
// obliczenia Meeusa dają kąt rosnący przeciwnie do ruchu wskazówek zegara od północy;
// tu odwracamy go tak, aby odczyt był bardziej intuicyjny (CW)
sprintf(limbAngleStr, "Kąt oświetlenia: %3.0f", 360.0 - limbAngle);
// parametry: stary ciąg do skasowania, nowy ciąg do wpisania, pozycja x, pozycja y
updateText(limbAngleStrO, limbAngleStr, 5, 13);
// zapamiętanie aktualnie wyświetlanego ciągu
strcpy(limbAngleStrO, limbAngleStr);
// stały napis, nic nie kasujemy
updateText((char *)"", (char *)" Wysokość Azymut", 70, 180);
updateText((char *)"", (char *)"Księżyc:", 5, 195);
sprintf(moonAltStr, "%4.0f", moonAlt);
// wysokość Księżyca
updateText(moonAltStrO, moonAltStr, 70, 195);
strcpy(moonAltStrO, moonAltStr);
sprintf(moonAzStr, "%4.0f", moonAz);
// azymut Księżyca
updateText(moonAzStrO, moonAzStr, 125, 195);
strcpy(moonAzStrO, moonAzStr);
updateText((char *)"", (char *)"Słońce:", 5, 210);
sprintf(sunAltStr, "%4.0f", sunAlt);
// wysokość Słońca
updateText(sunAltStrO, sunAltStr, 70, 210);
strcpy(sunAltStrO, sunAltStr);
sprintf(sunAzStr, "%4.0f", sunAz);
// azymut Słońca
updateText(sunAzStrO, sunAzStr, 125, 210);
strcpy(sunAzStrO, sunAzStr);
// aktualna lokalna data
updateText(thisDateO, thisDate, 5, 260);
strcpy(thisDateO, thisDate);
// aktualny lokalny czas
updateText(thisTimeO, thisTime, 115, 260);
strcpy(thisTimeO, thisTime);
updateText((char *)"", (char *)"Strefa:", 5, 280);
sprintf(TZstr, "%s", TZdata[TZselect][0]);
// nazwa strefy czasowej
updateText(TZstrO, TZstr, 115, 280);
strcpy(TZstrO, TZstr);
updateText((char *)"", (char *)"Miejsce:", 5, 300);
sprintf(obsLat, "Szer:%4.0f", observer_lat);
// szerokość geograficzna obserwatora
updateText(obsLatO, obsLat, 5, 315);
strcpy(obsLatO, obsLat);
sprintf(obsLon, "Dług:%4.0f", observer_lon);
// długość geograficzna obserwatora
updateText(obsLonO, obsLon, 115, 315);
strcpy(obsLonO, obsLon);
// etykiety linii dla czterech kolejnych faz (nów, I kwadra, pełnia, III kwadra)
for (i = 0; i < 4; 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(0, 20, 240, 85, TFT_BLACK);
// ponowny rysunek nagłówka tabeli
updateText((char *)"", (char *)"Następna faza Księżyca: ", 5, 35);
// 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);
updateText(tmp2strO, tmp2str, 5, 50 + i * 15);
}
// następny czarny księżyc
convUnix(BkMoon);
sprintf(BkMoonStr, " Czarny: %s %s", thisDate, thisTime);
updateText(BkMoonStrO, BkMoonStr, 5, 120);
strcpy(BkMoonStrO, BkMoonStr);
// następny miesięczny niebieski Księżyc
convUnix(BlMoonM);
sprintf(BlMoonMStr, "Niebieski-mies: %s %s", thisDate, thisTime);
updateText(BlMoonMStrO, BlMoonMStr, 5, 135);
strcpy(BlMoonMStrO, BlMoonMStr);
// następny sezonowy niebieski Księżyc
convUnix(BlMoonS);
sprintf(BlMoonSStr, "Niebieski-sez: %s %s", thisDate, thisTime);
updateText(BlMoonSStrO, BlMoonSStr, 5, 150);
strcpy(BlMoonSStrO, BlMoonSStr);
// rysowanie wyróżnionych ramek podczas zmiany ustawień
switch (menu) {
// ustawianie miesiąca
case 11:
tftText.drawRect(2, 247, 30, 18, TFT_BOXES);
break;
// ustawianie dnia
case 12:
tftText.drawRect(34, 247, 30, 18, TFT_BOXES);
break;
// ustawianie roku
case 13:
tftText.drawRect(66, 247, 30, 18, TFT_BOXES);
break;
// ustawianie godziny
case 14:
tftText.drawRect(112, 247, 30, 18, TFT_BOXES);
break;
// ustawianie minut
case 15:
tftText.drawRect(144, 247, 30, 18, TFT_BOXES);
break;
// ustawianie strefy czasowej
case 16:
tftText.drawRect(112, 267, 110, 18, TFT_BOXES);
break;
// ustawianie szerokości geograficznej
case 17:
tftText.drawRect(2, 302, 110, 18, TFT_BOXES);
break;
// ustawianie długości geograficznej
case 18:
tftText.drawRect(112, 302, 110, 18, TFT_BOXES);
break;
}
}
// 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);
}
// RTC / czas / menu enkodera
void updateClock() {
// pobranie czasu epoki z RTC
UTCsec = RTC.now().unixtime();
// ustawienie zegara programowego
setTime(UTCsec);
}
void updateRTC() {
// ustawienie zegara programowego
setTime(UTCsec);
// zapis czasu do RTC
RTC.adjust(UTCsec);
// natychmiastowe przeliczenie Księżyca, pominięcie okresu minutowego
phaseNow = true;
}
// pobranie lokalnego czasu na podstawie strefy i DST/STD
void GetTimeStr() {
// konwersja UTCsec na czas lokalny (struktura ltz)
localtime_r(&UTCsec, <z);
// DEBUG: sprawdź wszystkie wartości czasu
//Serial.printf("DEBUG TIME: UTC=%ld, Local: %04d-%02d-%02d %02d:%02d:%02d, isdst=%d, TZ=%s\n", UTCsec, ltz.tm_year + 1900, ltz.tm_mon + 1, ltz.tm_mday, ltz.tm_hour, ltz.tm_min, ltz.tm_sec, ltz.tm_isdst, TZdata[TZselect][0]);
char tmp[] = "xxxx";
if (ltz.tm_isdst == 1) {
strcpy(tmp, TZdata[TZselect][3]); // CEST
} else {
strcpy(tmp, TZdata[TZselect][2]); // CET
}
sprintf(thisTime, "%02d:%02d %s", ltz.tm_hour, ltz.tm_min, tmp);
sprintf(thisDate, "%04d.%02d.%02d", ltz.tm_year + 1900, ltz.tm_mon + 1, ltz.tm_mday);
}
// funkcja wywoływana przy obrocie w prawo
void countUp() {
switch (menu) {
case 11:
// dodaj jeden miesiąc
temp = month(UTCsec) + 1;
if (temp > 12) {
temp = 1;
}
UTCsec = DateTime(year(UTCsec), temp, day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
// UTCsec = mktime(&);
break;
case 12:
// dodaj jeden dzień
UTCsec += 86400;
break;
case 13:
// dodaj jeden rok
temp = year(UTCsec) + 1;
if (temp > 2099) {
temp = 2099;
}
UTCsec = DateTime(temp, month(UTCsec), day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
break;
case 14:
// dodaj jedną godzinę
UTCsec += 3600;
break;
case 15:
// dodaj jedną minutę
UTCsec += 60;
break;
case 16:
// następna strefa czasowa
TZselect += 1;
if (TZselect > TZrows - 1) {
TZselect = 0;
}
// ustaw strefę czasową na nowo wybraną
setenv("TZ", TZdata[TZselect][1], 1);
// uaktywnienie strefy
tzset();
// flaga wymuszająca odświeżenie tabeli kolejnych faz Księżyca
NextMoonUTCO = 0UL;
break;
case 17:
// dodaj 1 stopień szerokości geograficznej
observer_lat += 1;
if (observer_lat > 90) {
observer_lat -= 180;
}
break;
case 18:
// dodaj 1 stopień długości geograficznej
observer_lon += 1;
if (observer_lon > 180) {
observer_lon -= 360;
}
break;
default:
// brak zmian, jeśli nie jesteśmy w trybie ustawień (żeby uniknąć przypadkowej zmiany RTC)
return;
break;
}
// aktualizacja RTC tylko przy zmianie ustawień
updateRTC();
}
// funkcja wywoływana przy obrocie w lewo
void countDn() {
switch (menu) {
case 11:
// cofnięcie o jeden miesiąc
temp = month(UTCsec) - 1;
if (temp <= 0) {
temp = 12;
}
UTCsec = DateTime(year(UTCsec), temp, day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
break;
case 12:
// cofnięcie o jeden dzień
UTCsec -= 86400;
break;
case 13:
// cofnięcie o jeden rok
temp = year(UTCsec) - 1;
if (temp < 2000) {
temp = 2000;
}
UTCsec = DateTime(temp, month(UTCsec), day(UTCsec), hour(UTCsec), minute(UTCsec), second(UTCsec)).unixtime();
break;
case 14:
// cofnięcie o jedną godzinę
UTCsec -= 3600;
break;
case 15:
// cofnięcie o jedną minutę
UTCsec -= 60;
break;
case 16:
// poprzednia strefa czasowa
TZselect -= 1;
if (TZselect >= TZrows) {
TZselect = TZrows - 1;
}
// ustawienie strefy czasowej na nowo wybraną
setenv("TZ", TZdata[TZselect][1], 1);
// uaktywnienie strefy
tzset();
// flaga wymuszająca odświeżenie tabeli kolejnych faz Księżyca
NextMoonUTCO = 0UL;
break;
case 17:
// zmniejszenie szerokości geograficznej o 1 stopień
observer_lat -= 1;
if (observer_lat < -90) {
observer_lat += 180; // zawinięcie
}
break;
case 18:
// zmniejszenie długości geograficznej o 1 stopień
observer_lon -= 1;
if (observer_lon < -180) {
observer_lon += 360; // zawinięcie
}
break;
default:
// brak zmian, jeśli nie jesteśmy w trybie ustawień (żeby uniknąć przypadkowej zmiany RTC)
return;
break;
}
// aktualizacja RTC tylko przy zmianie ustawień
updateRTC();
}
// czas trwania kliknięcia używany do wejścia w tryb ustawień
void SelClick() {
// zapamiętanie aktualnego stanu przycisku
SelBtnOld = true;
// uniknięcie zmian, gdy wyświetlacz dopiero się włączył
/*
if (!dispActive) {
return;
}
*/
// zapamiętanie czasu pierwszego naciśnięcia
unsigned long firstPress = millis();
while (digitalRead(BtnPin) == LOW) {
// długie naciśnięcie (>600 ms)
if (millis() - firstPress > 600) {
switch (menu) {
case 0:
// ustawienia daty/czasu
menu = 11;
break;
default:
// nieużywane – powrót do ekranu głównego
menu = 0;
break;
}
// Serial.print("Long Click, Menu: ");
// Serial.println(menu);
return;
}
}
switch (menu) {
case 11:
menu = 12;
// wyczyszczenie ramki „Miesiąc”
tftText.drawRect(2, 247, 30, 18, TFT_BLACK);
break;
case 12:
menu = 13;
// „Dzień”
tftText.drawRect(34, 247, 30, 18, TFT_BLACK);
break;
case 13:
menu = 14;
// „Rok”
tftText.drawRect(66, 247, 30, 18, TFT_BLACK);
break;
case 14:
menu = 15;
// „Godzina”
tftText.drawRect(112, 247, 30, 18, TFT_BLACK);
break;
case 15:
menu = 16;
// „Minuta”
tftText.drawRect(144, 247, 30, 18, TFT_BLACK);
break;
case 16:
menu = 17;
// „Strefa czasowa”
tftText.drawRect(112, 267, 110, 18, TFT_BLACK);
break;
case 17:
menu = 18;
// „Szerokość”
tftText.drawRect(2, 302, 110, 18, TFT_BLACK);
break;
case 18:
menu = 0;
// „Długość”
tftText.drawRect(112, 302, 110, 18, TFT_BLACK);
// koniec menu – zapis ustawień
//write_NVRAM();
break;
default:
break;
}
}
// enkoder na panelu frontowym
void RotaryEnc(long EncPos) {
// odświeżenie timera wygaszania wyświetlacza
//dispTimer = millis() + periodDisplay;
// uniknięcie zmian, gdy wyświetlacz dopiero się włączył
/*
if (!dispActive) {
return;
}
*/
if (EncPos == 0) {
countDn();
} else {
countUp();
}
}
// Funkcja 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() {
Serial.println("Rezerwuję bufory obrazu PNG (early)...");
for (int i = 0; i < MAX_IMAGE_HEIGHT; i++) {
originalImage[i] = (uint16_t*)malloc(MAX_IMAGE_WIDTH * 2);
rotatedImage[i] = (uint16_t*)malloc(MAX_IMAGE_WIDTH * 2);
if (!originalImage[i] || !rotatedImage[i]) {
Serial.printf("Błąd alokacji wiersza %d\n", i);
// sprzątanie
for (int j = 0; j <= i; j++) {
if (originalImage[j]) { free(originalImage[j]); originalImage[j] = NULL; }
if (rotatedImage[j]) { free(rotatedImage[j]); rotatedImage[j] = NULL; }
}
return false;
}
// wypełnij tłem (np. białym)
memset(originalImage[i], 0xFF, MAX_IMAGE_WIDTH * 2);
memset(rotatedImage[i], 0xFF, MAX_IMAGE_WIDTH * 2);
}
pngBuffersReady = true;
Serial.println("Bufory obrazu zarezerwowane.");
return true;
}
/*
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);
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;
}
}
// Funkcja pomocnicza do drukowania czasu epoki (Unix) w czytelnym formacie
void printUnix(time_t unixTime) {
// Wskaźnik na strukturę czasu UTC
struct tm *tm_info = gmtime(&unixTime);
char buffer[30];
// Formatowanie daty i czasu UTC do bufora
// YYYY-MM-DD HH:MM:SS
strftime(buffer, 30, "%Y-%m-%d %H:%M:%S", tm_info);
Serial.print("Unix Time: ");
Serial.println(buffer);
}
// 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;
}
/*
A w rysowaniu też pilnuj zawsze:
// (w wersji z TFT_eSPI, zostawiam jako komentarz-historyjkę)
// rysuję na tekstowym
digitalWrite(DispText_CS, LOW);
digitalWrite(DispMoon_CS, HIGH);
tft.setRotation(0); // jak chcesz, możesz dla pewności dawać zawsze
// ... rysowanie ...
digitalWrite(DispText_CS, HIGH);
// rysuję na księżycowym
digitalWrite(DispMoon_CS, LOW);
digitalWrite(DispText_CS, HIGH);
tft.setRotation(0);
// ... rysowanie ...
digitalWrite(DispMoon_CS, HIGH);
*/