#include <TFT_eSPI.h>
#include <SPI.h>
#include <SD.h>
#include <PNGdec.h>
#include <Wire.h>
#include <RTClib.h>
#include <ESP32RotaryEncoder.h>
#include <TimeLib.h>
#include "colors.h"
#define FONT1 &FreeMono9pt7b
TFT_eSPI tft = TFT_eSPI();
// 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
// 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 = 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];
uint16_t *rotatedImage[MAX_IMAGE_HEIGHT];
// 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;
// plik z ustawieniami (strefa czasowa + pozycja obserwatora)
const char *CFG_FILE = "/moon_cfg.bin";
// 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 = 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;
double observer_lat = 45.6;
// długość geograficzna
double observer_lon = 10.3;
// szerokość geograficzna do/z NVRAM w pełnych stopniach (+/- 90°)
int8_t NVlat = 0;
// długość geograficzna do/z NVRAM w pełnych stopniach (+/- 180°)
int16_t NVlon = 0;
// 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[] = "00:00 xxxx";
// poprzednie wyświetlane ciągi czasu/dat
char thisTimeO[] = "00:00 xxxx";
char thisDate[] = "00/00/00 ";
char thisDateO[] = "00/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[] = "Lat:-xxx.x";
char obsLatO[] = "Lat:-xxx.x";
// tekstowy zapis długości geograficznej obserwatora
char obsLon[] = "Lon:-xxx.x";
char obsLonO[] = "Lon:-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[] = "***********************";
char tmp2strO[] = "***********************";
// nazwa pliku z obrazem fazy Księżyca: 0.png do 39.png
char SDfilnam[] = "xxxxxx.png";
char SDfilnamO[] = "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[22];
char BkMoonStrO[22];
char BlMoonMStr[22];
char BlMoonMStrO[22];
char BlMoonSStr[22];
char BlMoonSStrO[22];
void setup() {
Serial.begin(115200);
Wire.begin(); // I2C
// inicjalizacja zegara RTC DS3231
if (!RTC.begin()) {
Serial.println("Błąd: nie mogę znaleźć RTC DS3231!");
while (1) {
delay(100);
}
}
// 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__)));
}
// domyślne ustawienia – Caino + Europa
TZselect = 0; // wiersz 0 w TZdata: "Europe"
observer_lat = 45.6;
observer_lon = 10.3;
NVlat = (int8_t)observer_lat;
NVlon = (int16_t)observer_lon;
// ustawienie zegara programowego na podstawie RTC
updateClock(); // ustawia UTCsec z RTC
// wygenerowanie łańcuchów czasu/dat
GetTimeStr();
// inicjalizacja RTC, jeśli rok <2000 lub > 2099; w wyświetlaniu używany jest rok 2-cyfrowy
if (year(now()) < 2000 || year(now()) > 2099) {
RTC.adjust(DateTime(2000, 1, 1, 0, 0, 0));
updateClock();
GetTimeStr();
}
// ustawienie strefy czasowej na wcześniej zdefiniowaną
setenv("TZ", TZdata[TZselect][1], 1);
// uaktywnienie nowej strefy
tzset();
// 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 obu wyświetlaczy
pinMode(DispText_CS, OUTPUT);
pinMode(DispMoon_CS, OUTPUT);
// linia CS jest aktywna w stanie niskim
digitalWrite(DispText_CS, HIGH);
digitalWrite(DispMoon_CS, HIGH);
tft.begin();
tft.setTextSize(1);
tft.setFreeFont(FONT1);
tft.setTextColor(TFT_COLOR1);
// LCD z tekstem
digitalWrite(DispText_CS, LOW);
digitalWrite(DispMoon_CS, HIGH);
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
tft.drawRect(0, 0, tft.width(), tft.height(), TFT_BLUE);
digitalWrite(DispText_CS, HIGH);
// LCD z księżycem
digitalWrite(DispMoon_CS, LOW);
digitalWrite(DispText_CS, HIGH);
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
tft.drawRect(0, 0, tft.width(), tft.height(), TFT_BLUE);
digitalWrite(DispMoon_CS, HIGH);
// kolor dopasowany do „postarzonego” tła nieba
tft.setTextColor(TFT_COLOR1);
// odznaczenie (dezaktywacja) obu wyświetlaczy
digitalWrite(DispText_CS, 1);
digitalWrite(DispMoon_CS, 1);
// inicjalizacja karty SD na osobnej magistrali SPI
SPI.begin(SCLK, MISO, MOSI, SD_CS);
if (!SD.begin(SD_CS)) {
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 = (tft.width() - imageWidth) / 2;
int centerY = (tft.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;
}
}
// 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);
sprintf(thisTime, "%02d:%02d", ltz.tm_hour, ltz.tm_min);
sprintf(thisDate, "%02d/%02d/%02d", ltz.tm_mon + 1, ltz.tm_mday, ltz.tm_year - 100);
}
void displayMoonSunPos() {
digitalWrite(DispText_CS, 1);
digitalWrite(DispMoon_CS, 0);
int centerX = tft.width() / 2;
int centerY = tft.height() / 2;
// przesunięcie elipsy o 90 pikseli w dół od środka
centerY += 90;
// x,y,w,h,kolor – wymazanie dolnej połowy ekranu
tft.fillRect(0, 180, 240, 140, TFT_BLACK);
tft.drawCircle(centerX, centerY, 10, TFT_COLOR1);
// stary napis, nowy napis, pozycja x, pozycja y
updateText("", "E", centerX - 5, centerY + 4);
// rysowanie orbity
tft.drawEllipse(centerX, centerY, 100, 50, TFT_COLOR1);
// (N) wygenerowanie przerw w elipsie, aby narysować N/E/S/W
tft.fillRect(centerX - 6, centerY - 58, 12, 16, TFT_BLACK);
// (E)
tft.fillRect(centerX + 94, centerY - 8, 12, 16, TFT_BLACK);
// (S)
tft.fillRect(centerX - 6, centerY + 42, 12, 16, TFT_BLACK);
// (W)
tft.fillRect(centerX - 106, centerY - 8, 12, 16, TFT_BLACK);
int obsOffset = 90;
char compass[] = "SWNE";
// dodajemy 90°, żeby kąt położenia liczyć od dołu (północ) dla obserwatora na półkuli północnej.
// dla obserwatora na półkuli południowej odwracamy położenie Słońca/Księżyca,
// tak by kąt był liczony od góry (południe), i zamieniamy oznaczenia kompasu
if (observer_lat < 0) {
obsOffset = -90;
strcpy(compass, "NESW");
}
// N lub S
tft.setCursor(centerX - 5, centerY - 45);
tft.print(compass[0]);
// E lub W
tft.setCursor(centerX + 95, centerY + 4);
tft.print(compass[1]);
// S lub N
tft.setCursor(centerX - 5, centerY + 55);
tft.print(compass[2]);
// W lub E
tft.setCursor(centerX - 105, centerY + 4);
tft.print(compass[3]);
// położenie Księżyca wewnątrz elipsy
int MposX = centerX + 90 * cos(degToRad(moonAz + obsOffset));
int MposY = centerY + 40 * sin(degToRad(moonAz + obsOffset));
// stary napis, nowy napis, pozycja x, pozycja y
updateText("", "M", MposX - 4, MposY + 6);
// położenie Słońca na zewnątrz elipsy
int SposX = centerX + 110 * cos(degToRad(sunAz + obsOffset));
int SposY = centerY + 60 * sin(degToRad(sunAz + obsOffset));
// stary napis, nowy napis, pozycja x, pozycja y
updateText("", "S", SposX - 4, SposY + 6);
// linia łącząca Słońce i Księżyc
tft.drawLine(MposX, MposY, SposX, SposY, TFT_COLOR1);
digitalWrite(DispText_CS, 1);
digitalWrite(DispMoon_CS, 1);
}
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;
}
// 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) {
struct tm *tm_info = gmtime(&time);
// wyliczenie daty juliańskiej
double jd = julianDate(tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday, tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);
// obliczenie położenia Księżyca i Słońca
double moonRA, moonDec, moonDist;
calcMoonPosition(time, &moonRA, &moonDec, &moonDist);
Serial.print("MoonRA:");
Serial.print(moonRA, 1);
Serial.print(" MoonDec:");
Serial.println(moonDec, 1);
double sunRA, sunDec;
calcSunPosition(time, &sunRA, &sunDec);
Serial.print("SunRA:");
Serial.print(sunRA, 1);
Serial.print(" SunDec:");
Serial.println(sunDec, 1);
// obliczenie lokalnego czasu gwiazdowego
double T = (jd - 2451545.0) / 36525.0;
double theta0 = 280.46061837 + 360.98564736629 * (jd - 2451545.0) + 0.000387933 * T * T - T * T * T / 38710000.0;
theta0 = fmod(theta0, 360.0);
double LST = fmod(theta0 + observer_lon, 360.0);
// zamiana na współrzędne horyzontalne
equatorialToHorizontal(moonRA, moonDec, LST, observer_lat, &moonAz, &moonAlt);
Serial.print("MoonAz:");
Serial.print(moonAz, 1);
Serial.print(" MoonAlt:");
Serial.println(moonAlt, 1);
double sunAz, sunAlt;
equatorialToHorizontal(sunRA, sunDec, LST, observer_lat, &sunAz, &sunAlt);
Serial.print("SunAz:");
Serial.print(sunAz, 1);
Serial.print(" SunAlt:");
Serial.println(sunAlt, 1);
// obliczenie kąta położenia jasnego brzegu (względem północnego bieguna Księżyca)
double y = cos(degToRad(sunDec)) * sin(degToRad(sunRA - moonRA));
double x = sin(degToRad(sunDec)) * cos(degToRad(moonDec)) - cos(degToRad(sunDec)) * sin(degToRad(moonDec)) * cos(degToRad(sunRA - moonRA));
double chi = radToDeg(atan2(y, x));
// korekta dla perspektywy obserwatora (kąt paralaktyczny)
double parallacticAngle = 0.0;
if (cos(degToRad(moonDec)) != 0.0) {
double sinP = sin(degToRad(LST - moonRA));
double cosP = tan(degToRad(observer_lat)) * cos(degToRad(moonDec)) - sin(degToRad(moonDec)) * cos(degToRad(LST - moonRA));
parallacticAngle = radToDeg(atan2(sinP, cosP));
}
// obliczenie kąta jasnego brzegu (chi minus kąt paralaktyczny) i ograniczenie do zakresu 0–360°
double brightLimbAngle = fmod(chi - parallacticAngle, 360.0);
// normalizacja do 0–360°
if (brightLimbAngle < 0) brightLimbAngle += 360.0;
return brightLimbAngle;
}
void calcMoonPosition(time_t time, double *RA, double *Dec, double *distance) {
struct tm *tm_info = gmtime(&time);
// wyliczenie daty juliańskiej
double jd = julianDate(tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday, tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);
// czas w wiekach juliańskich od J2000.0
double T = (jd - 2451545.0) / 36525.0;
// średnia długość ekliptyczna Księżyca
double L = 218.316 + 13.176396 * (jd - 2451545.0);
L = fmod(L, 360.0);
// średnie wydłużenie Księżyca
double D = 297.8502 + 445267.1115 * T - 0.0016300 * T * T + T * T * T / 545868.0 - T * T * T * T / 113065000.0;
D = fmod(D, 360.0);
// średnia anomalia Słońca
double M = 357.5291 + 35999.0503 * T - 0.0001559 * T * T - 0.00000048 * T * T * T;
M = fmod(M, 360.0);
// średnia anomalia Księżyca
double M_moon = 134.9634 + 477198.8675 * T + 0.0089970 * T * T + T * T * T / 69699.0 - T * T * T * T / 14712000.0;
M_moon = fmod(M_moon, 360.0);
// argument szerokości Księżyca
double F = 93.2720 + 483202.0175 * T - 0.0034029 * T * T - T * T * T / 3526000.0 + T * T * T * T / 863310000.0;
F = fmod(F, 360.0);
// długość węzła wstępującego orbity Księżyca
double Omega = 125.0440 - 1934.1360 * T + 0.0020680 * T * T + T * T * T / 450000.0;
Omega = fmod(Omega, 360.0);
// obliczenie geocentrycznej długości i szerokości ekliptycznej (model uproszczony – dokładność wystarczająca do wizualizacji)
double lambda = L + 6.289 * sin(degToRad(M_moon)) + 1.274 * sin(degToRad(2 * D - M_moon)) + 0.658 * sin(degToRad(2 * D)) + 0.214 * sin(degToRad(2 * M_moon));
double beta = 5.128 * sin(degToRad(F)) + 0.281 * sin(degToRad(M_moon + F)) + 0.277 * sin(degToRad(M_moon - F));
// odległość w promieniach Ziemi
*distance = 60.36 - 3.27 * cos(degToRad(M_moon)) - 0.57 * cos(degToRad(2 * D - M_moon)) - 0.34 * cos(degToRad(2 * D)) - 0.11 * cos(degToRad(2 * M_moon));
// konwersja współrzędnych ekliptycznych na równikowe
// nachylenie ekliptyki
double epsilon = 23.439 - 0.0000004 * T;
double lambda_rad = degToRad(lambda);
double beta_rad = degToRad(beta);
double epsilon_rad = degToRad(epsilon);
double ra = atan2(sin(lambda_rad) * cos(epsilon_rad) - tan(beta_rad) * sin(epsilon_rad), cos(lambda_rad));
*RA = fmod(radToDeg(ra), 360.0);
if (*RA < 0)
*RA += 360.0;
double dec = asin(sin(beta_rad) * cos(epsilon_rad) + cos(beta_rad) * sin(epsilon_rad) * sin(lambda_rad));
*Dec = radToDeg(dec);
}
void calcSunPosition(time_t time, double *RA, double *Dec) {
struct tm *tm_info = gmtime(&time);
// wyliczenie daty juliańskiej
double jd = julianDate(tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday, tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);
// czas w wiekach juliańskich od J2000.0
double T = (jd - 2451545.0) / 36525.0;
// średnia długość ekliptyczna Słońca
double L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
L0 = fmod(L0, 360.0);
// średnia anomalia Słońca
double M = 357.52911 + 35999.05029 * T - 0.0001537 * T * T;
M = fmod(M, 360.0);
// równanie środka
double C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * sin(degToRad(M)) + (0.019993 - 0.000101 * T) * sin(degToRad(2 * M)) + 0.000289 * sin(degToRad(3 * M));
// długość ekliptyczna pozorna
double lambda = L0 + C;
// nachylenie ekliptyki
double epsilon = 23.439 - 0.0000004 * T;
// konwersja współrzędnych ekliptycznych na równikowe
double lambda_rad = degToRad(lambda);
double epsilon_rad = degToRad(epsilon);
double ra = atan2(cos(epsilon_rad) * sin(lambda_rad), cos(lambda_rad));
*RA = fmod(radToDeg(ra), 360.0);
if (*RA < 0) *RA += 360.0;
double dec = asin(sin(epsilon_rad) * sin(lambda_rad));
*Dec = radToDeg(dec);
}
void equatorialToHorizontal(double RA, double Dec, double LST, double lat, double *azimuth, double *altitude) {
// kąt godzinny
double HA = LST - RA;
if (HA < 0) HA += 360.0;
// konwersja na radiany
double ha_rad = degToRad(HA);
double dec_rad = degToRad(Dec);
double lat_rad = degToRad(lat);
// obliczenie wysokości
double sin_alt = sin(dec_rad) * sin(lat_rad) + cos(dec_rad) * cos(lat_rad) * cos(ha_rad);
double alt_rad = asin(sin_alt);
*altitude = radToDeg(alt_rad);
// obliczenie azymutu
double cos_az = (sin(dec_rad) - sin(lat_rad) * sin_alt) / (cos(lat_rad) * cos(alt_rad));
// korekta błędów numerycznych (ograniczenie zakresu)
if (cos_az > 1.0) cos_az = 1.0;
if (cos_az < -1.0) cos_az = -1.0;
double az = acos(cos_az);
// korekta azymutu w zależności od kąta godzinnego
if (sin(ha_rad) >= 0) az = 2 * M_PI - az;
*azimuth = radToDeg(az);
}
// obliczenie daty juliańskiej z daty kalendarzowej i czasu
double julianDate(int year, int month, int day, int hour, int minute, int second) {
if (month <= 2) {
year -= 1;
month += 12;
}
int A = floor(year / 100.0);
int B = 2 - A + floor(A / 4.0);
double JD = floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + day + B - 1524.5;
JD += (hour + minute / 60.0 + second / 3600.0) / 24.0;
return JD;
}
// funkcje pomocnicze do konwersji kątów
double degToRad(double degrees) {
return degrees * M_PI / 180.0;
}
double radToDeg(double radians) {
return radians * 180.0 / M_PI;
}
// aktualizacja części tekstowej wyświetlacza
void updateTxtDisplay() {
GetTimeStr();
int i = 0;
char tmpstr[] = "**********";
// wybór wyświetlacza tekstowego
digitalWrite(DispText_CS, 0);
digitalWrite(DispMoon_CS, 1);
// 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("", " Wysokość Azymut", 70, 180);
updateText("", "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("", "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("", "Strefa:", 5, 280);
sprintf(TZstr, "%s", TZdata[TZselect][0]);
// nazwa strefy czasowej
updateText(TZstrO, TZstr, 115, 280);
strcpy(TZstrO, TZstr);
updateText("", "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
tft.fillRect(0, 20, 240, 85, TFT_BLACK);
// ponowny rysunek nagłówka tabeli
updateText("", "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, "Black: %s %s", thisDate, thisTime);
updateText(BkMoonStrO, BkMoonStr, 5, 120);
strcpy(BkMoonStrO, BkMoonStr);
// następny miesięczny niebieski Księżyc
convUnix(BlMoonM);
sprintf(BlMoonMStr, "BlueM: %s %s", thisDate, thisTime);
updateText(BlMoonMStrO, BlMoonMStr, 5, 135);
strcpy(BlMoonMStrO, BlMoonMStr);
// następny sezonowy niebieski Księżyc
convUnix(BlMoonS);
sprintf(BlMoonSStr, "BlueS: %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:
tft.drawRect(2, 247, 30, 18, TFT_BOXES);
break;
// ustawianie dnia
case 12:
tft.drawRect(34, 247, 30, 18, TFT_BOXES);
break;
// ustawianie roku
case 13:
tft.drawRect(66, 247, 30, 18, TFT_BOXES);
break;
// ustawianie godziny
case 14:
tft.drawRect(112, 247, 30, 18, TFT_BOXES);
break;
// ustawianie minut
case 15:
tft.drawRect(144, 247, 30, 18, TFT_BOXES);
break;
// ustawianie strefy czasowej
case 16:
tft.drawRect(112, 267, 110, 18, TFT_BOXES);
break;
// ustawianie szerokości geograficznej
case 17:
tft.drawRect(2, 302, 110, 18, TFT_BOXES);
break;
// ustawianie długości geograficznej
case 18:
tft.drawRect(112, 302, 110, 18, TFT_BOXES);
break;
}
// odznaczenie (dezaktywacja) obu wyświetlaczy
digitalWrite(DispText_CS, 1);
digitalWrite(DispMoon_CS, 1);
}
void updateText(char tberased[20], char tbwritten[20], int xpos, int ypos) {
tft.setCursor(xpos, ypos);
// nadpisanie starego tekstu kolorem czarnym
tft.setTextColor(TFT_BLACK);
tft.print(tberased);
tft.setCursor(xpos, ypos);
// ustawienie koloru tekstu pierwszego planu
tft.setTextColor(TFT_COLOR1);
tft.print(tbwritten);
}
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);
char tmp[] = "xxxx";
if (ltz.tm_isdst == 1) {
// czas letni
strcpy(tmp, TZdata[TZselect][3]);
} else {
// czas standardowy
strcpy(tmp, TZdata[TZselect][2]);
}
sprintf(thisTime, "%02d:%02d %s", ltz.tm_hour, ltz.tm_min, tmp);
sprintf(thisDate, "%02d/%02d/%02d", ltz.tm_mon + 1, ltz.tm_mday, ltz.tm_year - 100);
// sprintf(thisWkDst,"Weekday %d DST %d", ltz.tm_wday, );
}
// 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;
}
}
digitalWrite(DispText_CS, 0);
digitalWrite(DispMoon_CS, 1);
switch (menu) {
case 11:
menu = 12;
// wyczyszczenie ramki „Miesiąc”
tft.drawRect(2, 247, 30, 18, TFT_BLACK);
break;
case 12:
menu = 13;
// „Dzień”
tft.drawRect(34, 247, 30, 18, TFT_BLACK);
break;
case 13:
menu = 14;
// „Rok”
tft.drawRect(66, 247, 30, 18, TFT_BLACK);
break;
case 14:
menu = 15;
// „Godzina”
tft.drawRect(112, 247, 30, 18, TFT_BLACK);
break;
case 15:
menu = 16;
// „Minuta”
tft.drawRect(144, 247, 30, 18, TFT_BLACK);
break;
case 16:
menu = 17;
// „Strefa czasowa”
tft.drawRect(112, 267, 110, 18, TFT_BLACK);
break;
case 17:
menu = 18;
// „Szerokość”
tft.drawRect(2, 302, 110, 18, TFT_BLACK);
break;
case 18:
menu = 0;
// „Długość”
tft.drawRect(112, 302, 110, 18, TFT_BLACK);
// koniec menu – zapis ustawień
//write_NVRAM();
break;
default:
break;
}
digitalWrite(DispText_CS, 1);
digitalWrite(DispMoon_CS, 1);
}
// wyświetlenie obróconego obrazu na TFT
void displayRotatedImage(int x, int y) {
digitalWrite(DispText_CS, 1);
digitalWrite(DispMoon_CS, 0);
for (int row = 0; row < imageHeight; row++) {
if (rotatedImage[row] != NULL) {
tft.pushImage(x, y + row, imageWidth, 1, rotatedImage[row], TRANSPARENT_COLOR);
}
}
digitalWrite(DispText_CS, 1);
digitalWrite(DispMoon_CS, 1);
}
// 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;
}
}
// 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);
// Skopiuj tę linię do naszego bufora obrazu, jeśli dany wiersz istnieje
if (pDraw->y < imageHeight && originalImage[pDraw->y] != NULL) {
for (int x = 0; x < pDraw->iWidth && x < imageWidth; x++) {
originalImage[pDraw->y][x] = lineBuffer[x];
}
}
// 1 = kontynuuj dekodowanie (wymagane przez tę wersję PNGdec)
return 1;
}
// przydzielenie pamięci na oba bufory obrazów
bool allocateImageBuffers() {
// inicjalizacja wszystkich wskaźników jako NULL
for (int i = 0; i < MAX_IMAGE_HEIGHT; i++) {
originalImage[i] = NULL;
rotatedImage[i] = NULL;
}
// przydzielenie pamięci dla każdego wiersza
for (int i = 0; i < imageHeight; i++) {
originalImage[i] = (uint16_t *)malloc(imageWidth * sizeof(uint16_t));
rotatedImage[i] = (uint16_t *)malloc(imageWidth * sizeof(uint16_t));
if (!originalImage[i] || !rotatedImage[i]) {
// błąd alokacji pamięci – zwolnij dotychczasowe przydziały
for (int j = 0; j <= i; j++) {
if (originalImage[j]) free(originalImage[j]);
if (rotatedImage[j]) free(rotatedImage[j]);
originalImage[j] = NULL;
rotatedImage[j] = NULL;
}
return false;
}
// wstępne wypełnienie wiersza obrazu obróconego kolorem „przezroczystym”
for (int x = 0; x < imageWidth; x++) {
// używane, by nie nadpisywać tła, jeśli dany piksel nie został dotknięty przez przekształcenie
rotatedImage[i][x] = TRANSPARENT_COLOR;
}
}
return true;
}
bool loadPngFromSD(const char *filename) {
// otwarcie pliku
File pngFile = SD.open(filename, FILE_READ);
if (!pngFile) {
Serial.println("Nie udało się otworzyć pliku PNG");
return false;
}
// utworzenie bufora na dane pliku
size_t fileSize = pngFile.size();
uint8_t *pngBuffer = (uint8_t *)malloc(fileSize);
if (!pngBuffer) {
Serial.println("Za mało pamięci, aby załadować PNG");
pngFile.close();
return false;
}
// odczyt całego pliku do bufora
pngFile.read(pngBuffer, fileSize);
pngFile.close();
// dekodowanie PNG z bufora
int16_t rc = png.openRAM(pngBuffer, fileSize, pngDraw);
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));
}
}
}
}
// 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);
}
/*
A w rysowaniu też pilnuj zawsze:
// 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);
*/