#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>
#include <ArduinoJson.h>
//#include <GxEPD2_BW.h>
#include <GxEPD2_3C.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
/////#include <U8g2_for_Adafruit_GFX.h>
#include <Wire.h>
#include "utf8_fix.h"
#include "beanstalk15.h"
#include "F12x6LED9pt7b.h"
#include "weathericons_regular_webfont10pt7b.h"
#define DEBUG_WEATHER false
#define TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" // Europa Środkowa z automatycznym DST
#define ENABLE_GxEPD2_GFX 0
#define BME280_POWER_PIN 0
// MOSI: GPIO 11, 35, 36, 37 (najczęściej 11 lub 35)
// SCK: GPIO 12, 13, 14, 15, 16, 17, 18, 19, 20, 21
#define EPD_SCK 12
#define EPD_MOSI 11
#define EPD_MISO -1
#define EPD_CS 10
#define EPD_DC 13
#define EPD_RST 46
#define EPD_BUSY 3
GFX_UTF8 display(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
// GDEM029C90 128x296, SSD1680 SCK(18), MISO(19), MOSI(23), SS(5)
//GxEPD2_3C<GxEPD2_290_C90c, GxEPD2_290_C90c::HEIGHT> display(GxEPD2_290_C90c(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));//GDEM029C90 128x296, SSD1680
//GxEPD2_BW<GxEPD2_290, GxEPD2_290::HEIGHT> display(GxEPD2_290(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
//GxEPD2_3C<GxEPD2_290c, GxEPD2_290c::HEIGHT> display(GxEPD2_290c(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
//GxEPD2_3C<GxEPD2_290_Z13c, GxEPD2_290_Z13c::HEIGHT> display(GxEPD2_290_Z13c(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));GDEH029Z13
//GxEPD2_3C<GxEPD2_290_C90c, GxEPD2_290_C90c::HEIGHT> display(GxEPD2_290_C90c(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));GDEM029C90
//U8G2_FOR_ADAFRUIT_GFX u8g2;
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* WeatherURL = "https://api.openweathermap.org/data/2.5/weather?lat=45.37&lon=10.19&appid=dbd9ecc273c56776e5aed832c0cf6fc2&lang=pl";
const char* DayForcastURL = "https://api.openweathermap.org/data/2.5/forecast?q=Caino,it&appid=dbd9ecc273c56776e5aed832c0cf6fc2&lang=pl";
//https://api.openweathermap.org/data/2.5/weather?lat=45.37&lon=10.19&appid=dbd9ecc273c56776e5aed832c0cf6fc2
//https://api.openweathermap.org/data/2.5/weather?q=Caino,it&appid=dbd9ecc273c56776e5aed832c0cf6fc2
//https://api.openweathermap.org/data/2.5/weather?lat=45.37&lon=10.19&appid=dbd9ecc273c56776e5aed832c0cf6fc2&lang=pl
//https://api.openweathermap.org/data/2.5/forecast?lat=45.37&lon=10.19&appid=dbd9ecc273c56776e5aed832c0cf6fc2
//https://api.openweathermap.org/data/2.5/forecast?q=Caino,it&appid=dbd9ecc273c56776e5aed832c0cf6fc2&lang=pl
//https://api.openweathermap.org/data/2.5/onecall?lat=45.37&lon=10.19&exclude=minutely&appid=dbd9ecc273c56776e5aed832c0cf6fc2
// kody błędów muszą być liczbami dodatnimi
#define ERROR_WEBSITE 1
#define ERROR_BAD_DATA 2
// Obecnie nie ma 100 różnych typów, ale powinno to objąć wszelkie zmiany w przyszłości
#define NUM_OF_WEATHERS 100
// jeśli true, wyświetl prędkość wiatru w milach na godzinę, w przeciwnym razie w metrach na sekundę
#define MPH false
// obecnie maksimum to 4, większy ekran może pozwolić na więcej
#define NUM_DAYS_FORCAST 4
// Rzeczy o głębokim śnie
// Współczynnik konwersji mikrosekund na sekundy
#define uS_TO_S_FACTOR 1000000
// współczynnik konwersji minut na sekundy
#define min_to_s_FACTOR 60
// Godzina, o której ESP32 przejdzie w tryb uśpienia (w minutach)
#define TIME_TO_SLEEP 20
const char* daysOfWeek[] = { "NI", "PO", "WT", "ŚR", "CZ", "PI", "SO" };
//const char* daysOfWeek[] = { "NIE", "PON", "WTO", "ŚRO", "CZW", "PIĄ", "SOB" };
//const char* daysOfWeek[] = { "NIEDZIELA", "PONIEDZIAŁEK, "WTOREK", "ŚRODA", "CZWARTEK", "PIĄ"TEK, "SOBOTA" };
// globalne zmienne temperatury
float main_temp, main_temp_min, main_temp_max, WindSpeed;
int main_pressure, main_humidity, WindAngle;
String WeatherDesc;
uint16_t WeatherID;
// Identyfikatory pogody na następne cztery dni
uint16_t ForecastID[4];
// Identyfikatory pogody na następne 12 godzin
uint16_t ThreeHourlyForcastID[4];
// Czas na kolejne 4 prognozy pogody w ciągu 3 godzin (dwie cyfry godziny)
String ThreeHourlyForcastTime[4];
struct tm DateTime;
time_t now = time(nullptr);
// przechowuj czytelną dla człowieka datę prognozy
char NowTimeStr[80];
// struktura pogody
struct Weather_Struct {
uint16_t ID;
const char* Line;
uint16_t IconD;
uint16_t IconN;
uint16_t IconChar(bool isNight) const {
return isNight ? IconN : IconD;
}
};
// tablica typów pogody, wypełniana w konfiguracji (za pomocą funkcji) b zmienia się,
// jeśli używasz innej witryny pogodowej lub jeśli dodają do liczby opisów
Weather_Struct Weathers[NUM_OF_WEATHERS];
// liczba różnych warunków pogodowych, sumuje się w miarę dodawania kolejnych w funkcji PopulateWeathers
uint8_t WeatherCount = 0;
// 1K jest wystarczające (zwroty wynoszą około 0,5K)
StaticJsonDocument<2048> Doc;//4096
Adafruit_BME280 bme;
bool BME280Found = true;
HTTPClient http;
int httpCode;
void setup() {
Serial.begin(115200);
delay(500);
Wire.begin(8, 9); // SDA, SCL
// Inicjalizacja BME280
if (!bme.begin(0x76, &Wire)) {
Serial.println("BME280 ERROR!");
BME280Found = false; // globalna zmienna
} else {
Serial.println("BME280 OK");
BME280Found = true;
}
// --- WiFi ---
WiFi.begin(ssid, password);
Serial.print("Łączenie z WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.print("\nPołączono, IP: ");
Serial.println(WiFi.localIP());
// --- Display ---
SPI.begin(EPD_SCK, EPD_MISO, EPD_MOSI, EPD_CS);
display.init(115200, true, 1000, false);
display.setRotation(1);
/////u8g2.begin(display);
PopulateWeathers();
// --- TIME (NTP + strefa) ---
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
//setenv("TZ", TIMEZONE, 1); // używa Twojego CET/CEST
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1);
tzset();
Serial.print("Synchronizacja NTP");
struct tm timeinfo;
int retry = 0;
while (!getLocalTime(&timeinfo) && retry < 30) {
Serial.print(".");
delay(500);
retry++;
}
if (retry < 30) {
Serial.println(" OK");
Serial.printf("Czas: %04d-%02d-%02d %02d:%02d:%02d\n", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
} else {
Serial.println(" TIMEOUT");
}
WeatherReport();
display.display();
Serial.println(display.epd2.panel);
/* display.hibernate(); // To wyłącza zasilanie panelu e-papieru
Serial.println("Zasypiam na 10 minut...");
Serial.flush();
// 600 sekund * 1 000 000 (mikrosekundy)
esp_sleep_enable_timer_wakeup(600ULL * 1000000ULL);
esp_deep_sleep_start();
Serial.println("--- DEBUG ---");
Serial.println(DayForcastURL);
Serial.println("-------------");*/
}
void WeatherReport() {
// Sprawdzenie połączenia z WiFi
if (WiFi.status() == WL_CONNECTED) {
Serial.println(F("[DEBUG] Connected..."));
// Zakładamy, że GetWeather() zwraca ErrorCode, a httpCode jest zmienną globalną lub pobieraną z obiektu http
uint8_t ErrorCode = GetWeather();
// Pobieramy kod HTTP (upewnij się, że Twoje GetWeather go ustawia)
int currentHttpCode = http.GET();
if (ErrorCode == 0 && httpCode == 200) {
Serial.println(F("[DEBUG] Wszystko OK..."));
// Wszystko OK - wyświetlamy pogodę
DisplayWeather();
// dane przy każdym sukcesie w debugu
if (DEBUG_WEATHER) {
Serial.println(F("[DEBUG] Pogoda zaktualizowana pomyślnie..."));
}
} else {
// Coś poszło nie tak - wywołujemy funkcję debugującą
displayWeatherSiteError(ErrorCode, currentHttpCode);
Serial.println(F("[DEBUG] Coś poszło nie tak..."));
}
} else {
// Debugowanie problemu z siecią
if (DEBUG_WEATHER) {
Serial.println(F("[BŁĄD] Brak połączenia z WiFi!"));
}
}
}
void loop() {
/*
tutaj nie ma nic do zobaczenia, w trybie głębokiego uśpienia (w tym przypadku ustawienia hibernacji)
pętla główna nigdy nie jest wywoływana, ponieważ procesor w efekcie restartuje się po wybudzeniu
i przechodzi ponownie w tryb hibernacji po zakończeniu procedury konfiguracyjnej
*/
}
int16_t IndexOfWeatherID(uint16_t ID) {
uint8_t Idx = 0;
while (Idx < WeatherCount) {
if (Weathers[Idx].ID == ID)
return Idx;
else
Idx++;
}
// dotarłem tak daleko, a potem nie znalazłem
return -1;
}
void PopulateWeathers() {
// Ciągi pogodowe, zaprojektowane tak, aby zajmowały dwie linijki wyświetlacza, pierwsza to pierwsza linijka, druga to druga linijka. Maksymalna liczba znaków dla wyświetlacza o rozdzielczości 200 pikseli to 18 znaków na linijkę.
// Użycie dostarczonych ciągów ze strony internetowej może czasami powodować rozbicie tekstu na dwie linijki, a opisy mogą być…
// czasami „dziwne”. Zastąp poniższy tekst dowolnym innym, nie zmieniając pierwszego parametru – kodu ID, który jest unikalny dla aplikacji pogodowej.
// Identyfikatory dotyczą OpenWeathermap.org.
// Grupa 2xx: Burza
AddWeather(200, "Burza z lekkim deszczem", 0x51, 0x51);
AddWeather(201, "Burza z deszczem", 0x52, 0x52);
AddWeather(202, "Burza z ulewą", 0x53, 0x53);
AddWeather(210, "Lekka burza", 0x54, 0x54);
AddWeather(211, "Burza", 0x55, 0x55);
AddWeather(212, "Silna burza", 0x56, 0x56);
AddWeather(221, "Gwałtowna burza, duże zachmurzenie", 0x57, 0x57);
AddWeather(230, "Burza z lekką mżawką", 0x58, 0x58);
AddWeather(231, "Burza z mżawką", 0x59, 0x59);
AddWeather(232, "Burza z silną mżawką", 0x5A, 0x5A);
// Grupa 3xx: Mżawka
AddWeather(300, "Lekka mżawka", 0x2A, 0x2A);
AddWeather(301, "Mżawka", 0x2B, 0x2B); // drizzle, pioggerella
AddWeather(302, "Silna mżawka", 0x2C, 0x2C);
AddWeather(310, "Lekki deszcz z mżawką", 0x2D, 0x2D);
AddWeather(311, "Deszcz z mżawką", 0x2E, 0x2E);
AddWeather(312, "Silny deszcz z mżawką", 0x2F, 0x2F);
AddWeather(313, "Ulewa z mżawką", 0x30, 0x30);
AddWeather(314, "Silna ulewa z mżawką", 0x31, 0x31);
AddWeather(321, "Mżawka przelotna", 0x32, 0x33);
// Grupa 5xx: Deszcz
AddWeather(500, "Lekki deszcz", 0x34, 0x34);
AddWeather(501, "Umiarkowany deszcz", 0x35, 0x35);
AddWeather(502, "Silny deszcz", 0x36, 0x36);
AddWeather(503, "Bardzo silny deszcz", 0x37, 0x37);
AddWeather(504, "Ekstremalna ulewa", 0x38, 0x38);
AddWeather(511, "Marznący deszcz", 0x39, 0x39);
AddWeather(520, "Lekki deszcz przelotny", 0x3A, 0x3B);
AddWeather(521, "Deszcz przelotny", 0x3C, 0x3D);
AddWeather(522, "Silny deszcz przelotny", 0x3E, 0x3F);
AddWeather(531, "Gwałtowny deszcz przelotny", 0x40, 0x41);
// Grupa 6xx: Śnieg
AddWeather(600, "Lekki śnieg", 0x42, 0x42);
AddWeather(601, "Śnieg", 0x43, 0x43);
AddWeather(602, "Silny śnieg", 0x44, 0x44);
AddWeather(611, "Deszcz ze śniegiem", 0x45, 0x45);
AddWeather(612, "Lekki deszcz ze śniegiem", 0x46, 0x46);
AddWeather(613, "Deszcz ze śniegiem przelotny", 0x47, 0x48);
AddWeather(615, "Lekki śnieg z deszczem", 0x49, 0x49);
AddWeather(616, "Śnieg z deszczem", 0x4A, 0x4A);
AddWeather(620, "Lekki śnieg przelotny", 0x4B, 0x4C);
AddWeather(621, "Śnieg przelotny", 0x4D, 0x4E);
AddWeather(622, "Silny śnieg przelotny", 0x4F, 0x50);
// Grupa 7xx: Atmosfera
AddWeather(701, "Zamglenie", 0x5B, 0x5C);
AddWeather(711, "Dym", 0x5D, 0x5E); // smoke
AddWeather(721, "Mgła / Zamglenie", 0x5F, 0x60); // fog
AddWeather(731, "Wiry pyłowe lub piaskowe", 0x61, 0x62); // dust or sand whirls
AddWeather(741, "Gęsta mgła", 0x63, 0x64);
AddWeather(751, "Piasek", 0x65, 0x66); // sand
AddWeather(761, "Pył", 0x67, 0x68); // dust
AddWeather(762, "Pył wulkaniczny", 0x69, 0x6A); // volcanic ash
AddWeather(771, "Szkwał", 0x6B, 0x6C);
AddWeather(781, "Tornado", 0x6D, 0x6E);
// Grupa 800: Czyste niebo
AddWeather(800, "Bezchmurnie", 0x21, 0x22);
// Grupa 80x: Chmury
AddWeather(801, "Małe zachmurzenie 11-25%", 0x23, 0x24);
AddWeather(802, "Średnie chmury 25-50%", 0x25, 0x26);
AddWeather(803, "Duże zachmurzenie 51-84%", 0x27, 0x28);
AddWeather(804, "Całkowite zachmurzenie 85-100%", 0x29, 0x29);
}
void AddWeather(uint16_t ID, const char* Line, uint16_t IconCharD, uint16_t IconCharN) {
Weathers[WeatherCount].ID = ID;
Weathers[WeatherCount].Line = Line;
Weathers[WeatherCount].IconD = IconCharD;
Weathers[WeatherCount].IconN = IconCharN;
WeatherCount++;
}
unsigned char GetWeatherIcon(uint16_t id, bool isNight) {
int idx = IndexOfWeatherID(id);
if (idx < 0) return '?'; // fallback
return isNight ? Weathers[idx].IconN : Weathers[idx].IconD;
}
void displayWeatherSiteError(uint8_t ErrorCode, int httpCode) {
/*
Serial.println(F("\n--- DEBUG POGODY ---"));
// 1. Status HTTP
Serial.print(F("Status HTTP: "));*/
Serial.print(httpCode);
if (httpCode > 0) {
Serial.printf(" (%s)\n", http.errorToString(httpCode).c_str());
} else {
Serial.printf(" [BŁĄD POŁĄCZENIA: %s]\n", http.errorToString(httpCode).c_str());
}
// 2. Kody błędów aplikacji
if (ErrorCode != 0) { // Zakładając, że 0 to brak błędu
Serial.print(F("Status aplikacji: "));
switch (ErrorCode) {
case ERROR_WEBSITE: Serial.println(F("Błąd strony (API)")); break;
case ERROR_BAD_DATA: Serial.println(F("Błędne dane JSON")); break;
default: Serial.printf("Nieznany błąd (%d)\n", ErrorCode); break;
}
}/*
// 3. Podgląd surowych danych (tylko jeśli połączenie OK)
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(F("Odebrane dane (Payload):"));
Serial.println(F("------------------------------------"));
Serial.println(payload); // Tu zobaczysz surowy JSON
Serial.println(F("------------------------------------"));
// deserializeJson(doc, payload);
}
Serial.println(F("--- KONIEC DEBUGU ---\n"));*/
}
void DrawWindRose(int cx, int cy, int r, float angleDeg) {
display.setFont(&beanstalk15);
int dotR = 1;
int d = (int)(r * 0.70710678f); // długość przekątnej dla 45°
display.fillCircle(cx, cy - r, dotR, GxEPD_RED); // N
display.fillCircle(cx, cy + r, dotR, GxEPD_RED); // S
display.fillCircle(cx + r, cy, dotR, GxEPD_RED); // E
display.fillCircle(cx - r, cy, dotR, GxEPD_RED); // W
display.fillCircle(cx + d, cy - d, dotR, GxEPD_RED); // NE
display.fillCircle(cx - d, cy - d, dotR, GxEPD_RED); // NW
display.fillCircle(cx + d, cy + d, dotR, GxEPD_RED); // SE
display.fillCircle(cx - d, cy + d, dotR, GxEPD_RED); // SW
display.setCursor(cx - 2, cy - r - 2); display.print("N");
display.setCursor(cx - 1, cy + r + 10); display.print("S");
display.setCursor(cx + r + 3, cy + 5); display.print("E");
display.setCursor(cx - r - 10, cy + 5); display.print("W");
display.setCursor(cx + d + 2, cy - d - 1); display.print("NE");
display.setCursor(cx - d - 14, cy - d - 1); display.print("NW");
display.setCursor(cx + d + 2, cy + d + 11); display.print("SE");
display.setCursor(cx - d - 14, cy + d + 11); display.print("SW");
// Wskaźnik kierunku wiatru
float rad = (angleDeg - 90.0f) * PI / 180.0f;
float cosA = cosf(rad);
float sinA = sinf(rad);
// Lanca 2px
int tx = cx + (int)((r - 2) * cosA);
int ty = cy + (int)((r - 2) * sinA);
display.drawLine(cx, cy, tx, ty, GxEPD_RED);
display.drawLine(cx + (int)sinA, cy - (int)cosA, tx + (int)sinA, ty - (int)cosA, GxEPD_RED);
// Romb na końcu
int rs = 5;
int p1x = tx + (int)(rs * cosA);
int p1y = ty + (int)(rs * sinA);
int p2x = tx - (int)(rs * cosA);
int p2y = ty - (int)(rs * sinA);
int p3x = tx + (int)((rs / 2) * sinA);
int p3y = ty - (int)((rs / 2) * cosA);
int p4x = tx - (int)((rs / 2) * sinA);
int p4y = ty + (int)((rs / 2) * cosA);
display.fillTriangle(p1x, p1y, p3x, p3y, p2x, p2y, GxEPD_RED);
display.fillTriangle(p1x, p1y, p4x, p4y, p2x, p2y, GxEPD_RED);
// Ogon 2px
int bx = cx - (int)((r / 2) * cosA);
int by = cy - (int)((r / 2) * sinA);
display.drawLine(cx, cy, bx, by, GxEPD_RED);
display.drawLine(cx + (int)sinA, cy - (int)cosA, bx + (int)sinA, by - (int)cosA, GxEPD_RED);
// Punkt środkowy
display.fillCircle(cx, cy, 2, GxEPD_RED);
}
void DisplayWeather() {
//Serial.println("[DEBUG] DisplayWeather start");
struct tm DateTime;
time_t now = time(nullptr);
localtime_r(&now, &DateTime);
//Serial.printf("DisplayWeather: tm_wday=%d\n", DateTime.tm_wday);
//Serial.printf("DateTime: %04d-%02d-%02d %02d:%02d wday=%d\n", DateTime.tm_year + 1900, DateTime.tm_mon + 1, DateTime.tm_mday, DateTime.tm_hour, DateTime.tm_min, DateTime.tm_wday);
// bufor dla ciągów float
static char outstr[20];
static char convstr[8];
int16_t WeatherIdx;
////WeatherID = 781;
// Wydrukuj wartości
WeatherIdx = IndexOfWeatherID(WeatherID);
display.setFullWindow();
display.firstPage();
do {
//Serial.println("[DEBUG] DisplayWeather do");
display.fillScreen(GxEPD_WHITE); // 128x296
display.setTextColor(GxEPD_BLACK);
display.drawRect(0, 0, 296, 128, GxEPD_BLACK);
display.drawFastHLine( 45, 16, 206, GxEPD_BLACK); // po zegarem
display.drawFastHLine( 45, 32, 206, GxEPD_BLACK); // pod pogodą
display.drawFastHLine( 45, 48, 145, GxEPD_BLACK); // pod nagłówkiem
display.drawFastHLine( 45, 64, 145, GxEPD_BLACK); // pod temperaturą
display.drawFastHLine( 45, 80, 145, GxEPD_BLACK); // pod wilgotnością
display.drawFastHLine( 45, 96, 206, GxEPD_BLACK); // pod ciśieniem
display.drawFastHLine( 45, 112, 206, GxEPD_BLACK); // pod wiatrem
display.drawFastHLine( 0, 32, 45, GxEPD_BLACK);
display.drawFastHLine( 0, 64, 45, GxEPD_BLACK);
display.drawFastHLine( 0, 96, 45, GxEPD_BLACK);
display.drawFastHLine(251, 32, 45, GxEPD_BLACK);
display.drawFastHLine(251, 64, 45, GxEPD_BLACK);
display.drawFastHLine(251, 96, 45, GxEPD_BLACK);
display.drawFastVLine( 44, 0, 128, GxEPD_BLACK);
display.drawFastVLine(120, 32, 65, GxEPD_BLACK);//
display.drawFastVLine(155, 32, 65, GxEPD_BLACK);//
display.drawFastVLine(190, 32, 81, GxEPD_BLACK);
display.drawFastVLine(251, 0, 128, GxEPD_BLACK);
display.setFont(&beanstalk15);
display.setCursor(148, 13);
CentreText(NowTimeStr);
if (WeatherIdx != -1) {
//Serial.println("[DEBUG] DisplayWeather WeatherIdx");
display.setCursor(148, 29);
CentreString(Weathers[WeatherIdx].Line);
} else {
display.setCursor(148, 29);
CentreString("Nieznana pogoda");
}
// --- Tabelka danych: lewa strona środka (x=45..169, y=48..128) ---
//u8g2_font_battery24_tr
//u8g2.setFont(u8g2_font_t0_16_te);
// nagłówek kolumn
display.setCursor(139, 45);
CentreText("ZEWN");
display.setCursor(173, 45);
CentreText("WEWN");
// wiersz Temperatura
outstr[0] = 0;
display.setCursor(46, 61);
display.print("Temperatura °C");
dtostrf(main_temp_min, 1, 0, convstr);
strcat(outstr, convstr);
strcat(outstr, "/");
dtostrf(main_temp_max, 1, 0, convstr);
strcat(outstr, convstr);
display.setCursor(139, 61);
CentreText(outstr);
// temperatura wewnętrzna
if (BME280Found) {
outstr[0] = 0;
dtostrf(bme.readTemperature(), 1, 0, convstr);
strcat(outstr, convstr);
display.setCursor(173, 61);
CentreText(outstr);
} else {
display.setCursor(173, 61);
CentreText("--");
}
// wiersz Wilgotność
outstr[0] = 0;
display.setCursor(46, 77);
display.print("Wilgotność %");
dtostrf(main_humidity, 1, 0, convstr);
strcat(outstr, convstr);
display.setCursor(139, 77);
CentreText(outstr);
if (BME280Found) {
outstr[0] = 0;
dtostrf(bme.readHumidity(), 1, 0, convstr);
strcat(outstr, convstr);
display.setCursor(173, 77);
CentreText(outstr);
} else {
display.setCursor(173, 77);
CentreText("--");
}
// wiersz Ciśnienie
display.setCursor(46, 93);
display.print("Ciśnienie hPa");
outstr[0] = 0;
dtostrf(main_pressure, 1, 0, convstr);
strcat(outstr, convstr);
display.setCursor(139, 93);
CentreText(outstr);
if (BME280Found) {
outstr[0] = 0;
dtostrf(bme.readPressure() / 100.0F, 1, 0, convstr);
strcat(outstr, convstr);
display.setCursor(173, 93);
CentreText(outstr);
} else {
display.setCursor(173, 93);
CentreText("--");
CentreText(outstr);
}
// wiersz Wiatr
display.setCursor(46, 109);
display.print("Wiatr");
display.setCursor(117, 109);
outstr[0] = 0;
dtostrf(WindSpeed, 1, 0, convstr);
strcat(outstr, convstr);
strcat(outstr, MPH ? "mph " : "m/s ");
GetDirectionFromBearing(WindAngle, outstr);
display.print(outstr);
// komunikat BME280
if (!BME280Found) {
display.setCursor(47, 124);
display.print("Czujnik BME280 nie znaleziony!");
}
// Busola
DrawWindRose(222, 65, 20, WindAngle);
// prognoza narysuj bloki
display.setTextColor(GxEPD_BLACK);
//Serial.printf("DateTime->tm_wday=%d DateTime->tm_hour=%d\n", DateTime.tm_wday, DateTime.tm_hour);
// LEWA STRONA: Prognoza 3-godzinna (4 bloki jeden pod drugim) 128x296
for (uint8_t i = 0; i < NUM_DAYS_FORCAST; i++) {
//Serial.printf("3h[%d]: ID=%d time=%s\n", i, ThreeHourlyForcastID[i], ThreeHourlyForcastTime[i].c_str());
uint16_t y_offset = i * 32;
// CZAS
uint8_t hour = ThreeHourlyForcastTime[i].toInt();
bool isNight = (hour < 6 || hour >= 20);
// przygotowanie tekstu dla wyświetlenia
String timeText = (hour < 10 ? "0" : "") + String(hour);
display.setFont(&beanstalk15);
display.setTextColor(GxEPD_RED);
display.setCursor(2, y_offset + 12);
display.print(timeText);
// IKONA
int idx = IndexOfWeatherID(ThreeHourlyForcastID[i]);
if (idx < 0) {
//Serial.printf("UWAGA: nieznane ID=%d\n", ThreeHourlyForcastID[i]);
display.write('?');
continue;
}
display.setFont(&weathericons_regular_webfont10pt7b);
display.setTextColor(GxEPD_BLACK);
display.setCursor(13, y_offset + 31);
display.write(Weathers[idx].IconChar(isNight));
}
// PRAWA STRONA: Prognoza na dni (4 bloki jeden pod drugim)
for (uint8_t i = 0; i < NUM_DAYS_FORCAST; i++) {
//Serial.printf("day[%d]: ID=%d\n", i, ForecastID[i]);
uint16_t y_offset = i * 32;
// DZIEŃ TYGODNIA
uint8_t dayIndex = (i + (DateTime.tm_wday + 1)) % 7;
display.setFont(&beanstalk15);
display.setTextColor(GxEPD_RED);
display.setCursor(253, y_offset + 12);
display.print(daysOfWeek[dayIndex]);
// IKONA – zawsze dzienna
int idx = IndexOfWeatherID(ForecastID[i]);
display.setFont(&weathericons_regular_webfont10pt7b);
display.setTextColor(GxEPD_BLACK);
display.setCursor(264, y_offset + 31);
display.write(Weathers[idx].IconD);
}
}
while (display.nextPage());
Serial.println("[DEBUG] DisplayWeather done");
}
uint16_t CentreText(const char *TheString) {
int16_t x1, y1;
uint16_t w, h;
int16_t centreX = display.getCursorX();
display.getTextBounds(TheString, 0, display.getCursorY(), &x1, &y1, &w, &h);
uint16_t X = centreX - (w / 2);
display.setCursor(X, display.getCursorY());
display.print(TheString);
return X;
}
uint16_t CentreString(String TheString) {
int16_t x1, y1;
uint16_t w, h;
int16_t centreX = display.getCursorX();
display.getTextBounds(TheString.c_str(), 0, display.getCursorY(), &x1, &y1, &w, &h);
uint16_t X = centreX - (w / 2);
display.setCursor(X, display.getCursorY());
display.print(TheString);
return X;
}
uint8_t GetWeather() {
const size_t capacity = 30000;
DynamicJsonDocument Doc(capacity);
JsonObject WeatherDoc;
// --- CZĘŚĆ 1: POGODA BIEŻĄCA ---
http.begin(WeatherURL);
httpCode = http.GET(); // Używamy globalnej zmiennej
if (httpCode == 200) {
String payload = http.getString();
DeserializationError error = deserializeJson(Doc, payload);
if (error) return ERROR_BAD_DATA;
// OpenWeather Current Weather ma tablicę "weather" bezpośrednio w Doc
if (Doc.containsKey("weather") && Doc["weather"].is<JsonArray>()) {
WeatherDoc = Doc["weather"][0];
WeatherID = WeatherDoc["id"];
WeatherDesc = WeatherDoc["description"].as<String>();
}
// Dane główne
main_temp = Doc["main"]["temp"];
main_temp_min = Doc["main"]["temp_min"];
main_temp_max = Doc["main"]["temp_max"];
main_pressure = Doc["main"]["pressure"];
main_humidity = Doc["main"]["humidity"];
// Wiatr
WindSpeed = Doc["wind"]["speed"];
WindAngle = Doc["wind"]["deg"];
if (MPH) WindSpeed *= 2.237;
WindSpeed = round(WindSpeed);
// Konwersja jednostek (Kelvin -> Celsius) - tylko jeśli temp jest wysoka
if (main_temp > 200) {
main_temp -= 273.15;
main_temp_min -= 273.15;
main_temp_max -= 273.15;
}
// Zaokrąglenia
main_temp = round(main_temp);
main_temp_min = round(main_temp_min);
main_temp_max = round(main_temp_max);
// Czas aktualizacji
struct tm DateTime;
time_t now = time(nullptr);
localtime_r(&now, &DateTime);
strftime(NowTimeStr, 80, "%d.%m.%Y %R", &DateTime);
} else {
http.end();
return ERROR_WEBSITE;
}
http.end();
// --- CZĘŚĆ 2: PROGNOZA ---
http.begin(DayForcastURL);
httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
Doc.clear(); // Czyścimy dokument przed nowymi danymi
DeserializationError error = deserializeJson(Doc, payload);
if (error) return ERROR_BAD_DATA;
JsonArray List = Doc["list"];
// 1. Prognoza 3-godzinna (następne 4 wpisy)
for (uint8_t Idx = 0; Idx < NUM_DAYS_FORCAST; Idx++) {
JsonObject Data = List[Idx];
if (Data["weather"].is<JsonArray>()) {
ThreeHourlyForcastID[Idx] = Data["weather"][0]["id"];
String fTime = Data["dt_txt"].as<String>(); // Format: "YYYY-MM-DD HH:MM:SS"
ThreeHourlyForcastTime[Idx] = fTime.substring(11, 13);
}
}
// 2. Prognoza dniowa (szukanie konkretnej godziny, np. 12:00)
char TimeBuffer[80];
struct tm tempDT;
time_t now = time(nullptr);
localtime_r(&now, &tempDT);
for (uint8_t Idx = 0; Idx < NUM_DAYS_FORCAST; Idx++) {
ForecastDate(&tempDT, 1); // Przesuń o 1 dzień
strftime(TimeBuffer, 80, "%Y-%m-%d 12:00:00", &tempDT);
int16_t wIdx = GetWeatherData(List, TimeBuffer);
if (wIdx >= 0) {
ForecastID[Idx] = List[wIdx]["weather"][0]["id"];
} else {
// Jeśli nie znaleziono 12:00, weź cokolwiek z tego dnia
ForecastID[Idx] = List[Idx * 8]["weather"][0]["id"];
}
}
} else {
http.end();
return ERROR_WEBSITE;
}
http.end();
return 0; // Sukces
}
void ForecastDate(tm *TheDate, uint8_t DaysInFuture) {
const time_t ONE_DAY = 24 * 60 * 60 ;
// Sekundy od początku epoki
time_t date_seconds = mktime( TheDate ) + (DaysInFuture * ONE_DAY) ;
// Zaktualizuj datę dzwoniącego
// Użyj czasu lokalnego, ponieważ mktime konwertuje się na UTC, więc data może ulec zmianie
localtime_r(&date_seconds, TheDate);
//*TheDate = *localtime( &date_seconds ) ; ;
}
int16_t GetWeatherData(JsonArray List, const char* DateTime) {
// biorąc pod uwagę główną tablicę danych elementów pogodowych, zwraca indeks obiektu danych dla podanej daty i godziny
// zwraca -1, jeśli nie znaleziono
JsonObject ThisEntry;
uint16_t ListSize, ListIdx;
bool Found = false;
ThisEntry = List[0];
const char* TheDate = ThisEntry["dt_txt"];
ListIdx = 0;
ListSize = List.size();
while ((ListIdx < ListSize) & (Found == false)) {
ThisEntry = List[ListIdx];
if (strcmp(ThisEntry["dt_txt"], DateTime) == 0)
Found = true;
else
ListIdx++;
}
if (Found)
return ListIdx;
else
return -1;
}
void GetDirectionFromBearing(uint16_t Bearing, char* Direction) {
// Zwraca tekstową wersję azymutu w stopniach. Należy pamiętać, że zwraca tylko 8 podstawowych kierunków,
// bardziej szczegółowe informacje mogą być mylące dla przeciętnego użytkownika.
// i tak naprawdę nie wymaga aż takiego poziomu szczegółowości, można go rozszerzyć o więcej kierunków
// podziel stopnie przez 45.
static char convstr[8];
uint8_t SimpleDirection = round(Bearing / 45);
switch (SimpleDirection) {
case 0: strcat(Direction, "północ"); break; // North
case 1: strcat(Direction, "płn.-wsch."); break; // North East
case 2: strcat(Direction, "wschód"); break; // East
case 3: strcat(Direction, "płd.-Wsch."); break; // South East
case 4: strcat(Direction, "południe"); break; // South
case 5: strcat(Direction, "płd.-zach."); break; // South West
case 6: strcat(Direction, "zachód"); break; // West
case 7: strcat(Direction, "płn.-zach."); break; // North West
case 8: strcat(Direction, "północ"); break; // North
default: {
//nigdy nie powinno tu dotrzeć!
strcat(Direction, "???");
dtostrf(Bearing, 1, 0, convstr);
strcat(Direction, convstr);
}
}
}
/*
// Konfiguracja czcionki pogodowej
// B - bold, I - italic
//u8g2.setFont(u8g2_font_unifont_te); // PL
//u8g2.setFont(uu8g2_font_helvB14_te); // PL
//u8g2.setFont(u8g2_font_ncenB14_te); // PL
//u8g2.setFont(u8g2_font_ncenR14_te); // PL
//u8g2.setFont(u8g2_font_lubB14_te); // PL
//u8g2.setFont(u8g2_font_lubBI14_te); // PL
//u8g2.setFont(u8g2_font_lubI14_te); // PL
//u8g2.setFont(u8g2_font_luBIS14_te); // PL
//u8g2.setFontu8g2_font_lubR14_te(); // PL
//u8g2.setFont(u8g2_font_luBS14_te); // PL
//u8g2.setFont(u8g2_font_luIS14_te); // PL
//u8g2.setFont(); // PL
//u8g2.setFont(); // PL
//u8g2.setFont(); // PL
//u8g2.setFont(); // PL
//u8g2.setFont(u8g2_font_timR08_tr);
//u8g2.setFont(u8g2_font_t0_14b_te);
//u8g2.setFont(u8g2_font_spleen8x16_me);
//u8g2.setFont(u8g2_font_6x13B_tr);
//u8g2.setFont(u8g2_font_Born2bSportySlab_te);
//u8g2.setFont(u8g2_font_7x13B_mf);
//u8g2.setFont(u8g2_font_Pixellari_te);//ok
//u8g2.setFont(u8g2_font_NokiaSmallBold_te);//niema
//u8g2.setFont(u8g2_font_busdisplay11x5_te);//niema
//u8g2.setFont(u8g2_font_busdisplay8x5_tr);//niema
//u8g2.setFont(u8g2_font_busdisplay11x5_te);//niema
//u8g2.setFont(u8g2_font_commodore64_tr);//niema
//u8g2.setFont(u8g2_font_dystopia_te);//niema
//u8g2.setFont(u8g2_font_frigidaire_mr);//niema
//u8g2.setFont(u8g2_font_sonicmania_te);//niema
//u8g2.setFont(u8g2_font_pixzillav1_te);//niema
//u8g2.setFont(u8g2_font_mildras_te);//niema
//u8g2.setFont(u8g2_font_lastapprenticethin_te);//niema
//u8g2.setFont(u8g2_font_resoledbold_tr);//niema
//u8g2.setFont(u8g2_font_luBS08_te);//lipa
//u8g2.setFont(u8g2_font_prospero_bold_nbp_tr);//bez pl, za duże
//u8g2.setFont(u8g2_font_pxplusibmvga8_mf);//bez pl, za duże
//u8g2.setFont(u8g2_font_sirclive_tr);//szerokie bez pl
//u8g2.setFont(u8g2_font_helvB08_te);//ok cyfry
//u8g2.setFont(u8g2_font_missingplanet_tr);//ok bez pl
//u8g2.setFont(u8g2_font_nerhoe_tr);//ok bez pl
//u8g2.setFont(u8g2_font_rosencrantz_nbp_t_all);//ok bez pl
u8g2.setFont(u8g2_font_unifont_t_weather);
u8g2.setFontMode(1);
u8g2.setFontDirection(0);
int x = 10;
int y = 30;Pamiętaj: Y to linia bazowa (dół znaku)
// Wyświetlamy symbole od kodu 32 (spacja/pierwszy symbol) do 64
// W czcionce weather pod kodami ASCII kryją się ikony
for (int i = 32; i < 64; i++) {
u8g2.setCursor(x, y);
u8g2.print((char)i);
x += 20;Ikony unifont są dość szerokie
if (x > 220) {
x = 10;
y += 35;
}
}*/