// By mircemk, April 2026
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <PNGdec.h>
#include <math.h>
#include "time.h"
#include <Wire.h>
const char* ssid = "***********";
const char* password = "***********";
const char* ntpServer = "pool.ntp.org";
const char* weatherUrl =
"https://api.open-meteo.com/v1/forecast?"
"latitude=41.1171&longitude=20.8016"
"¤t=temperature_2m,weather_code"
"&hourly=temperature_2m,weather_code,pressure_msl,cloud_cover"
"&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,"
"relative_humidity_2m_mean,wind_speed_10m_max,uv_index_max,shortwave_radiation_sum"
"&timezone=auto&forecast_days=16";
const char* owmApiKey = "*********************";
int myZoom = 6;
double myLat = 41.1171;
double myLon = 20.8016;
unsigned long lastUpdate = 0;
long radarTS = 0;
int appState = 0;
int lastMinute = -1;
int brightnessLevel = 100;
int mapStyle = 0; // 0 = dark_all, 1 = opentopomap, 2 = openstreetmap
int layerStyle = 0; // 0 = Radar, 1 = Clouds, 2 = Rain
String radarHost = "https://tilecache.rainviewer.com";
String radarPath = "";
const char* mapUrls[] = {
"https://basemaps.cartocdn.com/dark_all/",
"https://tile.opentopomap.org/",
"https://tile.openstreetmap.org/"
};
const char* mapNames[] = {"DARK", "TOPO", "OSM"};
const char* layerNames[] = {"RADAR", "CLOUDS", "RAIN"};
const char* owmLayerIds[] = {"", "clouds_new", "precipitation_new"};
class LGFX : public lgfx::LGFX_Device {
public:
lgfx::Bus_RGB _bus;
lgfx::Panel_RGB _panel;
lgfx::Light_PWM _light;
LGFX(void) {
{
auto cfg = _panel.config();
cfg.memory_width = 800;
cfg.memory_height = 480;
cfg.panel_width = 800;
cfg.panel_height = 480;
_panel.config(cfg);
}
{
auto cfg = _bus.config();
cfg.panel = &_panel;
cfg.pin_d0 = GPIO_NUM_15; cfg.pin_d1 = GPIO_NUM_7; cfg.pin_d2 = GPIO_NUM_6; cfg.pin_d3 = GPIO_NUM_5;
cfg.pin_d4 = GPIO_NUM_4; cfg.pin_d5 = GPIO_NUM_9; cfg.pin_d6 = GPIO_NUM_46; cfg.pin_d7 = GPIO_NUM_3;
cfg.pin_d8 = GPIO_NUM_8; cfg.pin_d9 = GPIO_NUM_16; cfg.pin_d10 = GPIO_NUM_1; cfg.pin_d11 = GPIO_NUM_14;
cfg.pin_d12 = GPIO_NUM_21; cfg.pin_d13 = GPIO_NUM_47; cfg.pin_d14 = GPIO_NUM_48; cfg.pin_d15 = GPIO_NUM_45;
cfg.pin_henable = GPIO_NUM_41; cfg.pin_vsync = GPIO_NUM_40; cfg.pin_hsync = GPIO_NUM_39; cfg.pin_pclk = GPIO_NUM_0;
cfg.freq_write = 12000000;
cfg.hsync_front_porch = 40;
cfg.hsync_pulse_width = 48;
cfg.hsync_back_porch = 40;
cfg.vsync_front_porch = 1;
cfg.vsync_pulse_width = 31;
cfg.vsync_back_porch = 13;
cfg.pclk_active_neg = 1;
cfg.de_idle_high = 0;
cfg.pclk_idle_high = 0;
_bus.config(cfg);
_panel.setBus(&_bus);
}
{
auto cfg = _light.config();
cfg.pin_bl = GPIO_NUM_2;
cfg.freq = 44100;
cfg.pwm_channel = 7;
_light.config(cfg);
_panel.setLight(&_light);
}
setPanel(&_panel);
}
};
LGFX lcd;
LGFX_Sprite mapCanvas(&lcd);
// Weather variables mapping for ZLATEN section
float currentTemp, morningTemp, noonTemp, eveningTemp;
int morningCode, noonCode, eveningCode;
float dMax[16], dMin[16], dRain[16], dPress[16], dCloud[16];
float dHum[16], dWind[16], dUV[16], dSolar[16];
int dCode[16];
// Прототипи на функции
void setBrightnessFromTouchY(int ty);
void getWeatherData();
void renderRadarMap();
void drawBottomDashboard();
void drawSideButtons();
void drawSignature();
void drawTopDate();
void drawProgressTimer();
void drawGraphPage(int type);
void drawWeatherIcon(int x, int y, int code);
void drawMiniWeatherIcon(int x, int y, int code);
int getDayOfMonthOffset(struct tm baseTime, int offsetDays);
bool fetchPngToBuffer(const String& url, uint8_t** outBuf, size_t* outLen);
#include "touch.h"
PNG png;
int globalX, globalY;
uint16_t panelColor = 0x0841;
const char* days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
const char* dayShort2[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
const char* months[] = {"Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."};
// --- ICON ENGINE ---
void drawSun(int x, int y, uint16_t color) {
lcd.fillCircle(x, y, 9, color);
for (int i = 0; i < 360; i += 45) {
float r = i * DEG_TO_RAD;
lcd.drawLine(x + cos(r) * 11, y + sin(r) * 11, x + cos(r) * 17, y + sin(r) * 17, color);
}
}
void drawCloud(int x, int y, uint16_t color, int scale = 1) {
int off = (scale == 0) ? -2 : 0;
lcd.fillCircle(x - 6 + off, y + 2, 7 + off, color);
lcd.fillCircle(x + 2 + off, y - 3, 9 + off, color);
lcd.fillCircle(x + 10 + off, y + 2, 7 + off, color);
lcd.fillRect(x - 6 + off, y + 2, 16, 7 + off, color);
}
void drawRainDrops(int x, int y, uint16_t color, bool heavy) {
lcd.drawLine(x - 4, y + 6, x - 6, y + 13, color);
lcd.drawLine(x + 4, y + 6, x + 2, y + 13, color);
if (heavy) lcd.drawLine(x, y + 8, x - 2, y + 15, color);
}
void drawSnowFlakes(int x, int y, uint16_t color, bool heavy) {
// left flake
lcd.drawLine(x - 5, y + 7, x - 1, y + 11, color);
lcd.drawLine(x - 1, y + 7, x - 5, y + 11, color);
lcd.drawLine(x - 3, y + 6, x - 3, y + 12, color);
lcd.drawLine(x - 6, y + 9, x, y + 9, color);
// right flake
lcd.drawLine(x + 3, y + 7, x + 7, y + 11, color);
lcd.drawLine(x + 7, y + 7, x + 3, y + 11, color);
lcd.drawLine(x + 5, y + 6, x + 5, y + 12, color);
lcd.drawLine(x + 2, y + 9, x + 8, y + 9, color);
if (heavy) {
// center flake
lcd.drawLine(x - 1, y + 10, x + 3, y + 14, color);
lcd.drawLine(x + 3, y + 10, x - 1, y + 14, color);
lcd.drawLine(x + 1, y + 9, x + 1, y + 15, color);
lcd.drawLine(x - 2, y + 12, x + 4, y + 12, color);
}
}
void drawMiniSnow(int x, int y, uint16_t color, bool heavy) {
// left flake
lcd.drawLine(x - 3, y + 4, x - 1, y + 6, color);
lcd.drawLine(x - 1, y + 4, x - 3, y + 6, color);
lcd.drawPixel(x - 2, y + 3, color);
lcd.drawPixel(x - 2, y + 7, color);
lcd.drawPixel(x - 4, y + 5, color);
lcd.drawPixel(x, y + 5, color);
// right flake
lcd.drawLine(x + 2, y + 4, x + 4, y + 6, color);
lcd.drawLine(x + 4, y + 4, x + 2, y + 6, color);
lcd.drawPixel(x + 3, y + 3, color);
lcd.drawPixel(x + 3, y + 7, color);
lcd.drawPixel(x + 1, y + 5, color);
lcd.drawPixel(x + 5, y + 5, color);
if (heavy) {
// small center flake
lcd.drawPixel(x, y + 6, color);
lcd.drawPixel(x, y + 5, color);
lcd.drawPixel(x, y + 7, color);
lcd.drawPixel(x - 1, y + 6, color);
lcd.drawPixel(x + 1, y + 6, color);
}
}
void drawMiniSun(int x, int y, uint16_t color) {
lcd.fillCircle(x, y, 4, color);
for (int i = 0; i < 360; i += 45) {
float r = i * DEG_TO_RAD;
lcd.drawLine(x + cos(r) * 6, y + sin(r) * 6,
x + cos(r) * 8, y + sin(r) * 8, color);
}
}
void drawMiniCloud(int x, int y, uint16_t color) {
lcd.fillCircle(x - 4, y + 1, 4, color);
lcd.fillCircle(x + 1, y - 2, 5, color);
lcd.fillCircle(x + 6, y + 1, 4, color);
lcd.fillRect(x - 4, y + 1, 11, 4, color);
}
void drawMiniRain(int x, int y, uint16_t color, bool heavy) {
lcd.drawLine(x - 3, y + 4, x - 4, y + 8, color);
lcd.drawLine(x + 2, y + 4, x + 1, y + 8, color);
if (heavy) lcd.drawLine(x, y + 5, x - 1, y + 9, color);
}
void drawMiniWeatherIcon(int x, int y, int code) {
uint16_t sunCol = TFT_YELLOW;
uint16_t cloudCol = 0x9E7F;
uint16_t rainCol = TFT_CYAN;
uint16_t snowCol = TFT_WHITE;
if (code == 0) {
drawMiniSun(x, y, sunCol);
}
else if (code == 1) {
drawMiniSun(x - 2, y - 1, sunCol);
drawMiniCloud(x + 4, y + 2, cloudCol);
}
else if (code == 2) {
drawMiniSun(x + 2, y - 3, sunCol);
drawMiniCloud(x, y + 1, cloudCol);
}
else if (code == 3) {
drawMiniCloud(x - 2, y, 0x7BEF);
drawMiniCloud(x + 3, y + 2, cloudCol);
}
// Rain / drizzle / rain showers
else if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) {
drawMiniCloud(x, y - 1, cloudCol);
drawMiniRain(x, y + 1, rainCol, (code == 65 || code == 82));
}
// Snow / snow grains / snow showers
else if ((code >= 71 && code <= 77) || (code >= 85 && code <= 86)) {
drawMiniCloud(x, y - 1, cloudCol);
drawMiniSnow(x, y + 1, snowCol, (code == 75 || code == 86));
}
else {
drawMiniCloud(x, y, cloudCol);
}
}
void drawWeatherIcon(int x, int y, int code) {
uint16_t sunCol = TFT_YELLOW;
uint16_t cloudCol = 0x9E7F;
uint16_t rainCol = TFT_CYAN;
uint16_t snowCol = TFT_WHITE;
if (code == 0) {
drawSun(x, y, sunCol);
}
else if (code == 1) {
drawSun(x, y, sunCol);
drawCloud(x + 10, y + 4, cloudCol, 0);
}
else if (code == 2) {
drawSun(x + 6, y - 5, sunCol);
drawCloud(x, y, cloudCol);
}
else if (code == 3) {
drawCloud(x - 4, y - 2, 0x7BEF);
drawCloud(x + 4, y + 2, cloudCol);
}
// Rain / drizzle / rain showers
else if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) {
drawCloud(x, y - 3, cloudCol);
drawRainDrops(x, y, rainCol, (code == 65 || code == 82));
}
// Snow / snow grains / snow showers
else if ((code >= 71 && code <= 77) || (code >= 85 && code <= 86)) {
drawCloud(x, y - 3, cloudCol);
drawSnowFlakes(x, y, snowCol, (code == 75 || code == 86));
}
else {
drawCloud(x, y, cloudCol);
}
}
int pngDrawCanvas(PNGDRAW *pDraw) {
uint16_t pix[256];
png.getLineAsRGB565(pDraw, pix, PNG_RGB565_BIG_ENDIAN, 0);
mapCanvas.pushImage(globalX, globalY + pDraw->y, pDraw->iWidth, 1, pix);
return 1;
}
int pngDrawOverlayCanvas(PNGDRAW *pDraw) {
uint16_t pix[256];
png.getLineAsRGB565(pDraw, pix, PNG_RGB565_BIG_ENDIAN, 0);
for (int x = 0; x < pDraw->iWidth; x++) {
uint16_t c = pix[x];
if (c == 0) continue;
if (layerStyle == 1) {
if (((globalX + x + globalY + pDraw->y) & 1) != 0) continue;
}
mapCanvas.drawPixel(globalX + x, globalY + pDraw->y, c);
}
return 1;
}
bool fetchPngToBuffer(const String& url, uint8_t** outBuf, size_t* outLen) {
*outBuf = nullptr;
*outLen = 0;
HTTPClient http;
http.setTimeout(15000);
if (!http.begin(url)) {
Serial.printf("HTTP begin failed: %s\n", url.c_str());
return false;
}
// VAZHNO: bara nekopresiran odgovor
http.addHeader("Accept-Encoding", "identity");
http.addHeader("User-Agent", "ESP32-WeatherDisplay/1.0");
http.useHTTP10(true); // pomaga kaj nekoi tile serveri
int code = http.GET();
if (code != HTTP_CODE_OK) {
Serial.printf("HTTP GET failed: %d | %s\n", code, url.c_str());
http.end();
return false;
}
String ctype = http.header("Content-Type");
String cenc = http.header("Content-Encoding");
int len = http.getSize();
Serial.printf("HTTP 200 | type=%s | enc=%s | len=%d\n",
ctype.c_str(), cenc.c_str(), len);
WiFiClient* stream = http.getStreamPtr();
// Ako nema content-length, citaj stream rachno
if (len <= 0) {
const size_t maxChunkedSize = 100000;
uint8_t* buf = (uint8_t*)ps_malloc(maxChunkedSize);
if (!buf) {
Serial.println("PSRAM alloc failed (chunked)");
http.end();
return false;
}
size_t total = 0;
unsigned long t0 = millis();
while (http.connected() && (millis() - t0 < 15000)) {
while (stream->available()) {
if (total >= maxChunkedSize) {
Serial.println("Chunked payload too large");
free(buf);
http.end();
return false;
}
buf[total++] = stream->read();
t0 = millis();
}
delay(1);
}
if (total < 16) {
Serial.printf("Payload too small: %u bytes\n", (unsigned)total);
free(buf);
http.end();
return false;
}
*outBuf = buf;
*outLen = total;
http.end();
return true;
}
uint8_t* buf = (uint8_t*)ps_malloc(len);
if (!buf) {
Serial.printf("PSRAM alloc failed: %d bytes\n", len);
http.end();
return false;
}
int actuallyRead = stream->readBytes(buf, len);
http.end();
if (actuallyRead != len || len < 16) {
Serial.printf("Read failed: expected %d got %d\n", len, actuallyRead);
free(buf);
return false;
}
*outBuf = buf;
*outLen = len;
return true;
}
void drawSignature() {
if (appState != 0) return;
int rssi = WiFi.RSSI();
int rx = 35, ry = 4, rw = 80, rh = 22;
lcd.fillRect(rx, ry, rw, rh, panelColor);
lcd.drawRect(rx, ry, rw, rh, TFT_WHITE);
lcd.setTextColor(TFT_WHITE);
lcd.setTextSize(1);
lcd.setTextDatum(middle_left);
char rStr[15];
sprintf(rStr, "%d dBm", rssi);
lcd.drawString(rStr, rx + 5, ry + 11);
int bars = 1;
if (rssi > -50) bars = 4;
else if (rssi > -60) bars = 3;
else if (rssi > -70) bars = 2;
for (int i = 0; i < 4; i++) {
uint16_t bCol = (i < bars) ? TFT_GREEN : TFT_DARKGREY;
lcd.fillRect(rx + 55 + (i * 5), ry + rh - 5 - (i * 3), 3, 3 + (i * 3), bCol);
}
int sx = rx + rw + 5;
lcd.fillRect(sx, ry, 100, rh, panelColor);
lcd.drawRect(sx, ry, 100, rh, TFT_WHITE);
lcd.setTextDatum(middle_center);
lcd.drawString("by mircemk", sx + 50, ry + 11);
lcd.fillRect(35, 388, 80, 22, panelColor);
lcd.drawRect(35, 388, 80, 22, TFT_WHITE);
lcd.drawString("OHRID", 35 + 40, 388 + 11);
}
void setBrightnessFromTouchY(int ty) {
int bx = 4, by = 4, bh = 409;
if (ty < by) ty = by;
if (ty > by + bh) ty = by + bh;
// gore = poslabo, dolu = pojako
brightnessLevel = map(ty, by, by + bh, 255, 20);
if (brightnessLevel < 20) brightnessLevel = 20;
if (brightnessLevel > 255) brightnessLevel = 255;
lcd.setBrightness(brightnessLevel);
}
void drawProgressTimer() {
if (appState != 0) return;
int bx = 4, by = 4, bw = 24, bh = 409;
lcd.drawRect(bx, by, bw, bh, TFT_WHITE);
lcd.drawRect(bx + 2, by + 2, bw - 4, bh - 4, TFT_WHITE);
lcd.fillRect(bx + 3, by + 3, bw - 6, bh - 6, TFT_BLACK);
// Progress do sledezen update
float p = (float)(millis() - lastUpdate) / 600000.0;
if (p > 1.0) p = 1.0;
int cH = (int)(397 * p);
for (int y = 0; y < cH; y += 4) {
lcd.drawFastHLine(bx + 7, (by + bh - 7) - y, bw - 14, TFT_SKYBLUE);
}
// Brightness marker
int markerY = map(brightnessLevel, 255, 20, by, by + bh);
lcd.drawFastHLine(bx + 4, markerY, bw - 8, TFT_YELLOW);
lcd.drawFastHLine(bx + 4, markerY - 1, bw - 8, TFT_YELLOW);
}
void drawBottomDashboard() {
struct tm timeinfo;
getLocalTime(&timeinfo);
int midW = 180;
int midH = 95;
int midX = (800 - midW) / 2;
int midY = 480 - midH;
lcd.fillRect(midX, midY, midW, midH, panelColor);
lcd.drawRect(midX, midY, midW, midH, TFT_WHITE);
char clockStr[10];
sprintf(clockStr, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
lcd.setTextColor(TFT_WHITE);
lcd.setTextSize(5);
lcd.setTextDatum(top_center);
lcd.drawString(clockStr, midX + (midW / 2), midY + 15);
lcd.setTextColor(TFT_YELLOW);
lcd.setTextSize(2);
lcd.setTextDatum(middle_center);
char tempStr[15];
sprintf(tempStr, "Temp: %d C", (int)currentTemp);
lcd.drawString(tempStr, midX + (midW / 2), midY + 74);
int sideH = 65;
lcd.fillRect(0, 480 - sideH, midX, sideH, panelColor);
lcd.drawRect(0, 480 - sideH, midX, sideH, TFT_DARKGREY);
int partW = midX / 3;
const char* lblL[] = {"Morning", "Noon", "Evening"};
float tmpL[] = {morningTemp, noonTemp, eveningTemp};
int codL[] = {morningCode, noonCode, eveningCode};
for (int i = 0; i < 3; i++) {
int cx = (partW * i) + (partW / 2);
if (i > 0) lcd.drawFastVLine(partW * i, 480 - sideH, sideH, TFT_DARKGREY);
lcd.setTextSize(1);
lcd.setTextColor(TFT_LIGHTGREY);
lcd.setTextDatum(top_center);
lcd.drawString(lblL[i], cx, 480 - 57);
drawWeatherIcon(partW * i + 25, 480 - 28, codL[i]);
lcd.setTextColor(TFT_WHITE);
lcd.setTextSize(3);
lcd.setTextDatum(middle_left);
lcd.drawNumber((int)tmpL[i], partW * i + 59, 480 - 28);
}
int rx = midX + midW;
int rw = 800 - rx;
lcd.fillRect(rx, 480 - sideH, rw, sideH, panelColor);
lcd.drawRect(rx, 480 - sideH, rw, sideH, TFT_DARKGREY);
int partRW = rw / 3;
for (int i = 1; i < 4; i++) {
int dayIdx = (timeinfo.tm_wday + i) % 7;
int curX = rx + (partRW * (i - 1));
int cx = curX + (partRW / 2);
if (i > 1) lcd.drawFastVLine(curX, 480 - sideH, sideH, TFT_DARKGREY);
lcd.setTextSize(1);
lcd.setTextColor(TFT_LIGHTGREY);
lcd.setTextDatum(top_center);
lcd.drawString(days[dayIdx], cx, 480 - 57);
drawWeatherIcon(curX + 25, 480 - 28, dCode[i]);
lcd.setTextColor(TFT_WHITE);
lcd.setTextSize(2);
lcd.setTextDatum(top_left);
lcd.drawNumber((int)dMax[i], curX + 59, 480 - 42);
lcd.setTextColor(0x7BEF);
lcd.drawNumber((int)dMin[i], curX + 59, 480 - 22);
}
}
void getWeatherData() {
HTTPClient http;
http.begin(weatherUrl);
if (http.GET() == 200) {
DynamicJsonDocument doc(32768);
deserializeJson(doc, http.getString());
currentTemp = doc["current"]["temperature_2m"];
morningTemp = doc["hourly"]["temperature_2m"][9];
morningCode = doc["hourly"]["weather_code"][9];
noonTemp = doc["hourly"]["temperature_2m"][14];
noonCode = doc["hourly"]["weather_code"][14];
eveningTemp = doc["hourly"]["temperature_2m"][21];
eveningCode = doc["hourly"]["weather_code"][21];
for (int i = 0; i < 16; i++) {
dMax[i] = doc["daily"]["temperature_2m_max"][i];
dMin[i] = doc["daily"]["temperature_2m_min"][i];
dCode[i] = doc["daily"]["weather_code"][i];
dRain[i] = doc["daily"]["precipitation_sum"][i];
dPress[i] = doc["hourly"]["pressure_msl"][i * 24 + 12];
dCloud[i] = doc["hourly"]["cloud_cover"][i * 24 + 12];
dHum[i] = doc["daily"]["relative_humidity_2m_mean"][i];
dWind[i] = doc["daily"]["wind_speed_10m_max"][i];
dUV[i] = doc["daily"]["uv_index_max"][i];
dSolar[i] = doc["daily"]["shortwave_radiation_sum"][i];
}
}
http.end();
http.begin("https://api.rainviewer.com/public/weather-maps.json");
if (http.GET() == 200) {
DynamicJsonDocument rDoc(8192);
deserializeJson(rDoc, http.getString());
radarHost = rDoc["host"] | "https://tilecache.rainviewer.com";
radarPath = "";
JsonArray past = rDoc["radar"]["past"].as<JsonArray>();
if (!past.isNull() && past.size() > 0) {
radarTS = past[past.size() - 1]["time"] | 0;
radarPath = (const char*)past[past.size() - 1]["path"];
Serial.printf("RainViewer frame time=%ld\n", radarTS);
Serial.printf("RainViewer path=%s\n", radarPath.c_str());
} else {
Serial.println("RainViewer: no past radar frames available");
}
} else {
Serial.println("RainViewer API request failed");
}
http.end();
}
int getDayOfMonthOffset(struct tm baseTime, int offsetDays) {
time_t raw = mktime(&baseTime);
raw += (offsetDays * 86400);
struct tm *t2 = localtime(&raw);
return t2->tm_mday;
}
void drawGraphPage(int type) {
struct tm ti;
getLocalTime(&ti);
lcd.fillScreen(TFT_BLACK);
lcd.drawRect(0, 0, 800, 480, TFT_WHITE);
lcd.fillRect(10, 415, 110, 55, TFT_DARKGREY);
lcd.drawRect(10, 415, 110, 55, TFT_WHITE);
lcd.setTextColor(TFT_WHITE);
lcd.setTextSize(2);
lcd.setTextDatum(middle_center);
lcd.drawString("BACK", 65, 442);
// Units label
const char* unitTxt = "";
if (type == 1) unitTxt = "Unit: C";
else if (type == 2) unitTxt = "Unit: hPa";
else if (type == 3) unitTxt = "Unit: mm";
else if (type == 4) unitTxt = "Unit: %";
else if (type == 5) unitTxt = "Unit: %";
else if (type == 6) unitTxt = "Unit: km/h";
else if (type == 7) unitTxt = "Unit: index";
else if (type == 8) unitTxt = "Unit: MJ/m2";
lcd.setTextDatum(middle_center);
lcd.setTextSize(3);
lcd.setTextColor(TFT_LIGHTGREY);
lcd.drawString(unitTxt, 370, 448 );
int sX = 75, eX = 770, sY = 380, eY = 85, dW = 43;
float minV, maxV;
// Titles
lcd.setTextColor(TFT_WHITE);
lcd.setTextSize(2);
lcd.setTextDatum(top_center);
if (type == 1) lcd.drawString("16-Day Temperature Forecast", 400, 8);
else if (type == 2) lcd.drawString("16-Day Pressure Forecast", 400, 8);
else if (type == 3) lcd.drawString("16-Day Rain Forecast", 400, 8);
else if (type == 4) lcd.drawString("16-Day Cloud Cover Forecast", 400, 8);
else if (type == 5) lcd.drawString("16-Day Humidity Forecast", 400, 8);
else if (type == 6) lcd.drawString("16-Day Wind Speed Forecast", 400, 8);
else if (type == 7) lcd.drawString("16-Day UV Index Forecast", 400, 8);
else if (type == 8) lcd.drawString("16-Day Shortwave Radiation Forecast", 400, 8);
// Range setup
if (type == 1) {
minV = 100;
maxV = -100;
for (int i = 0; i < 16; i++) {
if (dMax[i] > maxV) maxV = dMax[i];
if (dMin[i] < minV) minV = dMin[i];
}
minV = floor(minV / 5) * 5;
maxV = ceil(maxV / 5) * 5;
}
else if (type == 2) {
minV = 2000;
maxV = 0;
for (int i = 0; i < 16; i++) {
if (dPress[i] > maxV) maxV = dPress[i];
if (dPress[i] < minV) minV = dPress[i];
}
minV = floor(minV / 2) * 2 - 2;
maxV = ceil(maxV / 2) * 2 + 2;
}
else if (type == 3) {
minV = 0;
maxV = 0;
for (int i = 0; i < 16; i++) {
if (dRain[i] > maxV) maxV = dRain[i];
}
if (maxV < 5) maxV = 5;
else maxV = ceil(maxV / 5) * 5;
}
else if (type == 4) {
minV = 0;
maxV = 100;
}
else if (type == 5) {
minV = 0;
maxV = 100;
}
else if (type == 6) {
minV = 0;
maxV = 0;
for (int i = 0; i < 16; i++) {
if (dWind[i] > maxV) maxV = dWind[i];
}
maxV = ceil(maxV / 5) * 5;
if (maxV < 10) maxV = 10;
}
else if (type == 7) {
minV = 0;
maxV = 0;
for (int i = 0; i < 16; i++) {
if (dUV[i] > maxV) maxV = dUV[i];
}
maxV = ceil(maxV);
if (maxV < 5) maxV = 5;
}
else { // type == 8
minV = 0;
maxV = 0;
for (int i = 0; i < 16; i++) {
if (dSolar[i] > maxV) maxV = dSolar[i];
}
maxV = ceil(maxV / 5) * 5;
if (maxV < 5) maxV = 5;
}
// Cloud shaded area only for C
if (type == 4) {
for (int i = 0; i < 15; i++) {
int x1 = sX + (i * dW) + (dW / 2);
float v1 = dCloud[i], v2 = dCloud[i + 1];
for (int px = 0; px < dW; px++) {
float interVal = v1 + (v2 - v1) * (float)px / dW;
int interY = map(interVal, minV, maxV, sY, eY);
lcd.drawFastVLine(x1 + px, interY, sY - interY, lcd.color565(35, 35, 35));
}
}
}
// Weekend backgrounds
for (int i = 0; i < 16; i++) {
int wD = (ti.tm_wday + i) % 7;
if (wD == 6 || wD == 0) {
uint16_t c = (wD == 6) ? lcd.color565(20, 20, 35) : lcd.color565(35, 20, 20);
lcd.fillRect(sX + (i * dW), eY, dW, sY - eY, c);
}
}
// Left scale
lcd.setTextSize(2);
lcd.setTextDatum(middle_right);
float step = 5;
if (type == 2) step = 2;
else if (type == 3) step = maxV / 5;
else if (type == 4) step = 20;
else if (type == 5) step = 20;
else if (type == 6) step = maxV / 5;
else if (type == 7) step = 1;
else if (type == 8) step = maxV / 5;
if (step < 1) step = 1;
for (float v = minV; v <= maxV; v += step) {
int yP = map(v, minV, maxV, sY, eY);
lcd.drawFastHLine(sX, yP, eX - sX, lcd.color565(60, 60, 60));
lcd.drawNumber((int)v, sX - 12, yP);
}
// Main plot
for (int i = 0; i < 16; i++) {
int x1 = sX + (i * dW) + (dW / 2);
int wD = (ti.tm_wday + i) % 7;
int realDate = getDayOfMonthOffset(ti, i);
// Day label
lcd.setTextDatum(top_center);
lcd.setTextSize(2);
lcd.setTextColor(TFT_LIGHTGREY);
lcd.drawString(dayShort2[wD], x1, eY - 46);
// Mini icon only on T
if (type == 1) {
drawMiniWeatherIcon(x1, eY - 17, dCode[i]);
}
// Bottom date
lcd.setTextDatum(top_center);
lcd.setTextSize(2);
lcd.setTextColor(TFT_WHITE);
lcd.drawNumber(realDate, x1, sY + 8);
// BAR charts: Rain + Solar
if (type == 3 || type == 8) {
float barVal = (type == 3) ? dRain[i] : dSolar[i];
uint16_t barCol = (type == 3) ? TFT_BLUE : TFT_YELLOW;
int barH = map(barVal, 0, maxV, 0, sY - eY);
lcd.fillRect(x1 - 15, sY - barH, 30, barH, barCol);
lcd.drawRect(x1 - 15, sY - barH, 30, barH, TFT_WHITE);
if (barVal > 0) {
lcd.setTextSize(2);
lcd.setTextDatum(middle_center);
lcd.setTextColor(TFT_WHITE);
lcd.drawString(String(barVal, 1), x1, sY - barH - 16);
}
}
else {
float val1 = 0;
uint16_t col = TFT_WHITE;
if (type == 1) {
val1 = dMax[i];
col = TFT_RED;
} else if (type == 2) {
val1 = dPress[i];
col = TFT_YELLOW;
} else if (type == 4) {
val1 = dCloud[i];
col = TFT_WHITE;
} else if (type == 5) {
val1 = dHum[i];
col = TFT_BLUE;
} else if (type == 6) {
val1 = dWind[i];
col = lcd.color565(255, 140, 0);
} else if (type == 7) {
val1 = dUV[i];
col = TFT_MAGENTA;
}
int y1 = map(val1, minV, maxV, sY, eY);
if (i < 15) {
float val2 = 0;
if (type == 1) val2 = dMax[i + 1];
else if (type == 2) val2 = dPress[i + 1];
else if (type == 4) val2 = dCloud[i + 1];
else if (type == 5) val2 = dHum[i + 1];
else if (type == 6) val2 = dWind[i + 1];
else if (type == 7) val2 = dUV[i + 1];
int x2 = sX + ((i + 1) * dW) + (dW / 2);
int y2 = map(val2, minV, maxV, sY, eY);
lcd.drawLine(x1, y1, x2, y2, col);
if (type == 1) {
int yMin1 = map(dMin[i], minV, maxV, sY, eY);
int yMin2 = map(dMin[i + 1], minV, maxV, sY, eY);
lcd.drawLine(x1, yMin1, x2, yMin2, TFT_BLUE);
}
}
lcd.fillCircle(x1, y1, 4, col);
lcd.drawCircle(x1, y1, 4, TFT_WHITE);
if (type == 1) {
int yMin = map(dMin[i], minV, maxV, sY, eY);
lcd.fillCircle(x1, yMin, 4, TFT_BLUE);
lcd.drawCircle(x1, yMin, 4, TFT_WHITE);
}
}
}
}
void drawLocationMarker() {
double n = pow(2.0, myZoom);
// Global pixel coordinates at current zoom
double worldX = ((myLon + 180.0) / 360.0) * n * 256.0;
double latRad = myLat * M_PI / 180.0;
double worldY = (1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / M_PI) / 2.0 * n * 256.0;
// Center tile indices (isti kako vo renderRadarMap)
int cTX = (int)(floor((myLon + 180.0) / 360.0 * n));
int cTY = (int)(floor((1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / M_PI) / 2.0 * n));
// Top-left pixel of our composed 3x2 tile map
double topLeftWorldX = (cTX - 1) * 256.0;
double topLeftWorldY = (cTY + 0) * 256.0;
// Convert to local sprite coordinates
int px = (int)(worldX - topLeftWorldX) + 32;
int py = (int)(worldY - topLeftWorldY) - 16;
// Safety check
if (px < 0 || px >= 800 || py < 0 || py >= 415) return;
// Marker
mapCanvas.fillCircle(px, py, 5, TFT_RED);
mapCanvas.drawCircle(px, py, 6, TFT_WHITE);
mapCanvas.drawPixel(px, py, TFT_WHITE);
}
void renderRadarMap() {
if (appState == 0) {
lcd.setTextColor(TFT_WHITE, TFT_BLACK);
lcd.setTextSize(3);
lcd.setTextDatum(middle_center);
lcd.drawString("Loading map...", 400, 200);
}
mapCanvas.fillSprite(TFT_BLACK);
int cTX = (int)(floor((myLon + 180.0) / 360.0 * pow(2.0, myZoom)));
int cTY = (int)(floor((1.0 - log(tan(myLat * M_PI / 180.0) + 1.0 / cos(myLat * M_PI / 180.0)) / M_PI) / 2.0 * pow(2.0, myZoom)));
for (int i = -1; i < 2; i++) {
for (int j = 0; j < 2; j++) {
globalX = 32 + ((i + 1) * 256);
globalY = (j * 256) - 16;
String mU = String(mapUrls[mapStyle]) + String(myZoom) + "/" + String(cTX + i) + "/" + String(cTY + j) + ".png";
uint8_t* baseBuf = nullptr;
size_t baseLen = 0;
if (fetchPngToBuffer(mU, &baseBuf, &baseLen)) {
if (png.openRAM(baseBuf, baseLen, pngDrawCanvas) == PNG_SUCCESS) {
png.decode(NULL, 0);
png.close();
} else {
Serial.printf("Base PNG open failed: %s\n", mU.c_str());
}
free(baseBuf);
} else {
Serial.printf("Base fetch failed: %s\n", mU.c_str());
}
String layerUrl = "";
if (layerStyle == 0) {
if (radarPath.length() > 0) {
layerUrl = radarHost + radarPath + "/256/" +
String(myZoom) + "/" +
String(cTX + i) + "/" +
String(cTY + j) + "/1/1_1.png";
}
} else {
layerUrl = "https://tile.openweathermap.org/map/" +
String(owmLayerIds[layerStyle]) + "/" +
String(myZoom) + "/" +
String(cTX + i) + "/" +
String(cTY + j) +
".png?appid=" + String(owmApiKey);
}
if (layerUrl.length() > 0) {
uint8_t* overlayBuf = nullptr;
size_t overlayLen = 0;
Serial.printf("Layer=%s\n", layerNames[layerStyle]);
Serial.printf("URL=%s\n", layerUrl.c_str());
Serial.printf("Heap=%u PSRAM=%u\n", ESP.getFreeHeap(), ESP.getFreePsram());
if (fetchPngToBuffer(layerUrl, &overlayBuf, &overlayLen)) {
// PNG signature check
bool isPng =
overlayLen >= 8 &&
overlayBuf[0] == 0x89 &&
overlayBuf[1] == 0x50 &&
overlayBuf[2] == 0x4E &&
overlayBuf[3] == 0x47 &&
overlayBuf[4] == 0x0D &&
overlayBuf[5] == 0x0A &&
overlayBuf[6] == 0x1A &&
overlayBuf[7] == 0x0A;
if (!isPng) {
Serial.printf("Overlay is NOT PNG! len=%u\n", (unsigned)overlayLen);
Serial.print("First 32 bytes HEX: ");
for (size_t k = 0; k < 32 && k < overlayLen; k++) {
Serial.printf("%02X ", overlayBuf[k]);
}
Serial.println();
Serial.print("First 120 chars TXT: ");
for (size_t k = 0; k < 120 && k < overlayLen; k++) {
char c = (char)overlayBuf[k];
if (c >= 32 && c <= 126) Serial.print(c);
...
This file has been truncated, please download it to see its full contents.
Comments