// Imam kodo za ESP32-S3 MCN32R16V in TFT ST7796 4-palčni (480x320) in štiri zunanje Module ki komunicirajo z Glavni ESP. Koda Glavni ESP:
#include <SD.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7796S.h>
#include <XPT2046_Touchscreen.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <Wire.h>
#include <Adafruit_MCP23X17.h>
#include <RTClib.h>
#include <time.h>
#include <Preferences.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <Fonts/FreeSansBold24pt7b.h>
#include <Fonts/FreeMonoBold18pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSerifBold18pt7b.h>
#include <Fonts/FreeSerifBold12pt7b.h>
#include <Fonts/FreeSerifBold9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
// Definicije za trend puščice
enum TrendDirection {
TREND_NONE = 0,
TREND_UP = 1,
TREND_DOWN = 2,
TREND_STEADY = 3
};
// Tipi gumbov za različne stile
enum ButtonStyle {
BUTTON_NORMAL = 0,
BUTTON_COMPACT = 1,
BUTTON_WIDE = 2,
BUTTON_HIGHLIGHT = 3,
BUTTON_WARNING = 4,
BUTTON_SUCCESS = 5,
BUTTON_INFO = 6,
BUTTON_DISABLED = 7
};
// Struktura za konfiguracijo gumba
struct ButtonConfig {
int x, y;
int width, height;
uint16_t baseColor;
const char* label;
ButtonStyle style;
bool isActive;
const char* icon;
int textSize;
};
#define WHITE_COLOR 0xFFFF
#define BLACK_COLOR 0x0000
#define RED_COLOR 0xF800
#define GREEN_COLOR 0x07E0
#define BLUE_COLOR 0x001F
// === Časovni intervali za soil moisture ===
#define SOIL_UPDATE_INTERVAL 2000
unsigned long lastSoilUpdate = 0;
// ==================== MINIMALNI SISTEM ZA PREPREČEVANJE UTREPANJA ====================
unsigned long lastScreenDraw = 0;
#define MIN_DRAW_INTERVAL 2000
// ==================== ESP-NOW STRUKTURE ZA VSE MODULE ====================
enum ModuleType {
MODULE_WEATHER = 1,
MODULE_IRRIGATION = 2,
MODULE_SHADE_MOTOR = 3
};
enum ErrorCode {
ERR_NONE = 0,
ERR_SENSOR_AHT = 1,
ERR_SENSOR_BMP = 2,
ERR_SENSOR_LDR = 3,
ERR_I2C = 4,
ERR_MOTOR_STALL = 10,
ERR_LIMIT_SWITCH = 11,
ERR_CALIBRATION = 12,
ERR_OVER_CURRENT = 13,
CALIB_DARK = 100,
CALIB_BRIGHT = 101,
CALIB_CONFIRM = 102
};
enum ShadeCommands {
CMD_MOVE_TO_POSITION = 1,
CMD_MOVE_STEP = 2,
CMD_STOP = 3,
CMD_CALIBRATE = 4,
CMD_SET_SPEED = 5,
CMD_SET_AUTO_MODE = 6,
CMD_EMERGENCY_STOP = 7
};
struct ModuleData {
uint8_t moduleId;
uint8_t moduleType;
unsigned long timestamp;
uint8_t errorCode;
float batteryVoltage;
union {
struct {
float temperature;
float humidity;
float pressure;
float lux;
float bmpTemperature;
} weather;
struct {
float flowRate1, flowRate2, flowRate3;
float totalFlow1, totalFlow2, totalFlow3;
bool relay1State, relay2State, relay3State;
} irrigation;
struct {
int currentPosition;
int targetPosition;
bool isMoving;
bool isCalibrated;
bool limitSwitchOpen;
bool limitSwitchClosed;
float motorCurrent;
int motorSpeed;
} shade;
};
};
struct CommandData {
uint8_t targetModuleId;
uint8_t command;
float param1;
float param2;
};
// ==================== GLOBALNE SPREMENLJIVKE ZA MODUL 1 ====================
float externalLux = 0;
float externalTemperature = 0;
float externalHumidity = 0;
float externalPressure = 1013.25;
bool module1Active = false;
unsigned long lastModule1Time = 0;
#define MODULE1_TIMEOUT 60000
uint8_t module1MAC[] = {0xCE, 0x4D, 0xCC, 0x3F, 0xC8, 0x4D};
// ==================== GLOBALNE SPREMENLJIVKE ZA MODUL 2 ====================
uint8_t module2MAC[] = {0xB8, 0xF8, 0x62, 0xF8, 0x65, 0xC0};
bool module2Active = false;
unsigned long lastModule2Time = 0;
#define MODULE2_TIMEOUT 60000
float module2FlowRate1 = 0;
float module2FlowRate2 = 0;
float module2FlowRate3 = 0;
float module2TotalFlow1 = 0;
float module2TotalFlow2 = 0;
float module2TotalFlow3 = 0;
bool module2Relay1State = false;
bool module2Relay2State = false;
bool module2Relay3State = false;
float module2Battery = 0;
uint8_t module2ErrorCode = 0;
// ==================== LDR KALIBRACIJA ====================
// Notranji LDR (Modul 4) - pretvorba procentov v luks
float internalLuxMin = 0.0; // luks pri 0% (popolna tema)
float internalLuxMax = 20000.0; // luks pri 100% (največ svetlobe)
// Zunanji LDR (Modul 1) - linearna kalibracija: izhod = (vhod + offset) * scale
float externalLuxOffset = 0.0; // zamik v luksih
float externalLuxScale = 1.0; // faktor skaliranja
// Stanje kalibracije
enum LDRCalibStep {
CALIB_NONE = 0,
CALIB_INTERNAL_DARK = 1, // Čakam na temno za notranji
CALIB_INTERNAL_BRIGHT = 2, // Čakam na svetlo za notranji
CALIB_EXTERNAL_DARK = 3, // Čakam na temno za zunanji
CALIB_EXTERNAL_BRIGHT = 4 // Čakam na svetlo za zunanji
};
LDRCalibStep ldrCalibStep = CALIB_NONE;
float calibInternalDarkPercent = 0;
float calibInternalBrightPercent = 100;
float calibExternalDarkLux = 0;
float calibExternalBrightLux = 0;
// ==================== GLOBALNE SPREMENLJIVKE ZA MODUL 3 ====================
uint8_t module3MAC[] = {0x58, 0x8C, 0x81, 0xCB, 0xDC, 0x80};
bool module3Active = false;
unsigned long lastModule3Time = 0;
#define MODULE3_TIMEOUT 60000
int shadeCurrentPosition = 0;
int shadeTargetPosition = 0;
bool shadeIsMoving = false;
bool shadeIsCalibrated = false;
bool shadeLimitOpen = false;
bool shadeLimitClosed = false;
int shadeMotorSpeed = 0;
// ==================== MODUL 4 - VITRINA (GLAVNI VIR PODATKOV) ====================
uint8_t module4MAC[] = {0x1A, 0xD8, 0xCB, 0x3F, 0x14, 0xD8};
bool module4Active = false;
unsigned long lastModule4Time = 0;
#define MODULE4_TIMEOUT 30000
// Podatki iz Modula 4 (vitrina) - TO SO ZDAJ NOTRANJE VREDNOSTI
float module4AirTemp = 0;
float module4AirHum = 0;
float module4Pressure = 0;
int module4LightPercent = 0;
float module4SoilTemp = 0;
int module4SoilMoisture = 0;
float module4TVOC = 0;
float module4ECO2 = 0;
float module4ECH2O = 0;
String module4AirQuality = "---";
float module4Battery = 0;
// NOTRANJE SPREMENLJIVKE (zdaj iz Modula 4)
float internalTemperature = 0.0;
float internalHumidity = 0.0;
int ldrInternalPercent = 0;
float ldrInternalLux = 0.0;
int soilMoisturePercent = 0;
// Za sledenje sprememb na domačem zaslonu
float lastDrawnModule4Temp = -999;
float lastDrawnModule4Hum = -999;
int lastDrawnModule4Light = -999;
int lastDrawnModule4SoilM = -999;
float lastDrawnModule4TVOC = -999;
float lastDrawnModule4ECO2 = -999;
// ==================== PIN DEFINICIJE ====================
#define TFT_CS 5
#define TFT_RST 4
#define TFT_DC 2
// Pini za SD kartico
#define SD_CS_PIN 45
#define SD_SCK_PIN 46
#define SD_MOSI_PIN 47
#define SD_MISO_PIN 48
// Pini za TOUCH (XPT2046)
#define T_CS 39
#define T_CLK 40
#define T_DIN 41
#define T_DO 42
#define T_IRQ 3
// Pini za MCP23017 (I2C)
#define MCP_SDA 6
#define MCP_SCL 7
#define MCP_RST 8
#define MCP_ITA 9
#define MCP_ITB 10
#define MCP_ADDR 0x20
// I2C za AHT20 in BMP280 - zunanje meritve (na modulu 1, ne na glavnem)
#define EXTERNAL_SDA MCP_SDA
#define EXTERNAL_SCL MCP_SCL
// Pini za flow senzorje YF-S201
#define FLOW_SENSOR_1_PIN 18
#define FLOW_SENSOR_2_PIN 19
#define FLOW_SENSOR_3_PIN 21
// WiFi nastavitve
#define MAX_WIFI_NETWORKS 5
// NTP nastavitve
#define NTP_SERVER "pool.ntp.org"
#define GMT_OFFSET_SEC 3600
#define DAYLIGHT_OFFSET_SEC 3600
// Dimenzije statusne vrstice
#define STATUS_BAR_HEIGHT 20
#define STATUS_BAR_COLOR rgbTo565(0, 100, 200)
#define STATUS_TEXT_COLOR ST77XX_WHITE
// Ventilator - dodatni rele
#define VENTILATION_RELAY 8
#define VENTILATION_SYMBOL_X 400
#define VENTILATION_SYMBOL_Y STATUS_BAR_HEIGHT / 2 + 2
// Hamburger meni
#define HAMBURGER_X 440
#define HAMBURGER_Y STATUS_BAR_HEIGHT + 10
#define HAMBURGER_SIZE 22
#define HAMBURGER_TOUCH_PADDING 15
#define TOUCH_PADDING_RELAYS 12
#define TOUCH_PADDING_CIRCLES 20
// ==================== TROPSKE RASTLINE - VPD IN PROFILI ====================
#define MAX_PLANTS 20
#define VPD_UPDATE_INTERVAL 5000
#define MISTING_DURATION 5000
#define AIR_MOVEMENT_INTERVAL 1800000
#define AIR_MOVEMENT_DURATION 300000
#ifndef MISTING_RELAY
#define MISTING_RELAY 9
#endif
#ifndef AIR_FLOW_RELAY
#define AIR_FLOW_RELAY 10
#endif
#ifndef BOTTOM_WATERING_RELAY
#define BOTTOM_WATERING_RELAY 11
#endif
enum GrowthStage {
STAGE_SEEDLING,
STAGE_VEGETATIVE,
STAGE_FLOWERING,
STAGE_FRUITING,
STAGE_DORMANT
};
struct TropicalPlantProfile {
String species;
String commonName;
float vpdMin;
float vpdMax;
float tempMin;
float tempMax;
float humMin;
float humMax;
float lightMin;
float lightMax;
bool needsAirMovement;
bool needsMisting;
bool needsBottomWatering;
String lightPreference;
String careLevel;
String notes;
float soilMoistureMin;
float soilMoistureMax;
int wateringIntervalDays;
int mistingIntervalHours;
int mistingDurationSeconds;
};
struct IndividualPlant {
int id;
String name;
String species;
float currentHeight;
int leafCount;
unsigned long lastWatered;
unsigned long lastFertilized;
unsigned long lastMisted;
float personalVPDFactor;
bool isVariegated;
String location;
String imageFile;
};
// ==================== GLOBALNE SPREMENLJIVKE ZA TROPSKE RASTLINE ====================
TropicalPlantProfile tropicalSpecies[] = {
// Alocasie
{ "Alocasia amazonica", "Alocasia 'Amazonica'", 0.5, 1.0, 22, 28, 70, 85, 8000, 15000, true, true, false, "medium", "moderate", "Potrebuje visoko vlago, ne mara direktnega sonca", 40, 70, 5, 24, 30 },
{ "Alocasia dragon scale", "Alocasia 'Dragon Scale'", 0.5, 1.0, 23, 29, 70, 85, 8000, 15000, true, true, false, "medium", "moderate", "Cudoviti kovinski listi, obozuje vlago", 40, 70, 5, 24, 30 },
{ "Alocasia zebrina", "Alocasia 'Zebrina'", 0.5, 1.0, 22, 28, 70, 85, 10000, 18000, true, true, false, "medium", "moderate", "Zanimivo steblo, potrebuje oporo", 40, 70, 5, 24, 30 },
{ "Alocasia cuprea", "Alocasia 'Cuprea'", 0.5, 1.0, 23, 29, 70, 85, 8000, 15000, true, true, false, "medium", "expert", "Redka, bakreni listi, zelo obcutljiva", 45, 75, 4, 12, 20 },
{ "Alocasia frydek", "Alocasia 'Frydek'", 0.5, 1.0, 22, 28, 70, 85, 8000, 15000, true, true, false, "medium", "moderate", "Zeleni zametni listi", 40, 70, 5, 24, 30 },
{ "Alocasia black velvet", "Alocasia 'Black Velvet'", 0.5, 1.0, 22, 28, 70, 85, 6000, 12000, true, true, false, "low", "moderate", "Crni zametni listi", 45, 75, 4, 24, 30 },
{ "Alocasia silver dragon", "Alocasia 'Silver Dragon'", 0.5, 1.0, 22, 28, 70, 85, 7000, 13000, true, true, false, "medium", "moderate", "Srebrni listi", 40, 70, 5, 24, 30 },
// Anthuriumi
{ "Anthurium crystallinum", "Anthurium crystallinum", 0.3, 0.8, 20, 26, 75, 90, 5000, 12000, false, true, true, "low", "expert", "Cudoviti zametni listi, potrebuje zracno zemljo", 50, 80, 4, 12, 20 },
{ "Anthurium clarinervium", "Anthurium clarinervium", 0.3, 0.7, 19, 25, 75, 90, 5000, 12000, false, true, true, "low", "expert", "Izrazite zile, ne mara prevec vode", 45, 75, 5, 12, 20 },
{ "Anthurium warocqueanum", "Queen Anthurium", 0.3, 0.8, 21, 27, 75, 90, 6000, 13000, false, true, true, "low", "expert", "Kraljica Anthuriumov, zelo redka", 50, 80, 4, 12, 20 },
{ "Anthurium veitchii", "King Anthurium", 0.3, 0.8, 21, 27, 75, 90, 6000, 13000, false, true, true, "low", "expert", "Ogromni valoviti listi", 50, 80, 4, 12, 20 },
{ "Anthurium magnificum", "Anthurium magnificum", 0.3, 0.8, 20, 26, 75, 90, 5000, 12000, false, true, true, "low", "expert", "Veliki zametni listi", 50, 80, 4, 12, 20 },
{ "Anthurium regale", "Anthurium Regale", 0.3, 0.8, 21, 27, 75, 90, 5000, 12000, false, true, true, "low", "expert", "Ogromni, zametni listi", 50, 80, 4, 12, 20 },
{ "Anthurium luxurians", "Anthurium Luxurians", 0.3, 0.8, 21, 27, 75, 90, 5000, 12000, false, true, true, "low", "expert", "Hrapavi listi", 50, 80, 4, 12, 20 },
{ "Anthurium radicans", "Anthurium Radicans", 0.3, 0.8, 20, 26, 75, 90, 5000, 12000, false, true, true, "low", "expert", "Srcasti listi", 50, 80, 4, 12, 20 },
// Philodendroni
{ "Philodendron gloriosum", "Philodendron gloriosum", 0.4, 0.9, 21, 27, 70, 85, 8000, 15000, true, false, false, "low", "moderate", "Plezoci, belozilni listi", 40, 70, 5, 0, 0 },
{ "Philodendron pink princess", "Philodendron 'Pink Princess'", 0.6, 1.2, 22, 28, 65, 80, 12000, 20000, true, false, false, "bright_indirect", "expert", "Roznati listi, potrebuje mocno svetlobo", 35, 65, 5, 0, 0 },
{ "Philodendron melanochrysum", "Philodendron melanochrysum", 0.4, 0.9, 22, 28, 70, 85, 8000, 15000, true, true, false, "medium", "expert", "Crno-zlati listi, plezoc", 45, 75, 4, 24, 30 },
{ "Philodendron verrucosum", "Philodendron verrucosum", 0.4, 0.9, 21, 27, 70, 85, 8000, 15000, true, true, false, "medium", "expert", "Zametni listi, rdece spodnje strani", 45, 75, 4, 24, 30 },
{ "Philodendron micans", "Philodendron micans", 0.5, 1.1, 20, 28, 60, 80, 6000, 15000, true, false, false, "medium", "easy", "Zametni, vijolicasti listi", 35, 65, 6, 0, 0 },
{ "Philodendron billietiae", "Philodendron billietiae", 0.5, 1.0, 22, 28, 65, 80, 10000, 18000, true, false, false, "bright_indirect", "moderate", "Dolgi, oranzni listi", 35, 65, 5, 0, 0 },
{ "Philodendron spiritus sancti", "Philodendron Spiritus Sancti", 0.4, 0.9, 22, 28, 70, 85, 8000, 15000, true, true, false, "medium", "expert", "Eden najredkejsih philodendronov", 45, 75, 4, 24, 30 },
{ "Philodendron joepii", "Philodendron Joepii", 0.4, 0.9, 22, 28, 70, 85, 8000, 15000, true, true, false, "medium", "expert", "Cudni listi kot usesa", 45, 75, 4, 24, 30 },
{ "Philodendron patriciae", "Philodendron Patriciae", 0.4, 0.9, 22, 28, 70, 85, 8000, 15000, true, true, false, "medium", "expert", "Dolgi, ozki listi", 45, 75, 4, 24, 30 },
{ "Philodendron rugosum", "Philodendron Rugosum", 0.5, 1.0, 22, 28, 65, 80, 8000, 15000, true, true, false, "medium", "expert", "Hrapavi listi", 40, 70, 5, 24, 30 },
{ "Philodendron hederaceum", "Heartleaf Philodendron", 0.6, 1.2, 19, 28, 60, 80, 5000, 15000, true, false, false, "medium", "easy", "Klasika, zelo enostaven", 30, 60, 7, 0, 0 },
{ "Philodendron brasil", "Philodendron 'Brasil'", 0.6, 1.2, 19, 28, 60, 80, 6000, 15000, true, false, false, "medium", "easy", "Zeleno-rumeni listi", 30, 60, 7, 0, 0 },
{ "Philodendron cordatum", "Philodendron Cordatum", 0.6, 1.2, 19, 28, 60, 80, 5000, 15000, true, false, false, "medium", "easy", "Srcasti listi", 30, 60, 7, 0, 0 },
{ "Philodendron squamiferum", "Philodendron Squamiferum", 0.5, 1.0, 22, 28, 65, 80, 8000, 15000, true, true, false, "medium", "moderate", "Rdece dlakavo steblo", 40, 70, 5, 24, 30 },
// Monsterae
{ "Monstera deliciosa", "Monstera", 0.8, 1.5, 18, 30, 60, 80, 10000, 25000, true, false, false, "bright_indirect", "easy", "Klasicna, luknjicasti listi", 30, 60, 7, 0, 0 },
{ "Monstera adansonii", "Monkey Mask", 0.7, 1.3, 20, 28, 65, 80, 8000, 20000, true, false, false, "medium", "easy", "Majhni luknjicasti listi", 35, 65, 6, 0, 0 },
{ "Monstera albo", "Monstera 'Albo Variegata'", 0.6, 1.2, 21, 28, 65, 80, 12000, 20000, true, false, false, "bright_indirect", "expert", "Redka, pestra, draga", 35, 65, 5, 0, 0 },
{ "Monstera thai constellation", "Monstera Thai Constellation", 0.6, 1.2, 21, 28, 65, 80, 12000, 20000, true, false, false, "bright_indirect", "expert", "Kremasti vzorci, pocasna rast", 35, 65, 5, 0, 0 },
// Calathee in Marantae
{ "Calathea orbifolia", "Calathea orbifolia", 0.4, 0.9, 20, 26, 70, 85, 5000, 12000, false, true, false, "low", "expert", "Veliki, srebrnozeleni listi", 50, 80, 4, 24, 30 },
{ "Calathea medallion", "Calathea 'Medallion'", 0.4, 0.9, 20, 26, 70, 85, 5000, 12000, false, true, false, "low", "moderate", "Vzorcasti listi", 50, 80, 4, 24, 30 },
{ "Maranta leuconeura", "Prayer Plant", 0.5, 1.0, 20, 27, 65, 80, 5000, 12000, false, true, false, "low", "easy", "Listi se ponoci dvignejo", 45, 75, 5, 24, 20 },
// Druge tropske
{ "Syngonium podophyllum", "Arrowhead Plant", 0.5, 1.1, 19, 27, 60, 80, 6000, 15000, true, false, false, "medium", "easy", "Hitro rastoca", 35, 65, 6, 0, 0 },
{ "Syngonium albo", "Syngonium 'Albo Variegatum'", 0.5, 1.0, 20, 27, 65, 80, 8000, 15000, true, false, false, "bright_indirect", "moderate", "Pestri listi", 35, 65, 5, 0, 0 },
{ "Scindapsus pictus", "Satin Pothos", 0.5, 1.1, 20, 28, 55, 75, 5000, 15000, true, false, false, "medium", "easy", "Srebrnkasti listi", 30, 60, 7, 0, 0 },
{ "Hoya carnosa", "Wax Plant", 0.8, 1.5, 18, 28, 50, 70, 8000, 20000, true, false, false, "bright_indirect", "easy", "Vosceni listi, diseci cvetovi", 25, 50, 10, 0, 0 },
{ "Peperomia obtusifolia", "Baby Rubber Plant", 0.7, 1.3, 18, 26, 50, 70, 6000, 15000, true, false, false, "medium", "easy", "Meso listi", 25, 55, 8, 0, 0 },
{ "Peperomia caperata", "Emerald Ripple", 0.6, 1.2, 18, 26, 55, 75, 5000, 12000, true, false, false, "medium", "easy", "Nagubani listi", 30, 60, 7, 0, 0 },
{ "Ficus elastica", "Rubber Plant", 0.8, 1.5, 18, 28, 50, 70, 10000, 25000, true, false, false, "bright_indirect", "easy", "Veliki, temnozeleni listi", 25, 55, 8, 0, 0 },
{ "Ficus lyrata", "Fiddle Leaf Fig", 0.7, 1.4, 18, 28, 50, 70, 10000, 25000, true, false, false, "bright_indirect", "moderate", "Violinasti listi", 25, 55, 7, 0, 0 },
{ "Begonia maculata", "Polka Dot Begonia", 0.5, 1.0, 19, 26, 60, 80, 8000, 15000, true, true, false, "medium", "moderate", "Pikcasti listi, roza cvetovi", 40, 70, 5, 24, 20 },
{ "Begonia rex", "Rex Begonia", 0.5, 1.0, 19, 26, 65, 80, 6000, 12000, true, true, false, "medium", "expert", "Barviti listi", 45, 75, 4, 24, 20 },
{ "Spathiphyllum", "Peace Lily", 0.6, 1.2, 18, 26, 55, 75, 5000, 12000, true, false, false, "low", "easy", "Beli cvetovi, pokaze ko je zejna", 35, 70, 5, 0, 0 },
{ "Zamioculcas zamiifolia", "ZZ Plant", 1.0, 2.0, 18, 30, 40, 60, 3000, 10000, false, false, false, "low", "easy", "Neunicljiva, ne mara prevec vode", 15, 40, 14, 0, 0 },
{ "Sansevieria trifasciata", "Snake Plant", 1.2, 2.2, 15, 30, 30, 50, 2000, 8000, false, false, false, "low", "easy", "Skoraj neunicljiva", 10, 35, 21, 0, 0 }
};
int tropicalSpeciesCount = sizeof(tropicalSpecies) / sizeof(tropicalSpecies[0]);
IndividualPlant myPlants[MAX_PLANTS];
int plantCount = 0;
int selectedPlantId = -1;
String currentPlantSpecies = "";
float currentVPD = 0.0;
unsigned long lastVPDUpdate = 0;
unsigned long lastAirMovement = 0;
bool airMoving = false;
unsigned long mistingStartTime = 0;
bool isMisting = false;
// Kalibracija za Soil Moisture Sensor (za Modul 4, shranjena lokalno)
int SOIL_DRY_VALUE = 4095;
int SOIL_WET_VALUE = 1500;
// Nastavitve za flow senzorje YF-S201
#define PULSES_PER_LITER 450
#define FLOW_UPDATE_INTERVAL 1000
// Releji - pin definicije na MCP23017
#define RELAY_COUNT 9
#define RELAY_START_PIN 0
// Inicializacija zaslona
Adafruit_ST7796S tft(TFT_CS, TFT_DC, TFT_RST);
// Inicializacija touch
SPIClass touchSPI(HSPI);
XPT2046_Touchscreen ts(T_CS, T_IRQ);
// Inicializacija MCP23017
Adafruit_MCP23X17 mcp;
// Inicializacija RTC (DS3231)
RTC_DS3231 rtc;
// WiFi objekti
WiFiMulti wifiMulti;
// Kalibracijski parametri
#define TS_MINX 280
#define TS_MAXX 3840
#define TS_MINY 250
#define TS_MAXY 3740
#define LIGHTING_RELAY_1 4
#define LIGHTING_RELAY_2 5
// ==================== 48-URNI GRAFI - ZGODOVINA MERITEV ====================
#define GRAPH_HISTORY_SIZE 96
#define GRAPH_UPDATE_INTERVAL 1800000
float tempHistory48h[GRAPH_HISTORY_SIZE];
float humHistory48h[GRAPH_HISTORY_SIZE];
float luxHistory48h[GRAPH_HISTORY_SIZE];
int graphHistoryIndex = 0;
unsigned long lastGraphUpdate = 0;
unsigned long timeStamps48h[GRAPH_HISTORY_SIZE];
// ==================== 48-URNI GRAFI ZA ZUNANJE SENZORJE ====================
#define EXTERNAL_GRAPH_HISTORY_SIZE 96
float extTempHistory48h[EXTERNAL_GRAPH_HISTORY_SIZE];
float extHumHistory48h[EXTERNAL_GRAPH_HISTORY_SIZE];
float extLuxHistory48h[EXTERNAL_GRAPH_HISTORY_SIZE];
float extPressureHistory48h[EXTERNAL_GRAPH_HISTORY_SIZE];
int extGraphHistoryIndex = 0;
unsigned long lastExtGraphUpdate = 0;
unsigned long extTimeStamps48h[EXTERNAL_GRAPH_HISTORY_SIZE];
// Spremenljivke za SD kartico
bool sdInitialized = false;
bool useSDCard = false;
unsigned long lastSDLog = 0;
#define SD_LOG_INTERVAL 60000
#define DATA_LOG_FILE "/data_log.csv"
#define SETTINGS_FILE "/settings.txt"
#define HISTORY_FILE "/history.csv"
bool lightAutoMode = true;
bool lightManualOverride = false;
float lightOnThreshold = 50.0;
float lightOffThreshold = 1000.0;
bool lightTimeControlEnabled = false;
int lightStartHour = 18;
int lightStartMinute = 0;
int lightEndHour = 22;
int lightEndMinute = 0;
bool touchPressed = false;
int lastX = -1, lastY = -1;
bool ventilationAutoMode = true;
bool ventilationManualOverride = false;
float ventilationTempThreshold = 28.0;
float ventilationHumThreshold = 70.0;
int ventilationMinRuntime = 5;
int ventilationCooldownTime = 10;
bool ventilationDayMode = true;
float ventilationDayTempThreshold = 28.0;
float ventilationDayHumThreshold = 70.0;
float ventilationNightTempThreshold = 22.0;
float ventilationNightHumThreshold = 75.0;
int ventilationDayMinRuntime = 5;
int ventilationNightMinRuntime = 10;
int ventilationDayCooldown = 10;
int ventilationNightCooldown = 20;
int dayNightSwitchHour = 22;
int nightDaySwitchHour = 6;
// ==================== SPREMENLJIVKE ZA ČASOVNO KRMILJENJE RAZSVETLJAVE ====================
#define MAX_TIME_BLOCKS 4
#define DAYS_IN_WEEK 7
typedef struct {
bool enabled;
bool relayOn;
int startHour;
int startMinute;
int endHour;
int endMinute;
bool days[DAYS_IN_WEEK];
String description;
} TimeBlock;
TimeBlock timeBlocks[MAX_TIME_BLOCKS];
bool lightingAutoMode = true;
bool lightingManualOverride = false;
int currentEditingBlock = -1;
unsigned long lastLightingCheck = 0;
#define LIGHTING_CHECK_INTERVAL 1000
int selectedLightBlock = -1;
bool editTimeMode = false;
bool editDaysMode = false;
bool editActionMode = false;
int editTimeIndex = 0;
float shadeMinLux = 0.0;
float shadeMaxLux = 5000.0;
int shadeCalculatedPosition = 0;
int shadeActualPosition = 0;
bool shadeAutoControl = true;
unsigned long lastShadeUpdate = 0;
#define SHADE_UPDATE_INTERVAL 1000
bool wifiConnected = false;
bool autoConnecting = false;
String wifiSSID = "";
String wifiIP = "";
int wifiRSSI = 0;
unsigned long lastWifiCheck = 0;
#define WIFI_CHECK_INTERVAL 10000
unsigned long lastStatusUpdate = 0;
#define STATUS_UPDATE_INTERVAL 2000
unsigned long messageTimeout = 0;
bool showSyncMessage = false;
bool mcpInitialized = false;
unsigned long lastMCPUpdate = 0;
#define MCP_UPDATE_INTERVAL 100
uint16_t lastGPIOState = 0;
bool rtcInitialized = false;
String currentTime = "";
String currentDate = "";
unsigned long lastRTCUpdate = 0;
#define RTC_UPDATE_INTERVAL 1000
// ==================== DVO-NIVOJSKA ZGODOVINA VLAGE TAL ====================
#define SCREEN_HISTORY_SIZE 240
#define ARCHIVE_HISTORY_SIZE 365
float screenSoilHistory[SCREEN_HISTORY_SIZE] = { 0 };
int screenHistoryIndex = 0;
unsigned long lastScreenHistoryUpdate = 0;
#define SCREEN_HISTORY_INTERVAL 10800000
struct DailyRecord {
uint32_t date;
float avgMoisture;
float minMoisture;
float maxMoisture;
int samples;
};
DailyRecord yearArchive[ARCHIVE_HISTORY_SIZE] = { 0 };
int archiveIndex = 0;
unsigned long currentDay = 0;
float dailySum = 0;
float dailyMin = 100;
float dailyMax = 0;
int dailyCount = 0;
volatile unsigned long pulseCount1 = 0;
volatile unsigned long pulseCount2 = 0;
volatile unsigned long pulseCount3 = 0;
float flowRate1 = 0.0;
float flowRate2 = 0.0;
float flowRate3 = 0.0;
float totalFlow1 = 0.0;
float totalFlow2 = 0.0;
float totalFlow3 = 0.0;
unsigned long oldTime = 0;
unsigned long lastFlowUpdate = 0;
bool ntpSynchronized = false;
unsigned long lastNTPSync = 0;
#define NTP_SYNC_INTERVAL 3600000
bool relayStates[RELAY_COUNT] = { false, false, false, false, false, false, false, false, false };
unsigned long lastRelayUpdate = 0;
#define RELAY_UPDATE_INTERVAL 500
bool relayControlEnabled = true;
float targetTempMin = 18.0;
float targetTempMax = 25.0;
float targetHumMin = 40.0;
float targetHumMax = 60.0;
bool autoControlEnabled = true;
unsigned long lastControlCheck = 0;
#define CONTROL_CHECK_INTERVAL 5000
// WIFI OMREŽJA IN GESLA
const char* wifiNetworks[MAX_WIFI_NETWORKS][2] = {
{ "WiFi-192", "sijuan5768" },
{ "", "" },
{ "", "" },
{ "", "" },
{ "", "" }
};
// ==================== SPREMENLJIVKE ZA OPOZORILA ====================
#define WARNING_THRESHOLD_PERCENT 15
#define WARNING_DISPLAY_TIME 5000
#define WARNING_BLINK_INTERVAL 500
struct WarningStatus {
bool soilWarning;
bool tempWarning;
bool humWarning;
unsigned long soilWarningStart;
unsigned long tempWarningStart;
unsigned long humWarningStart;
float soilValue;
float soilMin;
float soilMax;
float tempValue;
float tempMin;
float tempMax;
float humValue;
float humMin;
float humMax;
};
WarningStatus warnings = { false, false, false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
unsigned long lastWarningBlink = 0;
bool warningBlinkState = false;
// Stanje aplikacije
enum AppState {
STATE_MAIN_MENU,
STATE_WIFI_SETUP,
STATE_WIFI_CONNECTING,
STATE_WIFI_CONNECTED,
STATE_MCP23017_TEST,
STATE_RTC_INFO,
STATE_INTERNAL_SENSORS_INFO,
STATE_EXTERNAL_SENSORS_INFO,
STATE_SOIL_MOISTURE_INFO,
STATE_FLOW_SENSORS_INFO,
STATE_RELAY_CONTROL,
STATE_BOOTING,
STATE_HOME_SCREEN,
STATE_TEMP_HUM_CONTROL,
STATE_SETTINGS,
STATE_SHADE_CONTROL,
STATE_LIGHTING_CONTROL,
STATE_EDIT_LIGHTING_BLOCK,
STATE_LIGHT_AUTO_CONTROL,
STATE_INTERNAL_GRAPH_48H,
STATE_EXTERNAL_GRAPH_48H,
STATE_EDIT_LIGHT_TIME,
STATE_VENTILATION_CONTROL,
STATE_EDIT_VENTILATION,
STATE_SD_MANAGEMENT,
STATE_MODULE2_INFO,
STATE_TROPICAL_PLANTS,
STATE_ADD_PLANT,
STATE_PLANT_DETAILS,
STATE_SELECT_PLANT_FOR_VIEW,
STATE_SELECT_PLANT_FOR_ADVICE,
STATE_YEAR_ARCHIVE,
STATE_SOIL_MOISTURE_GRAPH,
STATE_AIR_QUALITY,
STATE_LDR_CALIBRATION,
STATE_MODULE4_INFO
};
AppState currentState = STATE_BOOTING;
AppState lastState = STATE_BOOTING;
unsigned long lastStateChangeTime = 0;
#define STATE_CHANGE_DEBOUNCE 500
uint16_t lastMCPTestState = 0xFFFF;
unsigned long lastMCPDisplayTime = 0;
#define MCP_DISPLAY_MIN_INTERVAL 300
int lastActivePinsCount = -1;
unsigned long systemInfoStartTime = 0;
#define SYSTEM_INFO_TIMEOUT 15000
Preferences preferences;
// ==================== SPREMENLJIVKE ZA DODAJANJE RASTLIN ====================
String tempPlantName = "";
int tempSpeciesIndex = 0;
bool tempIsVariegated = false;
int tempLocationZone = 1;
bool editingName = false;
String nameInputBuffer = "";
// ==================== SISTEM ZA VEČPLASTNO RISANJE DOMAČEGA ZASLONA ====================
bool homeScreenBackgroundDrawn = false;
bool homeScreenStaticElementsDrawn = false;
float lastDrawnInternalTemp = -999;
float lastDrawnInternalHum = -999;
float lastDrawnSoilPercent = -999;
float lastDrawnExternalTemp = -999;
float lastDrawnExternalHum = -999;
float lastDrawnInternalLux = -999;
float lastDrawnExternalLux = -999;
int lastDrawnShadeCalcPos = -999;
int lastDrawnShadeActualPos = -999;
bool lastDrawnRelays[RELAY_COUNT];
bool lastDrawnModule1Active = false;
bool lastDrawnModule2Active = false;
bool lastDrawnModule3Active = false;
bool lastDrawnVentState = false;
TrendDirection lastDrawnTempTrend = TREND_NONE;
TrendDirection lastDrawnHumTrend = TREND_NONE;
unsigned long lastHomeScreenRedraw = 0;
unsigned long lastHomeScreenPartialUpdate = 0;
float lastDrawnExternalLuxBar = -999;
// ==================== DEKLARACIJE FUNKCIJ ====================
template<typename T, typename U>
T myMin(T a, U b) {
return (a < b) ? a : static_cast<T>(b);
}
template<typename T, typename U>
T myMax(T a, U b) {
return (a > b) ? a : static_cast<T>(b);
}
uint16_t rgbTo565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
// ==================== TREND TRACKING ====================
#define TREND_HISTORY_SIZE 900
#define TREND_THRESHOLD 0.1
#define TREND_TIMEOUT 900000
float tempHistory[TREND_HISTORY_SIZE];
float humHistory[TREND_HISTORY_SIZE];
int historyIndex = 0;
unsigned long lastHistoryUpdate = 0;
#define HISTORY_UPDATE_INTERVAL 1000
TrendDirection tempTrend = TREND_NONE;
TrendDirection humTrend = TREND_NONE;
unsigned long lastTrendChangeTime = 0;
float lastTrendTemp = 0.0;
float lastTrendHum = 0.0;
// Barve
#define DARK_GRAY rgbTo565(64, 64, 64)
#define HIGHLIGHT_GREEN rgbTo565(0, 255, 0)
#define MCP_INPUT_COLOR rgbTo565(100, 200, 100)
#define MCP_OUTPUT_COLOR rgbTo565(200, 100, 100)
#define MCP_ACTIVE_COLOR rgbTo565(255, 255, 0)
#define TEMP_COLOR rgbTo565(255, 100, 100)
#define HUMIDITY_COLOR rgbTo565(100, 150, 255)
#define PRESSURE_COLOR rgbTo565(100, 255, 100)
#define INTERNAL_COLOR rgbTo565(255, 150, 50)
#define EXTERNAL_COLOR rgbTo565(50, 150, 255)
#define LIGHT_COLOR rgbTo565(255, 255, 100)
#define SOIL_COLOR rgbTo565(139, 69, 19)
#define WIFI_STRONG_COLOR rgbTo565(0, 255, 0)
#define WIFI_MEDIUM_COLOR rgbTo565(255, 255, 0)
#define WIFI_WEAK_COLOR rgbTo565(255, 100, 0)
#define WIFI_NONE_COLOR rgbTo565(255, 50, 50)
#define FLOW_COLOR_1 rgbTo565(255, 100, 100)
#define FLOW_COLOR_2 rgbTo565(100, 255, 100)
#define FLOW_COLOR_3 rgbTo565(100, 100, 255)
#define RELAY_ON_COLOR rgbTo565(255, 50, 50)
#define RELAY_OFF_COLOR rgbTo565(50, 200, 50)
#define RELAY_DISABLED_COLOR rgbTo565(100, 100, 100)
#define CALIBRATION_COLOR rgbTo565(255, 165, 0)
#define NORMAL_COLOR rgbTo565(50, 200, 50)
#define TOO_LOW_COLOR rgbTo565(50, 100, 255)
#define TOO_HIGH_COLOR rgbTo565(255, 50, 50)
#define WARNING_COLOR rgbTo565(255, 150, 50)
#define PROGRESS_BG_COLOR rgbTo565(100, 100, 100)
#define PROGRESS_FG_COLOR rgbTo565(0, 150, 255)
#define TARGET_RANGE_COLOR rgbTo565(255, 200, 50)
#define EXT_TEMP_CIRCLE_COLOR rgbTo565(255, 100, 100)
#define EXT_TEMP_PROGRESS_COLOR rgbTo565(200, 50, 50)
#define EXT_HUM_CIRCLE_COLOR rgbTo565(100, 150, 255)
#define EXT_HUM_PROGRESS_COLOR rgbTo565(50, 100, 200)
#define SOIL_CIRCLE_COLOR rgbTo565(139, 69, 19)
#define SOIL_PROGRESS_COLOR rgbTo565(100, 50, 10)
#define ARCHIVE_COLOR rgbTo565(100, 150, 200)
#define YELLOW_COLOR 0xFFE0
// ==================== DEKLARACIJE FUNKCIJ ====================
void showSDCardManagementScreen();
void handleSDCardManagementTouch(int x, int y);
bool initializeSDCard();
void createSDStructure();
void logDataToSD();
void saveSettingsToSD();
void saveHistoryToSD();
void loadSettingsFromSD();
void toggleStorageMode();
void testSDHardware();
void sendModuleCommand(uint8_t moduleId, int command, float param1, float param2);
void sendModule1Command(int command, float param1, float param2);
void sendModule2Command(int command, float param1, float param2);
void setModule2Relay(int relayNum, bool state);
void markSettingsChanged();
void clearSettingsChangedIndicator();
void initializeGraphHistory();
void initializeExternalGraphHistory();
void updateExternalGraphHistory();
void updateGraphHistory();
void saveAllSettings(bool isShutdown = false);
void resetToDefaultSettings();
void loadAllSettings(bool isBoot = false);
void drawVentilationSymbol(int x, int y, bool isOn, bool isLarge = false);
void drawElegantVentilationIndicator(int x, int y, bool isOn);
void updateVentilationControl();
void drawDayNightIndicator(int x, int y, bool isDayMode);
void showVentilationControlScreen();
void handleVentilationControlTouch(int x, int y);
void loadVentilationSettings();
void checkSettingsIntegrity();
void fillArc(int x, int y, int start_angle, int end_angle, int inner_r, int outer_r, uint16_t color);
void initializeTimeBlocks();
void printLightingSettings();
bool isWithinTimeWindow();
bool isTimeInBlock(int blockIndex);
bool shouldRelaysBeOn();
void checkAndControlLighting();
void showLightAutoControlScreen();
void handleLightAutoControlTouch(int x, int y);
void handleEditLightTimeTouch(int x, int y);
void showEditLightTimeScreen();
void showLightingControlScreen();
void drawSimpleButton(int x, int y, int w, int h, uint16_t color, const char* label);
void showEditLightingBlockScreen(int blockIndex);
void drawWideButton(int x, int y, int w, int h, uint16_t color, const char* label);
void drawCompactButton(int x, int y, int w, int h, uint16_t color, const char* label, bool highlight);
String getNextLightingEvent();
void drawRelayStatusIndicator(int x, int y, bool isOn);
void handleLightingControlTouch(int x, int y);
void handleEditLightingBlockTouch(int x, int y);
void showShadeControlScreen();
void handleShadeControlTouch(int x, int y);
void drawClockwiseProgressRing(int x, int y, int outerRadius, int innerRadius, float currentValue, float minValue, float maxValue, uint16_t ringColor);
void drawClockwiseDoubleRing(int x, int y, int outerRadius, int middleRadius, int innerRadius, float currentValue, float minValue, float maxValue, float targetMin, float targetMax, uint16_t currentColor);
void autoControlRelays();
void drawHorizontalProgressBarWithLabel(int x, int y, int width, int height, int value, const char* label, uint16_t color);
void drawHomeScreenBackground();
void drawHomeScreenStaticElements();
void updateHomeScreenDynamicValues();
void showHomeScreen();
void handleHomeScreenTouch(int x, int y);
void showTempHumControlScreen();
void drawCenteredButtonWithText(int x, int y, int w, int h, uint16_t color, const char* text);
void handleTempHumControlTouch(int x, int y);
void initializeRelays();
void loadRelayStates();
void setRelay(int relayNum, bool state);
void toggleRelay(int relayNum);
void setAllRelays(bool state);
void IRAM_ATTR pulseCounter1();
void IRAM_ATTR pulseCounter2();
void IRAM_ATTR pulseCounter3();
void initializeFlowSensors();
void updateFlowSensors();
void resetFlowTotals();
void initializeMotor();
void calculateShadePosition();
void updateMotorPosition();
void setMotorPosition(int position);
void scanI2CDevices();
void updateSoilMoisture();
String getSoilMoistureString();
void initializeRTC();
void updateRTC();
String getRTCTemperature();
void showRTCInfo();
void handleRTCInfoTouch(int x, int y);
void saveSettingsOnShutdown();
void loadSettingsOnBoot();
void showInternalGraph48hScreen();
void drawThickLine(int x1, int y1, int x2, int y2, int thickness, uint16_t color);
void showExternalGraph48hScreen();
void saveExternalGraphHistory();
void showModule2Info();
void handleModule2InfoTouch(int x, int y);
void diagnoseESPNow();
void diagnoseSDCard();
void diagnoseSDPins();
void testSDCard();
void setupWatchdogForShutdown();
void emergencySaveOnPowerLoss();
void testPinsBeforeSD();
void debugSDCard();
void listSDFiles();
void autoSaveSettings();
void emergencySaveSettings();
void saveRelayStates();
void redrawCurrentScreen();
void updateTrendHistory();
void calculateTrends();
void drawTrendArrow(int x, int y, int direction, uint16_t color);
void initializeMCP23017();
void updateMCP23017();
void showBootScreen();
void drawHighlightedTitle(int x, int y, const char* text, int textSize);
void drawStatusBar();
void drawCompactWiFiIcon(int x, int y);
void drawCompactWiFiBars(int x, int y, int rssi);
uint16_t getSignalColor(int rssi);
void updateStatusBar();
void drawGradientBackground();
uint16_t blendColor(uint16_t color1, uint16_t color2, uint8_t ratio);
void drawModernButton(int x, int y, int w, int h, uint16_t baseColor, const char* label, const char* iconType);
void highlightButton(int x, int y, int w, int h, bool highlight);
void drawMenuButtonWithoutIcon(int x, int y, int w, int h, uint16_t color, const char* label);
void showMainMenu();
void handleMainMenuTouch(int x, int y);
void showWiFiQuickInfo();
void showRelayControl();
void handleRelayControlTouch(int x, int y);
void showFlowSensorsInfo();
void handleFlowSensorsTouch(int x, int y);
void showFlowGraphScreen();
void showSoilMoistureInfo();
void handleSoilMoistureTouch(int x, int y);
void saveSoilHistory();
void showSoilMoistureGraphScreen();
void calibrateSoilSensor();
void showMCP23017Test();
void drawAllMCPPins();
void displayMCP23017Pins();
void updateActivePinsCount(uint16_t gpioState);
void handleMCP23017Test();
void handleMCP23017TestTouch(int x, int y);
void showInternalSensorsInfo();
void handleInternalSensorsTouch(int x, int y);
void handleInternalGraph48hTouch(int x, int y);
void showExternalSensorsInfo();
void handleExternalGraph48hTouch(int x, int y);
void handleExternalSensorsTouch(int x, int y);
void setupWiFi();
void autoConnectToWiFi();
void checkWiFiStatus();
void connectToWiFi();
void handleWiFiConnecting();
void showWiFiConnectedScreen();
void showWiFiFailedScreen();
void handleWiFiConnectedTouch(int x, int y);
void scanWiFiNetworks();
void drawWiFiBars(int x, int y, int rssi);
void handleTouch();
void onModuleDataRecv(const uint8_t* mac, const uint8_t* incomingData, int len);
void sendShadeCommand(int command, float param1, float param2);
void moveShadeTo(int position);
void calibrateShade();
void stopShade();
void emergencyStopShade();
void setShadeSpeed(int speed);
void setShadeAutoMode(bool autoMode);
void initESPNow();
void onDataSent(const uint8_t* mac_addr, esp_now_send_status_t status);
void checkModulesTimeout();
void showSelectPlantForViewScreen();
void showSelectPlantForAdviceScreen();
bool haveValuesChanged();
void showYearArchiveScreen();
void saveYearArchiveToSD();
void loadYearArchiveFromSD();
void drawHamburgerMenu(int x, int y, int size);
void drawWarningBar();
void checkWarnings();
void showAdvicePopup(String advice);
void deletePlant(int plantId);
void showPlantDetailsScreen(int plantId);
void showAddPlantScreen();
void showTropicalPlantsMenu();
int findPlantSpeciesIndex(String species);
float calculateVPD(float temperature, float humidity);
String getVPDDescription(float vpd);
uint16_t getVPDColor(float vpd);
void updateVPD();
int addPlantToCollection(String name, String species, bool isVariegated, String location);
void autoConfigureForPlant(int speciesIndex);
void showMessage(String message, uint16_t color);
void savePlantCollection();
void loadPlantCollection();
void controlVPDForPlants();
void startMistingForPlant(int plantId);
void startBottomWatering(int plantId);
void updateAirMovement();
void checkMistingTimeout();
String getPlantCareAdvice(int plantId);
void showCurrentSettings();
void showAirQualityScreen();
void showModule4Info();
void forceFullHomeScreenRedraw();
void animateHamburgerMenu();
void showTemporaryMessage(String message, uint16_t color, int durationMs);
void updateHomeScreenRelays();
// ==================== GLOBALNE SPREMENLJIVKE ZA SHRANJEVANJE ====================
bool settingsChangedFlag = false;
unsigned long lastSettingsChangeTime = 0;
unsigned long lastAutoSaveTime = 0;
// ==================== KONSOLIDIRANO SHRANJEVANJE VSEH NASTAVITEV ====================
void markSettingsChanged() {
settingsChangedFlag = true;
lastSettingsChangeTime = millis();
if (currentState != STATE_BOOTING) {
tft.fillRect(470, 5, 6, 6, rgbTo565(255, 200, 0));
}
Serial.println("✓ Nastavitve spremenjene - oznaceno za shranjevanje");
}
void clearSettingsChangedIndicator() {
settingsChangedFlag = false;
if (currentState != STATE_BOOTING) {
tft.fillRect(470, 5, 6, 6, STATUS_BAR_COLOR);
}
Serial.println("✓ Indikator sprememb pociscen");
}
void initializeGraphHistory() {
for (int i = 0; i < GRAPH_HISTORY_SIZE; i++) {
tempHistory48h[i] = NAN;
humHistory48h[i] = NAN;
luxHistory48h[i] = NAN;
timeStamps48h[i] = 0;
}
Serial.println("48-urna zgodovina meritev inicializirana");
}
void initializeExternalGraphHistory() {
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
extTempHistory48h[i] = NAN;
extHumHistory48h[i] = NAN;
extLuxHistory48h[i] = NAN;
extPressureHistory48h[i] = NAN;
extTimeStamps48h[i] = 0;
}
Serial.println("48-urna zgodovina zunanjih meritev inicializirana");
}
void updateExternalGraphHistory() {
unsigned long currentTime = millis();
if (currentTime - lastExtGraphUpdate > GRAPH_UPDATE_INTERVAL) {
bool hasValidData = false;
if (!isnan(externalTemperature) && externalTemperature > -50 && externalTemperature < 100) {
hasValidData = true;
}
if (!isnan(externalHumidity) && externalHumidity >= 0 && externalHumidity <= 100) {
hasValidData = true;
}
if (externalLux > 0 && externalLux < 100000) {
hasValidData = true;
}
if (hasValidData) {
extTempHistory48h[extGraphHistoryIndex] = isnan(externalTemperature) ? 0 : externalTemperature;
extHumHistory48h[extGraphHistoryIndex] = isnan(externalHumidity) ? 0 : externalHumidity;
extLuxHistory48h[extGraphHistoryIndex] = (externalLux < 0 || externalLux > 100000) ? 0 : externalLux;
if (!isnan(externalPressure)) {
extPressureHistory48h[extGraphHistoryIndex] = externalPressure;
} else {
extPressureHistory48h[extGraphHistoryIndex] = 0;
}
extTimeStamps48h[extGraphHistoryIndex] = currentTime;
static unsigned long lastDebugSave = 0;
if (currentTime - lastDebugSave > 30000) {
Serial.printf("📊 SHRANJEVANJE: Index=%d, Čas=%lu, T=%.1f°C, H=%.1f%%, Lux=%.1f\n",
extGraphHistoryIndex, currentTime,
externalTemperature, externalHumidity, externalLux);
lastDebugSave = currentTime;
}
extGraphHistoryIndex = (extGraphHistoryIndex + 1) % EXTERNAL_GRAPH_HISTORY_SIZE;
if (extGraphHistoryIndex % 10 == 0) {
Serial.println("💾 Shranjujem zgodovino v FLASH...");
preferences.begin("ext-graph-data", false);
preferences.putInt("extGraphIndex", extGraphHistoryIndex);
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
preferences.putFloat(("extTemp" + String(i)).c_str(), extTempHistory48h[i]);
preferences.putFloat(("extHum" + String(i)).c_str(), extHumHistory48h[i]);
preferences.putULong(("extTime" + String(i)).c_str(), extTimeStamps48h[i]);
}
preferences.end();
}
} else {
static unsigned long lastNoDataPrint = 0;
if (currentTime - lastNoDataPrint > 60000) {
Serial.printf("⚠️ NI PODATKOV ZA SHRANJEVANJE: T=%.1f, H=%.1f, Lux=%.1f, modul1Active=%d\n",
externalTemperature, externalHumidity, externalLux, module1Active);
lastNoDataPrint = currentTime;
}
}
lastExtGraphUpdate = currentTime;
}
}
void updateGraphHistory() {
unsigned long currentTime = millis();
if (currentTime - lastGraphUpdate > GRAPH_UPDATE_INTERVAL) {
if (!isnan(internalTemperature) && !isnan(internalHumidity)) {
tempHistory48h[graphHistoryIndex] = internalTemperature;
humHistory48h[graphHistoryIndex] = internalHumidity;
luxHistory48h[graphHistoryIndex] = ldrInternalLux;
timeStamps48h[graphHistoryIndex] = currentTime;
static unsigned long lastDebugSave = 0;
if (currentTime - lastDebugSave > 30000) {
Serial.printf("📊 NOTRANJE: Index=%d, T=%.1f°C, H=%.1f%%, Lux=%.1f\n",
graphHistoryIndex, internalTemperature, internalHumidity, ldrInternalLux);
lastDebugSave = currentTime;
}
graphHistoryIndex = (graphHistoryIndex + 1) % GRAPH_HISTORY_SIZE;
if (graphHistoryIndex % 10 == 0) {
Serial.println("💾 Shranjujem notranjo zgodovino v FLASH...");
preferences.begin("graph-data", false);
preferences.putInt("graphIndex", graphHistoryIndex);
for (int i = 0; i < GRAPH_HISTORY_SIZE; i++) {
preferences.putFloat(("temp" + String(i)).c_str(), tempHistory48h[i]);
preferences.putFloat(("hum" + String(i)).c_str(), humHistory48h[i]);
preferences.putFloat(("lux" + String(i)).c_str(), luxHistory48h[i]);
preferences.putULong(("time" + String(i)).c_str(), timeStamps48h[i]);
}
preferences.end();
}
}
lastGraphUpdate = currentTime;
}
}
void saveAllSettings(bool isShutdown) {
Serial.println(isShutdown ? "\n=== SHRANJEVANJE NASTAVITEV OB IZKLOPU ===" : "\n=== SHRANJEVANJE NASTAVITEV ===");
if (!preferences.begin("system-settings", false)) {
Serial.println("Napaka pri odpiranju preferences! Poskusim znova...");
delay(100);
if (!preferences.begin("system-settings", false)) {
Serial.println("Napaka pri odpiranju preferences! Shranjevanje preskoceno.");
return;
}
}
try {
preferences.putFloat("tempMin", targetTempMin);
preferences.putFloat("tempMax", targetTempMax);
preferences.putFloat("humMin", targetHumMin);
preferences.putFloat("humMax", targetHumMax);
preferences.putBool("autoControl", autoControlEnabled);
preferences.putFloat("shadeMinLux", shadeMinLux);
preferences.putFloat("shadeMaxLux", shadeMaxLux);
preferences.putBool("shadeAuto", shadeAutoControl);
preferences.putInt("shadePos", shadeActualPosition);
preferences.putBool("lightAuto", lightAutoMode);
preferences.putBool("lightManual", lightManualOverride);
preferences.putFloat("lightOn", lightOnThreshold);
preferences.putFloat("lightOff", lightOffThreshold);
preferences.putBool("lightTime", lightTimeControlEnabled);
preferences.putInt("lightSH", lightStartHour);
preferences.putInt("lightSM", lightStartMinute);
preferences.putInt("lightEH", lightEndHour);
preferences.putInt("lightEM", lightEndMinute);
int maxRelaysToSave = (RELAY_COUNT < 4) ? RELAY_COUNT : 4;
for (int i = 0; i < maxRelaysToSave; i++) {
preferences.putBool(("relay" + String(i)).c_str(), relayStates[i]);
}
int maxBlocksToSave = (MAX_TIME_BLOCKS < 4) ? MAX_TIME_BLOCKS : 4;
for (int i = 0; i < maxBlocksToSave; i++) {
String blockKey = "tb" + String(i);
preferences.putBool((blockKey + "_e").c_str(), timeBlocks[i].enabled);
preferences.putBool((blockKey + "_on").c_str(), timeBlocks[i].relayOn);
preferences.putInt((blockKey + "_sh").c_str(), timeBlocks[i].startHour);
preferences.putInt((blockKey + "_sm").c_str(), timeBlocks[i].startMinute);
preferences.putInt((blockKey + "_eh").c_str(), timeBlocks[i].endHour);
preferences.putInt((blockKey + "_em").c_str(), timeBlocks[i].endMinute);
uint8_t daysBits = 0;
int maxDaysToSave = (DAYS_IN_WEEK < 7) ? DAYS_IN_WEEK : 7;
for (int d = 0; d < maxDaysToSave; d++) {
if (timeBlocks[i].days[d]) {
daysBits |= (1 << d);
}
}
preferences.putUChar((blockKey + "_d").c_str(), daysBits);
}
preferences.putBool("ventAuto", ventilationAutoMode);
preferences.putFloat("ventDayT", ventilationDayTempThreshold);
preferences.putFloat("ventDayH", ventilationDayHumThreshold);
preferences.putFloat("ventNightT", ventilationNightTempThreshold);
preferences.putFloat("ventNightH", ventilationNightHumThreshold);
preferences.putInt("ventDayN", dayNightSwitchHour);
preferences.putInt("ventNightD", nightDaySwitchHour);
preferences.putInt("soilDry", SOIL_DRY_VALUE);
preferences.putInt("soilWet", SOIL_WET_VALUE);
preferences.putULong("lastSave", millis());
preferences.end();
Serial.println("=== NASTAVITVE SHRANJENE ===");
settingsChangedFlag = false;
} catch (...) {
Serial.println("Napaka pri shranjevanju nastavitev!");
preferences.end();
}
}
void resetToDefaultSettings() {
Serial.println("RESET na privzete nastavitve!");
targetTempMin = 18.0;
targetTempMax = 25.0;
targetHumMin = 40.0;
targetHumMax = 60.0;
autoControlEnabled = true;
shadeMinLux = 0.0;
shadeMaxLux = 5000.0;
shadeAutoControl = true;
shadeActualPosition = 0;
lightAutoMode = true;
lightManualOverride = false;
lightOnThreshold = 50.0;
lightOffThreshold = 1000.0;
lightTimeControlEnabled = false;
lightStartHour = 18;
lightStartMinute = 0;
lightEndHour = 22;
lightEndMinute = 0;
lightingAutoMode = true;
lightingManualOverride = false;
for (int i = 0; i < RELAY_COUNT; i++) {
relayStates[i] = false;
}
relayControlEnabled = true;
SOIL_DRY_VALUE = 4095;
SOIL_WET_VALUE = 1500;
totalFlow1 = 0.0;
totalFlow2 = 0.0;
totalFlow3 = 0.0;
ventilationAutoMode = true;
ventilationManualOverride = false;
ventilationDayTempThreshold = 28.0;
ventilationDayHumThreshold = 70.0;
ventilationNightTempThreshold = 22.0;
ventilationNightHumThreshold = 75.0;
ventilationDayMinRuntime = 5;
ventilationNightMinRuntime = 10;
ventilationDayCooldown = 10;
ventilationNightCooldown = 20;
dayNightSwitchHour = 22;
nightDaySwitchHour = 6;
ntpSynchronized = false;
Serial.println("Privzete nastavitve nastavljene!");
}
void loadAllSettings(bool isBoot) {
Serial.println(isBoot ? "\n=== NALAGANJE NASTAVITEV OB ZAGONU ===" : "\n=== NALAGANJE NASTAVITEV ===");
if (!preferences.begin("system-settings", true)) {
Serial.println("Napaka pri odpiranju preferences! Uporabljam privzete vrednosti.");
resetToDefaultSettings();
return;
}
targetTempMin = preferences.getFloat("tempMin", 18.0);
targetTempMax = preferences.getFloat("tempMax", 25.0);
targetHumMin = preferences.getFloat("humMin", 40.0);
targetHumMax = preferences.getFloat("humMax", 60.0);
autoControlEnabled = preferences.getBool("autoControl", true);
shadeMinLux = preferences.getFloat("shadeMinLux", 0.0);
shadeMaxLux = preferences.getFloat("shadeMaxLux", 5000.0);
shadeAutoControl = preferences.getBool("shadeAuto", true);
shadeActualPosition = preferences.getInt("shadePos", 0);
lightAutoMode = preferences.getBool("lightAuto", true);
lightManualOverride = preferences.getBool("lightManual", false);
lightOnThreshold = preferences.getFloat("lightOn", 50.0);
lightOffThreshold = preferences.getFloat("lightOff", 1000.0);
lightTimeControlEnabled = preferences.getBool("lightTime", false);
lightStartHour = preferences.getInt("lightSH", 18);
lightStartMinute = preferences.getInt("lightSM", 0);
lightEndHour = preferences.getInt("lightEH", 22);
lightEndMinute = preferences.getInt("lightEM", 0);
for (int i = 0; i < min(4, RELAY_COUNT); i++) {
relayStates[i] = preferences.getBool(("relay" + String(i)).c_str(), false);
}
for (int i = 0; i < min(4, MAX_TIME_BLOCKS); i++) {
String blockKey = "tb" + String(i);
timeBlocks[i].enabled = preferences.getBool((blockKey + "_e").c_str(), false);
timeBlocks[i].relayOn = preferences.getBool((blockKey + "_on").c_str(), true);
timeBlocks[i].startHour = preferences.getInt((blockKey + "_sh").c_str(), 18);
timeBlocks[i].startMinute = preferences.getInt((blockKey + "_sm").c_str(), 0);
timeBlocks[i].endHour = preferences.getInt((blockKey + "_eh").c_str(), 22);
timeBlocks[i].endMinute = preferences.getInt((blockKey + "_em").c_str(), 0);
uint8_t daysBits = preferences.getUChar((blockKey + "_d").c_str(), 0);
for (int d = 0; d < min(7, DAYS_IN_WEEK); d++) {
timeBlocks[i].days[d] = (daysBits & (1 << d)) != 0;
}
if (isBoot && !timeBlocks[i].description.startsWith("Blok")) {
timeBlocks[i].description = "Blok " + String(i + 1);
}
}
ventilationAutoMode = preferences.getBool("ventAuto", true);
ventilationDayTempThreshold = preferences.getFloat("ventDayT", 28.0);
ventilationDayHumThreshold = preferences.getFloat("ventDayH", 70.0);
ventilationNightTempThreshold = preferences.getFloat("ventNightT", 22.0);
ventilationNightHumThreshold = preferences.getFloat("ventNightH", 75.0);
dayNightSwitchHour = preferences.getInt("ventDayN", 22);
nightDaySwitchHour = preferences.getInt("ventNightD", 6);
SOIL_DRY_VALUE = preferences.getInt("soilDry", 4095);
SOIL_WET_VALUE = preferences.getInt("soilWet", 1500);
preferences.end();
Serial.println("=== NASTAVITVE NALOZENE ===");
if (isBoot) {
initializeTimeBlocks();
}
}
void drawVentilationSymbol(int x, int y, bool isOn, bool isLarge) {
int size = isLarge ? 24 : 16;
for (int r = size; r >= 0; r -= 2) {
uint16_t color;
if (isOn) {
uint8_t blue = 200 + (55 * r / size);
uint8_t green = 150 + (105 * r / size);
color = rgbTo565(0, green, blue);
} else {
uint8_t gray = 80 + (40 * r / size);
color = rgbTo565(gray, gray, gray);
}
tft.drawCircle(x, y, r, color);
}
tft.drawCircle(x, y, size, isOn ? rgbTo565(0, 200, 255) : rgbTo565(120, 120, 140));
int rotation = isOn ? (millis() / 80) % 360 : 0;
for (int i = 0; i < 3; i++) {
float angle = i * 120 + rotation;
float rad = angle * PI / 180.0;
int x1 = x + (size / 3) * cos(rad);
int y1 = y + (size / 3) * sin(rad);
int x2 = x + (size - 3) * cos(rad);
int y2 = y + (size - 3) * sin(rad);
uint16_t bladeColor = isOn ? rgbTo565(0, 220, 255) : rgbTo565(150, 150, 170);
tft.drawLine(x1, y1, x2, y2, bladeColor);
tft.drawLine(x1 + 1, y1, x2 + 1, y2, bladeColor);
tft.drawLine(x1, y1 + 1, x2, y2 + 1, bladeColor);
tft.fillCircle(x2, y2, 2, bladeColor);
}
int centerSize = size / 4;
tft.fillCircle(x, y, centerSize, isOn ? rgbTo565(0, 180, 240) : rgbTo565(100, 100, 120));
tft.fillCircle(x - centerSize / 3, y - centerSize / 3, centerSize / 3,
isOn ? rgbTo565(100, 220, 255) : rgbTo565(140, 140, 160));
}
void drawElegantVentilationIndicator(int x, int y, bool isOn) {
int indicatorSize = 32;
tft.fillRoundRect(x - indicatorSize / 2, y - indicatorSize / 2, indicatorSize, indicatorSize, 6,
isOn ? rgbTo565(20, 60, 80) : rgbTo565(40, 40, 40));
tft.drawRoundRect(x - indicatorSize / 2, y - indicatorSize / 2, indicatorSize, indicatorSize, 6,
isOn ? rgbTo565(0, 150, 200) : rgbTo565(80, 80, 80));
drawVentilationSymbol(x, y, isOn, false);
tft.setTextSize(1);
tft.setTextColor(isOn ? rgbTo565(100, 255, 200) : rgbTo565(180, 180, 200));
String statusText = isOn ? "ON" : "OFF";
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds(statusText, 0, 0, &textX, &textY, &textW, &textH);
tft.setCursor(x - textW / 2, y + indicatorSize / 2 + 5);
tft.print(statusText);
}
void updateVentilationControl() {
if (!ventilationAutoMode || ventilationManualOverride) return;
static unsigned long lastVentilationCheck = 0;
static unsigned long ventilationStartTime = 0;
static bool ventilationRunning = false;
unsigned long currentTime = millis();
if (rtcInitialized) {
DateTime now = rtc.now();
int currentHour = now.hour();
bool isNightTime = false;
if (dayNightSwitchHour < nightDaySwitchHour) {
isNightTime = (currentHour >= dayNightSwitchHour || currentHour < nightDaySwitchHour);
} else {
isNightTime = (currentHour >= dayNightSwitchHour || currentHour < nightDaySwitchHour);
}
ventilationDayMode = !isNightTime;
}
if (currentTime - lastVentilationCheck > 60000) {
float currentTempThreshold = ventilationDayMode ? ventilationDayTempThreshold : ventilationNightTempThreshold;
float currentHumThreshold = ventilationDayMode ? ventilationDayHumThreshold : ventilationNightHumThreshold;
int currentMinRuntime = ventilationDayMode ? ventilationDayMinRuntime : ventilationNightMinRuntime;
bool shouldStart = false;
if (internalTemperature >= currentTempThreshold) {
shouldStart = true;
}
if (internalHumidity >= currentHumThreshold) {
shouldStart = true;
}
if (shouldStart && !ventilationRunning) {
setRelay(VENTILATION_RELAY, true);
ventilationRunning = true;
ventilationStartTime = currentTime;
}
if (ventilationRunning) {
unsigned long runtime = (currentTime - ventilationStartTime) / 60000;
if (runtime >= currentMinRuntime) {
setRelay(VENTILATION_RELAY, false);
ventilationRunning = false;
}
}
lastVentilationCheck = currentTime;
}
}
void drawDayNightIndicator(int x, int y, bool isDayMode) {
int size = 20;
if (isDayMode) {
tft.fillCircle(x, y, size, rgbTo565(255, 200, 0));
for (int i = 0; i < 8; i++) {
float angle = i * 45 * PI / 180;
int x1 = x + (size)*cos(angle);
int y1 = y + (size)*sin(angle);
int x2 = x + (size + 8) * cos(angle);
int y2 = y + (size + 8) * sin(angle);
tft.drawLine(x1, y1, x2, y2, rgbTo565(255, 200, 0));
}
tft.fillCircle(x - 5, y - 3, 2, ST77XX_BLACK);
tft.fillCircle(x + 5, y - 3, 2, ST77XX_BLACK);
for (int i = -3; i <= 3; i++) {
tft.fillCircle(x + i, y + 4, 1, ST77XX_BLACK);
}
} else {
tft.fillCircle(x, y, size, rgbTo565(200, 220, 255));
tft.fillCircle(x + 5, y - 5, 4, rgbTo565(180, 200, 240));
for (int i = 0; i < 4; i++) {
float angle = i * 90 * PI / 180;
int starX = x + (size + 12) * cos(angle);
int starY = y + (size + 12) * sin(angle);
tft.drawLine(starX - 2, starY, starX + 2, starY, rgbTo565(255, 255, 200));
tft.drawLine(starX, starY - 2, starX, starY + 2, rgbTo565(255, 255, 200));
}
tft.fillCircle(x - 5, y - 3, 2, ST77XX_BLACK);
tft.fillCircle(x + 5, y - 3, 2, ST77XX_BLACK);
for (int i = -2; i <= 2; i++) {
tft.fillCircle(x + i * 2, y + 7, 1, ST77XX_BLACK);
}
}
tft.setTextSize(1);
tft.setCursor(x - 3, y + size + 5);
tft.setTextColor(isDayMode ? rgbTo565(255, 150, 0) : rgbTo565(150, 180, 255));
tft.print(isDayMode ? "DAN" : "NOC");
}
// ========== POENOTENE FUNKCIJE ZA GUMBE ==========
void drawButton(ButtonConfig btn) {
if (btn.textSize == 0) btn.textSize = 1;
if (btn.style == BUTTON_DISABLED) btn.isActive = false;
uint16_t finalColor = btn.baseColor;
uint16_t shadowColor = rgbTo565(30, 30, 50);
uint16_t borderColor = ST77XX_WHITE;
uint16_t textColor = ST77XX_WHITE;
int cornerRadius = 8;
switch (btn.style) {
case BUTTON_NORMAL:
cornerRadius = 8;
break;
case BUTTON_COMPACT:
cornerRadius = 5;
shadowColor = rgbTo565(20, 20, 30);
break;
case BUTTON_WIDE:
cornerRadius = 10;
break;
case BUTTON_HIGHLIGHT:
if (btn.isActive) {
finalColor = rgbTo565(255, 255, 100);
textColor = ST77XX_BLACK;
}
break;
case BUTTON_WARNING:
finalColor = rgbTo565(245, 67, 54);
break;
case BUTTON_SUCCESS:
finalColor = rgbTo565(76, 175, 80);
break;
case BUTTON_INFO:
finalColor = rgbTo565(66, 135, 245);
break;
case BUTTON_DISABLED:
finalColor = rgbTo565(100, 100, 100);
textColor = rgbTo565(180, 180, 180);
borderColor = rgbTo565(150, 150, 150);
break;
}
if (btn.style != BUTTON_COMPACT) {
tft.fillRoundRect(btn.x + 2, btn.y + 2, btn.width, btn.height, cornerRadius, shadowColor);
} else {
tft.fillRoundRect(btn.x + 1, btn.y + 1, btn.width, btn.height, cornerRadius, shadowColor);
}
tft.fillRoundRect(btn.x, btn.y, btn.width, btn.height, cornerRadius, finalColor);
tft.drawRoundRect(btn.x, btn.y, btn.width, btn.height, cornerRadius, borderColor);
if (btn.textSize == 1) {
tft.setFont();
tft.setTextSize(1);
} else if (btn.textSize == 2) {
tft.setFont(&FreeSansBold12pt7b);
} else if (btn.textSize == 3) {
tft.setFont(&FreeSansBold18pt7b);
}
tft.setTextColor(textColor);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(btn.label, 0, 0, &x1, &y1, &w, &h);
int textX = btn.x + (btn.width - w) / 2 - x1;
int textY = btn.y + (btn.height - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print(btn.label);
if (btn.icon != nullptr && strlen(btn.icon) > 0) {
drawButtonIcon(btn.x, btn.y, btn.width, btn.height, btn.icon, textColor);
}
tft.setFont();
tft.setTextSize(1);
}
void drawButtonIcon(int btnX, int btnY, int btnW, int btnH, const char* icon, uint16_t color) {
int iconX = btnX + 10;
int iconY = btnY + btnH / 2;
if (strcmp(icon, "home") == 0) {
tft.fillTriangle(iconX, iconY + 5, iconX + 8, iconY - 8, iconX + 16, iconY + 5, color);
tft.fillRect(iconX + 4, iconY + 5, 8, 8, color);
}
else if (strcmp(icon, "back") == 0) {
for (int i = 0; i < 5; i++) {
tft.drawLine(iconX + i * 2, iconY - i, iconX + i * 2, iconY + i, color);
}
tft.drawLine(iconX, iconY, iconX + 10, iconY, color);
}
else if (strcmp(icon, "wifi") == 0) {
for (int r = 2; r <= 8; r += 2) {
for (int a = 180; a < 360; a += 20) {
int px = iconX + 8 + r * cos(a * PI / 180);
int py = iconY + r * sin(a * PI / 180);
tft.drawPixel(px, py, color);
}
}
tft.fillCircle(iconX + 8, iconY + 4, 2, color);
}
else if (strcmp(icon, "refresh") == 0) {
tft.drawCircle(iconX + 8, iconY, 6, color);
tft.fillTriangle(iconX + 14, iconY - 2, iconX + 16, iconY - 6, iconX + 18, iconY - 2, color);
}
else if (strcmp(icon, "save") == 0) {
tft.drawRect(iconX + 2, iconY - 6, 12, 12, color);
tft.fillRect(iconX + 6, iconY - 4, 4, 4, color);
}
else if (strcmp(icon, "plus") == 0) {
tft.drawLine(iconX + 8, iconY - 6, iconX + 8, iconY + 6, color);
tft.drawLine(iconX + 2, iconY, iconX + 14, iconY, color);
}
else if (strcmp(icon, "minus") == 0) {
tft.drawLine(iconX + 2, iconY, iconX + 14, iconY, color);
}
else if (strcmp(icon, "up") == 0) {
tft.fillTriangle(iconX + 8, iconY - 6, iconX + 2, iconY, iconX + 14, iconY, color);
}
else if (strcmp(icon, "down") == 0) {
tft.fillTriangle(iconX + 8, iconY + 6, iconX + 2, iconY, iconX + 14, iconY, color);
}
else if (strcmp(icon, "light") == 0) {
tft.fillCircle(iconX + 8, iconY - 2, 5, color);
tft.fillRect(iconX + 6, iconY + 2, 4, 4, color);
}
else if (strcmp(icon, "fan") == 0) {
tft.drawCircle(iconX + 8, iconY, 6, color);
for (int i = 0; i < 3; i++) {
float angle = i * 120 * PI / 180;
int x1 = iconX + 8 + 4 * cos(angle);
int y1 = iconY + 4 * sin(angle);
int x2 = iconX + 8 + 10 * cos(angle);
int y2 = iconY + 10 * sin(angle);
tft.drawLine(x1, y1, x2, y2, color);
}
}
else if (strcmp(icon, "info") == 0) {
tft.drawCircle(iconX + 8, iconY - 4, 2, color);
tft.fillRect(iconX + 7, iconY, 2, 6, color);
}
else if (strcmp(icon, "warning") == 0) {
tft.fillTriangle(iconX + 2, iconY + 6, iconX + 8, iconY - 6, iconX + 14, iconY + 6, color);
tft.fillCircle(iconX + 8, iconY + 2, 2, ST77XX_BLACK);
}
else if (strcmp(icon, "check") == 0) {
tft.drawLine(iconX + 2, iconY, iconX + 6, iconY + 4, color);
tft.drawLine(iconX + 6, iconY + 4, iconX + 14, iconY - 4, color);
}
else if (strcmp(icon, "cross") == 0) {
tft.drawLine(iconX + 2, iconY - 4, iconX + 14, iconY + 4, color);
tft.drawLine(iconX + 2, iconY + 4, iconX + 14, iconY - 4, color);
}
}
void drawNormalButton(int x, int y, int w, int h, uint16_t color, const char* label) {
ButtonConfig btn = {x, y, w, h, color, label, BUTTON_NORMAL, false, nullptr, 1};
drawButton(btn);
}
void drawCompactButton(int x, int y, int w, int h, uint16_t color, const char* label, bool highlight) {
ButtonStyle style = highlight ? BUTTON_HIGHLIGHT : BUTTON_COMPACT;
ButtonConfig btn = {x, y, w, h, color, label, style, highlight, nullptr, 1};
drawButton(btn);
}
void drawIconButton(int x, int y, int w, int h, uint16_t color, const char* label, const char* icon) {
ButtonConfig btn = {x, y, w, h, color, label, BUTTON_NORMAL, false, icon, 1};
drawButton(btn);
}
void drawMenuButton(int x, int y, int w, int h, uint16_t color, const char* label) {
ButtonConfig btn = {x, y, w, h, color, label, BUTTON_WIDE, false, nullptr, 1};
drawButton(btn);
}
void drawHighlightButton(int x, int y, int w, int h, uint16_t color, const char* label, bool active) {
ButtonConfig btn = {x, y, w, h, color, label, BUTTON_HIGHLIGHT, active, nullptr, 1};
drawButton(btn);
}
void drawTextButton(int x, int y, int w, int h, uint16_t color, const char* label, int textSize) {
ButtonConfig btn = {x, y, w, h, color, label, BUTTON_NORMAL, false, nullptr, textSize};
drawButton(btn);
}
void showVentilationControlScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(0, 200, 255));
tft.setCursor(150, 10);
tft.println("VENTILACIJA");
int frameY = 40;
int frameHeight = 225;
tft.drawRect(10, frameY, 460, frameHeight, rgbTo565(0, 150, 255));
tft.fillRect(11, frameY + 1, 458, frameHeight - 1, rgbTo565(20, 40, 70));
int startY = frameY + 18;
int lineHeight = 28;
int row1Y = startY;
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 0));
tft.setCursor(15, row1Y + 18);
tft.print("DAN Temp:");
tft.setCursor(150, row1Y + 18);
tft.printf("%.1f C", ventilationDayTempThreshold);
drawCompactButton(350, row1Y, 40, 22, rgbTo565(255, 200, 0), "-0.5", false);
drawCompactButton(395, row1Y, 40, 22, rgbTo565(255, 200, 0), "+0.5", false);
int row2Y = startY + lineHeight;
tft.setTextColor(rgbTo565(255, 200, 0));
tft.setCursor(15, row2Y + 18);
tft.print("DAN Vlaga:");
tft.setCursor(150, row2Y + 18);
tft.printf("%.0f %%", ventilationDayHumThreshold);
drawCompactButton(350, row2Y, 40, 22, rgbTo565(255, 200, 0), "-5", false);
drawCompactButton(395, row2Y, 40, 22, rgbTo565(255, 200, 0), "+5", false);
int row3Y = startY + 2 * lineHeight;
tft.setTextColor(rgbTo565(150, 180, 255));
tft.setCursor(15, row3Y + 18);
tft.print("NOC Temp:");
tft.setCursor(150, row3Y + 18);
tft.printf("%.1f C", ventilationNightTempThreshold);
drawCompactButton(350, row3Y, 40, 22, rgbTo565(150, 180, 255), "-0.5", false);
drawCompactButton(395, row3Y, 40, 22, rgbTo565(150, 180, 255), "+0.5", false);
int row4Y = startY + 3 * lineHeight;
tft.setTextColor(rgbTo565(150, 180, 255));
tft.setCursor(15, row4Y + 18);
tft.print("NOC Vlaga:");
tft.setCursor(150, row4Y + 18);
tft.printf("%.0f %%", ventilationNightHumThreshold);
drawCompactButton(350, row4Y, 40, 22, rgbTo565(150, 180, 255), "-5", false);
drawCompactButton(395, row4Y, 40, 22, rgbTo565(150, 180, 255), "+5", false);
int row5Y = startY + 4 * lineHeight;
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(15, row5Y + 18);
tft.print("Delovanje:");
tft.setCursor(150, row5Y + 18);
tft.printf("%d/%d min", ventilationDayMinRuntime, ventilationNightMinRuntime);
drawCompactButton(350, row5Y, 40, 22, rgbTo565(255, 200, 100), "-1", false);
drawCompactButton(395, row5Y, 40, 22, rgbTo565(255, 200, 100), "+1", false);
int row6Y = startY + 5 * lineHeight;
tft.setTextColor(rgbTo565(255, 150, 100));
tft.setCursor(15, row6Y + 18);
tft.print("Pavza:");
tft.setCursor(150, row6Y + 18);
tft.printf("%d/%d min", ventilationDayCooldown, ventilationNightCooldown);
drawCompactButton(350, row6Y, 40, 22, rgbTo565(255, 150, 100), "-1", false);
drawCompactButton(395, row6Y, 40, 22, rgbTo565(255, 150, 100), "+1", false);
int statusY = startY + 6 * lineHeight + 28;
tft.setTextSize(1);
tft.setCursor(20, statusY);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("VENT:");
tft.setCursor(90, statusY);
bool ventState = relayStates[VENTILATION_RELAY];
tft.setTextColor(ventState ? rgbTo565(0, 255, 100) : rgbTo565(255, 100, 100));
tft.print(ventState ? "VKLOP" : "IZKLOP");
tft.setCursor(180, statusY);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("NACIN:");
tft.setCursor(250, statusY);
tft.setTextColor(ventilationAutoMode ? rgbTo565(100, 200, 255) : rgbTo565(255, 200, 100));
tft.print(ventilationAutoMode ? "AVTO" : "ROCNO");
tft.setCursor(330, statusY);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("REZIM:");
tft.setCursor(400, statusY);
tft.setTextColor(ventilationDayMode ? rgbTo565(255, 200, 0) : rgbTo565(150, 180, 255));
tft.print(ventilationDayMode ? "DAN" : "NOC");
int buttonY = 280;
int buttonWidth = 90;
int buttonHeight = 28;
int buttonSpacing = 8;
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2;
drawMenuButton(startX, buttonY, buttonWidth, buttonHeight,
relayStates[VENTILATION_RELAY] ? rgbTo565(255, 80, 80) : rgbTo565(80, 220, 80),
relayStates[VENTILATION_RELAY] ? "IZKLOP" : "VKLOP");
drawMenuButton(startX + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
ventilationAutoMode ? rgbTo565(100, 200, 255) : rgbTo565(255, 180, 80),
ventilationAutoMode ? "AVTO" : "ROCNO");
drawMenuButton(startX + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "SHRANI");
drawMenuButton(startX + 3 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_VENTILATION_CONTROL;
}
void handleVentilationControlTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int startY = 58;
int lineHeight = 28;
for (int row = 0; row < 6; row++) {
int rowY = startY + (row * lineHeight);
if (y > rowY - 5 && y < rowY + 22) {
if (x > 350 && x < 390) {
switch (row) {
case 0:
ventilationDayTempThreshold -= 0.5;
if (ventilationDayTempThreshold < 15) ventilationDayTempThreshold = 15;
break;
case 1:
ventilationDayHumThreshold -= 5;
if (ventilationDayHumThreshold < 30) ventilationDayHumThreshold = 30;
break;
case 2:
ventilationNightTempThreshold -= 0.5;
if (ventilationNightTempThreshold < 10) ventilationNightTempThreshold = 10;
break;
case 3:
ventilationNightHumThreshold -= 5;
if (ventilationNightHumThreshold < 30) ventilationNightHumThreshold = 30;
break;
case 4:
ventilationDayMinRuntime = max(1, ventilationDayMinRuntime - 1);
ventilationNightMinRuntime = max(1, ventilationNightMinRuntime - 1);
break;
case 5:
ventilationDayCooldown = max(1, ventilationDayCooldown - 1);
ventilationNightCooldown = max(1, ventilationNightCooldown - 1);
break;
}
markSettingsChanged();
showVentilationControlScreen();
return;
}
if (x > 395 && x < 435) {
switch (row) {
case 0:
ventilationDayTempThreshold += 0.5;
if (ventilationDayTempThreshold > 40) ventilationDayTempThreshold = 40;
break;
case 1:
ventilationDayHumThreshold += 5;
if (ventilationDayHumThreshold > 95) ventilationDayHumThreshold = 95;
break;
case 2:
ventilationNightTempThreshold += 0.5;
if (ventilationNightTempThreshold > 35) ventilationNightTempThreshold = 35;
break;
case 3:
ventilationNightHumThreshold += 5;
if (ventilationNightHumThreshold > 95) ventilationNightHumThreshold = 95;
break;
case 4:
ventilationDayMinRuntime = min(60, ventilationDayMinRuntime + 1);
ventilationNightMinRuntime = min(60, ventilationNightMinRuntime + 1);
break;
case 5:
ventilationDayCooldown = min(60, ventilationDayCooldown + 1);
ventilationNightCooldown = min(60, ventilationNightCooldown + 1);
break;
}
markSettingsChanged();
showVentilationControlScreen();
return;
}
}
}
int buttonY = 280;
int buttonWidth = 90;
int buttonSpacing = 8;
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2;
if (x > startX && x < startX + buttonWidth && y > buttonY && y < buttonY + 28) {
Serial.println("Gumb VKLOP/IZKLOP pritisnjen");
bool newState = !relayStates[VENTILATION_RELAY];
setRelay(VENTILATION_RELAY, newState);
ventilationManualOverride = true;
ventilationAutoMode = false;
markSettingsChanged();
showVentilationControlScreen();
return;
}
if (x > startX + buttonWidth + buttonSpacing && x < startX + 2 * buttonWidth + buttonSpacing && y > buttonY && y < buttonY + 28) {
Serial.println("Gumb AVTO/ROCNO pritisnjen");
ventilationAutoMode = !ventilationAutoMode;
if (ventilationAutoMode) {
ventilationManualOverride = false;
} else {
ventilationManualOverride = true;
}
markSettingsChanged();
showVentilationControlScreen();
return;
}
if (x > startX + 2 * (buttonWidth + buttonSpacing) && x < startX + 3 * buttonWidth + 2 * buttonSpacing && y > buttonY && y < buttonY + 28) {
saveAllSettings();
showVentilationControlScreen();
return;
}
if (x > startX + 3 * (buttonWidth + buttonSpacing) && x < startX + 4 * buttonWidth + 3 * buttonSpacing && y > buttonY && y < buttonY + 28) {
showMainMenu();
return;
}
}
void loadVentilationSettings() {
preferences.begin("ventilation", true);
ventilationAutoMode = preferences.getBool("autoMode", true);
ventilationManualOverride = preferences.getBool("manualOverride", false);
ventilationDayTempThreshold = preferences.getFloat("dayTempThreshold", 28.0);
ventilationDayHumThreshold = preferences.getFloat("dayHumThreshold", 70.0);
ventilationNightTempThreshold = preferences.getFloat("nightTempThreshold", 22.0);
ventilationNightHumThreshold = preferences.getFloat("nightHumThreshold", 75.0);
ventilationDayMinRuntime = preferences.getInt("dayMinRuntime", 5);
ventilationNightMinRuntime = preferences.getInt("nightMinRuntime", 10);
ventilationDayCooldown = preferences.getInt("dayCooldown", 10);
ventilationNightCooldown = preferences.getInt("nightCooldown", 20);
dayNightSwitchHour = preferences.getInt("dayNightSwitchHour", 22);
nightDaySwitchHour = preferences.getInt("nightDaySwitchHour", 6);
preferences.end();
}
void checkSettingsIntegrity() {
Serial.println("\n=== PREVERJANJE INTEGRITETE NASTAVITEV ===");
if (targetTempMin >= targetTempMax) {
Serial.println("POPRAVLJAM: targetTempMin >= targetTempMax");
targetTempMin = 18.0;
targetTempMax = 25.0;
markSettingsChanged();
}
if (targetHumMin >= targetHumMax) {
Serial.println("POPRAVLJAM: targetHumMin >= targetHumMax");
targetHumMin = 40.0;
targetHumMax = 60.0;
markSettingsChanged();
}
if (lightOnThreshold >= lightOffThreshold) {
Serial.println("POPRAVLJAM: lightOnThreshold >= lightOffThreshold");
lightOnThreshold = 50.0;
lightOffThreshold = 1000.0;
markSettingsChanged();
}
if (shadeMinLux >= shadeMaxLux) {
Serial.println("POPRAVLJAM: shadeMinLux >= shadeMaxLux");
shadeMinLux = 0.0;
shadeMaxLux = 5000.0;
markSettingsChanged();
}
if (ventilationDayTempThreshold < 10 || ventilationDayTempThreshold > 40) {
Serial.println("POPRAVLJAM: ventilationDayTempThreshold izven obsega");
ventilationDayTempThreshold = 28.0;
markSettingsChanged();
}
if (settingsChangedFlag) {
saveAllSettings();
}
Serial.println("=== INTEGRITETA NASTAVITEV PREVERJENA ===");
}
void fillArc(int x, int y, int start_angle, int end_angle, int inner_r, int outer_r, uint16_t color) {
if (start_angle > end_angle) {
int temp = start_angle;
start_angle = end_angle;
end_angle = temp;
}
start_angle = constrain(start_angle, 0, 360);
end_angle = constrain(end_angle, 0, 360);
if (inner_r >= outer_r) {
inner_r = outer_r - 2;
}
if (inner_r < 0) inner_r = 0;
for (int a = start_angle; a <= end_angle; a += 3) {
float rad = a * PI / 180.0;
for (int r = inner_r; r <= outer_r; r++) {
int px = x + r * cos(rad);
int py = y + r * sin(rad);
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, color);
}
}
}
}
void initializeTimeBlocks() {
Serial.println("Inicializacija casovnih blokov za razsvetljavo...");
for (int i = 0; i < MAX_TIME_BLOCKS; i++) {
timeBlocks[i].enabled = false;
timeBlocks[i].relayOn = true;
timeBlocks[i].startHour = 18;
timeBlocks[i].startMinute = 0;
timeBlocks[i].endHour = 22;
timeBlocks[i].endMinute = 0;
timeBlocks[i].description = "Blok " + String(i + 1);
for (int d = 0; d < DAYS_IN_WEEK; d++) {
timeBlocks[i].days[d] = false;
}
}
timeBlocks[0].enabled = true;
timeBlocks[0].description = "Vikend vecer";
timeBlocks[0].days[5] = true;
timeBlocks[0].days[6] = true;
timeBlocks[1].enabled = true;
timeBlocks[1].relayOn = false;
timeBlocks[1].startHour = 12;
timeBlocks[1].endHour = 13;
timeBlocks[1].description = "Izklop opoldne";
for (int d = 0; d < 5; d++) {
timeBlocks[1].days[d] = true;
}
}
void printLightingSettings() {
Serial.println("\n=== TRENUTNE NASTAVITVE RAZSVETLJAVE ===");
Serial.printf("lightAutoMode: %s\n", lightAutoMode ? "DA" : "NE");
Serial.printf("lightManualOverride: %s\n", lightManualOverride ? "DA" : "NE");
Serial.printf("lightOnThreshold: %.1f lx\n", lightOnThreshold);
Serial.printf("lightOffThreshold: %.1f lx\n", lightOffThreshold);
Serial.printf("lightTimeControlEnabled: %s\n", lightTimeControlEnabled ? "DA" : "NE");
Serial.printf("lightStartHour: %02d:%02d\n", lightStartHour, lightStartMinute);
Serial.printf("lightEndHour: %02d:%02d\n", lightEndHour, lightEndMinute);
for (int i = 0; i < MAX_TIME_BLOCKS; i++) {
if (timeBlocks[i].enabled) {
Serial.printf("Blok %d: %s, %02d:%02d-%02d:%02d, relayOn: %s\n",
i, timeBlocks[i].description.c_str(),
timeBlocks[i].startHour, timeBlocks[i].startMinute,
timeBlocks[i].endHour, timeBlocks[i].endMinute,
timeBlocks[i].relayOn ? "VKLOP" : "IZKLOP");
}
}
Serial.println("=======================================\n");
}
bool isWithinTimeWindow() {
if (!rtcInitialized || !lightTimeControlEnabled) return true;
DateTime now = rtc.now();
int currentHour = now.hour();
int currentMinute = now.minute();
int currentTime = currentHour * 60 + currentMinute;
int startTime = lightStartHour * 60 + lightStartMinute;
int endTime = lightEndHour * 60 + lightEndMinute;
if (endTime <= startTime) {
endTime += 24 * 60;
if (currentTime < startTime) {
currentTime += 24 * 60;
}
}
return (currentTime >= startTime && currentTime < endTime);
}
bool isTimeInBlock(int blockIndex) {
if (blockIndex < 0 || blockIndex >= MAX_TIME_BLOCKS || !timeBlocks[blockIndex].enabled) {
return false;
}
if (!rtcInitialized) return false;
DateTime now = rtc.now();
int currentDay = now.dayOfTheWeek();
int currentHour = now.hour();
int currentMinute = now.minute();
if (!timeBlocks[blockIndex].days[currentDay]) {
return false;
}
int currentTimeInMinutes = currentHour * 60 + currentMinute;
int startTimeInMinutes = timeBlocks[blockIndex].startHour * 60 + timeBlocks[blockIndex].startMinute;
int endTimeInMinutes = timeBlocks[blockIndex].endHour * 60 + timeBlocks[blockIndex].endMinute;
if (endTimeInMinutes <= startTimeInMinutes) {
endTimeInMinutes += 24 * 60;
if (currentTimeInMinutes < startTimeInMinutes) {
currentTimeInMinutes += 24 * 60;
}
}
return (currentTimeInMinutes >= startTimeInMinutes && currentTimeInMinutes < endTimeInMinutes);
}
bool shouldRelaysBeOn() {
if (!lightingAutoMode || lightingManualOverride) {
return lightingManualOverride ? relayStates[LIGHTING_RELAY_1] : false;
}
bool overallState = false;
for (int i = 0; i < MAX_TIME_BLOCKS; i++) {
if (!timeBlocks[i].enabled) continue;
if (isTimeInBlock(i)) {
if (timeBlocks[i].relayOn) {
overallState = true;
} else {
overallState = false;
break;
}
}
}
return overallState;
}
void checkAndControlLighting() {
if (!rtcInitialized) return;
unsigned long currentTime = millis();
if (currentTime - lastLightingCheck > LIGHTING_CHECK_INTERVAL) {
bool relay6ShouldBeOn = false;
if (!lightAutoMode || lightManualOverride) {
relay6ShouldBeOn = lightManualOverride ? relayStates[LIGHTING_RELAY_2] : false;
} else {
if (isWithinTimeWindow()) {
if (ldrInternalLux < lightOnThreshold) {
relay6ShouldBeOn = true;
Serial.printf("💡 Avto razsvetljava: TEMNO (%.0f lx < %.0f) → VKLOP\n",
ldrInternalLux, lightOnThreshold);
} else if (ldrInternalLux > lightOffThreshold) {
relay6ShouldBeOn = false;
Serial.printf("💡 Avto razsvetljava: SVETLO (%.0f lx > %.0f) → IZKLOP\n",
ldrInternalLux, lightOffThreshold);
} else {
relay6ShouldBeOn = relayStates[LIGHTING_RELAY_2];
}
} else {
relay6ShouldBeOn = false;
Serial.printf("💡 Avto razsvetljava: ZUNAJ ČASOVNEGA OKNA → IZKLOP\n");
}
}
if (relay6ShouldBeOn != relayStates[LIGHTING_RELAY_2]) {
setRelay(LIGHTING_RELAY_2, relay6ShouldBeOn);
markSettingsChanged();
Serial.printf("💡 Rele 5 (avto razsvetljava): %s\n",
relay6ShouldBeOn ? "VKLOPLJEN" : "IZKLOPLJEN");
}
lastLightingCheck = currentTime;
}
}
void showLightAutoControlScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 215, 0));
tft.setCursor(130, 35);
tft.println("AVTOMATSKA RAZSVETLJAVA");
tft.drawRoundRect(10, 45, 460, 155, 10, rgbTo565(255, 200, 0));
tft.fillRoundRect(11, 46, 458, 153, 10, rgbTo565(40, 30, 10));
int leftColumnX = 30;
int rightColumnX = 260;
int yOffset = 58;
int lineHeight = 14;
tft.setTextSize(1);
tft.setCursor(leftColumnX, yOffset);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("STATUS RELE 6");
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Nacin: ");
tft.setTextColor(lightAutoMode ? rgbTo565(100, 200, 100) : rgbTo565(255, 150, 50));
tft.println(lightAutoMode ? "AVTO" : "ROCNO");
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Stanje: ");
tft.setTextColor(relayStates[LIGHTING_RELAY_2] ? rgbTo565(100, 200, 100) : rgbTo565(200, 200, 200));
tft.println(relayStates[LIGHTING_RELAY_2] ? "VKLOP" : "IZKLOP");
tft.setCursor(leftColumnX, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Svetloba: ");
tft.setTextColor(LIGHT_COLOR);
tft.printf("%.0f lx", ldrInternalLux);
tft.setCursor(rightColumnX, yOffset);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("CASOVNO OBMOCJE");
tft.setCursor(rightColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Status: ");
tft.setTextColor(lightTimeControlEnabled ? rgbTo565(100, 200, 100) : rgbTo565(200, 200, 200));
tft.println(lightTimeControlEnabled ? "VKLOP" : "IZKLOP");
if (lightTimeControlEnabled) {
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Od: ");
tft.setTextColor(rgbTo565(100, 200, 255));
tft.printf("%02d:%02d", lightStartHour, lightStartMinute);
tft.setCursor(rightColumnX, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Do: ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%02d:%02d", lightEndHour, lightEndMinute);
}
drawMenuButton(rightColumnX, yOffset + 4 * lineHeight, 110, 24,
rgbTo565(66, 135, 245), "UREDI");
int pragoviStartY = yOffset + 7 * lineHeight + 8;
tft.drawRoundRect(leftColumnX - 10, pragoviStartY - 8, 440, 75, 8, rgbTo565(255, 200, 100));
tft.fillRoundRect(leftColumnX - 9, pragoviStartY - 7, 438, 73, 8, rgbTo565(30, 30, 50));
int vklopX = leftColumnX;
int vklopY = pragoviStartY;
tft.setCursor(vklopX, vklopY);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("VKLOP: ");
tft.setTextColor(rgbTo565(100, 200, 100));
tft.printf("%.0f lx", lightOnThreshold);
int btnVklopY = vklopY + 22;
drawCompactButton(vklopX, btnVklopY, 60, 26, rgbTo565(100, 200, 100), "-100", false);
drawCompactButton(vklopX + 70, btnVklopY, 60, 26, rgbTo565(100, 200, 100), "+100", false);
int izklopX = rightColumnX;
int izklopY = pragoviStartY;
tft.setCursor(izklopX, izklopY);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("IZKLOP: ");
tft.setTextColor(rgbTo565(255, 100, 100));
tft.printf("%.0f lx", lightOffThreshold);
int btnIzklopY = izklopY + 22;
drawCompactButton(izklopX, btnIzklopY, 60, 26, rgbTo565(255, 100, 100), "-100", false);
drawCompactButton(izklopX + 70, btnIzklopY, 60, 26, rgbTo565(255, 100, 100), "+100", false);
int buttonY = 235;
int buttonWidth = 90;
int buttonHeight = 30;
int buttonSpacing = 10;
int totalWidth1 = 3 * buttonWidth + 2 * buttonSpacing;
int startX1 = (tft.width() - totalWidth1) / 2;
int row1Y = buttonY;
drawMenuButton(startX1, row1Y, buttonWidth, buttonHeight,
lightAutoMode ? rgbTo565(100, 200, 100) : rgbTo565(255, 150, 50),
"AVTO");
drawMenuButton(startX1 + buttonWidth + buttonSpacing, row1Y, buttonWidth, buttonHeight,
lightTimeControlEnabled ? rgbTo565(100, 200, 100) : rgbTo565(200, 200, 200),
"CAS");
drawMenuButton(startX1 + 2 * (buttonWidth + buttonSpacing), row1Y, buttonWidth, buttonHeight,
relayStates[LIGHTING_RELAY_2] ? rgbTo565(255, 100, 100) : rgbTo565(100, 255, 100),
relayStates[LIGHTING_RELAY_2] ? "IZKL" : "VKL");
int row2Y = row1Y + buttonHeight + buttonSpacing;
int buttonWidth2 = 110;
int totalWidth2 = 2 * buttonWidth2 + buttonSpacing;
int startX2 = (tft.width() - totalWidth2) / 2;
drawIconButton(startX2, row2Y, buttonWidth2, buttonHeight,
rgbTo565(76, 175, 80), "SHRANI", "save");
drawMenuButton(startX2 + buttonWidth2 + buttonSpacing, row2Y, buttonWidth2, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_LIGHT_AUTO_CONTROL;
}
void handleLightAutoControlTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int leftColumnX = 30;
int rightColumnX = 260;
int yOffset = 58;
int lineHeight = 14;
int pragoviStartY = yOffset + 7 * lineHeight + 8;
int vklopX = leftColumnX;
int btnVklopY = pragoviStartY + 22;
int btnHeight = 26;
int btnWidth = 60;
if (x > vklopX && x < vklopX + btnWidth && y > btnVklopY && y < btnVklopY + btnHeight) {
lightOnThreshold -= 100.0;
if (lightOnThreshold < 0) lightOnThreshold = 0;
if (lightOnThreshold > lightOffThreshold) lightOnThreshold = lightOffThreshold - 100;
if (lightOnThreshold < 0) lightOnThreshold = 0;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
if (x > vklopX + 70 && x < vklopX + 70 + btnWidth && y > btnVklopY && y < btnVklopY + btnHeight) {
lightOnThreshold += 100.0;
if (lightOnThreshold > lightOffThreshold) lightOnThreshold = lightOffThreshold;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
int izklopX = rightColumnX;
int btnIzklopY = pragoviStartY + 22;
if (x > izklopX && x < izklopX + btnWidth && y > btnIzklopY && y < btnIzklopY + btnHeight) {
lightOffThreshold -= 100.0;
if (lightOffThreshold < lightOnThreshold) lightOffThreshold = lightOnThreshold + 100;
if (lightOffThreshold < 0) lightOffThreshold = 0;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
if (x > izklopX + 70 && x < izklopX + 70 + btnWidth && y > btnIzklopY && y < btnIzklopY + btnHeight) {
lightOffThreshold += 100.0;
if (lightOffThreshold > 20000) lightOffThreshold = 20000;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
if (x > rightColumnX && x < rightColumnX + 110 && y > yOffset + 4 * lineHeight && y < yOffset + 4 * lineHeight + 24) {
showEditLightTimeScreen();
return;
}
int buttonY = 235;
int buttonWidth = 90;
int buttonHeight = 30;
int buttonSpacing = 10;
int totalWidth1 = 3 * buttonWidth + 2 * buttonSpacing;
int startX1 = (tft.width() - totalWidth1) / 2;
int row1Y = buttonY;
if (x > startX1 && x < startX1 + buttonWidth && y > row1Y && y < row1Y + buttonHeight) {
lightAutoMode = !lightAutoMode;
if (lightAutoMode) {
lightManualOverride = false;
}
markSettingsChanged();
showLightAutoControlScreen();
return;
}
if (x > startX1 + buttonWidth + buttonSpacing && x < startX1 + 2 * buttonWidth + buttonSpacing &&
y > row1Y && y < row1Y + buttonHeight) {
lightTimeControlEnabled = !lightTimeControlEnabled;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
if (x > startX1 + 2 * (buttonWidth + buttonSpacing) && x < startX1 + 3 * buttonWidth + 2 * buttonSpacing &&
y > row1Y && y < row1Y + buttonHeight) {
lightManualOverride = true;
lightAutoMode = false;
toggleRelay(LIGHTING_RELAY_2);
markSettingsChanged();
showLightAutoControlScreen();
return;
}
int row2Y = row1Y + buttonHeight + buttonSpacing;
int buttonWidth2 = 110;
int totalWidth2 = 2 * buttonWidth2 + buttonSpacing;
int startX2 = (tft.width() - totalWidth2) / 2;
if (x > startX2 && x < startX2 + buttonWidth2 && y > row2Y && y < row2Y + buttonHeight) {
saveAllSettings();
showLightAutoControlScreen();
return;
}
if (x > startX2 + buttonWidth2 + buttonSpacing && x < startX2 + 2 * buttonWidth2 + buttonSpacing &&
y > row2Y && y < row2Y + buttonHeight) {
showMainMenu();
return;
}
}
void handleEditLightTimeTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
if (x > 50 && x < 110 && y > 115 && y < 145) {
lightStartHour = (lightStartHour - 1 + 24) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
if (x > 120 && x < 180 && y > 115 && y < 145) {
lightStartHour = (lightStartHour + 1) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
if (x > 50 && x < 110 && y > 175 && y < 205) {
lightStartMinute = (lightStartMinute - 5 + 60) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
if (x > 120 && x < 180 && y > 175 && y < 205) {
lightStartMinute = (lightStartMinute + 5) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
if (x > 270 && x < 330 && y > 115 && y < 145) {
lightEndHour = (lightEndHour - 1 + 24) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
if (x > 340 && x < 400 && y > 115 && y < 145) {
lightEndHour = (lightEndHour + 1) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
if (x > 270 && x < 330 && y > 175 && y < 205) {
lightEndMinute = (lightEndMinute - 5 + 60) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
if (x > 340 && x < 400 && y > 175 && y < 205) {
lightEndMinute = (lightEndMinute + 5) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
int buttonWidth = 180;
int buttonHeight = 40;
int buttonSpacing = 20;
int totalWidth = 2 * buttonWidth + buttonSpacing;
int startX = (tft.width() - totalWidth) / 2;
int buttonY = 260;
if (x > startX && x < startX + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
saveAllSettings();
showLightAutoControlScreen();
return;
}
if (x > startX + buttonWidth + buttonSpacing && x < startX + 2 * buttonWidth + buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
showLightAutoControlScreen();
return;
}
}
void showEditLightTimeScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 215, 0));
tft.setCursor(110, 46);
tft.println("NASTAVI CASOVNO OBMOCJE");
tft.drawRoundRect(30, 60, 200, 170, 8, rgbTo565(100, 200, 255));
tft.fillRoundRect(31, 61, 198, 168, 8, rgbTo565(40, 30, 10));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(60, 78);
tft.println("ZACETNI CAS");
tft.setCursor(50, 100);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Ura: ");
tft.setTextColor(rgbTo565(100, 200, 255));
tft.printf("%02d", lightStartHour);
drawCompactButton(50, 115, 60, 30, rgbTo565(100, 150, 255), "-1", false);
drawCompactButton(120, 115, 60, 30, rgbTo565(100, 150, 255), "+1", false);
tft.setCursor(50, 164);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Minuta: ");
tft.setTextColor(rgbTo565(100, 200, 255));
tft.printf("%02d", lightStartMinute);
drawCompactButton(50, 175, 60, 30, rgbTo565(100, 150, 255), "-5", false);
drawCompactButton(120, 175, 60, 30, rgbTo565(100, 150, 255), "+5", false);
tft.drawRoundRect(250, 60, 200, 170, 8, rgbTo565(255, 150, 100));
tft.fillRoundRect(251, 61, 198, 168, 8, rgbTo565(40, 30, 10));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(280, 78);
tft.println("KONCNI CAS");
tft.setCursor(270, 100);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Ura: ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%02d", lightEndHour);
drawCompactButton(270, 115, 60, 30, rgbTo565(255, 150, 100), "-1", false);
drawCompactButton(340, 115, 60, 30, rgbTo565(255, 150, 100), "+1", false);
tft.setCursor(270, 164);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Minuta: ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%02d", lightEndMinute);
drawCompactButton(270, 175, 60, 30, rgbTo565(255, 150, 100), "-5", false);
drawCompactButton(340, 175, 60, 30, rgbTo565(255, 150, 100), "+5", false);
int buttonWidth = 180;
int buttonHeight = 40;
int buttonSpacing = 20;
int totalWidth = 2 * buttonWidth + buttonSpacing;
int startX = (tft.width() - totalWidth) / 2;
int buttonY = 260;
drawMenuButton(startX, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "SHRANI");
drawMenuButton(startX + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_EDIT_LIGHT_TIME;
}
void showLightingControlScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 215, 0));
tft.setCursor(110, 28);
tft.println("CASOVNA RAZSVETLJAVA (RELE 4)");
int blockStartY = 48;
int blockWidth = 400;
int blockHeight = 28;
int blockSpacing = 6;
int infoY = blockStartY + 4 * (blockHeight + blockSpacing) + 3;
int infoHeight = 45;
int okvirHeight = (4 * blockHeight + 3 * blockSpacing) + infoHeight + 15;
tft.drawRoundRect(20, 40, 440, okvirHeight, 10, rgbTo565(255, 200, 0));
tft.fillRoundRect(21, 41, 438, okvirHeight - 1, 10, rgbTo565(40, 30, 20));
tft.setTextSize(1);
for (int i = 0; i < 4; i++) {
int y = blockStartY + i * (blockHeight + blockSpacing);
uint16_t blockColor;
if (!timeBlocks[i].enabled) {
blockColor = rgbTo565(80, 80, 80);
} else if (isTimeInBlock(i)) {
blockColor = timeBlocks[i].relayOn ? rgbTo565(100, 200, 100) : rgbTo565(255, 100, 100);
} else {
blockColor = timeBlocks[i].relayOn ? rgbTo565(100, 150, 200) : rgbTo565(200, 150, 100);
}
tft.fillRoundRect(40, y, blockWidth, blockHeight, 5, blockColor);
tft.drawRoundRect(40, y, blockWidth, blockHeight, 5, ST77XX_WHITE);
String blockLabel = timeBlocks[i].description;
if (blockLabel.length() > 10) {
blockLabel = blockLabel.substring(0, 8) + "..";
}
int16_t labelX1, labelY1;
uint16_t labelW, labelH;
tft.getTextBounds(blockLabel, 0, 0, &labelX1, &labelY1, &labelW, &labelH);
int labelX = 48;
int labelY = y + (blockHeight - labelH) / 2 - labelY1;
tft.setCursor(labelX, labelY);
tft.setTextColor(timeBlocks[i].enabled ? ST77XX_BLACK : rgbTo565(200, 200, 200));
tft.print(blockLabel);
String timeStr = String(timeBlocks[i].startHour < 10 ? "0" : "") + String(timeBlocks[i].startHour) + ":" +
String(timeBlocks[i].startMinute < 10 ? "0" : "") + String(timeBlocks[i].startMinute) + "-" +
String(timeBlocks[i].endHour < 10 ? "0" : "") + String(timeBlocks[i].endHour) + ":" +
String(timeBlocks[i].endMinute < 10 ? "0" : "") + String(timeBlocks[i].endMinute);
int16_t timeX1, timeY1;
uint16_t timeW, timeH;
tft.getTextBounds(timeStr, 0, 0, &timeX1, &timeY1, &timeW, &timeH);
int timeX = 115;
int timeY = y + (blockHeight - timeH) / 2 - timeY1;
tft.setCursor(timeX, timeY);
tft.setTextColor(timeBlocks[i].enabled ? ST77XX_BLACK : rgbTo565(200, 200, 200));
tft.print(timeStr);
String actionStr = timeBlocks[i].relayOn ? "ON" : "OFF";
int16_t actionX1, actionY1;
uint16_t actionW, actionH;
tft.getTextBounds(actionStr, 0, 0, &actionX1, &actionY1, &actionW, &actionH);
int actionX = 280;
int actionY = y + (blockHeight - actionH) / 2 - actionY1;
tft.setCursor(actionX, actionY);
tft.setTextColor(timeBlocks[i].enabled ? ST77XX_BLACK : rgbTo565(200, 200, 200));
tft.print(actionStr);
if (timeBlocks[i].enabled && isTimeInBlock(i)) {
String activeStr = "AKTIVNO";
int16_t activeX1, activeY1;
uint16_t activeW, activeH;
tft.getTextBounds(activeStr, 0, 0, &activeX1, &activeY1, &activeW, &activeH);
int activeX = 365;
int activeY = y + (blockHeight - activeH) / 2 - activeY1;
tft.setCursor(activeX, activeY);
tft.setTextColor(ST77XX_BLACK);
tft.print(activeStr);
}
}
tft.setTextSize(1);
tft.setCursor(40, infoY);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Nacin: ");
tft.setTextColor(lightingAutoMode ? rgbTo565(100, 200, 100) : rgbTo565(255, 200, 50));
tft.println(lightingAutoMode ? "AVTOMATSKI" : "ROCNI");
tft.setCursor(40, infoY + 14);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Stanje releja 4: ");
tft.setTextColor(relayStates[LIGHTING_RELAY_1] ? rgbTo565(100, 255, 100) : rgbTo565(200, 200, 200));
tft.println(relayStates[LIGHTING_RELAY_1] ? "VKLOPLJEN" : "IZKLOPLJEN");
tft.setCursor(40, infoY + 28);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Naslednji: ");
tft.setTextColor(rgbTo565(255, 255, 150));
String nextEvent = getNextLightingEvent();
if (nextEvent.length() > 25) {
nextEvent = nextEvent.substring(0, 22) + "...";
}
tft.println(nextEvent);
int buttonY = 40 + okvirHeight + 25;
int buttonWidth = 95;
int buttonHeight = 32;
int buttonSpacing = 8;
int buttonCount = 4;
int totalButtonsWidth = buttonCount * buttonWidth + (buttonCount - 1) * buttonSpacing;
int startX = (tft.width() - totalButtonsWidth) / 2;
drawNormalButton(startX, buttonY, buttonWidth, buttonHeight,
lightingAutoMode ? rgbTo565(100, 200, 100) : rgbTo565(255, 150, 50),
lightingAutoMode ? "AVTO" : "ROCNO");
drawNormalButton(startX + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(255, 215, 0), "AVTO REL6");
drawNormalButton(startX + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "SHRANI");
drawNormalButton(startX + 3 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_LIGHTING_CONTROL;
}
void showEditLightingBlockScreen(int blockIndex) {
selectedLightBlock = blockIndex;
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
TimeBlock* block = &timeBlocks[blockIndex];
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 215, 0));
tft.setCursor(150, 35);
tft.printf("UREDI BLOK %d", blockIndex + 1);
int frameY = 50;
int frameHeight = 180;
tft.drawRoundRect(20, frameY, 440, frameHeight, 10, rgbTo565(255, 200, 0));
tft.fillRoundRect(21, frameY + 1, 438, frameHeight - 1, 10, rgbTo565(40, 30, 20));
int leftColX = 30;
int rightColX = 260;
int yStart = frameY + 15;
int lineSpacing = 28;
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(leftColX, yStart);
tft.println("CASOVNO OBMOCJE");
tft.setCursor(leftColX, yStart + 18);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Od: ");
tft.setTextColor(rgbTo565(100, 200, 255));
tft.printf("%02d:%02d", block->startHour, block->startMinute);
int smallBtnW = 45;
int smallBtnH = 22;
int btnY = yStart + 26;
drawCompactButton(leftColX, btnY, smallBtnW, smallBtnH, rgbTo565(100, 150, 255), "-1H", false);
drawCompactButton(leftColX + 50, btnY, smallBtnW, smallBtnH, rgbTo565(100, 150, 255), "+1H", false);
drawCompactButton(leftColX + 100, btnY, smallBtnW, smallBtnH, rgbTo565(100, 150, 255), "-5M", false);
drawCompactButton(leftColX + 150, btnY, smallBtnW, smallBtnH, rgbTo565(100, 150, 255), "+5M", false);
tft.setCursor(leftColX, yStart + 78);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Do: ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%02d:%02d", block->endHour, block->endMinute);
btnY = yStart + 90;
drawCompactButton(leftColX, btnY, smallBtnW, smallBtnH, rgbTo565(255, 150, 100), "-1H", false);
drawCompactButton(leftColX + 50, btnY, smallBtnW, smallBtnH, rgbTo565(255, 150, 100), "+1H", false);
drawCompactButton(leftColX + 100, btnY, smallBtnW, smallBtnH, rgbTo565(255, 150, 100), "-5M", false);
drawCompactButton(leftColX + 150, btnY, smallBtnW, smallBtnH, rgbTo565(255, 150, 100), "+5M", false);
tft.setCursor(rightColX, yStart);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("NASTAVITVE");
tft.setCursor(rightColX, yStart + 15);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Aktiven: ");
drawCompactButton(rightColX + 70, yStart + 12, 60, 22,
block->enabled ? rgbTo565(100, 255, 100) : rgbTo565(200, 200, 200),
block->enabled ? "DA" : "NE", false);
tft.setCursor(rightColX, yStart + 40);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Akcija: ");
drawCompactButton(rightColX + 70, yStart + 37, 60, 22,
block->relayOn ? rgbTo565(100, 200, 100) : rgbTo565(200, 100, 100),
block->relayOn ? "VKLOP" : "IZKLOP", false);
tft.setCursor(rightColX, yStart + 65);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Dnevi:");
const char* dayLetters[7] = { "P", "T", "S", "C", "P", "S", "N" };
int dayBtnY = yStart + 80;
for (int d = 0; d < 7; d++) {
int x = rightColX + d * 25;
drawCompactButton(x, dayBtnY, 22, 22,
block->days[d] ? rgbTo565(100, 200, 100) : rgbTo565(80, 80, 80),
dayLetters[d], block->days[d]);
}
int quickBtnY = yStart + 105;
drawCompactButton(rightColX, quickBtnY, 45, 22, rgbTo565(100, 150, 255), "VSI", false);
drawCompactButton(rightColX + 50, quickBtnY, 45, 22, rgbTo565(255, 150, 150), "NIC", false);
drawCompactButton(rightColX + 100, quickBtnY, 45, 22, rgbTo565(255, 200, 100), "VIK", false);
int descY = yStart + 150;
String desc = block->description;
if (desc.length() > 20) {
desc = desc.substring(0, 17) + "...";
}
tft.setCursor(leftColX, descY);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Opis: ");
tft.setTextColor(rgbTo565(255, 255, 200));
tft.println(desc);
int bottomBtnY = frameY + frameHeight + 10;
int bottomBtnW = 140;
int bottomBtnH = 32;
int bottomBtnSpacing = 10;
int columnSpacing = 40;
int totalWidth = 2 * bottomBtnW + columnSpacing;
int leftColumnX = (tft.width() - totalWidth) / 2;
int rightColumnX = leftColumnX + bottomBtnW + columnSpacing;
int btnRow1Y = bottomBtnY;
if (editTimeMode) {
drawNormalButton(leftColumnX, btnRow1Y, bottomBtnW, bottomBtnH, rgbTo565(255, 255, 100), "✓ CAS");
} else {
drawNormalButton(leftColumnX, btnRow1Y, bottomBtnW, bottomBtnH, rgbTo565(66, 135, 245), "UREDI CAS");
}
int btnRow2Y = btnRow1Y + bottomBtnH + bottomBtnSpacing;
if (editDaysMode) {
drawNormalButton(leftColumnX, btnRow2Y, bottomBtnW, bottomBtnH, rgbTo565(255, 255, 100), "✓ DNEVI");
} else {
drawNormalButton(leftColumnX, btnRow2Y, bottomBtnW, bottomBtnH, rgbTo565(66, 135, 245), "UREDI DNEVE");
}
drawNormalButton(rightColumnX, btnRow1Y, bottomBtnW, bottomBtnH, rgbTo565(76, 175, 80), "SHRANI");
drawNormalButton(rightColumnX, btnRow2Y, bottomBtnW, bottomBtnH, rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_EDIT_LIGHTING_BLOCK;
}
String getNextLightingEvent() {
if (!rtcInitialized) return "Ni podatkov";
DateTime now = rtc.now();
int currentMinute = now.hour() * 60 + now.minute();
int currentDay = now.dayOfTheWeek();
int nextEventTime = 24 * 60;
String eventType = "brez";
int eventBlock = -1;
for (int i = 0; i < MAX_TIME_BLOCKS; i++) {
if (!timeBlocks[i].enabled) continue;
for (int d = 0; d < DAYS_IN_WEEK; d++) {
if (!timeBlocks[i].days[d]) continue;
int startMinute = timeBlocks[i].startHour * 60 + timeBlocks[i].startMinute;
int endMinute = timeBlocks[i].endHour * 60 + timeBlocks[i].endMinute;
if (d == currentDay && startMinute > currentMinute && startMinute < nextEventTime) {
nextEventTime = startMinute;
eventType = timeBlocks[i].relayOn ? "VKLOP" : "IZKLOP";
eventBlock = i;
}
if (d == (currentDay + 1) % 7 && startMinute < nextEventTime) {
nextEventTime = startMinute;
eventType = timeBlocks[i].relayOn ? "VKLOP" : "IZKLOP";
eventBlock = i;
}
}
}
if (nextEventTime == 24 * 60) {
return "Ni nacrtovanih";
}
int hours = nextEventTime / 60;
int minutes = nextEventTime % 60;
String blockInfo = "";
if (eventBlock >= 0 && eventBlock < MAX_TIME_BLOCKS) {
blockInfo = " (" + timeBlocks[eventBlock].description + ")";
}
return String(eventType) + " ob " + String(hours < 10 ? "0" : "") + String(hours) + ":" +
String(minutes < 10 ? "0" : "") + String(minutes) + blockInfo;
}
void drawRelayStatusIndicator(int x, int y, bool isOn) {
int indicatorSize = 12;
if (isOn) {
tft.fillCircle(x, y, indicatorSize, rgbTo565(100, 200, 100));
tft.fillCircle(x, y, indicatorSize - 4, rgbTo565(50, 150, 50));
tft.drawCircle(x, y, indicatorSize, ST77XX_WHITE);
} else {
tft.fillCircle(x, y, indicatorSize, rgbTo565(100, 100, 100));
tft.fillCircle(x, y, indicatorSize - 4, rgbTo565(40, 40, 40));
tft.drawCircle(x, y, indicatorSize, rgbTo565(150, 150, 150));
}
}
void handleLightingControlTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int blockStartY = 48;
int blockWidth = 400;
int blockHeight = 28;
int blockSpacing = 6;
for (int i = 0; i < 4; i++) {
int blockY = blockStartY + i * (blockHeight + blockSpacing);
if (x > 40 && x < 40 + blockWidth && y > blockY && y < blockY + blockHeight) {
showEditLightingBlockScreen(i);
return;
}
}
int infoY = blockStartY + 4 * (blockHeight + blockSpacing) + 3;
int okvirHeight = (4 * blockHeight + 3 * blockSpacing) + 45 + 15;
int buttonY = 40 + okvirHeight + 25;
int buttonWidth = 95;
int buttonHeight = 32;
int buttonSpacing = 8;
int buttonCount = 4;
int totalButtonsWidth = buttonCount * buttonWidth + (buttonCount - 1) * buttonSpacing;
int startX = (tft.width() - totalButtonsWidth) / 2;
if (x > startX && x < startX + buttonWidth &&
y > buttonY && y < buttonY + buttonHeight) {
lightingAutoMode = !lightingAutoMode;
if (lightingAutoMode) {
lightingManualOverride = false;
}
markSettingsChanged();
showLightingControlScreen();
return;
}
if (x > startX + buttonWidth + buttonSpacing &&
x < startX + 2 * buttonWidth + buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
showLightAutoControlScreen();
return;
}
if (x > startX + 2 * (buttonWidth + buttonSpacing) &&
x < startX + 3 * buttonWidth + 2 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
saveAllSettings();
showLightingControlScreen();
return;
}
if (x > startX + 3 * (buttonWidth + buttonSpacing) &&
x < startX + 4 * buttonWidth + 3 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
showMainMenu();
return;
}
}
void handleEditLightingBlockTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
if (selectedLightBlock < 0 || selectedLightBlock >= MAX_TIME_BLOCKS) return;
TimeBlock* block = &timeBlocks[selectedLightBlock];
int frameY = 50;
int leftColX = 30;
int rightColX = 260;
int yStart = frameY + 15;
int smallBtnW = 45;
int smallBtnH = 22;
int bottomBtnY = frameY + 180 + 10;
int bottomBtnW = 140;
int bottomBtnH = 32;
int bottomBtnSpacing = 10;
int columnSpacing = 40;
int totalWidth = 2 * bottomBtnW + columnSpacing;
int leftColumnX = (tft.width() - totalWidth) / 2;
int rightColumnX = leftColumnX + bottomBtnW + columnSpacing;
if (x > leftColX && x < leftColX + smallBtnW && y > yStart + 30 && y < yStart + 30 + smallBtnH) {
block->startHour = (block->startHour - 1 + 24) % 24;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColX + 50 && x < leftColX + 50 + smallBtnW && y > yStart + 30 && y < yStart + 30 + smallBtnH) {
block->startHour = (block->startHour + 1) % 24;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColX + 100 && x < leftColX + 100 + smallBtnW && y > yStart + 30 && y < yStart + 30 + smallBtnH) {
block->startMinute = (block->startMinute - 5 + 60) % 60;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColX + 150 && x < leftColX + 150 + smallBtnW && y > yStart + 30 && y < yStart + 30 + smallBtnH) {
block->startMinute = (block->startMinute + 5) % 60;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColX && x < leftColX + smallBtnW && y > yStart + 90 && y < yStart + 90 + smallBtnH) {
block->endHour = (block->endHour - 1 + 24) % 24;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColX + 50 && x < leftColX + 50 + smallBtnW && y > yStart + 90 && y < yStart + 90 + smallBtnH) {
block->endHour = (block->endHour + 1) % 24;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColX + 100 && x < leftColX + 100 + smallBtnW && y > yStart + 90 && y < yStart + 90 + smallBtnH) {
block->endMinute = (block->endMinute - 5 + 60) % 60;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColX + 150 && x < leftColX + 150 + smallBtnW && y > yStart + 90 && y < yStart + 90 + smallBtnH) {
block->endMinute = (block->endMinute + 5) % 60;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > rightColX + 70 && x < rightColX + 130 && y > yStart + 12 && y < yStart + 12 + 22) {
block->enabled = !block->enabled;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > rightColX + 70 && x < rightColX + 130 && y > yStart + 37 && y < yStart + 37 + 22) {
block->relayOn = !block->relayOn;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
for (int d = 0; d < 7; d++) {
int dayX = rightColX + d * 25;
if (x > dayX && x < dayX + 22 && y > yStart + 80 && y < yStart + 80 + 22) {
block->days[d] = !block->days[d];
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
}
if (x > rightColX && x < rightColX + 45 && y > yStart + 105 && y < yStart + 105 + 22) {
for (int d = 0; d < 7; d++) block->days[d] = true;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > rightColX + 50 && x < rightColX + 95 && y > yStart + 105 && y < yStart + 105 + 22) {
for (int d = 0; d < 7; d++) block->days[d] = false;
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > rightColX + 100 && x < rightColX + 145 && y > yStart + 105 && y < yStart + 105 + 22) {
for (int d = 0; d < 7; d++) {
block->days[d] = (d == 5 || d == 6);
}
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
int btnRow1Y = bottomBtnY;
int btnRow2Y = btnRow1Y + bottomBtnH + bottomBtnSpacing;
if (x > leftColumnX && x < leftColumnX + bottomBtnW && y > btnRow1Y && y < btnRow1Y + bottomBtnH) {
editTimeMode = !editTimeMode;
if (editTimeMode) {
editDaysMode = false;
}
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > leftColumnX && x < leftColumnX + bottomBtnW && y > btnRow2Y && y < btnRow2Y + bottomBtnH) {
editDaysMode = !editDaysMode;
if (editDaysMode) {
editTimeMode = false;
}
markSettingsChanged();
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > rightColumnX && x < rightColumnX + bottomBtnW && y > btnRow1Y && y < btnRow1Y + bottomBtnH) {
saveAllSettings();
showTemporaryMessage("Nastavitve shranjene!\nV flash pomnilniku", rgbTo565(80, 220, 100), 1500);
showEditLightingBlockScreen(selectedLightBlock);
return;
}
if (x > rightColumnX && x < rightColumnX + bottomBtnW && y > btnRow2Y && y < btnRow2Y + bottomBtnH) {
showLightingControlScreen();
return;
}
}
void showShadeControlScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(0, 150, 255));
tft.setCursor(150, 30);
tft.println("NASTAVITVE SENCENJA");
tft.drawRoundRect(20, 55, 440, 220, 10, rgbTo565(0, 150, 255));
tft.fillRoundRect(21, 56, 438, 218, 10, rgbTo565(20, 40, 70));
int leftColumnX = 40;
int rightColumnX = 260;
int yOffset = 75;
int lineHeight = 20;
tft.setTextSize(1);
tft.setCursor(leftColumnX, yOffset - 12);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Modul 3: ");
tft.setTextColor(module3Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(module3Active ? "AKTIVEN" : "NEDOSEGLJIV");
tft.setCursor(leftColumnX, yOffset);
tft.setTextColor(LIGHT_COLOR);
tft.println("NOTRANJA SVETLOBA");
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Trenutna: ");
tft.setTextColor(LIGHT_COLOR);
if (ldrInternalLux < 10) {
tft.printf("%.1f lx", ldrInternalLux);
} else if (ldrInternalLux < 1000) {
tft.printf("%.0f lx", ldrInternalLux);
} else {
tft.printf("%.1f klx", ldrInternalLux / 1000.0);
}
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Min: ");
tft.setTextColor(rgbTo565(100, 100, 255));
tft.printf("%.0f lx", shadeMinLux);
drawCompactButton(leftColumnX, yOffset + 2 * lineHeight + 12, 45, 22,
rgbTo565(100, 100, 255), "-100", false);
drawCompactButton(leftColumnX + 55, yOffset + 2 * lineHeight + 12, 45, 22,
rgbTo565(100, 100, 255), "+100", false);
tft.setCursor(leftColumnX, yOffset + 4 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Max: ");
tft.setTextColor(rgbTo565(255, 200, 50));
tft.printf("%.0f lx", shadeMaxLux);
drawCompactButton(leftColumnX, yOffset + 4 * lineHeight + 12, 45, 22,
rgbTo565(255, 200, 50), "-100", false);
drawCompactButton(leftColumnX + 55, yOffset + 4 * lineHeight + 12, 45, 22,
rgbTo565(255, 200, 50), "+100", false);
tft.setCursor(rightColumnX, yOffset);
tft.setTextColor(rgbTo565(0, 150, 255));
tft.println("POZICIJA");
tft.setCursor(rightColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Trenutna: ");
if (module3Active) {
tft.setTextColor(rgbTo565(0, 150, 255));
tft.printf("%d %%", shadeCurrentPosition);
} else {
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("-- %");
}
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Cilj: ");
if (module3Active) {
tft.setTextColor(rgbTo565(255, 150, 50));
tft.printf("%d %%", shadeTargetPosition);
} else {
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("-- %");
}
tft.setCursor(rightColumnX, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Kalibriran: ");
if (module3Active) {
tft.setTextColor(shadeIsCalibrated ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(shadeIsCalibrated ? "DA" : "NE");
} else {
tft.setTextColor(rgbTo565(150, 150, 150));
tft.println("?");
}
tft.setCursor(rightColumnX, yOffset + 4 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Status: ");
if (module3Active) {
if (shadeIsMoving) {
tft.setTextColor(rgbTo565(255, 200, 50));
tft.print("PREMIK");
if (shadeTargetPosition > shadeCurrentPosition) {
tft.print(" ↑");
} else if (shadeTargetPosition < shadeCurrentPosition) {
tft.print(" ↓");
}
} else {
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("MIROVANJE");
}
} else {
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("?");
}
int barY = yOffset + 6 * lineHeight;
int barWidth = 200;
int barHeight = 22;
tft.setCursor(leftColumnX, barY - 15);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Pozicija sencnika:");
tft.fillRoundRect(leftColumnX, barY, barWidth, barHeight, 6, rgbTo565(60, 60, 60));
if (module3Active) {
int progressWidth = map(constrain(shadeCurrentPosition, 0, 100), 0, 100, 0, barWidth);
if (progressWidth > 0) {
tft.fillRoundRect(leftColumnX, barY, progressWidth, barHeight, 6, rgbTo565(0, 150, 255));
}
}
tft.drawRoundRect(leftColumnX, barY, barWidth, barHeight, 6, ST77XX_WHITE);
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(leftColumnX + barWidth / 2 - 15, barY + 5);
if (module3Active) {
tft.printf("%d%%", shadeCurrentPosition);
} else {
tft.print("--%");
}
int buttonStartY = barY + barHeight + 12;
int buttonWidth = 85;
int buttonHeight = 28;
int buttonSpacing = 6;
int totalWidth1 = 5 * buttonWidth + 4 * buttonSpacing;
int startX1 = (tft.width() - totalWidth1) / 2;
drawCompactButton(startX1, buttonStartY, buttonWidth, buttonHeight,
rgbTo565(100, 150, 255), "0%", false);
drawCompactButton(startX1 + buttonWidth + buttonSpacing, buttonStartY, buttonWidth, buttonHeight,
rgbTo565(100, 150, 255), "25%", false);
drawCompactButton(startX1 + 2 * (buttonWidth + buttonSpacing), buttonStartY, buttonWidth, buttonHeight,
rgbTo565(100, 150, 255), "50%", false);
drawCompactButton(startX1 + 3 * (buttonWidth + buttonSpacing), buttonStartY, buttonWidth, buttonHeight,
rgbTo565(100, 150, 255), "75%", false);
drawCompactButton(startX1 + 4 * (buttonWidth + buttonSpacing), buttonStartY, buttonWidth, buttonHeight,
rgbTo565(100, 150, 255), "100%", false);
int buttonStartY2 = buttonStartY + buttonHeight + 8;
int buttonWidth2 = 100;
int totalWidth2 = 4 * buttonWidth2 + 3 * buttonSpacing;
int startX2 = (tft.width() - totalWidth2) / 2;
drawMenuButton(startX2, buttonStartY2, buttonWidth2, buttonHeight,
rgbTo565(245, 67, 54), "USTAVI");
drawMenuButton(startX2 + buttonWidth2 + buttonSpacing, buttonStartY2, buttonWidth2, buttonHeight,
rgbTo565(66, 135, 245), "KALIBRIRAJ");
drawMenuButton(startX2 + 2 * (buttonWidth2 + buttonSpacing), buttonStartY2, buttonWidth2, buttonHeight,
shadeAutoControl ? rgbTo565(255, 100, 100) : rgbTo565(100, 255, 100),
shadeAutoControl ? "AVTO IZKL" : "AVTO VKL");
drawMenuButton(startX2 + 3 * (buttonWidth2 + buttonSpacing), buttonStartY2, buttonWidth2, buttonHeight,
rgbTo565(76, 175, 80), "SHRANI");
int buttonStartY3 = buttonStartY2 + buttonHeight + 8;
int backButtonWidth = 140;
int backStartX = (tft.width() - backButtonWidth) / 2;
drawMenuButton(backStartX, buttonStartY3, backButtonWidth, buttonHeight,
rgbTo565(156, 39, 176), "NAZAJ");
if (!module3Active) {
tft.fillRect(100, 150, 280, 50, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 50, rgbTo565(255, 80, 80));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(130, 165);
tft.println("MODUL 3 NI DOSEGLJIV!");
tft.setCursor(150, 180);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Preverite povezavo");
}
currentState = STATE_SHADE_CONTROL;
}
void handleShadeControlTouch(int x, int y) {
Serial.printf("\n🔘 DOTIK: x=%d, y=%d\n", x, y);
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int leftColumnX = 40;
int yOffset = 75;
int lineHeight = 20;
if (x > leftColumnX && x < leftColumnX + 45 &&
y > yOffset + 2 * lineHeight + 12 && y < yOffset + 2 * lineHeight + 34) {
Serial.println(" → MIN -100");
shadeMinLux -= 100;
if (shadeMinLux < 0) shadeMinLux = 0;
if (shadeMinLux > shadeMaxLux) shadeMinLux = shadeMaxLux;
markSettingsChanged();
showShadeControlScreen();
return;
}
if (x > leftColumnX + 55 && x < leftColumnX + 100 &&
y > yOffset + 2 * lineHeight + 12 && y < yOffset + 2 * lineHeight + 34) {
Serial.println(" → MIN +100");
shadeMinLux += 100;
if (shadeMinLux > shadeMaxLux) shadeMinLux = shadeMaxLux;
markSettingsChanged();
showShadeControlScreen();
return;
}
if (x > leftColumnX && x < leftColumnX + 45 &&
y > yOffset + 4 * lineHeight + 12 && y < yOffset + 4 * lineHeight + 34) {
Serial.println(" → MAX -100");
shadeMaxLux -= 100;
if (shadeMaxLux < shadeMinLux) shadeMaxLux = shadeMinLux;
markSettingsChanged();
showShadeControlScreen();
return;
}
if (x > leftColumnX + 55 && x < leftColumnX + 100 &&
y > yOffset + 4 * lineHeight + 12 && y < yOffset + 4 * lineHeight + 34) {
Serial.println(" → MAX +100");
shadeMaxLux += 100;
markSettingsChanged();
showShadeControlScreen();
return;
}
int barY = yOffset + 6 * lineHeight;
int barHeight = 22;
int buttonStartY = barY + barHeight + 12;
int buttonWidth = 85;
int buttonHeight = 28;
int buttonSpacing = 6;
int totalWidth1 = 5 * buttonWidth + 4 * buttonSpacing;
int startX1 = (tft.width() - totalWidth1) / 2;
if (y > buttonStartY && y < buttonStartY + buttonHeight) {
if (x > startX1 && x < startX1 + buttonWidth) {
Serial.println(" → GUMB: 0%");
if (module3Active) {
sendShadeCommand(CMD_MOVE_TO_POSITION, 0, 0);
} else {
showTemporaryMessage("Modul 3 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
}
showShadeControlScreen();
return;
}
if (x > startX1 + buttonWidth + buttonSpacing && x < startX1 + 2 * buttonWidth + buttonSpacing) {
Serial.println(" → GUMB: 25%");
if (module3Active) {
sendShadeCommand(CMD_MOVE_TO_POSITION, 25, 0);
} else {
showTemporaryMessage("Modul 3 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
}
showShadeControlScreen();
return;
}
if (x > startX1 + 2 * (buttonWidth + buttonSpacing) && x < startX1 + 3 * buttonWidth + 2 * buttonSpacing) {
Serial.println(" → GUMB: 50%");
if (module3Active) {
sendShadeCommand(CMD_MOVE_TO_POSITION, 50, 0);
} else {
showTemporaryMessage("Modul 3 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
}
showShadeControlScreen();
return;
}
if (x > startX1 + 3 * (buttonWidth + buttonSpacing) && x < startX1 + 4 * buttonWidth + 3 * buttonSpacing) {
Serial.println(" → GUMB: 75%");
if (module3Active) {
sendShadeCommand(CMD_MOVE_TO_POSITION, 75, 0);
} else {
showTemporaryMessage("Modul 3 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
}
showShadeControlScreen();
return;
}
if (x > startX1 + 4 * (buttonWidth + buttonSpacing) && x < startX1 + 5 * buttonWidth + 4 * buttonSpacing) {
Serial.println(" → GUMB: 100%");
if (module3Active) {
sendShadeCommand(CMD_MOVE_TO_POSITION, 100, 0);
} else {
showTemporaryMessage("Modul 3 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
}
showShadeControlScreen();
return;
}
}
int buttonStartY2 = buttonStartY + buttonHeight + 8;
int buttonWidth2 = 100;
int totalWidth2 = 4 * buttonWidth2 + 3 * buttonSpacing;
int startX2 = (tft.width() - totalWidth2) / 2;
if (y > buttonStartY2 && y < buttonStartY2 + buttonHeight) {
if (x > startX2 && x < startX2 + buttonWidth2) {
Serial.println(" → GUMB: USTAVI");
if (module3Active) {
sendShadeCommand(CMD_STOP, 0, 0);
} else {
showTemporaryMessage("Modul 3 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
}
showShadeControlScreen();
return;
}
if (x > startX2 + buttonWidth2 + buttonSpacing && x < startX2 + 2 * buttonWidth2 + buttonSpacing) {
Serial.println(" → GUMB: KALIBRIRAJ");
if (module3Active) {
sendShadeCommand(CMD_CALIBRATE, 0, 0);
showTemporaryMessage("Kalibracija senčenja\nse izvaja...", rgbTo565(255, 200, 50), 2000);
} else {
showTemporaryMessage("Modul 3 ni dosegljiv!\nPreverite povezavo", rgbTo565(255, 80, 80), 2000);
}
showShadeControlScreen();
return;
}
if (x > startX2 + 2 * (buttonWidth2 + buttonSpacing) && x < startX2 + 3 * buttonWidth2 + 2 * buttonSpacing) {
Serial.printf(" → GUMB: AVTO NAČIN (trenutno: %s)\n", shadeAutoControl ? "VKLOPLJEN" : "IZKLOPLJEN");
shadeAutoControl = !shadeAutoControl;
markSettingsChanged();
if (module3Active) {
sendShadeCommand(CMD_SET_AUTO_MODE, shadeAutoControl ? 1 : 0, 0);
}
showShadeControlScreen();
return;
}
if (x > startX2 + 3 * (buttonWidth2 + buttonSpacing) && x < startX2 + 4 * buttonWidth2 + 3 * buttonSpacing) {
Serial.println(" → GUMB: SHRANI");
saveAllSettings();
showTemporaryMessage("Nastavitve shranjene!", rgbTo565(80, 220, 100), 1500);
showShadeControlScreen();
return;
}
}
int buttonStartY3 = buttonStartY2 + buttonHeight + 8;
int backButtonWidth = 140;
int backStartX = (tft.width() - backButtonWidth) / 2;
if (y > buttonStartY3 && y < buttonStartY3 + buttonHeight) {
if (x > backStartX && x < backStartX + backButtonWidth) {
Serial.println(" → GUMB: NAZAJ");
showMainMenu();
return;
}
}
}
void autoControlShade() {
static unsigned long lastAutoCommand = 0;
static int lastSentPosition = -1;
static bool lastAutoState = false;
if (!shadeAutoControl) {
if (lastAutoState) {
Serial.println("⚠️ Avtomatsko senčenje IZKLOPLJENO");
lastAutoState = false;
}
return;
}
lastAutoState = true;
if (!module3Active) {
static unsigned long lastModuleWarning = 0;
if (millis() - lastModuleWarning > 30000) {
Serial.println("⚠️ Modul 3 ni aktiven! Ukazov ne morem poslati!");
lastModuleWarning = millis();
}
return;
}
unsigned long now = millis();
if (now - lastAutoCommand >= 2000) {
lastAutoCommand = now;
calculateShadePosition();
if (abs(shadeCalculatedPosition - shadeCurrentPosition) > 2 ||
lastSentPosition != shadeCalculatedPosition) {
Serial.printf("\n🎯 AVTOMATSKO SENCENJE (NOTRANJA SVETLOBA):");
Serial.printf("\n Notranja svetloba: %.0f lx", ldrInternalLux);
Serial.printf("\n Meje: %.0f - %.0f lx", shadeMinLux, shadeMaxLux);
Serial.printf("\n Izračunana pozicija: %d%%", shadeCalculatedPosition);
Serial.printf("\n Trenutna pozicija: %d%%", shadeCurrentPosition);
Serial.printf("\n → Pošiljam ukaz za premik na %d%%\n", shadeCalculatedPosition);
sendShadeCommand(CMD_MOVE_TO_POSITION, shadeCalculatedPosition, 0);
lastSentPosition = shadeCalculatedPosition;
}
}
}
void drawClockwiseProgressRing(int x, int y, int outerRadius, int innerRadius,
float currentValue, float minValue, float maxValue,
uint16_t ringColor) {
float progressPercent = 0;
if (maxValue > minValue) {
float constrainedValue = constrain(currentValue, minValue, maxValue);
progressPercent = (constrainedValue - minValue) / (maxValue - minValue) * 100.0;
}
float progressAngle = progressPercent * 3.6;
tft.fillCircle(x, y, outerRadius, PROGRESS_BG_COLOR);
if (progressAngle > 0) {
float startAngle = -90.0;
float endAngle = startAngle + progressAngle;
float startRad = startAngle * PI / 180.0;
float endRad = endAngle * PI / 180.0;
for (int r = innerRadius; r <= outerRadius; r++) {
float angleStep = 1.0 / r * 5;
if (angleStep < 0.5) angleStep = 0.5;
for (float angle = startAngle; angle <= endAngle; angle += angleStep) {
float rad = angle * PI / 180.0;
int px = x + r * cos(rad);
int py = y + r * sin(rad);
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, ringColor);
}
}
}
}
tft.fillCircle(x, y, innerRadius, ST77XX_BLACK);
tft.drawCircle(x, y, innerRadius, ST77XX_WHITE);
}
void drawClockwiseDoubleRing(int x, int y, int outerRadius, int middleRadius, int innerRadius,
float currentValue, float minValue, float maxValue,
float targetMin, float targetMax, uint16_t currentColor) {
float currentPercent = map(constrain(currentValue, minValue, maxValue), minValue, maxValue, 0, 100);
float currentAngle = currentPercent * 3.6;
float startAngle = -90.0;
for (int r = innerRadius; r <= middleRadius - 2; r++) {
tft.drawCircle(x, y, middleRadius, PROGRESS_BG_COLOR);
tft.drawCircle(x, y, outerRadius, PROGRESS_BG_COLOR);
}
for (int r = outerRadius; r <= outerRadius + 1; r++) {
tft.drawCircle(x, y, outerRadius + 1, ST77XX_WHITE);
}
if (currentAngle > 0) {
for (float angle = 0; angle <= currentAngle; angle += 0.5) {
float rad = (startAngle + angle) * PI / 180.0;
int x1 = x + middleRadius * cos(rad);
int y1 = y + middleRadius * sin(rad);
int x2 = x + outerRadius * cos(rad);
int y2 = y + outerRadius * sin(rad);
tft.drawLine(x1, y1, x2, y2, currentColor);
}
}
float targetStartPercent = map(targetMin, minValue, maxValue, 0, 100);
float targetEndPercent = map(targetMax, minValue, maxValue, 0, 100);
float targetStartAngle = targetStartPercent * 3.6;
float targetEndAngle = targetEndPercent * 3.6;
for (int r = innerRadius; r <= middleRadius - 2; r++) {
tft.drawCircle(x, y, r, PROGRESS_BG_COLOR);
}
if (targetStartAngle < targetEndAngle) {
for (float angle = targetStartAngle; angle <= targetEndAngle; angle += 0.5) {
float rad = (startAngle + angle) * PI / 180.0;
int x1 = x + innerRadius * cos(rad);
int y1 = y + innerRadius * sin(rad);
int x2 = x + (middleRadius - 2) * cos(rad);
int y2 = y + (middleRadius - 2) * sin(rad);
tft.drawLine(x1, y1, x2, y2, TARGET_RANGE_COLOR);
}
}
}
void autoControlRelays() {
if (!autoControlEnabled) {
Serial.println("AVTO KRMILJENJE: Onemogočeno (autoControlEnabled = false)");
return;
}
if (!mcpInitialized) {
Serial.println("AVTO KRMILJENJE: MCP23017 ni inicializiran!");
return;
}
if (isnan(internalTemperature) || isnan(internalHumidity)) {
Serial.println("AVTO KRMILJENJE: Podatki niso veljavni!");
return;
}
unsigned long currentTime = millis();
if (currentTime - lastControlCheck < CONTROL_CHECK_INTERVAL) {
return;
}
Serial.println("\n========== AVTOMATSKO KRMILJENJE RELEJEV ==========");
Serial.printf("Čas: %lu ms\n", currentTime);
Serial.printf("Trenutna temperatura: %.1f°C (cilj: %.1f-%.1f°C)\n",
internalTemperature, targetTempMin, targetTempMax);
Serial.printf("Trenutna vlaga: %.1f%% (cilj: %.1f-%.1f%%)\n",
internalHumidity, targetHumMin, targetHumMax);
Serial.printf("Stanje relejev - GRETJE(R0):%s HLAJENJE(R1):%s VLAŽENJE(R2):%s SUŠENJE(R3):%s\n",
relayStates[0]?"VKLOP":"IZKLOP",
relayStates[1]?"VKLOP":"IZKLOP",
relayStates[2]?"VKLOP":"IZKLOP",
relayStates[3]?"VKLOP":"IZKLOP");
if (internalTemperature > targetTempMax) {
if (!relayStates[1]) {
Serial.println("→ UKAZ: PREVISOKA temperatura - VKLOP HLAJENJA (rele 1)");
setRelay(1, true);
} else {
Serial.println("→ HLAJENJE že vklopljeno");
}
if (relayStates[0]) {
Serial.println("→ UKAZ: Izklop GRETJA (rele 0) - ker je temperatura previsoka");
setRelay(0, false);
}
}
else if (internalTemperature < targetTempMin) {
if (!relayStates[0]) {
Serial.println("→ UKAZ: PRENIZKA temperatura - VKLOP GRETJA (rele 0)");
setRelay(0, true);
} else {
Serial.println("→ GRETJE že vklopljeno");
}
if (relayStates[1]) {
Serial.println("→ UKAZ: Izklop HLAJENJA (rele 1) - ker je temperatura prenizka");
setRelay(1, false);
}
}
else {
if (relayStates[0]) {
Serial.println("→ UKAZ: Temperatura OK - IZKLOP GRETJA (rele 0)");
setRelay(0, false);
}
if (relayStates[1]) {
Serial.println("→ UKAZ: Temperatura OK - IZKLOP HLAJENJA (rele 1)");
setRelay(1, false);
}
if (!relayStates[0] && !relayStates[1]) {
Serial.println("→ Temperatura OK - GRETJE in HLAJENJE že izklopljena");
}
}
if (internalHumidity > targetHumMax) {
if (!relayStates[3]) {
Serial.println("→ UKAZ: PREVISOKA vlaga - VKLOP SUŠENJA (rele 3)");
setRelay(3, true);
} else {
Serial.println("→ SUŠENJE že vklopljeno");
}
if (relayStates[2]) {
Serial.println("→ UKAZ: Izklop VLAŽENJA (rele 2) - ker je vlaga previsoka");
setRelay(2, false);
}
}
else if (internalHumidity < targetHumMin) {
if (!relayStates[2]) {
Serial.println("→ UKAZ: PRENIZKA vlaga - VKLOP VLAŽENJA (rele 2)");
setRelay(2, true);
} else {
Serial.println("→ VLAŽENJE že vklopljeno");
}
if (relayStates[3]) {
Serial.println("→ UKAZ: Izklop SUŠENJA (rele 3) - ker je vlaga prenizka");
setRelay(3, false);
}
}
else {
if (relayStates[2]) {
Serial.println("→ UKAZ: Vlaga OK - IZKLOP VLAŽENJA (rele 2)");
setRelay(2, false);
}
if (relayStates[3]) {
Serial.println("→ UKAZ: Vlaga OK - IZKLOP SUŠENJA (rele 3)");
setRelay(3, false);
}
if (!relayStates[2] && !relayStates[3]) {
Serial.println("→ Vlaga OK - VLAŽENJE in SUŠENJE že izklopljena");
}
}
Serial.printf("Končno stanje - GRETJE(R0):%s HLAJENJE(R1):%s VLAŽENJE(R2):%s SUŠENJE(R3):%s\n",
relayStates[0]?"VKLOP":"IZKLOP",
relayStates[1]?"VKLOP":"IZKLOP",
relayStates[2]?"VKLOP":"IZKLOP",
relayStates[3]?"VKLOP":"IZKLOP");
Serial.println("==============================================\n");
lastControlCheck = currentTime;
}
void drawHorizontalProgressBarWithLabel(int x, int y, int width, int height, int value,
const char* label, uint16_t color) {
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(x - 25, y + 4);
tft.print(label);
tft.fillRoundRect(x, y, width, height, height / 2, PROGRESS_BG_COLOR);
int progressWidth = map(constrain(value, 0, 100), 0, 100, 0, width);
if (progressWidth > 0) {
tft.fillRoundRect(x, y, progressWidth, height, height / 2, color);
}
tft.drawRoundRect(x, y, width, height, height / 2, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(x + width + 5, y + 4);
tft.printf("%d%%", value);
}
void drawHomeScreenBackground() {
for (int y = STATUS_BAR_HEIGHT; y < tft.height(); y++) {
int progress = map(y, STATUS_BAR_HEIGHT, tft.height(), 0, 100);
uint8_t r = 10 + (progress * 2 / 100);
uint8_t g = 20 + (progress * 3 / 100);
uint8_t b = 40 + (progress * 6 / 100);
tft.drawFastHLine(0, y, tft.width(), rgbTo565(r, g, b));
}
for (int i = 0; i < 20; i++) {
int x = random(0, tft.width());
int y = random(STATUS_BAR_HEIGHT + 50, tft.height() - 50);
int size = random(1, 3);
uint16_t color = rgbTo565(40, 40, 60);
tft.fillCircle(x, y, size, color);
}
homeScreenBackgroundDrawn = true;
}
void showHomeScreen() {
if (lastState != STATE_HOME_SCREEN) {
homeScreenBackgroundDrawn = false;
homeScreenStaticElementsDrawn = false;
lastDrawnExternalLuxBar = -999;
tft.fillScreen(ST77XX_BLACK);
}
unsigned long now = millis();
if (currentState == STATE_HOME_SCREEN && now - lastHomeScreenRedraw < 2000) {
updateHomeScreenDynamicValues();
return;
}
Serial.println("\n=== RISANJE DOMACEGA ZASLONA ===");
if (!homeScreenBackgroundDrawn) {
drawHomeScreenBackground();
}
drawStatusBar();
if (!homeScreenStaticElementsDrawn) {
drawHomeScreenStaticElements();
}
int circleRadius = 70;
int outerRingRadius = circleRadius + 10;
int middleRingRadius = circleRadius + 5;
int innerRingRadius = circleRadius - 8;
int leftCircleX = 90;
int rightCircleX = 390;
int circleY = tft.height() / 2 - 30 - 6;
int smallCircleRadius = 32;
int smallOuterRadius = smallCircleRadius + 6;
int smallInnerRadius = smallCircleRadius - 3;
int smallCircleX = 240;
int smallCircleY = circleY - 36;
int extTempCircleX = 160;
int extTempCircleY = circleY - 58;
int extHumCircleX = 320;
int extHumCircleY = circleY - 58;
int progressBarHeight = 20;
int progressBarWidth = tft.width() - 20;
int progressBarX = (tft.width() - progressBarWidth) / 2;
int controlY = extTempCircleY + smallCircleRadius + 40 - 6;
int relayStartY = controlY - 5;
int relayButtonHeight = 16;
int relayVerticalSpacing = 4;
int lightBarY = relayStartY + 4 * (relayButtonHeight + relayVerticalSpacing) + 5;
int barSpacing = 10;
int calcBarY = lightBarY + progressBarHeight + barSpacing;
int actualBarY = calcBarY + progressBarHeight + barSpacing;
int topProgressBarY = STATUS_BAR_HEIGHT + 5;
int topProgressBarWidth = (extHumCircleX - extTempCircleX) - 70;
int topProgressBarHeight = 20;
int topProgressBarX = extTempCircleX + (extHumCircleX - extTempCircleX - topProgressBarWidth) / 2;
if (topProgressBarWidth > 20) {
int luxPercent = 0;
if (module1Active && externalLux >= 0) {
float constrainedLux = constrain(externalLux, 0, 20000);
luxPercent = (constrainedLux / 20000.0) * 100.0;
}
tft.fillRoundRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, 8, PROGRESS_BG_COLOR);
if (luxPercent > 0 && module1Active) {
int progressWidth = map(luxPercent, 0, 100, 0, topProgressBarWidth);
if (progressWidth > 0) {
tft.fillRoundRect(topProgressBarX, topProgressBarY, progressWidth, topProgressBarHeight, 8, LIGHT_COLOR);
}
}
tft.drawRoundRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, 8, ST77XX_WHITE);
tft.setFont();
tft.setTextSize(1);
String luxStr;
if (module1Active && externalLux >= 0) {
if (externalLux < 10) {
luxStr = String(externalLux, 1) + " lx";
} else if (externalLux < 1000) {
luxStr = String((int)externalLux) + " lx";
} else {
luxStr = String(externalLux / 1000.0, 1) + " klx";
}
} else {
luxStr = "-- lx";
}
int16_t luxX1, luxY1;
uint16_t luxW, luxH;
tft.getTextBounds(luxStr, 0, 0, &luxX1, &luxY1, &luxW, &luxH);
int textCenterY = topProgressBarY + (topProgressBarHeight / 2);
int luxTextX = topProgressBarX + (topProgressBarWidth - luxW) / 2;
int luxTextY = textCenterY - (luxH / 2) - luxY1;
bool isOverProgress = false;
if (module1Active && luxPercent > 0) {
int progressWidth = map(luxPercent, 0, 100, 0, topProgressBarWidth);
isOverProgress = (luxTextX + luxW / 2) < (topProgressBarX + progressWidth);
}
uint16_t textColor = (isOverProgress && module1Active) ? ST77XX_BLACK : ST77XX_WHITE;
tft.setCursor(luxTextX, luxTextY);
tft.setTextColor(textColor);
tft.print(luxStr);
}
// ========== DVOJNI KROG ZA TEMPERATURO ZRAKA (levi) ==========
uint16_t tempColor;
if (internalTemperature < targetTempMin) {
tempColor = TOO_LOW_COLOR;
} else if (internalTemperature > targetTempMax) {
tempColor = TOO_HIGH_COLOR;
} else {
tempColor = NORMAL_COLOR;
}
drawClockwiseDoubleRing(leftCircleX, circleY, outerRingRadius, middleRingRadius, innerRingRadius,
internalTemperature, 0.0, 50.0, targetTempMin, targetTempMax, tempColor);
tft.fillCircle(leftCircleX, circleY, circleRadius - 12, tempColor);
tft.drawCircle(leftCircleX, circleY, circleRadius - 12, ST77XX_WHITE);
if (tempTrend != TREND_NONE && (millis() - lastTrendChangeTime) < TREND_TIMEOUT) {
int arrowX = leftCircleX;
int arrowY = circleY - circleRadius + 15;
uint16_t arrowColor = (tempTrend == TREND_UP) ? rgbTo565(0, 255, 100) : rgbTo565(255, 100, 100);
drawTrendArrow(arrowX, arrowY, tempTrend, arrowColor);
}
tft.setFont(&FreeSansBold24pt7b);
String tempStr = String(internalTemperature, 1);
int16_t tempX1, tempY1;
uint16_t tempW, tempH;
tft.getTextBounds(tempStr, 0, 0, &tempX1, &tempY1, &tempW, &tempH);
int tempTextX = leftCircleX - tempW / 2;
int tempTextY = circleY + tempH / 2 - 15;
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(tempTextX, tempTextY);
tft.print(tempStr);
tft.setFont(&FreeSansBold18pt7b);
String tempSymbolStr = "°C";
int16_t tempSymbolX1, tempSymbolY1;
uint16_t tempSymbolW, tempSymbolH;
tft.getTextBounds(tempSymbolStr, 0, 0, &tempSymbolX1, &tempSymbolY1, &tempSymbolW, &tempSymbolH);
int tempSymbolX = leftCircleX - tempSymbolW / 2;
int tempSymbolY = tempTextY + tempH - 4;
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(tempSymbolX, tempSymbolY);
tft.print(tempSymbolStr);
// ========== DVOJNI KROG ZA VLAGO ZRAKA (desni) ==========
uint16_t humColor;
if (internalHumidity < targetHumMin) {
humColor = TOO_LOW_COLOR;
} else if (internalHumidity > targetHumMax) {
humColor = TOO_HIGH_COLOR;
} else {
humColor = NORMAL_COLOR;
}
drawClockwiseDoubleRing(rightCircleX, circleY, outerRingRadius, middleRingRadius, innerRingRadius,
internalHumidity, 0.0, 100.0, targetHumMin, targetHumMax, humColor);
tft.fillCircle(rightCircleX, circleY, circleRadius - 12, humColor);
tft.drawCircle(rightCircleX, circleY, circleRadius - 12, ST77XX_WHITE);
if (humTrend != TREND_NONE && (millis() - lastTrendChangeTime) < TREND_TIMEOUT) {
int arrowX = rightCircleX;
int arrowY = circleY - circleRadius + 15;
uint16_t arrowColor = (humTrend == TREND_UP) ? rgbTo565(0, 255, 100) : rgbTo565(255, 100, 100);
drawTrendArrow(arrowX, arrowY, humTrend, arrowColor);
}
tft.setFont(&FreeSansBold24pt7b);
String humStr = String(internalHumidity, 1);
int16_t humX1, humY1;
uint16_t humW, humH;
tft.getTextBounds(humStr, 0, 0, &humX1, &humY1, &humW, &humH);
int humTextX = rightCircleX - humW / 2;
int humTextY = circleY + humH / 2 - 15;
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(humTextX, humTextY);
tft.print(humStr);
tft.setFont(&FreeSansBold18pt7b);
String humSymbolStr = "%";
int16_t humSymbolX1, humSymbolY1;
uint16_t humSymbolW, humSymbolH;
tft.getTextBounds(humSymbolStr, 0, 0, &humSymbolX1, &humSymbolY1, &humSymbolW, &humSymbolH);
int humSymbolX = rightCircleX - humSymbolW / 2;
int humSymbolY = humTextY + humH - 4;
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(humSymbolX, humSymbolY);
tft.print(humSymbolStr);
// ========== DVOJNI KROG ZA TLA ==========
int soilCircleX = 240;
int soilCircleY = circleY - 36;
int soilRadius = 38;
drawSoilDoubleRing(soilCircleX, soilCircleY, soilRadius, soilMoisturePercent, module4SoilTemp);
// ========== ZUNANJI SENZORJI (majhni krogi) ==========
drawClockwiseProgressRing(extTempCircleX, extTempCircleY, smallOuterRadius, smallInnerRadius,
module1Active ? externalTemperature : 0,
-10.0, 40.0,
module1Active ? EXT_TEMP_PROGRESS_COLOR : rgbTo565(60, 60, 60));
tft.fillCircle(extTempCircleX, extTempCircleY, smallCircleRadius - 4,
module1Active ? EXT_TEMP_CIRCLE_COLOR : rgbTo565(80, 80, 80));
tft.drawCircle(extTempCircleX, extTempCircleY, smallCircleRadius - 4, ST77XX_WHITE);
tft.drawCircle(extTempCircleX, extTempCircleY, smallOuterRadius, ST77XX_WHITE);
tft.setFont(&FreeSansBold12pt7b);
String extTempStr = module1Active ? String(externalTemperature, 1) : "--.-";
int16_t extTempX1, extTempY1;
uint16_t extTempW, extTempH;
tft.getTextBounds(extTempStr, 0, 0, &extTempX1, &extTempY1, &extTempW, &extTempH);
int extTempTextX = extTempCircleX - extTempW / 2;
int extTempTextY = extTempCircleY + extTempH / 2 - 5;
tft.setTextColor(module1Active ? ST77XX_WHITE : rgbTo565(100, 100, 100));
tft.setCursor(extTempTextX, extTempTextY);
tft.print(extTempStr);
tft.setFont(&FreeSansBold9pt7b);
String extTempSymbolStr = "°C";
int16_t extTempSymbolX1, extTempSymbolY1;
uint16_t extTempSymbolW, extTempSymbolH;
tft.getTextBounds(extTempSymbolStr, 0, 0, &extTempSymbolX1, &extTempSymbolY1, &extTempSymbolW, &extTempSymbolH);
int extTempSymbolX = extTempCircleX - extTempSymbolW / 2;
int extTempSymbolY = extTempTextY + extTempH - 2;
tft.setTextColor(module1Active ? ST77XX_WHITE : rgbTo565(100, 100, 100));
tft.setCursor(extTempSymbolX, extTempSymbolY);
tft.print(extTempSymbolStr);
drawClockwiseProgressRing(extHumCircleX, extHumCircleY, smallOuterRadius, smallInnerRadius,
module1Active ? externalHumidity : 0,
0.0, 100.0,
module1Active ? EXT_HUM_PROGRESS_COLOR : rgbTo565(60, 60, 60));
tft.fillCircle(extHumCircleX, extHumCircleY, smallCircleRadius - 4,
module1Active ? EXT_HUM_CIRCLE_COLOR : rgbTo565(80, 80, 80));
tft.drawCircle(extHumCircleX, extHumCircleY, smallCircleRadius - 4, ST77XX_WHITE);
tft.drawCircle(extHumCircleX, extHumCircleY, smallOuterRadius, ST77XX_WHITE);
tft.setFont(&FreeSansBold12pt7b);
String extHumStr = module1Active ? String(externalHumidity, 1) : "--.-";
int16_t extHumX1, extHumY1;
uint16_t extHumW, extHumH;
tft.getTextBounds(extHumStr, 0, 0, &extHumX1, &extHumY1, &extHumW, &extHumH);
int extHumTextX = extHumCircleX - extHumW / 2;
int extHumTextY = extHumCircleY + extHumH / 2 - 5;
tft.setTextColor(module1Active ? ST77XX_WHITE : rgbTo565(100, 100, 100));
tft.setCursor(extHumTextX, extHumTextY);
tft.print(extHumStr);
tft.setFont(&FreeSansBold9pt7b);
String extHumSymbolStr = "%";
int16_t extHumSymbolX1, extHumSymbolY1;
uint16_t extHumSymbolW, extHumSymbolH;
tft.getTextBounds(extHumSymbolStr, 0, 0, &extHumSymbolX1, &extHumSymbolY1, &extHumSymbolW, &extHumSymbolH);
int extHumSymbolX = extHumCircleX - extHumSymbolW / 2;
int extHumSymbolY = extHumTextY + extHumH - 2;
tft.setTextColor(module1Active ? ST77XX_WHITE : rgbTo565(100, 100, 100));
tft.setCursor(extHumSymbolX, extHumSymbolY);
tft.print(extHumSymbolStr);
// ========== PREOSTALI RELEJI (R5, R6, R7, R8) ==========
int relayColumnWidth = 56;
int relayColumnSpacing = 4;
int totalRelaysWidth = 2 * relayColumnWidth + relayColumnSpacing;
int availableSpace = rightCircleX - leftCircleX - 10;
if (totalRelaysWidth > availableSpace) {
relayColumnWidth = (availableSpace - relayColumnSpacing) / 2;
totalRelaysWidth = 2 * relayColumnWidth + relayColumnSpacing;
}
int relayStartX = (tft.width() - totalRelaysWidth) / 2;
int rightColumnX = relayStartX + relayColumnWidth + relayColumnSpacing;
int leftColumnRelays[2] = { 4, 6 };
String leftLabels[2] = { "R5", "R7" };
int rightColumnRelays[2] = { 5, 7 };
String rightLabels[2] = { "R6", "R8" };
tft.setFont();
tft.setTextSize(1);
for (int i = 0; i < 2; i++) {
int relayIndex = leftColumnRelays[i];
int buttonY = relayStartY + i * (relayButtonHeight + relayVerticalSpacing);
uint16_t relayColor = relayStates[relayIndex] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
if (!relayControlEnabled) relayColor = RELAY_DISABLED_COLOR;
tft.fillRoundRect(relayStartX, buttonY, relayColumnWidth, relayButtonHeight, 3, relayColor);
tft.drawRoundRect(relayStartX, buttonY, relayColumnWidth, relayButtonHeight, 3, ST77XX_WHITE);
if (relayColor == RELAY_ON_COLOR) {
tft.setTextColor(ST77XX_BLACK);
} else if (relayColor == RELAY_OFF_COLOR) {
tft.setTextColor(ST77XX_WHITE);
} else {
tft.setTextColor(rgbTo565(200, 200, 200));
}
String relayLabel = leftLabels[i];
int16_t x1, y1;
uint16_t textWidth, textHeight;
tft.getTextBounds(relayLabel, 0, 0, &x1, &y1, &textWidth, &textHeight);
int textX = relayStartX + (relayColumnWidth - textWidth) / 2 - x1;
int textY = buttonY + (relayButtonHeight - textHeight) / 2 - y1;
tft.setCursor(textX, textY);
tft.print(relayLabel);
}
for (int i = 0; i < 2; i++) {
int relayIndex = rightColumnRelays[i];
int buttonY = relayStartY + i * (relayButtonHeight + relayVerticalSpacing);
uint16_t relayColor = relayStates[relayIndex] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
if (!relayControlEnabled) relayColor = RELAY_DISABLED_COLOR;
tft.fillRoundRect(rightColumnX, buttonY, relayColumnWidth, relayButtonHeight, 3, relayColor);
tft.drawRoundRect(rightColumnX, buttonY, relayColumnWidth, relayButtonHeight, 3, ST77XX_WHITE);
if (relayColor == RELAY_ON_COLOR) {
tft.setTextColor(ST77XX_BLACK);
} else if (relayColor == RELAY_OFF_COLOR) {
tft.setTextColor(ST77XX_WHITE);
} else {
tft.setTextColor(rgbTo565(200, 200, 200));
}
String relayLabel = rightLabels[i];
int16_t x1, y1;
uint16_t textWidth, textHeight;
tft.getTextBounds(relayLabel, 0, 0, &x1, &y1, &textWidth, &textHeight);
int textX = rightColumnX + (relayColumnWidth - textWidth) / 2 - x1;
int textY = buttonY + (relayButtonHeight - textHeight) / 2 - y1;
tft.setCursor(textX, textY);
tft.print(relayLabel);
}
// ========== PROGRESS BARJI ==========
tft.fillRoundRect(progressBarX, lightBarY, progressBarWidth, progressBarHeight, 10, PROGRESS_BG_COLOR);
int lightProgressWidth = map(constrain(ldrInternalPercent, 0, 100), 0, 100, 0, progressBarWidth);
if (lightProgressWidth > 0) {
tft.fillRoundRect(progressBarX, lightBarY, lightProgressWidth, progressBarHeight, 10, LIGHT_COLOR);
}
tft.drawRoundRect(progressBarX, lightBarY, progressBarWidth, progressBarHeight, 10, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(progressBarX + 14, lightBarY + 4);
tft.print("Svetloba v vitrini ");
String lightPercentStr = String(ldrInternalPercent) + "%";
int16_t lightX1, lightY1;
uint16_t lightW, lightH;
tft.getTextBounds(lightPercentStr, 0, 0, &lightX1, &lightY1, &lightW, &lightH);
tft.setCursor(progressBarX + progressBarWidth - lightW - 10, lightBarY + 4);
tft.print(lightPercentStr);
tft.fillRoundRect(progressBarX, calcBarY, progressBarWidth, progressBarHeight, 10, PROGRESS_BG_COLOR);
int calcProgressWidth = map(shadeCalculatedPosition, 0, 100, 0, progressBarWidth);
if (calcProgressWidth > 0) {
tft.fillRoundRect(progressBarX, calcBarY, calcProgressWidth, progressBarHeight, 10, rgbTo565(0, 150, 255));
}
tft.drawRoundRect(progressBarX, calcBarY, progressBarWidth, progressBarHeight, 10, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(progressBarX + 14, calcBarY + 4);
tft.print("Izrac. pozicija ");
String calcPosStr = String(shadeCalculatedPosition) + "%";
int16_t calcX1, calcY1;
uint16_t calcW, calcH;
tft.getTextBounds(calcPosStr, 0, 0, &calcX1, &calcY1, &calcW, &calcH);
tft.setCursor(progressBarX + progressBarWidth - calcW - 10, calcBarY + 4);
tft.print(calcPosStr);
tft.fillRoundRect(progressBarX, actualBarY, progressBarWidth, progressBarHeight, 10, PROGRESS_BG_COLOR);
int actualProgressWidth = map(shadeCurrentPosition, 0, 100, 0, progressBarWidth);
if (actualProgressWidth > 0) {
tft.fillRoundRect(progressBarX, actualBarY, actualProgressWidth, progressBarHeight, 10, rgbTo565(255, 100, 0));
}
tft.drawRoundRect(progressBarX, actualBarY, progressBarWidth, progressBarHeight, 10, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(progressBarX + 14, actualBarY + 4);
tft.print("Dejanska pozicija ");
String actualPosStr = String(shadeCurrentPosition) + "%";
int16_t actualX1, actualY1;
uint16_t actualW, actualH;
tft.getTextBounds(actualPosStr, 0, 0, &actualX1, &actualY1, &actualW, &actualH);
tft.setCursor(progressBarX + progressBarWidth - actualW - 10, actualBarY + 4);
tft.print(actualPosStr);
if (!module4Active || isnan(internalTemperature)) {
tft.setFont();
tft.setTextSize(0);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(progressBarX + 14, actualBarY + progressBarHeight + 8);
tft.print("⚠️ Modul 4 (vitrina) ni dosegljiv - senzorji niso na voljo!");
} else if (!shadeAutoControl) {
tft.setFont();
tft.setTextSize(0);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.setCursor(progressBarX + 14, actualBarY + progressBarHeight + 8);
tft.print("ℹ️ Avtomatsko senčenje je izklopljeno");
}
drawWarningBar();
// ========== GUMBI V KROGIH (NARISANI NA KONCU, DA SO V OSPREDJU) ==========
// Gumb GRETJE (zgoraj v levem krogu)
int gretjeBtnW = 38;
int gretjeBtnH = 16;
int gretjeBtnX = leftCircleX - gretjeBtnW / 2;
int gretjeBtnY = circleY - circleRadius + 5;
uint16_t gretjeColor = relayStates[0] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRoundRect(gretjeBtnX, gretjeBtnY, gretjeBtnW, gretjeBtnH, 3, gretjeColor);
tft.drawRoundRect(gretjeBtnX, gretjeBtnY, gretjeBtnW, gretjeBtnH, 3, ST77XX_WHITE);
tft.setTextSize(1);
tft.setTextColor(gretjeColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds("GRET", 0, 0, &x1, &y1, &w, &h);
int textX = gretjeBtnX + (gretjeBtnW - w) / 2 - x1;
int textY = gretjeBtnY + (gretjeBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("GRET");
// Gumb HLAJENJE (spodaj v levem krogu)
int hlajenjeBtnW = 38;
int hlajenjeBtnH = 16;
int hlajenjeBtnX = leftCircleX - hlajenjeBtnW / 2;
int hlajenjeBtnY = circleY + circleRadius - hlajenjeBtnH - 5;
uint16_t hlajenjeColor = relayStates[1] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRoundRect(hlajenjeBtnX, hlajenjeBtnY, hlajenjeBtnW, hlajenjeBtnH, 3, hlajenjeColor);
tft.drawRoundRect(hlajenjeBtnX, hlajenjeBtnY, hlajenjeBtnW, hlajenjeBtnH, 3, ST77XX_WHITE);
tft.setTextColor(hlajenjeColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.getTextBounds("HLAJ", 0, 0, &x1, &y1, &w, &h);
textX = hlajenjeBtnX + (hlajenjeBtnW - w) / 2 - x1;
textY = hlajenjeBtnY + (hlajenjeBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("HLAJ");
// Gumb VLAŽENJE (zgoraj v desnem krogu)
int vlazBtnW = 38;
int vlazBtnH = 16;
int vlazBtnX = rightCircleX - vlazBtnW / 2;
int vlazBtnY = circleY - circleRadius + 5;
uint16_t vlazColor = relayStates[2] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRoundRect(vlazBtnX, vlazBtnY, vlazBtnW, vlazBtnH, 3, vlazColor);
tft.drawRoundRect(vlazBtnX, vlazBtnY, vlazBtnW, vlazBtnH, 3, ST77XX_WHITE);
tft.setTextColor(vlazColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.getTextBounds("VLAZ", 0, 0, &x1, &y1, &w, &h);
textX = vlazBtnX + (vlazBtnW - w) / 2 - x1;
textY = vlazBtnY + (vlazBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("VLAZ");
// Gumb SUŠENJE (spodaj v desnem krogu)
int susBtnW = 38;
int susBtnH = 16;
int susBtnX = rightCircleX - susBtnW / 2;
int susBtnY = circleY + circleRadius - susBtnH - 5;
uint16_t susColor = relayStates[3] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRoundRect(susBtnX, susBtnY, susBtnW, susBtnH, 3, susColor);
tft.drawRoundRect(susBtnX, susBtnY, susBtnW, susBtnH, 3, ST77XX_WHITE);
tft.setTextColor(susColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.getTextBounds("SUS", 0, 0, &x1, &y1, &w, &h);
textX = susBtnX + (susBtnW - w) / 2 - x1;
textY = susBtnY + (susBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("SUS");
lastDrawnInternalTemp = internalTemperature;
lastDrawnInternalHum = internalHumidity;
lastDrawnSoilPercent = soilMoisturePercent;
lastDrawnExternalTemp = externalTemperature;
lastDrawnExternalHum = externalHumidity;
lastDrawnInternalLux = ldrInternalLux;
lastDrawnExternalLux = externalLux;
lastDrawnModule4Temp = module4AirTemp;
lastDrawnModule4Hum = module4AirHum;
lastDrawnModule4Light = module4LightPercent;
lastDrawnModule4SoilM = module4SoilMoisture;
lastDrawnModule4TVOC = module4TVOC;
lastDrawnModule1Active = module1Active;
lastDrawnModule2Active = module2Active;
lastDrawnModule3Active = module3Active;
lastDrawnVentState = relayStates[VENTILATION_RELAY];
lastDrawnShadeCalcPos = shadeCalculatedPosition;
lastDrawnShadeActualPos = shadeCurrentPosition;
lastDrawnTempTrend = tempTrend;
lastDrawnHumTrend = humTrend;
for (int i = 0; i < RELAY_COUNT; i++) {
lastDrawnRelays[i] = relayStates[i];
}
lastHomeScreenRedraw = now;
currentState = STATE_HOME_SCREEN;
lastState = STATE_HOME_SCREEN;
Serial.println("=== DOMACI ZASLON NARISAN ===\n");
}
void drawHomeScreenStaticElements() {
int circleRadius = 70;
int outerRingRadius = circleRadius + 10;
int middleRingRadius = circleRadius + 5;
int innerRingRadius = circleRadius - 8;
int leftCircleX = 90;
int rightCircleX = 390;
int circleY = tft.height() / 2 - 30 - 6;
int smallCircleRadius = 32;
int extTempCircleX = 160;
int extTempCircleY = circleY - 58;
int controlY = extTempCircleY + smallCircleRadius + 40 - 6;
int relayStartY = controlY - 5;
int relayButtonHeight = 16;
int relayVerticalSpacing = 4;
int ventSymbolX = 20;
int ventSymbolY = STATUS_BAR_HEIGHT + 28;
for (int r = 16; r >= 0; r -= 2) {
tft.drawCircle(ventSymbolX, ventSymbolY, r, rgbTo565(120, 120, 140));
}
tft.drawCircle(ventSymbolX, ventSymbolY, 16, rgbTo565(120, 120, 140));
tft.fillCircle(ventSymbolX, ventSymbolY, 4, rgbTo565(100, 100, 120));
drawHamburgerMenu(HAMBURGER_X, HAMBURGER_Y, HAMBURGER_SIZE);
// Ozadje za dvojni krog temperature zraka (levi)
for (int r = innerRingRadius; r <= outerRingRadius; r += 2) {
tft.drawCircle(leftCircleX, circleY, r, PROGRESS_BG_COLOR);
}
// Ozadje za dvojni krog vlage zraka (desni)
for (int r = innerRingRadius; r <= outerRingRadius; r += 2) {
tft.drawCircle(rightCircleX, circleY, r, PROGRESS_BG_COLOR);
}
// ========== DVOJNI KROG ZA TLA - samo statično ozadje ==========
int soilCircleX = 240;
int soilCircleY = circleY - 36;
int soilRadius = 38;
int ringThickness = 7;
int innerRadius = soilRadius - ringThickness;
// Nariši samo ozadje obročev (brez vrednosti)
for (int r = innerRadius - 2; r <= soilRadius + 2; r++) {
tft.drawCircle(soilCircleX, soilCircleY, r, PROGRESS_BG_COLOR);
}
tft.drawCircle(soilCircleX, soilCircleY, soilRadius, ST77XX_WHITE);
tft.drawCircle(soilCircleX, soilCircleY, innerRadius, ST77XX_WHITE);
tft.drawCircle(soilCircleX, soilCircleY, innerRadius - ringThickness + 1, ST77XX_WHITE);
// Sredina - črna podlaga
tft.fillCircle(soilCircleX, soilCircleY, innerRadius - ringThickness, ST77XX_BLACK);
tft.drawCircle(soilCircleX, soilCircleY, innerRadius - ringThickness, ST77XX_WHITE);
homeScreenStaticElementsDrawn = true;
}
void updateHomeScreenDynamicValues() {
unsigned long now = millis();
if (now - lastHomeScreenPartialUpdate < 1000) {
return;
}
bool externalLuxChanged = (abs(lastDrawnExternalLuxBar - externalLux) > 5);
int circleRadius = 70;
int leftCircleX = 90;
int rightCircleX = 390;
int circleY = tft.height() / 2 - 30 - 6;
int smallCircleRadius = 32;
int extTempCircleX = 160;
int extTempCircleY = circleY - 58;
int extHumCircleX = 320;
int extHumCircleY = circleY - 58;
// Posodobi zunanji LDR progress bar
if (externalLuxChanged) {
int topProgressBarY = STATUS_BAR_HEIGHT + 5;
int topProgressBarWidth = (extHumCircleX - extTempCircleX) - 70;
int topProgressBarHeight = 20;
int topProgressBarX = extTempCircleX + (extHumCircleX - extTempCircleX - topProgressBarWidth) / 2;
if (topProgressBarWidth > 20) {
int luxPercent = 0;
if (module1Active && externalLux >= 0) {
float constrainedLux = constrain(externalLux, 0, 20000);
luxPercent = (constrainedLux / 20000.0) * 100.0;
}
tft.fillRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, ST77XX_BLACK);
tft.fillRoundRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, 8, PROGRESS_BG_COLOR);
tft.drawRoundRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, 8, ST77XX_WHITE);
if (luxPercent > 0 && module1Active) {
int progressWidth = map(luxPercent, 0, 100, 0, topProgressBarWidth);
if (progressWidth > 0) {
tft.fillRoundRect(topProgressBarX, topProgressBarY, progressWidth, topProgressBarHeight, 8, LIGHT_COLOR);
}
}
tft.setFont();
tft.setTextSize(1);
String luxStr;
if (module1Active && externalLux >= 0) {
if (externalLux < 10) luxStr = String(externalLux, 1) + " lx";
else if (externalLux < 1000) luxStr = String((int)externalLux) + " lx";
else luxStr = String(externalLux / 1000.0, 1) + " klx";
} else {
luxStr = "-- lx";
}
int16_t luxX1, luxY1;
uint16_t luxW, luxH;
tft.getTextBounds(luxStr, 0, 0, &luxX1, &luxY1, &luxW, &luxH);
int textCenterY = topProgressBarY + (topProgressBarHeight / 2);
int luxTextX = topProgressBarX + (topProgressBarWidth - luxW) / 2;
int luxTextY = textCenterY - (luxH / 2) - luxY1;
bool isOverProgress = false;
if (module1Active && luxPercent > 0) {
int progressWidth = map(luxPercent, 0, 100, 0, topProgressBarWidth);
isOverProgress = (luxTextX + luxW / 2) < (topProgressBarX + progressWidth);
}
uint16_t textColor = (isOverProgress && module1Active) ? ST77XX_BLACK : ST77XX_WHITE;
tft.setCursor(luxTextX, luxTextY);
tft.setTextColor(textColor);
tft.print(luxStr);
lastDrawnExternalLuxBar = externalLux;
}
}
// ========== POSODOBITEV DVOJNEGA KROGA ZA TLA ==========
int soilCircleX = 240;
int soilCircleY = circleY - 36;
int soilRadius = 38;
// Samo posodobi obroča (ne riše celega zaslona)
updateSoilDoubleRing(soilCircleX, soilCircleY, soilRadius, soilMoisturePercent, module4SoilTemp);
// ========== POSODOBI PROGRESS BAR ZA SVETLOBO ==========
int controlY = extTempCircleY + smallCircleRadius + 40 - 6;
int relayStartY = controlY - 5;
int relayButtonHeight = 16;
int relayVerticalSpacing = 4;
int progressBarHeight = 20;
int progressBarWidth = tft.width() - 20;
int progressBarX = (tft.width() - progressBarWidth) / 2;
int lightBarY = relayStartY + 4 * (relayButtonHeight + relayVerticalSpacing) + 5;
int progressWidth = map(constrain(ldrInternalPercent, 0, 100), 0, 100, 0, progressBarWidth);
tft.fillRect(progressBarX, lightBarY, progressBarWidth, progressBarHeight, ST77XX_BLACK);
tft.fillRoundRect(progressBarX, lightBarY, progressBarWidth, progressBarHeight, 10, PROGRESS_BG_COLOR);
tft.drawRoundRect(progressBarX, lightBarY, progressBarWidth, progressBarHeight, 10, ST77XX_WHITE);
if (progressWidth > 0) {
tft.fillRoundRect(progressBarX, lightBarY, progressWidth, progressBarHeight, 10, LIGHT_COLOR);
}
tft.setFont();
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(progressBarX + 14, lightBarY + 4);
tft.print("Svetloba v vitrini ");
String lightPercentStr = String(ldrInternalPercent) + "%";
int16_t luxX1_inner, luxY1_inner;
uint16_t luxW_inner, luxH_inner;
tft.getTextBounds(lightPercentStr, 0, 0, &luxX1_inner, &luxY1_inner, &luxW_inner, &luxH_inner);
tft.setCursor(progressBarX + progressBarWidth - luxW_inner - 10, lightBarY + 4);
tft.setTextColor(ST77XX_WHITE);
tft.print(lightPercentStr);
lastDrawnModule4Temp = module4AirTemp;
lastDrawnModule4Hum = module4AirHum;
lastDrawnModule4Light = module4LightPercent;
lastDrawnModule4SoilM = module4SoilMoisture;
lastDrawnModule4TVOC = module4TVOC;
lastDrawnModule4ECO2 = module4ECO2;
lastHomeScreenPartialUpdate = now;
}
void drawThinDoubleRing(int x, int y, int radius,
float value1, float min1, float max1, uint16_t color1,
float value2, float min2, float max2, uint16_t color2,
int ringThickness = 6) {
int innerRadius = radius - ringThickness;
// Zunanji obroč (value1 - npr. vlaga)
float percent1 = 0;
if (max1 > min1) {
percent1 = constrain((value1 - min1) / (max1 - min1), 0.0, 1.0) * 100.0;
}
float angle1 = percent1 * 3.6;
// Notranji obroč (value2 - npr. temperatura)
float percent2 = 0;
if (max2 > min2) {
percent2 = constrain((value2 - min2) / (max2 - min2), 0.0, 1.0) * 100.0;
}
float angle2 = percent2 * 3.6;
// Nariši ozadje za oba obroča
for (int r = innerRadius; r <= radius; r++) {
tft.drawCircle(x, y, r, PROGRESS_BG_COLOR);
}
// Nariši zunanji obroč (tanek)
if (angle1 > 0) {
for (int r = innerRadius + 2; r <= radius - 2; r++) {
for (float a = -90; a <= -90 + angle1; a += 1.5) {
float rad = a * PI / 180.0;
int px = x + r * cos(rad);
int py = y + r * sin(rad);
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, color1);
}
}
}
}
// Nariši notranji obroč (tanek)
if (angle2 > 0) {
for (int r = innerRadius - 2; r >= innerRadius - ringThickness + 2; r--) {
for (float a = -90; a <= -90 + angle2; a += 1.5) {
float rad = a * PI / 180.0;
int px = x + r * cos(rad);
int py = y + r * sin(rad);
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, color2);
}
}
}
}
// Obrobe obročev
tft.drawCircle(x, y, radius, ST77XX_WHITE);
tft.drawCircle(x, y, innerRadius, ST77XX_WHITE);
}
// Funkcija za risanje dvojnega koncentričnega kroga za tla
void drawSoilDoubleRing(int x, int y, int radius, int moisturePercent, float soilTemp) {
int ringThickness = 7;
int innerRadius = radius - ringThickness;
// Zunanji obroč - vlaga tal
float moisturePercentFloat = constrain(moisturePercent, 0, 100);
float moistureAngle = moisturePercentFloat * 3.6;
// Notranji obroč - temperatura zemlje
float tempPercent = constrain((soilTemp - 0) / (40.0 - 0), 0.0, 1.0) * 100.0;
float tempAngle = tempPercent * 3.6;
// Počisti območje
for (int r = innerRadius - 2; r <= radius + 2; r++) {
tft.drawCircle(x, y, r, ST77XX_BLACK);
}
// Ozadje obročev
for (int r = innerRadius; r <= radius; r++) {
tft.drawCircle(x, y, r, PROGRESS_BG_COLOR);
}
// Zunanji obroč (vlaga)
if (moistureAngle > 0) {
uint16_t moistureColor = SOIL_PROGRESS_COLOR;
if (moisturePercent < 30) moistureColor = rgbTo565(255, 100, 100);
else if (moisturePercent < 60) moistureColor = rgbTo565(255, 200, 50);
else moistureColor = rgbTo565(100, 150, 255);
for (int r = innerRadius + 1; r <= radius - 1; r++) {
for (float a = -90; a <= -90 + moistureAngle; a += 1.2) {
float rad = a * PI / 180.0;
int px = x + r * cos(rad);
int py = y + r * sin(rad);
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, moistureColor);
}
}
}
}
// Notranji obroč (temperatura)
if (tempAngle > 0) {
uint16_t tempColor = rgbTo565(255, 150, 50);
if (soilTemp < 10) tempColor = rgbTo565(80, 100, 255);
else if (soilTemp < 20) tempColor = rgbTo565(100, 200, 100);
else if (soilTemp < 30) tempColor = rgbTo565(255, 200, 50);
else tempColor = rgbTo565(255, 80, 80);
for (int r = innerRadius - 3; r >= innerRadius - ringThickness + 2; r--) {
for (float a = -90; a <= -90 + tempAngle; a += 1.2) {
float rad = a * PI / 180.0;
int px = x + r * cos(rad);
int py = y + r * sin(rad);
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, tempColor);
}
}
}
}
// Obrobe
tft.drawCircle(x, y, radius, ST77XX_WHITE);
tft.drawCircle(x, y, innerRadius, ST77XX_WHITE);
tft.drawCircle(x, y, innerRadius - ringThickness + 1, ST77XX_WHITE);
// Sredina - prikaz obeh vrednosti
tft.fillCircle(x, y, innerRadius - ringThickness, ST77XX_BLACK);
tft.drawCircle(x, y, innerRadius - ringThickness, ST77XX_WHITE);
// Vlaga (velika)
tft.setFont(&FreeSansBold12pt7b);
String moistureStr = String(moisturePercent);
int16_t mX1, mY1;
uint16_t mW, mH;
tft.getTextBounds(moistureStr, 0, 0, &mX1, &mY1, &mW, &mH);
tft.setCursor(x - mW / 2, y - 3);
tft.setTextColor(ST77XX_WHITE);
tft.print(moistureStr);
tft.setFont(&FreeSansBold9pt7b);
tft.setCursor(x + mW / 2 - 3, y - 3);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("%");
// Temperatura (mala, pod vlago)
tft.setFont(&FreeSansBold9pt7b);
String tempStr = String(soilTemp, 1);
int16_t tX1, tY1;
uint16_t tW, tH;
tft.getTextBounds(tempStr, 0, 0, &tX1, &tY1, &tW, &tH);
tft.setCursor(x - tW / 2, y + 12);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print(tempStr);
tft.setFont(&FreeSansBold9pt7b);
tft.setCursor(x + tW / 2 - 2, y + 12);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("°");
}
// Funkcija za posodobitev dvojnega koncentričnega kroga za tla
void updateSoilDoubleRing(int x, int y, int radius, int moisturePercent, float soilTemp) {
int ringThickness = 7;
int innerRadius = radius - ringThickness;
// Samo prebarvaj obroča (poceni operacija)
for (int r = innerRadius - 2; r <= radius + 2; r++) {
tft.drawCircle(x, y, r, ST77XX_BLACK);
}
drawSoilDoubleRing(x, y, radius, moisturePercent, soilTemp);
}
void forceFullHomeScreenRedraw() {
homeScreenBackgroundDrawn = false;
homeScreenStaticElementsDrawn = false;
lastDrawnExternalLuxBar = -999;
tft.fillScreen(ST77XX_BLACK);
lastDrawnInternalTemp = -999;
lastDrawnInternalHum = -999;
lastDrawnSoilPercent = -999;
lastDrawnExternalTemp = -999;
lastDrawnExternalHum = -999;
lastDrawnInternalLux = -999;
lastDrawnExternalLux = -999;
lastDrawnShadeCalcPos = -999;
lastDrawnShadeActualPos = -999;
lastDrawnModule1Active = false;
lastDrawnModule2Active = false;
lastDrawnModule3Active = false;
lastDrawnVentState = false;
lastDrawnTempTrend = TREND_NONE;
lastDrawnHumTrend = TREND_NONE;
for (int i = 0; i < RELAY_COUNT; i++) {
lastDrawnRelays[i] = false;
}
showHomeScreen();
}
void handleHomeScreenTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int hamburgerTouchX = HAMBURGER_X - HAMBURGER_TOUCH_PADDING;
int hamburgerTouchY = HAMBURGER_Y - HAMBURGER_TOUCH_PADDING;
int hamburgerTouchWidth = HAMBURGER_SIZE + 2 * HAMBURGER_TOUCH_PADDING;
int hamburgerTouchHeight = 3 * (3 + 5) - 5 + 2 * HAMBURGER_TOUCH_PADDING;
if (x >= hamburgerTouchX && x <= hamburgerTouchX + hamburgerTouchWidth &&
y >= hamburgerTouchY && y <= hamburgerTouchY + hamburgerTouchHeight) {
Serial.println("=== HAMBURGER MENI PRITISNJEN ===");
animateHamburgerMenu();
showMainMenu();
return;
}
int ventSymbolY = STATUS_BAR_HEIGHT + 28;
int ventSymbolX = 20;
int ventTouchRadius = 35;
int distanceToVent = sqrt(pow(x - ventSymbolX, 2) + pow(y - ventSymbolY, 2));
if (distanceToVent < ventTouchRadius) {
Serial.println("=== VENTILATOR PRITISNJEN ===");
showVentilationControlScreen();
return;
}
int circleRadius = 70;
int leftCircleX = 90;
int rightCircleX = 390;
int circleY = tft.height() / 2 - 30 - 6;
int smallCircleRadius = 32;
int smallCircleX = 240;
int smallCircleY = circleY - 36;
int extTempCircleX = 160;
int extTempCircleY = circleY - 58;
int extHumCircleX = 320;
int extHumCircleY = circleY - 58;
// ========== DVOJNI KROG ZA VLAGO IN TEMPERATURO TAL ==========
int soilCircleX = 240;
int soilCircleY = circleY - 36;
int soilOuterRadius = 38;
int distanceToSoilCircle = sqrt(pow(x - soilCircleX, 2) + pow(y - soilCircleY, 2));
if (distanceToSoilCircle < soilOuterRadius + TOUCH_PADDING_CIRCLES) {
Serial.println("=== KROG ZA VLAGO IN TEMPERATURO TAL PRITISNJEN ===");
showSoilMoistureInfo();
return;
}
int distanceToExtTempCircle = sqrt(pow(x - extTempCircleX, 2) + pow(y - extTempCircleY, 2));
if (distanceToExtTempCircle < smallCircleRadius + TOUCH_PADDING_CIRCLES) {
Serial.println("=== ZUNANJA TEMPERATURA PRITISNJENA ===");
showExternalSensorsInfo();
return;
}
int distanceToExtHumCircle = sqrt(pow(x - extHumCircleX, 2) + pow(y - extHumCircleY, 2));
if (distanceToExtHumCircle < smallCircleRadius + TOUCH_PADDING_CIRCLES) {
Serial.println("=== ZUNANJA VLAGA PRITISNJENA ===");
showExternalSensorsInfo();
return;
}
int distanceToLeftCircle = sqrt(pow(x - leftCircleX, 2) + pow(y - circleY, 2));
if (distanceToLeftCircle < circleRadius + 15) {
Serial.println("=== NOTRANJA TEMPERATURA PRITISNJENA ===");
showInternalSensorsInfo();
return;
}
int distanceToRightCircle = sqrt(pow(x - rightCircleX, 2) + pow(y - circleY, 2));
if (distanceToRightCircle < circleRadius + 15) {
if (x > HAMBURGER_X - 30) {
return;
}
Serial.println("=== NOTRANJA VLAGA PRITISNJENA ===");
showInternalSensorsInfo();
return;
}
// ========== GUMBI V KROGU TEMPERATURE (GRETJE in HLAJENJE) ==========
int tempCircleX = leftCircleX;
int tempCircleY = circleY;
// Gumb GRETJE (zgoraj v krogu)
int gretjeBtnW = 38;
int gretjeBtnH = 16;
int gretjeBtnX = tempCircleX - gretjeBtnW / 2;
int gretjeBtnY = tempCircleY - circleRadius + 5;
if (x > gretjeBtnX && x < gretjeBtnX + gretjeBtnW &&
y > gretjeBtnY && y < gretjeBtnY + gretjeBtnH) {
Serial.println("=== GRETJE (RELE 0) PRITISNJEN ===");
if (relayControlEnabled) {
tft.fillRoundRect(gretjeBtnX, gretjeBtnY, gretjeBtnW, gretjeBtnH, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(0);
updateHomeScreenRelays();
}
return;
}
// Gumb HLAJENJE (spodaj v krogu)
int hlajenjeBtnW = 38;
int hlajenjeBtnH = 16;
int hlajenjeBtnX = tempCircleX - hlajenjeBtnW / 2;
int hlajenjeBtnY = tempCircleY + circleRadius - hlajenjeBtnH - 5;
if (x > hlajenjeBtnX && x < hlajenjeBtnX + hlajenjeBtnW &&
y > hlajenjeBtnY && y < hlajenjeBtnY + hlajenjeBtnH) {
Serial.println("=== HLAJENJE (RELE 1) PRITISNJEN ===");
if (relayControlEnabled) {
tft.fillRoundRect(hlajenjeBtnX, hlajenjeBtnY, hlajenjeBtnW, hlajenjeBtnH, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(1);
updateHomeScreenRelays();
}
return;
}
// ========== GUMBI V KROGU VLAGE (VLAŽENJE in SUŠENJE) ==========
int humCircleX = rightCircleX;
int humCircleY = circleY;
// Gumb VLAŽENJE (zgoraj v krogu)
int vlazBtnW = 38;
int vlazBtnH = 16;
int vlazBtnX = humCircleX - vlazBtnW / 2;
int vlazBtnY = humCircleY - circleRadius + 5;
if (x > vlazBtnX && x < vlazBtnX + vlazBtnW &&
y > vlazBtnY && y < vlazBtnY + vlazBtnH) {
Serial.println("=== VLAŽENJE (RELE 2) PRITISNJEN ===");
if (relayControlEnabled) {
tft.fillRoundRect(vlazBtnX, vlazBtnY, vlazBtnW, vlazBtnH, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(2);
updateHomeScreenRelays();
}
return;
}
// Gumb SUŠENJE (spodaj v krogu)
int susBtnW = 38;
int susBtnH = 16;
int susBtnX = humCircleX - susBtnW / 2;
int susBtnY = humCircleY + circleRadius - susBtnH - 5;
if (x > susBtnX && x < susBtnX + susBtnW &&
y > susBtnY && y < susBtnY + susBtnH) {
Serial.println("=== SUŠENJE (RELE 3) PRITISNJEN ===");
if (relayControlEnabled) {
tft.fillRoundRect(susBtnX, susBtnY, susBtnW, susBtnH, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(3);
updateHomeScreenRelays();
}
return;
}
int controlY = extTempCircleY + smallCircleRadius + 40 - 6;
int relayColumnWidth = 56;
int relayButtonHeight = 16;
int relayVerticalSpacing = 4;
int relayColumnSpacing = 4;
int totalRelaysWidth = 2 * relayColumnWidth + relayColumnSpacing;
int availableSpace = rightCircleX - leftCircleX - 10;
if (totalRelaysWidth > availableSpace) {
relayColumnWidth = (availableSpace - relayColumnSpacing) / 2;
totalRelaysWidth = 2 * relayColumnWidth + relayColumnSpacing;
}
int relayStartX = (tft.width() - totalRelaysWidth) / 2;
int relayStartY = controlY - 5;
int rightColumnX = relayStartX + relayColumnWidth + relayColumnSpacing;
// Leva kolona: R5 (indeks 4) in R7 (indeks 6)
int leftColumnRelays[2] = { 4, 6 };
// Desna kolona: R6 (indeks 5) in R8 (indeks 7)
int rightColumnRelays[2] = { 5, 7 };
int relayTouchPadding = TOUCH_PADDING_RELAYS;
// R5 in R7 (leva kolona)
for (int i = 0; i < 2; i++) {
int buttonY = relayStartY + i * (relayButtonHeight + relayVerticalSpacing);
int touchLeft = relayStartX - relayTouchPadding;
int touchRight = relayStartX + relayColumnWidth + relayTouchPadding;
int touchTop = buttonY - relayTouchPadding;
int touchBottom = buttonY + relayButtonHeight + relayTouchPadding;
if (x >= touchLeft && x <= touchRight && y >= touchTop && y <= touchBottom) {
int relayIndex = leftColumnRelays[i];
if (relayControlEnabled) {
Serial.printf("=== RELE %d PRITISNJEN ===\n", relayIndex);
tft.fillRoundRect(relayStartX, buttonY, relayColumnWidth, relayButtonHeight, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(relayIndex);
updateHomeScreenRelays();
}
return;
}
}
// R6 in R8 (desna kolona)
for (int i = 0; i < 2; i++) {
int buttonY = relayStartY + i * (relayButtonHeight + relayVerticalSpacing);
int touchLeft = rightColumnX - relayTouchPadding;
int touchRight = rightColumnX + relayColumnWidth + relayTouchPadding;
int touchTop = buttonY - relayTouchPadding;
int touchBottom = buttonY + relayButtonHeight + relayTouchPadding;
if (x >= touchLeft && x <= touchRight && y >= touchTop && y <= touchBottom) {
int relayIndex = rightColumnRelays[i];
if (relayControlEnabled) {
Serial.printf("=== RELE %d PRITISNJEN ===\n", relayIndex);
tft.fillRoundRect(rightColumnX, buttonY, relayColumnWidth, relayButtonHeight, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(relayIndex);
updateHomeScreenRelays();
}
return;
}
}
int progressBarHeight = 20;
int progressBarWidth = tft.width() - 20;
int progressBarX = (tft.width() - progressBarWidth) / 2;
int lightBarY = 210;
if (x >= progressBarX - 10 && x <= progressBarX + progressBarWidth + 10 &&
y >= lightBarY - 10 && y <= lightBarY + progressBarHeight + 10) {
Serial.println("=== PROGRESS BAR SVETLOBE PRITISNJEN ===");
showInternalSensorsInfo();
return;
}
int calcBarY = 238;
if (x >= progressBarX - 10 && x <= progressBarX + progressBarWidth + 10 &&
y >= calcBarY - 10 && y <= calcBarY + progressBarHeight + 10) {
Serial.println("=== PROGRESS BAR IZRAČUNANE POZICIJE PRITISNJEN ===");
showShadeControlScreen();
return;
}
int actualBarY = 266;
if (x >= progressBarX - 10 && x <= progressBarX + progressBarWidth + 10 &&
y >= actualBarY - 10 && y <= actualBarY + progressBarHeight + 10) {
Serial.println("=== PROGRESS BAR DEJANSKE POZICIJE PRITISNJEN ===");
showShadeControlScreen();
return;
}
int module4ProgressBarY = actualBarY + progressBarHeight + 15;
int module4BoxHeight = 65;
int module4BoxWidth = 440;
int module4BoxX = (tft.width() - module4BoxWidth) / 2;
int detailButtonX = module4BoxX + 340;
int detailButtonY = module4ProgressBarY + 38;
int detailButtonWidth = 90;
int detailButtonHeight = 24;
if (x >= detailButtonX && x <= detailButtonX + detailButtonWidth &&
y >= detailButtonY && y <= detailButtonY + detailButtonHeight) {
Serial.println("=== DETALJI MODULA 4 PRITISNJENI ===");
showModule4Info();
return;
}
int warningBarY = 288;
int warningBarHeight = 32;
if (warnings.soilWarning || warnings.tempWarning || warnings.humWarning) {
if (x >= 10 && x <= tft.width() - 10 &&
y >= warningBarY && y <= warningBarY + warningBarHeight) {
Serial.println("=== OPOZORILNA VRSTICA PRITISNJENA ===");
String warningDetails = "";
if (warnings.soilWarning) {
warningDetails += "Vlaga tal: " + String(warnings.soilValue, 0) + "%\n";
}
if (warnings.tempWarning) {
warningDetails += "Temperatura: " + String(warnings.tempValue, 1) + "°C\n";
}
if (warnings.humWarning) {
warningDetails += "Vlaga: " + String(warnings.humValue, 1) + "%";
}
showTemporaryMessage(warningDetails, WARNING_COLOR, 2000);
return;
}
}
if (x > tft.width() - 60 && y < 60) {
Serial.println("=== HITRI DOSTOP DO NASTAVITEV ===");
showTempHumControlScreen();
return;
}
if (x < 60 && y < 60) {
Serial.println("=== HITRI DOSTOP DO MODULOV ===");
String moduleStatus = "";
if (module1Active || module2Active || module3Active || module4Active) {
moduleStatus = "M1: " + String(module1Active ? "✓" : "✗") +
" M2: " + String(module2Active ? "✓" : "✗") +
" M3: " + String(module3Active ? "✓" : "✗") +
" M4: " + String(module4Active ? "✓" : "✗");
} else {
moduleStatus = "Vsi moduli nedosegljivi";
}
showTemporaryMessage(moduleStatus, rgbTo565(100, 200, 255), 1500);
return;
}
}
void animateHamburgerMenu() {
int originalX = HAMBURGER_X;
int originalY = HAMBURGER_Y;
int size = HAMBURGER_SIZE;
tft.fillRect(originalX - 10, originalY - 10, size + 20, 3 * (3 + 5) - 5 + 20, ST77XX_BLACK);
for (int step = 0; step < 2; step++) {
for (int j = 0; j < 3; j++) {
int barY = originalY + j * (3 + 5) + step * 2;
int barWidth = size - step * 2;
int barX = originalX + step;
if (barWidth > 15) {
tft.fillRoundRect(barX, barY, barWidth, 3, 2, rgbTo565(255, 255, 100));
}
}
delay(30);
}
tft.fillRect(originalX - 10, originalY - 10, size + 20, 3 * (3 + 5) - 5 + 20, ST77XX_BLACK);
drawHamburgerMenu(originalX, originalY, size);
}
void showTempHumControlScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(0, 200, 255));
tft.setCursor(180, 35);
tft.println("NADZOR KLIMA");
int frameY = 50;
int frameHeight = 200;
tft.drawRoundRect(20, frameY, 440, frameHeight, 10, rgbTo565(0, 150, 255));
tft.fillRoundRect(21, frameY + 1, 438, frameHeight - 1, 10, rgbTo565(20, 40, 70));
tft.setTextSize(1);
int leftColumnX = 40;
int rightColumnX = 260;
int yOffset = frameY + 20;
int lineHeight = 20;
tft.setCursor(leftColumnX, yOffset);
tft.setTextColor(TEMP_COLOR);
tft.println("TEMPERATURA");
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Trenutna: ");
tft.setTextColor(TEMP_COLOR);
tft.printf("%.1f °C", internalTemperature);
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Min: ");
tft.setTextColor(rgbTo565(100, 100, 255));
tft.printf("%.1f °C", targetTempMin);
int buttonWidth = 80;
int buttonHeight = 32;
int buttonSpacing = 16;
drawNormalButton(leftColumnX, yOffset + 3 * lineHeight - 6, buttonWidth, buttonHeight,
rgbTo565(100, 100, 255), "-0.5");
drawNormalButton(leftColumnX + buttonWidth + buttonSpacing, yOffset + 3 * lineHeight - 6,
buttonWidth, buttonHeight, rgbTo565(100, 100, 255), "+0.5");
tft.setCursor(leftColumnX, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Max: ");
tft.setTextColor(TOO_HIGH_COLOR);
tft.printf("%.1f °C", targetTempMax);
drawNormalButton(leftColumnX, yOffset + 5 * lineHeight + 16, buttonWidth, buttonHeight,
TOO_HIGH_COLOR, "-0.5");
drawNormalButton(leftColumnX + buttonWidth + buttonSpacing, yOffset + 5 * lineHeight + 16,
buttonWidth, buttonHeight, TOO_HIGH_COLOR, "+0.5");
tft.setCursor(rightColumnX, yOffset);
tft.setTextColor(HUMIDITY_COLOR);
tft.println("VLAGA");
tft.setCursor(rightColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Trenutna: ");
tft.setTextColor(HUMIDITY_COLOR);
tft.printf("%.1f %%", internalHumidity);
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Min: ");
tft.setTextColor(rgbTo565(100, 100, 255));
tft.printf("%.1f %%", targetHumMin);
drawNormalButton(rightColumnX, yOffset + 3 * lineHeight - 6, buttonWidth, buttonHeight,
rgbTo565(100, 100, 255), "-1");
drawNormalButton(rightColumnX + buttonWidth + buttonSpacing, yOffset + 3 * lineHeight - 6,
buttonWidth, buttonHeight, rgbTo565(100, 100, 255), "+1");
tft.setCursor(rightColumnX, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Max: ");
tft.setTextColor(TOO_HIGH_COLOR);
tft.printf("%.1f %%", targetHumMax);
drawNormalButton(rightColumnX, yOffset + 5 * lineHeight + 16, buttonWidth, buttonHeight,
TOO_HIGH_COLOR, "-1");
drawNormalButton(rightColumnX + buttonWidth + buttonSpacing, yOffset + 5 * lineHeight + 16,
buttonWidth, buttonHeight, TOO_HIGH_COLOR, "+1");
int statusY = yOffset + 6 * lineHeight + 50;
tft.setCursor(leftColumnX, statusY);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Nadzor: ");
tft.setTextColor(autoControlEnabled ? NORMAL_COLOR : rgbTo565(200, 200, 200));
tft.println(autoControlEnabled ? "VKLOPLJEN" : "IZKLOPLJEN");
tft.setCursor(rightColumnX, statusY);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Aktivni: ");
int activeRelays = 0;
for (int i = 0; i < 4; i++) {
if (relayStates[i]) activeRelays++;
}
if (relayControlEnabled) {
tft.setTextColor(activeRelays > 0 ? RELAY_ON_COLOR : RELAY_OFF_COLOR);
tft.printf("%d/4", activeRelays);
} else {
tft.setTextColor(RELAY_DISABLED_COLOR);
tft.print("ONEMOGOCENI");
}
int bottomButtonY = 275;
int bottomButtonWidth = 120;
int bottomButtonHeight = 40;
int buttonSpacingX = 20;
int totalButtonWidth = 3 * bottomButtonWidth + 2 * buttonSpacingX;
int startX = (tft.width() - totalButtonWidth) / 2;
auto drawCenteredButton = [&](int x, int y, int w, int h, uint16_t color, const char* label) {
tft.fillRoundRect(x + 2, y + 2, w, h, 8, rgbTo565(30, 30, 50));
tft.fillRoundRect(x, y, w, h, 8, color);
tft.drawRoundRect(x, y, w, h, 8, blendColor(color, ST77XX_WHITE, 70));
tft.setTextSize(1);
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds(label, 0, 0, &textX, &textY, &textW, &textH);
int textPosX = x + (w - textW) / 2 - textX;
int textPosY = y + (h - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.setTextColor(ST77XX_WHITE);
tft.print(label);
};
drawCenteredButton(startX, bottomButtonY, bottomButtonWidth, bottomButtonHeight,
rgbTo565(76, 175, 80), "SHRANI");
drawCenteredButton(startX + bottomButtonWidth + buttonSpacingX, bottomButtonY,
bottomButtonWidth, bottomButtonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
String autoLabel = autoControlEnabled ? "OFF AVTO" : "ON AVTO";
drawCenteredButton(startX + 2 * (bottomButtonWidth + buttonSpacingX), bottomButtonY,
bottomButtonWidth, bottomButtonHeight,
autoControlEnabled ? rgbTo565(255, 100, 100) : rgbTo565(100, 255, 100),
autoLabel.c_str());
currentState = STATE_TEMP_HUM_CONTROL;
}
void handleTempHumControlTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int frameY = 50;
int leftColumnX = 40;
int rightColumnX = 260;
int yOffset = frameY + 20;
int lineHeight = 20;
int buttonWidth = 80;
int buttonHeight = 32;
int buttonSpacing = 16;
int bottomButtonY = 275;
if (x > leftColumnX && x < leftColumnX + buttonWidth && y > yOffset + 3 * lineHeight - 6 && y < yOffset + 3 * lineHeight - 6 + buttonHeight) {
targetTempMin -= 0.5;
if (targetTempMin < 0) targetTempMin = 0;
markSettingsChanged();
showTempHumControlScreen();
return;
}
if (x > leftColumnX + buttonWidth + buttonSpacing && x < leftColumnX + buttonWidth + buttonSpacing + buttonWidth && y > yOffset + 3 * lineHeight - 6 && y < yOffset + 3 * lineHeight - 6 + buttonHeight) {
targetTempMin += 0.5;
if (targetTempMin > targetTempMax) targetTempMin = targetTempMax;
markSettingsChanged();
showTempHumControlScreen();
return;
}
if (x > leftColumnX && x < leftColumnX + buttonWidth && y > yOffset + 5 * lineHeight + 16 && y < yOffset + 5 * lineHeight + 16 + buttonHeight) {
targetTempMax -= 0.5;
if (targetTempMax < targetTempMin) targetTempMax = targetTempMin;
markSettingsChanged();
showTempHumControlScreen();
return;
}
if (x > leftColumnX + buttonWidth + buttonSpacing && x < leftColumnX + buttonWidth + buttonSpacing + buttonWidth && y > yOffset + 5 * lineHeight + 16 && y < yOffset + 5 * lineHeight + 16 + buttonHeight) {
targetTempMax += 0.5;
markSettingsChanged();
showTempHumControlScreen();
return;
}
if (x > rightColumnX && x < rightColumnX + buttonWidth && y > yOffset + 3 * lineHeight - 6 && y < yOffset + 3 * lineHeight - 6 + buttonHeight) {
targetHumMin -= 1.0;
if (targetHumMin < 0) targetHumMin = 0;
markSettingsChanged();
showTempHumControlScreen();
return;
}
if (x > rightColumnX + buttonWidth + buttonSpacing && x < rightColumnX + buttonWidth + buttonSpacing + buttonWidth && y > yOffset + 3 * lineHeight - 6 && y < yOffset + 3 * lineHeight - 6 + buttonHeight) {
targetHumMin += 1.0;
if (targetHumMin > targetHumMax) targetHumMin = targetHumMax;
markSettingsChanged();
showTempHumControlScreen();
return;
}
if (x > rightColumnX && x < rightColumnX + buttonWidth && y > yOffset + 5 * lineHeight + 16 && y < yOffset + 5 * lineHeight + 16 + buttonHeight) {
targetHumMax -= 1.0;
if (targetHumMax < targetHumMin) targetHumMax = targetHumMin;
markSettingsChanged();
showTempHumControlScreen();
return;
}
if (x > rightColumnX + buttonWidth + buttonSpacing && x < rightColumnX + buttonWidth + buttonSpacing + buttonWidth && y > yOffset + 5 * lineHeight + 16 && y < yOffset + 5 * lineHeight + 16 + buttonHeight) {
targetHumMax += 1.0;
markSettingsChanged();
showTempHumControlScreen();
return;
}
int totalButtonWidth = 3 * 120 + 2 * 20;
int startX = (tft.width() - totalButtonWidth) / 2;
if (x > startX && x < startX + 120 && y > bottomButtonY && y < bottomButtonY + 40) {
saveAllSettings();
showTempHumControlScreen();
return;
}
if (x > startX + 120 + 20 && x < startX + 120 + 20 + 120 && y > bottomButtonY && y < bottomButtonY + 40) {
showHomeScreen();
return;
}
if (x > startX + 2 * (120 + 20) && x < startX + 2 * (120 + 20) + 120 && y > bottomButtonY && y < bottomButtonY + 40) {
autoControlEnabled = !autoControlEnabled;
markSettingsChanged();
showTempHumControlScreen();
return;
}
}
void initializeRelays() {
Serial.println("Inicializacija relejev...");
if (!mcpInitialized) {
Serial.println("MCP23017 ni inicializiran! Releji ne morejo biti nastavljeni.");
return;
}
for (int i = 0; i < RELAY_COUNT; i++) {
mcp.pinMode(RELAY_START_PIN + i, OUTPUT);
mcp.digitalWrite(RELAY_START_PIN + i, HIGH);
relayStates[i] = false;
}
}
void loadRelayStates() {
preferences.begin("system-settings", true);
for (int i = 0; i < RELAY_COUNT; i++) {
bool savedState = preferences.getBool(("relay" + String(i)).c_str(), false);
if (savedState != relayStates[i]) {
setRelay(i, savedState);
}
}
preferences.end();
}
void setRelay(int relayNum, bool state) {
if (relayNum < 0 || relayNum >= RELAY_COUNT || !mcpInitialized) {
Serial.printf("Napaka: Rele %d ni veljaven ali MCP ni inicializiran!\n", relayNum);
return;
}
if (relayStates[relayNum] == state) {
return;
}
if (relayControlEnabled) {
mcp.digitalWrite(RELAY_START_PIN + relayNum, state ? LOW : HIGH);
relayStates[relayNum] = state;
String relayNames[] = {"GRETJE", "HLAJENJE", "VLAŽENJE", "SUŠENJE",
"R5", "R6", "R7", "R8", "VENTILATOR"};
String name = (relayNum < 9) ? relayNames[relayNum] : "Rele " + String(relayNum);
Serial.printf("RELE %d (%s): %s\n", relayNum, name.c_str(), state ? "VKLOP" : "IZKLOP");
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenRelays();
}
markSettingsChanged();
} else {
Serial.println("OPOZORILO: Krmiljenje relejev je onemogočeno!");
}
}
void updateHomeScreenRelays() {
if (currentState != STATE_HOME_SCREEN) return;
int leftCircleX = 90;
int rightCircleX = 390;
int circleY = tft.height() / 2 - 30 - 6;
int circleRadius = 70;
// ========== POSODOBITEV GUMBOV V KROGU TEMPERATURE ==========
// Gumb GRETJE (zgoraj)
int gretjeBtnW = 38;
int gretjeBtnH = 16;
int gretjeBtnX = leftCircleX - gretjeBtnW / 2;
int gretjeBtnY = circleY - circleRadius + 5;
uint16_t gretjeColor = relayStates[0] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRect(gretjeBtnX, gretjeBtnY, gretjeBtnW, gretjeBtnH, ST77XX_BLACK);
tft.fillRoundRect(gretjeBtnX, gretjeBtnY, gretjeBtnW, gretjeBtnH, 3, gretjeColor);
tft.drawRoundRect(gretjeBtnX, gretjeBtnY, gretjeBtnW, gretjeBtnH, 3, ST77XX_WHITE);
tft.setTextSize(1);
tft.setTextColor(gretjeColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds("GRET", 0, 0, &x1, &y1, &w, &h);
int textX = gretjeBtnX + (gretjeBtnW - w) / 2 - x1;
int textY = gretjeBtnY + (gretjeBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("GRET");
// Gumb HLAJENJE (spodaj)
int hlajenjeBtnW = 38;
int hlajenjeBtnH = 16;
int hlajenjeBtnX = leftCircleX - hlajenjeBtnW / 2;
int hlajenjeBtnY = circleY + circleRadius - hlajenjeBtnH - 5;
uint16_t hlajenjeColor = relayStates[1] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRect(hlajenjeBtnX, hlajenjeBtnY, hlajenjeBtnW, hlajenjeBtnH, ST77XX_BLACK);
tft.fillRoundRect(hlajenjeBtnX, hlajenjeBtnY, hlajenjeBtnW, hlajenjeBtnH, 3, hlajenjeColor);
tft.drawRoundRect(hlajenjeBtnX, hlajenjeBtnY, hlajenjeBtnW, hlajenjeBtnH, 3, ST77XX_WHITE);
tft.setTextColor(hlajenjeColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.getTextBounds("HLAJ", 0, 0, &x1, &y1, &w, &h);
textX = hlajenjeBtnX + (hlajenjeBtnW - w) / 2 - x1;
textY = hlajenjeBtnY + (hlajenjeBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("HLAJ");
// ========== POSODOBITEV GUMBOV V KROGU VLAGE ==========
// Gumb VLAŽENJE (zgoraj)
int vlazBtnW = 38;
int vlazBtnH = 16;
int vlazBtnX = rightCircleX - vlazBtnW / 2;
int vlazBtnY = circleY - circleRadius + 5;
uint16_t vlazColor = relayStates[2] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRect(vlazBtnX, vlazBtnY, vlazBtnW, vlazBtnH, ST77XX_BLACK);
tft.fillRoundRect(vlazBtnX, vlazBtnY, vlazBtnW, vlazBtnH, 3, vlazColor);
tft.drawRoundRect(vlazBtnX, vlazBtnY, vlazBtnW, vlazBtnH, 3, ST77XX_WHITE);
tft.setTextColor(vlazColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.getTextBounds("VLAZ", 0, 0, &x1, &y1, &w, &h);
textX = vlazBtnX + (vlazBtnW - w) / 2 - x1;
textY = vlazBtnY + (vlazBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("VLAZ");
// Gumb SUŠENJE (spodaj)
int susBtnW = 38;
int susBtnH = 16;
int susBtnX = rightCircleX - susBtnW / 2;
int susBtnY = circleY + circleRadius - susBtnH - 5;
uint16_t susColor = relayStates[3] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
tft.fillRect(susBtnX, susBtnY, susBtnW, susBtnH, ST77XX_BLACK);
tft.fillRoundRect(susBtnX, susBtnY, susBtnW, susBtnH, 3, susColor);
tft.drawRoundRect(susBtnX, susBtnY, susBtnW, susBtnH, 3, ST77XX_WHITE);
tft.setTextColor(susColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.getTextBounds("SUS", 0, 0, &x1, &y1, &w, &h);
textX = susBtnX + (susBtnW - w) / 2 - x1;
textY = susBtnY + (susBtnH - h) / 2 - y1;
tft.setCursor(textX, textY);
tft.print("SUS");
// ========== PREOSTALI RELEJI (R5, R6, R7, R8) ==========
int smallCircleRadius = 32;
int extTempCircleX = 160;
int extTempCircleY = circleY - 58;
int controlY = extTempCircleY + smallCircleRadius + 40 - 6;
int relayColumnWidth = 56;
int relayButtonHeight = 16;
int relayVerticalSpacing = 4;
int relayColumnSpacing = 4;
int totalRelaysWidth = 2 * relayColumnWidth + relayColumnSpacing;
int availableSpace = rightCircleX - leftCircleX - 10;
if (totalRelaysWidth > availableSpace) {
relayColumnWidth = (availableSpace - relayColumnSpacing) / 2;
totalRelaysWidth = 2 * relayColumnWidth + relayColumnSpacing;
}
int relayStartX = (tft.width() - totalRelaysWidth) / 2;
int relayStartY = controlY - 5;
int rightColumnX = relayStartX + relayColumnWidth + relayColumnSpacing;
int leftColumnRelays[2] = { 4, 6 };
String leftLabels[2] = { "R5", "R7" };
int rightColumnRelays[2] = { 5, 7 };
String rightLabels[2] = { "R6", "R8" };
tft.setFont();
tft.setTextSize(1);
for (int i = 0; i < 2; i++) {
int relayIndex = leftColumnRelays[i];
int buttonY = relayStartY + i * (relayButtonHeight + relayVerticalSpacing);
uint16_t relayColor = relayStates[relayIndex] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
if (!relayControlEnabled) relayColor = RELAY_DISABLED_COLOR;
tft.fillRect(relayStartX, buttonY, relayColumnWidth, relayButtonHeight, ST77XX_BLACK);
tft.fillRoundRect(relayStartX, buttonY, relayColumnWidth, relayButtonHeight, 3, relayColor);
tft.drawRoundRect(relayStartX, buttonY, relayColumnWidth, relayButtonHeight, 3, ST77XX_WHITE);
if (relayColor == RELAY_ON_COLOR) {
tft.setTextColor(ST77XX_BLACK);
} else if (relayColor == RELAY_OFF_COLOR) {
tft.setTextColor(ST77XX_WHITE);
} else {
tft.setTextColor(rgbTo565(200, 200, 200));
}
String relayLabel = leftLabels[i];
int16_t x1, y1;
uint16_t textWidth, textHeight;
tft.getTextBounds(relayLabel, 0, 0, &x1, &y1, &textWidth, &textHeight);
int textX = relayStartX + (relayColumnWidth - textWidth) / 2 - x1;
int textY = buttonY + (relayButtonHeight - textHeight) / 2 - y1;
tft.setCursor(textX, textY);
tft.print(relayLabel);
}
for (int i = 0; i < 2; i++) {
int relayIndex = rightColumnRelays[i];
int buttonY = relayStartY + i * (relayButtonHeight + relayVerticalSpacing);
uint16_t relayColor = relayStates[relayIndex] ? RELAY_ON_COLOR : RELAY_OFF_COLOR;
if (!relayControlEnabled) relayColor = RELAY_DISABLED_COLOR;
tft.fillRect(rightColumnX, buttonY, relayColumnWidth, relayButtonHeight, ST77XX_BLACK);
tft.fillRoundRect(rightColumnX, buttonY, relayColumnWidth, relayButtonHeight, 3, relayColor);
tft.drawRoundRect(rightColumnX, buttonY, relayColumnWidth, relayButtonHeight, 3, ST77XX_WHITE);
if (relayColor == RELAY_ON_COLOR) {
tft.setTextColor(ST77XX_BLACK);
} else if (relayColor == RELAY_OFF_COLOR) {
tft.setTextColor(ST77XX_WHITE);
} else {
tft.setTextColor(rgbTo565(200, 200, 200));
}
String relayLabel = rightLabels[i];
int16_t x1, y1;
uint16_t textWidth, textHeight;
tft.getTextBounds(relayLabel, 0, 0, &x1, &y1, &textWidth, &textHeight);
int textX = rightColumnX + (relayColumnWidth - textWidth) / 2 - x1;
int textY = buttonY + (relayButtonHeight - textHeight) / 2 - y1;
tft.setCursor(textX, textY);
tft.print(relayLabel);
}
for (int i = 0; i < RELAY_COUNT; i++) {
lastDrawnRelays[i] = relayStates[i];
}
}
void toggleRelay(int relayNum) {
if (relayNum < 0 || relayNum >= RELAY_COUNT) return;
bool newState = !relayStates[relayNum];
setRelay(relayNum, newState);
}
void setAllRelays(bool state) {
for (int i = 0; i < RELAY_COUNT; i++) {
setRelay(i, state);
}
}
void IRAM_ATTR pulseCounter1() {
pulseCount1++;
}
void IRAM_ATTR pulseCounter2() {
pulseCount2++;
}
void IRAM_ATTR pulseCounter3() {
pulseCount3++;
}
void initializeFlowSensors() {
Serial.println("Inicializacija flow senzorjev YF-S201...");
if (FLOW_SENSOR_1_PIN >= 0 && FLOW_SENSOR_1_PIN < 50) {
pinMode(FLOW_SENSOR_1_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_1_PIN), pulseCounter1, FALLING);
}
if (FLOW_SENSOR_2_PIN >= 0 && FLOW_SENSOR_2_PIN < 50) {
pinMode(FLOW_SENSOR_2_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_2_PIN), pulseCounter2, FALLING);
}
if (FLOW_SENSOR_3_PIN >= 0 && FLOW_SENSOR_3_PIN < 50) {
pinMode(FLOW_SENSOR_3_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_3_PIN), pulseCounter3, FALLING);
}
oldTime = millis();
lastFlowUpdate = millis();
}
void updateFlowSensors() {
unsigned long currentTimeMillis = millis();
if (currentTimeMillis - lastFlowUpdate > FLOW_UPDATE_INTERVAL) {
float timeInSeconds = (currentTimeMillis - oldTime) / 1000.0;
if (timeInSeconds > 0) {
flowRate1 = (pulseCount1 / PULSES_PER_LITER) / (timeInSeconds / 60.0);
flowRate2 = (pulseCount2 / PULSES_PER_LITER) / (timeInSeconds / 60.0);
flowRate3 = (pulseCount3 / PULSES_PER_LITER) / (timeInSeconds / 60.0);
totalFlow1 += (pulseCount1 / PULSES_PER_LITER);
totalFlow2 += (pulseCount2 / PULSES_PER_LITER);
totalFlow3 += (pulseCount3 / PULSES_PER_LITER);
pulseCount1 = 0;
pulseCount2 = 0;
pulseCount3 = 0;
oldTime = currentTimeMillis;
}
lastFlowUpdate = currentTimeMillis;
}
}
void resetFlowTotals() {
totalFlow1 = 0;
totalFlow2 = 0;
totalFlow3 = 0;
}
void initializeMotor() {
Serial.println("Inicializacija motorja za sencenje na modulu 3...");
}
void calculateShadePosition() {
if (shadeAutoControl) {
if (ldrInternalLux <= shadeMinLux) {
shadeCalculatedPosition = 0;
Serial.printf("🌑 NOTRANJA SVETLOBA: %.0f lx (pod min %.0f) → ODPRI (0%%)\n",
ldrInternalLux, shadeMinLux);
}
else if (ldrInternalLux >= shadeMaxLux) {
shadeCalculatedPosition = 100;
Serial.printf("☀️ NOTRANJA SVETLOBA: %.0f lx (nad max %.0f) → ZAPRI (100%%)\n",
ldrInternalLux, shadeMaxLux);
}
else {
shadeCalculatedPosition = map(ldrInternalLux, shadeMinLux, shadeMaxLux, 0, 100);
shadeCalculatedPosition = constrain(shadeCalculatedPosition, 0, 100);
Serial.printf("🌤️ NOTRANJA SVETLOBA: %.0f lx (%.0f-%.0f) → POZICIJA: %d%%\n",
ldrInternalLux, shadeMinLux, shadeMaxLux, shadeCalculatedPosition);
}
}
}
void updateMotorPosition() {
unsigned long currentTime = millis();
if (currentTime - lastShadeUpdate > SHADE_UPDATE_INTERVAL) {
calculateShadePosition();
lastShadeUpdate = currentTime;
}
}
void setMotorPosition(int position) {
position = constrain(position, 0, 100);
shadeActualPosition = position;
moveShadeTo(position);
}
void scanI2CDevices() {
Serial.println("\n=== I2C SKENIRANJE ===");
byte error, address;
int nDevices = 0;
delay(250);
for (address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.printf("I2C naprava najdena na naslovu 0x%02X", address);
if (address == 0x20 || address == 0x21) {
Serial.print(" (MCP23017)");
} else if (address == 0x68) {
Serial.print(" (DS3231)");
}
Serial.println();
nDevices++;
} else if (error == 4) {
Serial.printf("Napaka na naslovu 0x%02X\n", address);
}
delay(10);
}
if (nDevices == 0) {
Serial.println("Ni najdenih I2C naprav!");
} else {
Serial.printf("Skupaj najdenih I2C naprav: %d\n", nDevices);
}
Serial.println("========================\n");
}
void updateSoilMoisture() {
unsigned long currentTimeMillis = millis();
if (currentTimeMillis - lastSoilUpdate > SOIL_UPDATE_INTERVAL) {
// Shrani v 30-dnevni graf
if (currentTimeMillis - lastScreenHistoryUpdate > SCREEN_HISTORY_INTERVAL) {
screenSoilHistory[screenHistoryIndex] = soilMoisturePercent;
screenHistoryIndex = (screenHistoryIndex + 1) % SCREEN_HISTORY_SIZE;
lastScreenHistoryUpdate = currentTimeMillis;
}
// Letni arhiv
if (rtcInitialized) {
DateTime now = rtc.now();
unsigned long today = now.year() * 10000 + now.month() * 100 + now.day();
if (today != currentDay) {
if (dailyCount > 0) {
yearArchive[archiveIndex].date = currentDay;
yearArchive[archiveIndex].avgMoisture = dailySum / dailyCount;
yearArchive[archiveIndex].minMoisture = dailyMin;
yearArchive[archiveIndex].maxMoisture = dailyMax;
yearArchive[archiveIndex].samples = dailyCount;
archiveIndex = (archiveIndex + 1) % ARCHIVE_HISTORY_SIZE;
if (archiveIndex % 7 == 0 || archiveIndex == 0) {
saveYearArchiveToSD();
}
}
currentDay = today;
dailySum = 0;
dailyMin = 100;
dailyMax = 0;
dailyCount = 0;
}
dailySum += soilMoisturePercent;
dailyCount++;
if (soilMoisturePercent < dailyMin) dailyMin = soilMoisturePercent;
if (soilMoisturePercent > dailyMax) dailyMax = soilMoisturePercent;
}
lastSoilUpdate = currentTimeMillis;
}
}
void saveYearArchiveToSD() {
if (!sdInitialized || !useSDCard) return;
digitalWrite(TFT_CS, HIGH);
delay(10);
File archiveFile = SD.open("/soil_year_archive.csv", FILE_WRITE);
if (!archiveFile) {
digitalWrite(TFT_CS, LOW);
return;
}
archiveFile.println("Datum,Povprecje,Minimum,Maksimum,Vzorci");
int validCount = 0;
int startIdx = 0;
for (int i = 0; i < ARCHIVE_HISTORY_SIZE; i++) {
if (yearArchive[i].samples > 0) {
startIdx = i;
break;
}
}
for (int i = 0; i < ARCHIVE_HISTORY_SIZE; i++) {
int idx = (startIdx + i) % ARCHIVE_HISTORY_SIZE;
if (yearArchive[idx].samples > 0) {
archiveFile.printf("%lu,%.1f,%.1f,%.1f,%d\n",
yearArchive[idx].date,
yearArchive[idx].avgMoisture,
yearArchive[idx].minMoisture,
yearArchive[idx].maxMoisture,
yearArchive[idx].samples);
validCount++;
}
}
archiveFile.close();
digitalWrite(TFT_CS, LOW);
}
void loadYearArchiveFromSD() {
if (!sdInitialized || !useSDCard) return;
if (!SD.exists("/soil_year_archive.csv")) return;
digitalWrite(TFT_CS, HIGH);
delay(10);
File archiveFile = SD.open("/soil_year_archive.csv", FILE_READ);
if (!archiveFile) {
digitalWrite(TFT_CS, LOW);
return;
}
archiveFile.readStringUntil('\n');
for (int i = 0; i < ARCHIVE_HISTORY_SIZE; i++) {
yearArchive[i].samples = 0;
}
archiveIndex = 0;
int loadedCount = 0;
while (archiveFile.available() && loadedCount < ARCHIVE_HISTORY_SIZE) {
String line = archiveFile.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int commas[5], commaCount = 0;
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) == ',') commas[commaCount++] = i;
}
if (commaCount >= 4) {
yearArchive[archiveIndex].date = line.substring(0, commas[0]).toInt();
yearArchive[archiveIndex].avgMoisture = line.substring(commas[0]+1, commas[1]).toFloat();
yearArchive[archiveIndex].minMoisture = line.substring(commas[1]+1, commas[2]).toFloat();
yearArchive[archiveIndex].maxMoisture = line.substring(commas[2]+1, commas[3]).toFloat();
yearArchive[archiveIndex].samples = line.substring(commas[3]+1).toInt();
archiveIndex = (archiveIndex + 1) % ARCHIVE_HISTORY_SIZE;
loadedCount++;
}
}
archiveFile.close();
digitalWrite(TFT_CS, LOW);
}
void saveAllGraphsToSD() {
if (!sdInitialized || !useSDCard) return;
digitalWrite(TFT_CS, HIGH);
delay(10);
File graph48File = SD.open("/graph_48h.csv", FILE_WRITE);
if (graph48File) {
graph48File.println("Type,Index,Value1,Value2,Value3,Timestamp");
for (int i = 0; i < GRAPH_HISTORY_SIZE; i++) {
if (!isnan(tempHistory48h[i])) {
graph48File.printf("INT,%d,%.1f,%.1f,%.1f,%lu\n",
i, tempHistory48h[i], humHistory48h[i], luxHistory48h[i], timeStamps48h[i]);
}
}
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
if (!isnan(extTempHistory48h[i])) {
graph48File.printf("EXT,%d,%.1f,%.1f,%.1f,%lu\n",
i, extTempHistory48h[i], extHumHistory48h[i], extLuxHistory48h[i], extTimeStamps48h[i]);
}
}
graph48File.close();
Serial.println("✅ 48-urni graf shranjen na SD");
}
File soil30File = SD.open("/soil_30d.csv", FILE_WRITE);
if (soil30File) {
soil30File.println("Index,MoisturePercent,Timestamp");
for (int i = 0; i < SCREEN_HISTORY_SIZE; i++) {
if (screenSoilHistory[i] > 0) {
soil30File.printf("%d,%.1f,%lu\n", i, screenSoilHistory[i],
(i * SCREEN_HISTORY_INTERVAL) / 1000);
}
}
soil30File.close();
Serial.println("✅ 30-dnevna zgodovina vlage shranjena na SD");
}
digitalWrite(TFT_CS, LOW);
}
void loadAllGraphsFromSD() {
if (!sdInitialized || !useSDCard) return;
digitalWrite(TFT_CS, HIGH);
delay(10);
if (SD.exists("/soil_30d.csv")) {
File soil30File = SD.open("/soil_30d.csv", FILE_READ);
if (soil30File) {
soil30File.readStringUntil('\n');
int loadedCount = 0;
while (soil30File.available() && loadedCount < SCREEN_HISTORY_SIZE) {
String line = soil30File.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int comma1 = line.indexOf(',');
int comma2 = line.indexOf(',', comma1 + 1);
if (comma1 > 0 && comma2 > 0) {
int idx = line.substring(0, comma1).toInt();
float value = line.substring(comma1 + 1, comma2).toFloat();
if (idx >= 0 && idx < SCREEN_HISTORY_SIZE) {
screenSoilHistory[idx] = value;
loadedCount++;
}
}
}
screenHistoryIndex = loadedCount;
soil30File.close();
Serial.printf("✅ Naloženih %d meritev 30-dnevnega grafa\n", loadedCount);
}
}
loadGraphHistoryFromFlash();
digitalWrite(TFT_CS, LOW);
}
void loadGraphHistoryFromFlash() {
Serial.println("=== NALAGANJE 48-URNE ZGODOVINE IZ FLASH ===");
preferences.begin("ext-graph-data", true);
extGraphHistoryIndex = preferences.getInt("extGraphIndex", 0);
int loadedCount = 0;
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
extTempHistory48h[i] = preferences.getFloat(("extTemp" + String(i)).c_str(), NAN);
extHumHistory48h[i] = preferences.getFloat(("extHum" + String(i)).c_str(), NAN);
extLuxHistory48h[i] = preferences.getFloat(("extLux" + String(i)).c_str(), NAN);
extPressureHistory48h[i] = preferences.getFloat(("extPressure" + String(i)).c_str(), NAN);
extTimeStamps48h[i] = preferences.getULong(("extTime" + String(i)).c_str(), 0);
if (extTimeStamps48h[i] > 0) {
loadedCount++;
}
}
preferences.end();
Serial.printf("Naloženih %d podatkov iz FLASH\n", loadedCount);
preferences.begin("graph-data", true);
graphHistoryIndex = preferences.getInt("graphIndex", 0);
for (int i = 0; i < GRAPH_HISTORY_SIZE; i++) {
tempHistory48h[i] = preferences.getFloat(("temp" + String(i)).c_str(), NAN);
humHistory48h[i] = preferences.getFloat(("hum" + String(i)).c_str(), NAN);
luxHistory48h[i] = preferences.getFloat(("lux" + String(i)).c_str(), NAN);
timeStamps48h[i] = preferences.getULong(("time" + String(i)).c_str(), 0);
}
preferences.end();
Serial.println("=== KONEC NALAGANJA ===");
}
void saveExternalGraphHistory() {
if (sdInitialized && useSDCard) {
File histFile = SD.open("/ext_history.csv", FILE_APPEND);
if (histFile) {
unsigned long timestamp = rtcInitialized ? rtc.now().unixtime() : millis() / 1000;
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
if (!isnan(extTempHistory48h[i]) && extTempHistory48h[i] != 0) {
histFile.printf("%lu,ext_temp,%.1f\n", timestamp, extTempHistory48h[i]);
}
}
histFile.close();
}
} else {
preferences.begin("ext-graph", false);
preferences.putInt("extGraphIdx", extGraphHistoryIndex);
}
}
void showModule2Info() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 200, 255));
tft.setCursor(150, 30);
tft.println("MODUL 2 - NAMAKALNI SISTEM");
tft.setCursor(350, 30);
tft.setTextColor(module2Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.print("●");
tft.setTextColor(ST77XX_WHITE);
tft.print(module2Active ? " Aktiven" : " Nedosegljiv");
tft.drawRoundRect(10, 40, 460, 190, 8, rgbTo565(100, 200, 255));
tft.fillRoundRect(11, 41, 458, 188, 8, rgbTo565(20, 40, 70));
int leftColX = 25;
int rightColX = 245;
int yOffset = 48;
int lineHeight = 16;
tft.setTextSize(1);
if (!module2Active) {
tft.setCursor(150, 120);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.println("MODUL 2 NI DOSEGLJIV!");
tft.setCursor(120, 140);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Preverite povezavo");
} else {
tft.setCursor(leftColX, yOffset);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("PRETOK (L/min)");
tft.setCursor(leftColX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.print("F1:");
tft.setCursor(leftColX + 30, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.printf("%.2f", module2FlowRate1);
tft.setCursor(leftColX + 90, yOffset + lineHeight);
tft.setTextColor(rgbTo565(100, 255, 100));
tft.print("F2:");
tft.setCursor(leftColX + 120, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.printf("%.2f", module2FlowRate2);
tft.setCursor(leftColX + 180, yOffset + lineHeight);
tft.setTextColor(rgbTo565(100, 100, 255));
tft.print("F3:");
tft.setCursor(leftColX + 210, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.printf("%.2f", module2FlowRate3);
tft.setCursor(leftColX, yOffset + 2 * lineHeight + 2);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.print("SKUPAJ (L):");
float totalAll = module2TotalFlow1 + module2TotalFlow2 + module2TotalFlow3;
tft.setCursor(leftColX + 90, yOffset + 2 * lineHeight + 2);
tft.setTextColor(rgbTo565(150, 255, 150));
tft.printf("%.1f", totalAll);
tft.print(" L");
tft.setCursor(rightColX, yOffset);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("RELEJI");
tft.setCursor(rightColX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("R1:");
tft.setCursor(rightColX + 30, yOffset + lineHeight);
tft.setTextColor(module2Relay1State ? rgbTo565(100, 255, 100) : rgbTo565(200, 200, 200));
tft.println(module2Relay1State ? "ON" : "OFF");
tft.setCursor(rightColX + 70, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("R2:");
tft.setCursor(rightColX + 100, yOffset + lineHeight);
tft.setTextColor(module2Relay2State ? rgbTo565(100, 255, 100) : rgbTo565(200, 200, 200));
tft.println(module2Relay2State ? "ON" : "OFF");
tft.setCursor(rightColX + 140, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("R3:");
tft.setCursor(rightColX + 170, yOffset + lineHeight);
tft.setTextColor(module2Relay3State ? rgbTo565(100, 255, 100) : rgbTo565(200, 200, 200));
tft.println(module2Relay3State ? "ON" : "OFF");
tft.setCursor(rightColX, yOffset + 2 * lineHeight + 2);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print("SISTEM");
tft.setCursor(rightColX, yOffset + 2 * lineHeight + 18);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Bat:");
uint16_t battColor;
if (module2Battery > 3.5) battColor = rgbTo565(80, 220, 100);
else if (module2Battery > 3.2) battColor = rgbTo565(255, 200, 50);
else battColor = rgbTo565(255, 80, 80);
tft.setCursor(rightColX + 35, yOffset + 2 * lineHeight + 18);
tft.setTextColor(battColor);
tft.printf("%.2fV", module2Battery);
tft.setCursor(rightColX, yOffset + 2 * lineHeight + 34);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Pred:");
unsigned long secondsAgo = (millis() - lastModule2Time) / 1000;
tft.setCursor(rightColX + 40, yOffset + 2 * lineHeight + 34);
if (secondsAgo < 60) {
tft.setTextColor(rgbTo565(150, 255, 150));
tft.printf("%lu s", secondsAgo);
} else if (secondsAgo < 3600) {
tft.setTextColor(rgbTo565(255, 255, 150));
tft.printf("%lu min", secondsAgo / 60);
} else {
tft.setTextColor(rgbTo565(255, 150, 50));
tft.printf("%lu h", secondsAgo / 3600);
}
int barY = yOffset + 5 * lineHeight + 5;
int barWidth = 200;
int barHeight = 8;
int barSpacing = 12;
tft.setTextSize(1);
tft.setCursor(leftColX, barY - 10);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.print("Flow 1");
int barWidth1 = map(constrain(module2FlowRate1, 0, 10), 0, 10, 0, barWidth);
tft.fillRect(leftColX, barY, barWidth, barHeight, rgbTo565(40, 40, 40));
tft.fillRect(leftColX, barY, barWidth1, barHeight, rgbTo565(255, 100, 100));
tft.drawRect(leftColX, barY, barWidth, barHeight, ST77XX_WHITE);
tft.setCursor(leftColX + barWidth + 5, barY - 2);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.printf("%.1f L", module2TotalFlow1);
barY += barHeight + barSpacing;
tft.setCursor(leftColX, barY - 10);
tft.setTextColor(rgbTo565(100, 255, 100));
tft.print("Flow 2");
int barWidth2 = map(constrain(module2FlowRate2, 0, 10), 0, 10, 0, barWidth);
tft.fillRect(leftColX, barY, barWidth, barHeight, rgbTo565(40, 40, 40));
tft.fillRect(leftColX, barY, barWidth2, barHeight, rgbTo565(100, 255, 100));
tft.drawRect(leftColX, barY, barWidth, barHeight, ST77XX_WHITE);
tft.setCursor(leftColX + barWidth + 5, barY - 2);
tft.setTextColor(rgbTo565(100, 255, 100));
tft.printf("%.1f L", module2TotalFlow2);
barY += barHeight + barSpacing;
tft.setCursor(leftColX, barY - 10);
tft.setTextColor(rgbTo565(100, 100, 255));
tft.print("Flow 3");
int barWidth3 = map(constrain(module2FlowRate3, 0, 10), 0, 10, 0, barWidth);
tft.fillRect(leftColX, barY, barWidth, barHeight, rgbTo565(40, 40, 40));
tft.fillRect(leftColX, barY, barWidth3, barHeight, rgbTo565(100, 100, 255));
tft.drawRect(leftColX, barY, barWidth, barHeight, ST77XX_WHITE);
tft.setCursor(leftColX + barWidth + 5, barY - 2);
tft.setTextColor(rgbTo565(100, 100, 255));
tft.printf("%.1f L", module2TotalFlow3);
int buttonY = barY + barHeight + 15;
int buttonWidth = 110;
int buttonHeight = 30;
int buttonSpacing = 10;
drawMenuButton(30, buttonY, buttonWidth, buttonHeight,
rgbTo565(66, 135, 245), "KALIBRACIJA");
drawMenuButton(30 + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "RESET");
drawMenuButton(30 + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(156, 39, 176), "GRAF");
int buttonY2 = buttonY + buttonHeight + 8;
drawMenuButton(100, buttonY2, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
drawMenuButton(100 + buttonWidth + buttonSpacing, buttonY2, buttonWidth, buttonHeight,
rgbTo565(255, 150, 0), "RELEJI");
}
currentState = STATE_MODULE2_INFO;
}
void handleModule2InfoTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int barY = 48 + 5 * 16 + 5;
int barHeight = 8;
int barSpacing = 12;
int flow1BarY = barY;
int flow2BarY = flow1BarY + barHeight + barSpacing;
int flow3BarY = flow2BarY + barHeight + barSpacing;
int buttonY = flow3BarY + barHeight + 15;
int buttonY2 = buttonY + 30 + 8;
if (x > 30 && x < 140 && y > buttonY && y < buttonY + 30) {
showModule2Info();
return;
}
if (x > 150 && x < 260 && y > buttonY && y < buttonY + 30) {
sendModule2Command(2, 0, 0);
showTemporaryMessage("Ukaz poslan!\nReset flow totals", rgbTo565(80, 220, 100), 1500);
showModule2Info();
return;
}
if (x > 270 && x < 380 && y > buttonY && y < buttonY + 30) {
return;
}
if (x > 100 && x < 210 && y > buttonY2 && y < buttonY2 + 30) {
showMainMenu();
return;
}
if (x > 220 && x < 330 && y > buttonY2 && y < buttonY2 + 30) {
showMainMenu();
return;
}
int leftColX = 25;
int barWidth = 200;
if (x > leftColX && x < leftColX + barWidth && y > flow1BarY && y < flow1BarY + barHeight) {
return;
}
if (x > leftColX && x < leftColX + barWidth && y > flow2BarY && y < flow2BarY + barHeight) {
return;
}
if (x > leftColX && x < leftColX + barWidth && y > flow3BarY && y < flow3BarY + barHeight) {
return;
}
int rightColX = 245;
int yOffset = 48;
int lineHeight = 16;
if (x > rightColX && x < rightColX + 60 && y > yOffset + lineHeight - 5 && y < yOffset + lineHeight + 15) {
if (module2Active) {
setModule2Relay(1, !module2Relay1State);
showModule2Info();
}
return;
}
if (x > rightColX + 70 && x < rightColX + 130 && y > yOffset + lineHeight - 5 && y < yOffset + lineHeight + 15) {
if (module2Active) {
setModule2Relay(2, !module2Relay2State);
showModule2Info();
}
return;
}
if (x > rightColX + 140 && x < rightColX + 200 && y > yOffset + lineHeight - 5 && y < yOffset + lineHeight + 15) {
if (module2Active) {
setModule2Relay(3, !module2Relay3State);
showModule2Info();
}
return;
}
}
void toggleStorageMode() {
if (!sdInitialized) {
if (initializeSDCard()) {
} else {
showTemporaryMessage("SD kartica ni najdena!\nPreverite povezavo", rgbTo565(255, 80, 80), 2000);
return;
}
}
useSDCard = !useSDCard;
String message = "Shranjevanje: " + String(useSDCard ? "SD KARTICA" : "FLASH");
showTemporaryMessage(message, rgbTo565(80, 220, 100), 2000);
if (useSDCard && sdInitialized) {
saveSettingsToSD();
} else {
saveAllSettings(false);
}
}
void testSDCardWrite() {
Serial.println("\n=== TEST PISANJA NA SD KARTICO ===");
if (!sdInitialized) {
Serial.println("SD kartica ni inicializirana!");
return;
}
File testFile = SD.open("/test_write.txt", FILE_WRITE);
if (testFile) {
String testData = "Test pisanja: " + String(millis()) + "\n";
size_t bytesWritten = testFile.print(testData);
testFile.close();
if (bytesWritten > 0) {
Serial.printf("✓ Test pisanja USPESEN: %d bytes zapisanih\n", bytesWritten);
testFile = SD.open("/test_write.txt", FILE_READ);
if (testFile) {
String readData = testFile.readString();
testFile.close();
Serial.printf(" Prebrano: %s", readData.c_str());
Serial.println(" Preverjanje branja USPESNO");
}
if (SD.remove("/test_write.txt")) {
Serial.println(" Testna datoteka izbrisana");
}
} else {
Serial.println("✗ Test pisanja NEUSPESEN - 0 bytes zapisanih");
}
} else {
Serial.println("✗ Test pisanja NEUSPESEN - ni mogoce odpreti datoteke");
}
Serial.println("=== KONEC TESTA ===\n");
}
void autoSaveSettings() {
unsigned long currentTime = millis();
if (settingsChangedFlag && (currentTime - lastSettingsChangeTime > 5000)) {
try {
if (sdInitialized && useSDCard) {
saveSettingsToSD();
} else {
saveAllSettings(false);
}
} catch (...) {
}
clearSettingsChangedIndicator();
lastAutoSaveTime = currentTime;
}
if (currentTime - lastAutoSaveTime > 60000) {
try {
if (sdInitialized && useSDCard) {
saveSettingsToSD();
} else {
saveAllSettings(false);
}
} catch (...) {
}
lastAutoSaveTime = currentTime;
}
}
void setupWatchdogForShutdown() {
Serial.println("Nastavljam varnostno shranjevanje ob izklopu...");
esp_sleep_enable_ext0_wakeup((gpio_num_t)GPIO_NUM_0, LOW);
}
void emergencySaveOnPowerLoss() {
Serial.println("\n=== VARNOSTNO SHRANJEVANJE - IZPAD NAPETOSTI ===");
if (sdInitialized && useSDCard) {
saveAllGraphsToSD();
}
saveAllSettings(true);
saveRelayStates();
Serial.println("=== VARNOSTNO SHRANJEVANJE KONČANO ===");
}
void cleanOldGraphData() {
if (!sdInitialized || !useSDCard) return;
File graphFile = SD.open("/graph_48h.csv", FILE_READ);
if (!graphFile) return;
int lineCount = 0;
while (graphFile.available()) {
graphFile.readStringUntil('\n');
lineCount++;
}
graphFile.close();
if (lineCount > 5000) {
SD.remove("/graph_48h_old.csv");
SD.rename("/graph_48h.csv", "/graph_48h_old.csv");
saveAllGraphsToSD();
Serial.println("🔄 Stari podatki grafov arhivirani");
}
}
void testPinsBeforeSD() {
Serial.println("\n=== TEST PINOV PRED SD INICIALIZACIJO ===");
int pins[] = { 35, 36, 37, 38 };
const char* names[] = { "CS", "SCK", "MISO", "MOSI" };
for (int i = 0; i < 4; i++) {
pinMode(pins[i], OUTPUT);
digitalWrite(pins[i], LOW);
delay(10);
digitalWrite(pins[i], HIGH);
delay(10);
Serial.printf("Pin %d (%s): OK\n", pins[i], names[i]);
}
pinMode(37, INPUT_PULLUP);
int misoValue = digitalRead(37);
Serial.printf("MISO pin 37 zacetna vrednost: %d\n", misoValue);
Serial.println("=== KONEC TESTA ===\n");
}
bool initializeSDCard() {
Serial.println("\n=== INICIALIZACIJA SD KARTICE Z NOVIMI PINI ===");
Serial.printf(" CS: %d\n", SD_CS_PIN);
Serial.printf(" SCK: %d\n", SD_SCK_PIN);
Serial.printf(" MOSI: %d\n", SD_MOSI_PIN);
Serial.printf(" MISO: %d\n", SD_MISO_PIN);
pinMode(SD_CS_PIN, OUTPUT);
pinMode(SD_SCK_PIN, OUTPUT);
pinMode(SD_MOSI_PIN, OUTPUT);
pinMode(SD_MISO_PIN, INPUT_PULLUP);
digitalWrite(SD_CS_PIN, HIGH);
delay(50);
int misoValue = digitalRead(SD_MISO_PIN);
Serial.printf(" MISO vrednost: %d (1=HIGH - kartica se ne odziva, 0=LOW - kartica se odziva)\n", misoValue);
SPIClass* sdSPI = new SPIClass(FSPI);
sdSPI->begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
digitalWrite(TFT_CS, HIGH);
delay(10);
bool sdOk = false;
int speeds[] = { 400000, 200000, 100000 };
for (int s = 0; s < 3; s++) {
Serial.printf("Poskus %d: %d Hz\n", s + 1, speeds[s]);
for (int attempt = 0; attempt < 3; attempt++) {
if (SD.begin(SD_CS_PIN, *sdSPI, speeds[s])) {
uint8_t cardType = SD.cardType();
if (cardType != CARD_NONE) {
Serial.println("✓ SD kartica USPESNO inicializirana!");
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf(" Velikost: %llu MB\n", cardSize);
sdInitialized = true;
useSDCard = true;
sdOk = true;
break;
}
}
delay(100);
}
if (sdOk) break;
}
if (!sdOk) {
Serial.println("\n❌ SD kartica NI inicializirana!");
delete sdSPI;
sdInitialized = false;
useSDCard = false;
}
digitalWrite(TFT_CS, LOW);
Serial.println("=== KONEC SD INICIALIZACIJE ===\n");
return sdInitialized;
}
void debugSDCard() {
Serial.println("\n=== SD KARTICA DEBUG ===");
if (!sdInitialized) {
Serial.println("SD kartica ni inicializirana!");
initializeSDCard();
if (!sdInitialized) {
return;
}
}
uint8_t cardType = SD.cardType();
Serial.printf("Tip kartice: %d - ", cardType);
switch (cardType) {
case CARD_NONE: Serial.println("NONE"); break;
case CARD_MMC: Serial.println("MMC"); break;
case CARD_SD: Serial.println("SDSC"); break;
case CARD_SDHC: Serial.println("SDHC"); break;
default: Serial.println("Unknown"); break;
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
uint64_t totalSize = SD.totalBytes() / (1024 * 1024);
uint64_t usedSize = SD.usedBytes() / (1024 * 1024);
Serial.printf("Velikost kartice: %llu MB\n", cardSize);
Serial.printf("Skupaj prostora: %llu MB\n", totalSize);
Serial.printf("Zasedeno: %llu MB\n", usedSize);
Serial.printf("Prostega: %llu MB\n", totalSize - usedSize);
listSDFiles();
File testFile = SD.open("/debug_test.txt", FILE_WRITE);
if (testFile) {
String testData = "Debug test: " + String(millis());
testFile.println(testData);
testFile.close();
Serial.println("Test pisanja: USPESNO");
testFile = SD.open("/debug_test.txt", FILE_READ);
if (testFile) {
Serial.print("Prebrano: ");
while (testFile.available()) {
Serial.write(testFile.read());
}
testFile.close();
Serial.println();
}
SD.remove("/debug_test.txt");
} else {
Serial.println("Test pisanja: NEUSPESNO");
}
Serial.println("=== KONEC DEBUG ===");
}
void createSDStructure() {
if (!sdInitialized) {
return;
}
int filesCreated = 0;
int filesExist = 0;
struct {
const char* path;
const char* header;
} files[] = {
{ DATA_LOG_FILE, "timestamp,temp_in,hum_in,temp_out,hum_out,pressure,lux_in,lux_out,soil_moisture,flow1,flow2,flow3" },
{ HISTORY_FILE, "timestamp,type,value" },
{ SETTINGS_FILE, "=== SMART VITROGROV NASTAVITVE ===" }
};
for (int i = 0; i < 3; i++) {
if (!SD.exists(files[i].path)) {
File file = SD.open(files[i].path, FILE_WRITE);
if (file) {
file.println(files[i].header);
file.close();
filesCreated++;
}
} else {
filesExist++;
}
}
}
void listSDFiles() {
if (!sdInitialized) return;
Serial.println("\n=== DATOTEKE NA SD KARTICI ===");
File root = SD.open("/");
if (!root) {
return;
}
File file = root.openNextFile();
int fileCount = 0;
int dirCount = 0;
while (file) {
if (file.isDirectory()) {
Serial.printf(" [DIR] %s\n", file.name());
dirCount++;
} else {
Serial.printf(" [FILE] %s (%d bytes)\n", file.name(), file.size());
fileCount++;
}
file = root.openNextFile();
}
root.close();
Serial.printf("Skupaj: %d datotek, %d map\n", fileCount, dirCount);
Serial.println("===============================\n");
}
void logDataToSD() {
if (!sdInitialized || !useSDCard) return;
digitalWrite(TFT_CS, HIGH);
delay(1);
if (SD.cardType() == CARD_NONE) {
sdInitialized = false;
digitalWrite(TFT_CS, LOW);
return;
}
File logFile = SD.open(DATA_LOG_FILE, FILE_APPEND);
if (!logFile) {
digitalWrite(TFT_CS, LOW);
return;
}
logFile.printf("%lu,%.1f,%.1f,%.1f,%.1f\n",
millis() / 1000,
internalTemperature,
internalHumidity,
externalTemperature,
externalHumidity);
logFile.close();
digitalWrite(TFT_CS, LOW);
}
void showTemporaryMessage(String message, uint16_t color, int durationMs) {
if (durationMs <= 0) durationMs = 3000;
AppState previousState = currentState;
int msgX = 80;
int msgY = 140;
int msgWidth = 320;
int msgHeight = 80;
tft.fillRect(msgX - 3, msgY - 3, msgWidth + 6, msgHeight + 6, ST77XX_BLACK);
tft.fillRoundRect(msgX, msgY, msgWidth, msgHeight, 10, rgbTo565(20, 40, 70));
tft.drawRoundRect(msgX, msgY, msgWidth, msgHeight, 10, rgbTo565(0, 150, 255));
tft.fillRoundRect(msgX + 2, msgY + 2, msgWidth - 4, msgHeight - 4, 8, rgbTo565(20, 40, 70));
tft.setTextSize(1);
tft.setTextColor(color);
int lineHeight = 16;
int startY = msgY + 25;
int currentY = startY;
String remainingMessage = message;
while (remainingMessage.length() > 0) {
int newlinePos = remainingMessage.indexOf('\n');
String line;
if (newlinePos >= 0) {
line = remainingMessage.substring(0, newlinePos);
remainingMessage = remainingMessage.substring(newlinePos + 1);
} else {
line = remainingMessage;
remainingMessage = "";
}
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds(line, 0, 0, &textX, &textY, &textW, &textH);
int textPosX = msgX + (msgWidth - textW) / 2 - textX;
tft.setCursor(textPosX, currentY);
tft.println(line);
currentY += lineHeight;
}
unsigned long startTime = millis();
while (millis() - startTime < durationMs) {
handleTouch();
delay(10);
}
tft.fillRect(msgX - 5, msgY - 5, msgWidth + 10, msgHeight + 10, ST77XX_BLACK);
if (previousState == STATE_HOME_SCREEN) {
forceFullHomeScreenRedraw();
} else {
redrawCurrentScreen();
}
}
void saveSettingsToSD() {
if (!sdInitialized) {
if (!initializeSDCard()) {
return;
}
}
if (!useSDCard) {
return;
}
Serial.println("\n=== SHRANJEVANJE NASTAVITEV NA SD KARTICO ===");
digitalWrite(TFT_CS, HIGH);
delay(10);
if (SD.cardType() == CARD_NONE) {
sdInitialized = false;
useSDCard = false;
digitalWrite(TFT_CS, LOW);
return;
}
if (SD.exists(SETTINGS_FILE)) {
SD.remove(SETTINGS_FILE);
}
File settingsFile = SD.open(SETTINGS_FILE, FILE_WRITE);
if (!settingsFile) {
digitalWrite(TFT_CS, LOW);
return;
}
settingsFile.println("=== SMART VITROGROV NASTAVITVE ===");
settingsFile.print("timestamp:");
if (rtcInitialized) {
settingsFile.println(rtc.now().unixtime());
} else {
settingsFile.println(millis() / 1000);
}
settingsFile.println();
settingsFile.println("[TEMPERATURA_VLAGA]");
settingsFile.printf("temp_min:%.1f\n", targetTempMin);
settingsFile.printf("temp_max:%.1f\n", targetTempMax);
settingsFile.printf("hum_min:%.1f\n", targetHumMin);
settingsFile.printf("hum_max:%.1f\n", targetHumMax);
settingsFile.printf("auto_control:%s\n", autoControlEnabled ? "true" : "false");
settingsFile.println();
settingsFile.println("[SENCENJE]");
settingsFile.printf("shade_min:%.0f\n", shadeMinLux);
settingsFile.printf("shade_max:%.0f\n", shadeMaxLux);
settingsFile.printf("shade_auto:%s\n", shadeAutoControl ? "true" : "false");
settingsFile.printf("shade_position:%d\n", shadeActualPosition);
settingsFile.println();
settingsFile.println("[RAZSVETLJAVA_REL6]");
settingsFile.printf("light_auto:%s\n", lightAutoMode ? "true" : "false");
settingsFile.printf("light_manual:%s\n", lightManualOverride ? "true" : "false");
settingsFile.printf("light_on:%.0f\n", lightOnThreshold);
settingsFile.printf("light_off:%.0f\n", lightOffThreshold);
settingsFile.printf("light_time_enabled:%s\n", lightTimeControlEnabled ? "true" : "false");
settingsFile.printf("light_start:%02d:%02d\n", lightStartHour, lightStartMinute);
settingsFile.printf("light_end:%02d:%02d\n", lightEndHour, lightEndMinute);
settingsFile.println();
settingsFile.println("[CASOVNA_RAZSVETLJAVA]");
settingsFile.printf("lighting_auto:%s\n", lightingAutoMode ? "true" : "false");
settingsFile.printf("lighting_manual:%s\n", lightingManualOverride ? "true" : "false");
settingsFile.println();
settingsFile.println("[CASOVNI_BLOKI]");
for (int i = 0; i < MAX_TIME_BLOCKS; i++) {
settingsFile.printf("block_%d_enabled:%s\n", i, timeBlocks[i].enabled ? "true" : "false");
settingsFile.printf("block_%d_action:%s\n", i, timeBlocks[i].relayOn ? "ON" : "OFF");
settingsFile.printf("block_%d_start:%02d:%02d\n", i,
timeBlocks[i].startHour, timeBlocks[i].startMinute);
settingsFile.printf("block_%d_end:%02d:%02d\n", i,
timeBlocks[i].endHour, timeBlocks[i].endMinute);
String daysStr = "";
for (int d = 0; d < DAYS_IN_WEEK; d++) {
if (timeBlocks[i].days[d]) {
if (daysStr.length() > 0) daysStr += ",";
daysStr += String(d);
}
}
if (daysStr.length() == 0) daysStr = "none";
settingsFile.printf("block_%d_days:%s\n", i, daysStr.c_str());
settingsFile.printf("block_%d_desc:%s\n", i, timeBlocks[i].description.c_str());
}
settingsFile.println();
settingsFile.println("[RELEJI]");
for (int i = 0; i < RELAY_COUNT; i++) {
settingsFile.printf("relay_%d:%s\n", i, relayStates[i] ? "true" : "false");
}
settingsFile.printf("relay_control:%s\n", relayControlEnabled ? "true" : "false");
settingsFile.println();
settingsFile.println("[VENTILACIJA]");
settingsFile.printf("vent_auto:%s\n", ventilationAutoMode ? "true" : "false");
settingsFile.printf("vent_manual:%s\n", ventilationManualOverride ? "true" : "false");
settingsFile.printf("vent_day_temp:%.1f\n", ventilationDayTempThreshold);
settingsFile.printf("vent_day_hum:%.0f\n", ventilationDayHumThreshold);
settingsFile.printf("vent_night_temp:%.1f\n", ventilationNightTempThreshold);
settingsFile.printf("vent_night_hum:%.0f\n", ventilationNightHumThreshold);
settingsFile.printf("vent_day_runtime:%d\n", ventilationDayMinRuntime);
settingsFile.printf("vent_night_runtime:%d\n", ventilationNightMinRuntime);
settingsFile.printf("vent_day_cooldown:%d\n", ventilationDayCooldown);
settingsFile.printf("vent_night_cooldown:%d\n", ventilationNightCooldown);
settingsFile.printf("vent_day_switch:%d\n", dayNightSwitchHour);
settingsFile.printf("vent_night_switch:%d\n", nightDaySwitchHour);
settingsFile.println();
settingsFile.println("[ZEMELJSKA_VLAGA]");
settingsFile.printf("soil_dry:%d\n", SOIL_DRY_VALUE);
settingsFile.printf("soil_wet:%d\n", SOIL_WET_VALUE);
settingsFile.println();
settingsFile.println("[VENTILATOR]");
settingsFile.printf("vent_relay_state:%s\n", relayStates[VENTILATION_RELAY] ? "true" : "false");
settingsFile.println();
settingsFile.println("[SISTEM]");
settingsFile.printf("storage_mode:%s\n", useSDCard ? "SD" : "FLASH");
settingsFile.printf("wifi_auto_connect:%s\n", autoConnecting ? "true" : "false");
settingsFile.println();
settingsFile.println("=== KONEC NASTAVITEV ===");
settingsFile.close();
if (SD.exists(SETTINGS_FILE)) {
File checkFile = SD.open(SETTINGS_FILE, FILE_READ);
if (checkFile) {
size_t fileSize = checkFile.size();
checkFile.close();
Serial.printf("✓ Nastavitve uspesno shranjene na SD kartico!\n");
Serial.printf(" Velikost settings.txt: %d bytes\n", fileSize);
digitalWrite(TFT_CS, LOW);
delay(10);
showTemporaryMessage("Nastavitve shranjene!\nNa SD kartico", rgbTo565(80, 220, 100), 3000);
} else {
digitalWrite(TFT_CS, LOW);
}
} else {
digitalWrite(TFT_CS, LOW);
}
Serial.println("=== KONEC SHRANJEVANJA ===\n");
}
void loadSettingsFromSD() {
if (!sdInitialized) {
if (!initializeSDCard()) {
return;
}
}
digitalWrite(TFT_CS, HIGH);
delay(10);
if (!SD.exists(SETTINGS_FILE)) {
digitalWrite(TFT_CS, LOW);
return;
}
Serial.println("\n=== NALAGANJE NASTAVITEV IZ SD KARTICE ===");
File settingsFile = SD.open(SETTINGS_FILE, FILE_READ);
if (!settingsFile) {
digitalWrite(TFT_CS, LOW);
return;
}
int loadedCount = 0;
int errorCount = 0;
while (settingsFile.available()) {
String line = settingsFile.readStringUntil('\n');
line.trim();
if (line.length() == 0 || line.startsWith("===") || line.startsWith("[")) {
continue;
}
int colonPos = line.indexOf(':');
if (colonPos == -1) continue;
String key = line.substring(0, colonPos);
String value = line.substring(colonPos + 1);
if (key == "temp_min") {
targetTempMin = value.toFloat();
loadedCount++;
} else if (key == "temp_max") {
targetTempMax = value.toFloat();
loadedCount++;
} else if (key == "hum_min") {
targetHumMin = value.toFloat();
loadedCount++;
} else if (key == "hum_max") {
targetHumMax = value.toFloat();
loadedCount++;
} else if (key == "auto_control") {
autoControlEnabled = (value == "true");
loadedCount++;
}
else if (key == "shade_min") {
shadeMinLux = value.toFloat();
loadedCount++;
} else if (key == "shade_max") {
shadeMaxLux = value.toFloat();
loadedCount++;
} else if (key == "shade_auto") {
shadeAutoControl = (value == "true");
loadedCount++;
} else if (key == "shade_position") {
shadeActualPosition = value.toInt();
loadedCount++;
}
else if (key == "light_auto") {
lightAutoMode = (value == "true");
loadedCount++;
} else if (key == "light_manual") {
lightManualOverride = (value == "true");
loadedCount++;
} else if (key == "light_on") {
lightOnThreshold = value.toFloat();
loadedCount++;
} else if (key == "light_off") {
lightOffThreshold = value.toFloat();
loadedCount++;
} else if (key == "light_time_enabled") {
lightTimeControlEnabled = (value == "true");
loadedCount++;
} else if (key == "light_start") {
int colonPos2 = value.indexOf(':');
if (colonPos2 != -1) {
lightStartHour = value.substring(0, colonPos2).toInt();
lightStartMinute = value.substring(colonPos2 + 1).toInt();
loadedCount++;
}
} else if (key == "light_end") {
int colonPos2 = value.indexOf(':');
if (colonPos2 != -1) {
lightEndHour = value.substring(0, colonPos2).toInt();
lightEndMinute = value.substring(colonPos2 + 1).toInt();
loadedCount++;
}
}
else if (key == "lighting_auto") {
lightingAutoMode = (value == "true");
loadedCount++;
} else if (key == "lighting_manual") {
lightingManualOverride = (value == "true");
loadedCount++;
}
else if (key.startsWith("block_")) {
int underscore1 = key.indexOf('_');
int underscore2 = key.indexOf('_', underscore1 + 1);
if (underscore1 != -1 && underscore2 != -1) {
int blockNum = key.substring(underscore1 + 1, underscore2).toInt();
String param = key.substring(underscore2 + 1);
if (blockNum >= 0 && blockNum < MAX_TIME_BLOCKS) {
if (param == "enabled") {
timeBlocks[blockNum].enabled = (value == "true");
loadedCount++;
} else if (param == "action") {
timeBlocks[blockNum].relayOn = (value == "ON");
loadedCount++;
} else if (param == "start") {
int colonPos2 = value.indexOf(':');
if (colonPos2 != -1) {
timeBlocks[blockNum].startHour = value.substring(0, colonPos2).toInt();
timeBlocks[blockNum].startMinute = value.substring(colonPos2 + 1).toInt();
loadedCount++;
}
} else if (param == "end") {
int colonPos2 = value.indexOf(':');
if (colonPos2 != -1) {
timeBlocks[blockNum].endHour = value.substring(0, colonPos2).toInt();
timeBlocks[blockNum].endMinute = value.substring(colonPos2 + 1).toInt();
loadedCount++;
}
} else if (param == "days" && value != "none") {
for (int d = 0; d < DAYS_IN_WEEK; d++) {
timeBlocks[blockNum].days[d] = false;
}
int startPos = 0;
int commaPos;
do {
commaPos = value.indexOf(',', startPos);
String dayStr;
if (commaPos == -1) {
dayStr = value.substring(startPos);
} else {
dayStr = value.substring(startPos, commaPos);
startPos = commaPos + 1;
}
int day = dayStr.toInt();
if (day >= 0 && day < DAYS_IN_WEEK) {
timeBlocks[blockNum].days[day] = true;
}
} while (commaPos != -1);
loadedCount++;
} else if (param == "desc") {
timeBlocks[blockNum].description = value;
loadedCount++;
}
}
}
}
else if (key.startsWith("relay_")) {
int relayNum = key.substring(6).toInt();
if (relayNum >= 0 && relayNum < RELAY_COUNT) {
relayStates[relayNum] = (value == "true");
loadedCount++;
}
} else if (key == "relay_control") {
relayControlEnabled = (value == "true");
loadedCount++;
}
else if (key == "vent_auto") {
ventilationAutoMode = (value == "true");
loadedCount++;
} else if (key == "vent_manual") {
ventilationManualOverride = (value == "true");
loadedCount++;
} else if (key == "vent_day_temp") {
ventilationDayTempThreshold = value.toFloat();
loadedCount++;
} else if (key == "vent_day_hum") {
ventilationDayHumThreshold = value.toFloat();
loadedCount++;
} else if (key == "vent_night_temp") {
ventilationNightTempThreshold = value.toFloat();
loadedCount++;
} else if (key == "vent_night_hum") {
ventilationNightHumThreshold = value.toFloat();
loadedCount++;
} else if (key == "vent_day_runtime") {
ventilationDayMinRuntime = value.toInt();
loadedCount++;
} else if (key == "vent_night_runtime") {
ventilationNightMinRuntime = value.toInt();
loadedCount++;
} else if (key == "vent_day_cooldown") {
ventilationDayCooldown = value.toInt();
loadedCount++;
} else if (key == "vent_night_cooldown") {
ventilationNightCooldown = value.toInt();
loadedCount++;
} else if (key == "vent_day_switch") {
dayNightSwitchHour = value.toInt();
loadedCount++;
} else if (key == "vent_night_switch") {
nightDaySwitchHour = value.toInt();
loadedCount++;
}
else if (key == "soil_dry") {
SOIL_DRY_VALUE = value.toInt();
loadedCount++;
} else if (key == "soil_wet") {
SOIL_WET_VALUE = value.toInt();
loadedCount++;
}
}
settingsFile.close();
digitalWrite(TFT_CS, LOW);
Serial.printf("Nalozenih %d nastavitev iz SD kartice!\n", loadedCount);
if (mcpInitialized) {
for (int i = 0; i < RELAY_COUNT; i++) {
mcp.digitalWrite(RELAY_START_PIN + i, relayStates[i] ? LOW : HIGH);
}
}
Serial.println("=== KONEC NALAGANJA IZ SD ===\n");
}
void showSDCardManagementScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 200, 100));
tft.setCursor(150, 40);
tft.println("UPRAVLJANJE SD KARTICE");
tft.drawRoundRect(10, 50, 460, 150, 10, rgbTo565(100, 200, 100));
tft.fillRoundRect(11, 51, 458, 148, 10, rgbTo565(20, 40, 50));
int yOffset = 65;
int lineHeight = 16;
tft.setTextSize(1);
tft.setCursor(30, yOffset);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Status: ");
tft.setTextColor(sdInitialized ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(sdInitialized ? "PRISOTNA" : "ODSOTNA");
if (sdInitialized) {
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
uint64_t usedSpace = SD.usedBytes() / (1024 * 1024);
uint64_t freeSpace = cardSize - usedSpace;
tft.setCursor(30, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Velikost: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.printf("%llu MB", cardSize);
tft.setCursor(30, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Prostor: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.printf("%llu MB", freeSpace);
tft.setCursor(30, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Shranjevanje: ");
tft.setTextColor(useSDCard ? rgbTo565(100, 200, 100) : rgbTo565(255, 200, 50));
tft.println(useSDCard ? "SD KARTICA" : "FLASH");
} else {
tft.setCursor(30, yOffset + lineHeight);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("SD kartica ni najdena!");
tft.setCursor(30, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.println("Preverite povezavo in");
tft.setCursor(30, yOffset + 3 * lineHeight);
tft.println("vstavite SD kartico.");
}
int buttonWidth = 140;
int buttonHeight = 35;
int buttonSpacing = 20;
int buttonY1 = 220;
drawMenuButton(40, buttonY1, buttonWidth, buttonHeight,
rgbTo565(100, 200, 100), sdInitialized ? (useSDCard ? "UPORABI FLASH" : "UPORABI SD") : "INICIALIZIRAJ SD");
drawMenuButton(40 + buttonWidth + buttonSpacing, buttonY1, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "OSVEZI");
int buttonY2 = buttonY1 + buttonHeight + 10;
drawMenuButton(40, buttonY2, buttonWidth, buttonHeight,
rgbTo565(66, 135, 245), "NALOZI IZ SD");
drawMenuButton(40 + buttonWidth + buttonSpacing, buttonY2, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_SD_MANAGEMENT;
}
void handleSDCardManagementTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int buttonWidth = 140;
int buttonHeight = 35;
int buttonSpacing = 20;
int buttonY1 = 220;
int buttonY2 = buttonY1 + buttonHeight + 10;
int leftColumnX = 40;
int rightColumnX = leftColumnX + buttonWidth + buttonSpacing;
if (x > leftColumnX && x < leftColumnX + buttonWidth && y > buttonY1 && y < buttonY1 + buttonHeight) {
if (!sdInitialized) {
initializeSDCard();
} else {
toggleStorageMode();
}
showSDCardManagementScreen();
return;
}
if (x > rightColumnX && x < rightColumnX + buttonWidth && y > buttonY1 && y < buttonY1 + buttonHeight) {
showSDCardManagementScreen();
return;
}
if (x > leftColumnX && x < leftColumnX + buttonWidth && y > buttonY2 && y < buttonY2 + buttonHeight) {
if (sdInitialized && useSDCard) {
loadSettingsFromSD();
tft.fillRect(100, 150, 280, 60, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 60, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(80, 220, 100));
tft.setCursor(120, 170);
tft.println("Nastavitve nalozene!");
delay(1500);
} else {
tft.fillRect(100, 150, 280, 60, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 60, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 80, 80));
tft.setCursor(110, 170);
if (!sdInitialized) {
tft.println("SD kartica ni najdena!");
} else {
tft.println("Shranjevanje ni na SD!");
}
delay(1500);
}
showSDCardManagementScreen();
return;
}
if (x > rightColumnX && x < rightColumnX + buttonWidth && y > buttonY2 && y < buttonY2 + buttonHeight) {
showMainMenu();
return;
}
}
void testSDPins() {
Serial.println("\n=== TEST SD PINOV (NOVI PINI) ===");
digitalWrite(TFT_CS, HIGH);
delay(10);
pinMode(SD_CS_PIN, OUTPUT);
pinMode(SD_SCK_PIN, OUTPUT);
pinMode(SD_MOSI_PIN, OUTPUT);
pinMode(SD_MISO_PIN, INPUT_PULLUP);
digitalWrite(SD_CS_PIN, HIGH);
delay(10);
int misoValue = digitalRead(SD_MISO_PIN);
Serial.printf("MISO pin %d vrednost: %d\n", SD_MISO_PIN, misoValue);
SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
if (SD.begin(SD_CS_PIN, SPI, 4000000)) {
Serial.println("✓ SD kartica USPESNO inicializirana!");
uint8_t cardType = SD.cardType();
Serial.printf(" Tip kartice: %d\n", cardType);
File testFile = SD.open("/test.txt", FILE_WRITE);
if (testFile) {
testFile.println("Test pisanja na SD kartico");
testFile.close();
Serial.println("✓ Pisanje USPESNO!");
testFile = SD.open("/test.txt", FILE_READ);
if (testFile) {
Serial.print(" Prebrano: ");
while (testFile.available()) {
Serial.write(testFile.read());
}
testFile.close();
Serial.println();
}
if (SD.remove("/test.txt")) {
Serial.println(" Testna datoteka izbrisana");
}
} else {
Serial.println("✗ Pisanje NEUSPESNO!");
}
SD.end();
} else {
Serial.println("✗ SD kartica NI inicializirana!");
}
digitalWrite(TFT_CS, LOW);
Serial.println("=== KONEC TESTA ===\n");
}
void initESPNow() {
Serial.println("\n=== INICIALIZACIJA ESP-NOW ===");
// 1. Najprej nastavi WiFi način
WiFi.mode(WIFI_STA);
WiFi.disconnect(true);
delay(100);
// 2. Nastavi kanal na 1
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
Serial.println("📡 Kanal nastavljen na 1 za ESP-NOW");
// 3. Inicializiraj ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("❌ ESP-NOW napaka pri inicializaciji!");
return;
}
Serial.println("✅ ESP-NOW inicializiran!");
// 4. Registriraj callback za prejemanje
esp_now_register_recv_cb(esp_now_recv_cb_t(onModuleDataRecv));
// 5. Dodaj peerje za VSE module
esp_now_peer_info_t peerInfo;
// ===== SEZNAM VSEH MAC NASLOVOV (iz logov) =====
const uint8_t allMACs[][6] = {
{0x80, 0xB5, 0x4E, 0xC6, 0x0A, 0x04}, // Modul 1 - vremenski
{0xB8, 0xF8, 0x62, 0xF8, 0x65, 0xC0}, // Modul 2 - namakalni
{0x58, 0x8C, 0x81, 0xCB, 0xDC, 0x80}, // Modul 3 - senčenje
{0xCE, 0x4D, 0xCC, 0x3F, 0xC8, 0x4D}, // Modul 4 - stari MAC
{0x1A, 0xD8, 0xCB, 0x3F, 0x14, 0xD8}, // Modul 4 - novi MAC
{0xC6, 0x48, 0xCC, 0x3F, 0xC0, 0x48}, // Modul 4 - še en MAC
{0xC2, 0xF2, 0xCB, 0x3F, 0xBC, 0xF2}, // Neznani MAC
};
const char* macNames[] = {
"Modul 1 (vremenski)",
"Modul 2 (namakalni)",
"Modul 3 (senčenje)",
"Modul 4 (stari MAC)",
"Modul 4 (novi MAC)",
"Modul 4 (tretji MAC)",
"Neznani MAC"
};
int addedCount = 0;
for(int m = 0; m < 7; m++) {
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, allMACs[m], 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
// Preveri, če peer že obstaja, preden ga dodamo
if (!esp_now_is_peer_exist(allMACs[m])) {
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.printf(" ✅ Peer %s: %02X:%02X:%02X:%02X:%02X:%02X\n",
macNames[m],
allMACs[m][0], allMACs[m][1], allMACs[m][2],
allMACs[m][3], allMACs[m][4], allMACs[m][5]);
addedCount++;
} else {
Serial.printf(" ❌ Napaka pri dodajanju peer-ja %s\n", macNames[m]);
}
} else {
Serial.printf(" ⚠️ Peer %s že obstaja\n", macNames[m]);
}
}
// ===== DODAJ ŠE GLOBALNI BROADCAST (če je potrebno) =====
// Nekateri moduli morda pošiljajo na broadcast naslov
const uint8_t broadcastMAC[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
if (!esp_now_is_peer_exist(broadcastMAC)) {
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, broadcastMAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println(" ✅ Peer BROADCAST (FF:FF:FF:FF:FF:FF) dodan");
addedCount++;
}
}
// ===== PREVERI VSE PEERJE =====
esp_now_peer_num_t peerNum;
esp_err_t err = esp_now_get_peer_num(&peerNum);
if (err == ESP_OK) {
Serial.printf("📊 Skupaj aktivnih peerjev: %d\n", peerNum.total_num);
}
Serial.printf("📊 Skupaj dodanih peerjev: %d\n", addedCount);
Serial.println("=== KONEC INICIALIZACIJE ESP-NOW ===\n");
}
void testSendToModule3() {
Serial.println("\n╔════════════════════════════════════════════════════╗");
Serial.println("║ TEST POŠILJANJA UKAZA MODULU 3 ║");
Serial.println("╚════════════════════════════════════════════════════╝");
Serial.print("MAC naslov modula 3: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module3MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
bool peerExists = esp_now_is_peer_exist(module3MAC);
Serial.printf("Peer obstaja: %s\n", peerExists ? "DA" : "NE");
if (!peerExists) {
Serial.println("Poskušam dodati peer...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module3MAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
esp_err_t addResult = esp_now_add_peer(&peerInfo);
if (addResult == ESP_OK) {
Serial.println("✅ Peer dodan!");
} else {
Serial.printf("❌ Napaka pri dodajanju: %d\n", addResult);
return;
}
}
Serial.println("\nPošiljam testni ukaz STOP...");
sendShadeCommand(CMD_STOP, 0, 0);
delay(500);
Serial.println("Pošiljam testni ukaz za premik na 50%...");
sendShadeCommand(CMD_MOVE_TO_POSITION, 50, 0);
delay(500);
Serial.println("Pošiljam testni ukaz za premik na 0%...");
sendShadeCommand(CMD_MOVE_TO_POSITION, 0, 0);
Serial.println("\n╔════════════════════════════════════════════════════╗");
Serial.println("║ TEST KONČAN - PREVERITE SERIAL MONITOR ║");
Serial.println("║ MODULA 3 ZA ODZIV ║");
Serial.println("╚════════════════════════════════════════════════════╝\n");
}
void sendShadeCommand(int command, float param1, float param2) {
Serial.println("\n╔════════════════════════════════════════════════════╗");
Serial.printf("📡 POŠILJAM UKAZ NA MODUL 3: cmd=%d, p1=%.1f, p2=%.1f\n", command, param1, param2);
Serial.print(" MAC naslov modula 3: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module3MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.printf(" Trenutni kanal: %d\n", WiFi.channel());
CommandData cmd;
cmd.targetModuleId = 3;
cmd.command = command;
cmd.param1 = param1;
cmd.param2 = param2;
bool peerExists = esp_now_is_peer_exist(module3MAC);
Serial.printf(" Peer obstaja: %s\n", peerExists ? "DA" : "NE");
if (!peerExists) {
Serial.println(" ⚠ Peer ne obstaja, ga dodajam...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module3MAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
esp_err_t addResult = esp_now_add_peer(&peerInfo);
if (addResult == ESP_OK) {
Serial.println(" ✅ Peer dodan!");
} else {
Serial.printf(" ❌ Napaka pri dodajanju peer-ja: %d\n", addResult);
Serial.println("╚════════════════════════════════════════════════════╝\n");
return;
}
}
esp_err_t result = esp_now_send(module3MAC, (uint8_t*)&cmd, sizeof(cmd));
if (result == ESP_OK) {
Serial.println(" ✅ Ukaz POSLAN!");
} else {
Serial.printf(" ❌ Napaka pri pošiljanju: %d\n", result);
if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
Serial.println(" → Peer ne obstaja, poskusim ponovno dodati...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module3MAC, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
esp_now_add_peer(&peerInfo);
result = esp_now_send(module3MAC, (uint8_t*)&cmd, sizeof(cmd));
if (result == ESP_OK) {
Serial.println(" ✅ Ukaz POSLAN (po ponovnem dodajanju)!");
} else {
Serial.printf(" ❌ Ponovni poskus prav tako ni uspel: %d\n", result);
}
}
}
Serial.println("╚════════════════════════════════════════════════════╝\n");
}
void moveShadeTo(int position) {
position = constrain(position, 0, 100);
Serial.printf("\n🎮 ROČNO KRMILJENJE: Premik senčnika na %d%%\n", position);
sendShadeCommand(CMD_MOVE_TO_POSITION, position, 0);
}
void calibrateShade() {
Serial.println("\n🔧 KALIBRACIJA: Začenjam kalibracijo senčnika...");
sendShadeCommand(CMD_CALIBRATE, 0, 0);
}
void stopShade() {
Serial.println("\n⏹️ ZAUSTAVITEV: Zaustavljam senčnik");
sendShadeCommand(CMD_STOP, 0, 0);
}
void emergencyStopShade() {
Serial.println("\n🚨 NUJNA ZAUSTAVITEV: Takojšnja zaustavitev motorja!");
sendShadeCommand(CMD_EMERGENCY_STOP, 0, 0);
}
void setShadeSpeed(int speed) {
speed = constrain(speed, 100, 2000);
Serial.printf("\n⚙️ NASTAVITEV HITROSTI: %d\n", speed);
sendShadeCommand(CMD_SET_SPEED, speed, 0);
}
void setShadeAutoMode(bool autoMode) {
shadeAutoControl = autoMode;
Serial.printf("\n🔄 AVTOMATSKI NAČIN: %s\n", autoMode ? "VKLOPLJEN" : "IZKLOPLJEN");
sendShadeCommand(CMD_SET_AUTO_MODE, autoMode ? 1 : 0, 0);
markSettingsChanged();
}
void sendModuleCommand(uint8_t moduleId, int command, float param1, float param2) {
uint8_t* targetMAC = nullptr;
String moduleName;
bool* moduleActiveFlag = nullptr;
if (moduleId == 1) {
targetMAC = module1MAC;
moduleName = "Modul 1";
moduleActiveFlag = &module1Active;
} else if (moduleId == 2) {
targetMAC = module2MAC;
moduleName = "Modul 2";
moduleActiveFlag = &module2Active;
} else if (moduleId == 3) {
targetMAC = module3MAC;
moduleName = "Modul 3";
moduleActiveFlag = &module3Active;
} else {
Serial.printf(" ❌ Neznan modul ID: %d\n", moduleId);
return;
}
bool hasValidMAC = false;
for(int i = 0; i < 6; i++) {
if(targetMAC[i] != 0) {
hasValidMAC = true;
break;
}
}
if (!hasValidMAC) {
Serial.printf(" ❌ MAC naslov %s ni znan! Čakam na prvi prejem podatkov...\n", moduleName.c_str());
return;
}
CommandData cmd;
cmd.targetModuleId = moduleId;
cmd.command = command;
cmd.param1 = param1;
cmd.param2 = param2;
esp_err_t result = esp_now_send(targetMAC, (uint8_t*)&cmd, sizeof(cmd));
if (result == ESP_OK) {
Serial.printf(" ✅ Ukaz poslan na %s: cmd=%d, p1=%.1f, p2=%.1f\n",
moduleName.c_str(), command, param1, param2);
} else {
Serial.printf(" ❌ Napaka pri pošiljanju na %s: %d\n", moduleName.c_str(), result);
if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
Serial.printf(" → Peer za %s ne obstaja, poskusim dodati...\n", moduleName.c_str());
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, targetMAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.printf(" ✅ Peer za %s dodan!\n", moduleName.c_str());
result = esp_now_send(targetMAC, (uint8_t*)&cmd, sizeof(cmd));
if (result == ESP_OK) {
Serial.printf(" ✅ Ukaz poslan na %s (po ponovnem dodajanju)!\n", moduleName.c_str());
} else {
Serial.printf(" ❌ Ponovni poskus za %s prav tako ni uspel: %d\n", moduleName.c_str(), result);
}
} else {
Serial.printf(" ❌ Napaka pri dodajanju peer-ja za %s\n", moduleName.c_str());
}
}
}
}
void sendModule1Command(int command, float param1, float param2) {
sendModuleCommand(1, command, param1, param2);
}
void sendModule2Command(int command, float param1, float param2) {
sendModuleCommand(2, command, param1, param2);
}
void setModule2Relay(int relayNum, bool state) {
if (relayNum < 1 || relayNum > 3) {
return;
}
sendModule2Command(1, (float)relayNum, (float)state);
switch (relayNum) {
case 1: module2Relay1State = state; break;
case 2: module2Relay2State = state; break;
case 3: module2Relay3State = state; break;
}
}
void onModuleDataRecv(const uint8_t* mac, const uint8_t* incomingData, int len) {
// Izpiši MAC naslov pošiljatelja
Serial.print("📡 PREJETO OD: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", mac[i]);
if(i < 5) Serial.print(":");
}
Serial.printf(" (len=%d)\n", len);
// ==================== PREVERI MODUL 4 (besedilni format) ====================
bool isTextData = false;
for(int i = 0; i < min(len, 10); i++) {
if(incomingData[i] == 'M' && incomingData[i+1] == '4' && incomingData[i+2] == ':') {
isTextData = true;
break;
}
}
if (isTextData) {
String received = String((char*)incomingData);
if (received.startsWith("M4:")) {
received = received.substring(3);
int comma1 = received.indexOf(',');
int comma2 = received.indexOf(',', comma1 + 1);
int comma3 = received.indexOf(',', comma2 + 1);
int comma4 = received.indexOf(',', comma3 + 1);
int comma5 = received.indexOf(',', comma4 + 1);
int comma6 = received.indexOf(',', comma5 + 1);
if (comma1 > 0 && comma2 > 0 && comma3 > 0 && comma4 > 0 && comma5 > 0 && comma6 > 0) {
module4AirTemp = received.substring(0, comma1).toFloat();
module4AirHum = received.substring(comma1 + 1, comma2).toFloat();
module4Pressure = received.substring(comma2 + 1, comma3).toFloat();
module4LightPercent = received.substring(comma3 + 1, comma4).toInt();
module4SoilTemp = received.substring(comma4 + 1, comma5).toFloat();
module4SoilMoisture = received.substring(comma5 + 1, comma6).toInt();
// Uporabi kalibrirano pretvorbo za notranji LDR
internalTemperature = module4AirTemp;
internalHumidity = module4AirHum;
soilMoisturePercent = module4SoilMoisture;
ldrInternalPercent = module4LightPercent;
// 🔧 KALIBRIRANA PRETVORBA za notranji LDR
updateLDRConversion(); // Ta funkcija posodobi ldrInternalLux iz module4LightPercent
module4Active = true;
lastModule4Time = millis();
Serial.printf(" 🌡️ Modul 4: T=%.1f°C, H=%.1f%%, Light=%d%%, Lux=%d lx (kalibrirano)\n",
module4AirTemp, module4AirHum, module4LightPercent, (int)ldrInternalLux);
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
}
}
return;
}
// ==================== PREVERI BINARNE PODATKE ====================
if (len < 4) return;
uint8_t moduleId = incomingData[0];
uint8_t moduleType = incomingData[1];
// ===== MODUL 1 - VREMENSKI (PRAVILNI OFFSETI: 16,20,24,28) =====
if (moduleId == 1 || moduleType == 1) {
if (len >= 32) {
// Pravilni odmiki iz testiranja:
// Temperatura: byte 16-19
// Vlaga: byte 20-23
// Tlak: byte 24-27
// Svetloba: byte 28-31
memcpy(&externalTemperature, incomingData + 16, 4);
memcpy(&externalHumidity, incomingData + 20, 4);
memcpy(&externalPressure, incomingData + 24, 4);
memcpy(&externalLux, incomingData + 28, 4);
// Preveri veljavnost podatkov
if (externalTemperature < -10 || externalTemperature > 50) externalTemperature = 0;
if (externalHumidity < 0 || externalHumidity > 100) externalHumidity = 0;
if (externalPressure < 800 || externalPressure > 1100) externalPressure = 1013.25;
if (externalLux < 0 || externalLux > 100000) externalLux = 0;
// 🔧 KALIBRIRANA PRETVORBA za zunanji LDR
float rawExternalLux = externalLux; // Shranimo surovo vrednost za debug
updateLDRConversion(); // Ta funkcija posodobi externalLux s kalibrirano vrednostjo
module1Active = true;
lastModule1Time = millis();
Serial.printf(" 🌤️ Zunanji: T=%.1f°C, H=%.1f%%, P=%.1fhPa, Lux=%.0f lx (surovo: %.0f, kalibrirano: %.0f)\n",
externalTemperature, externalHumidity, externalPressure,
externalLux, rawExternalLux, externalLux);
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
}
return;
}
// ===== MODUL 2 - NAMAKALNI =====
if (moduleId == 2 || moduleType == 2) {
if (len >= 32) {
memcpy(&module2FlowRate1, incomingData + 8, 4);
memcpy(&module2FlowRate2, incomingData + 12, 4);
memcpy(&module2FlowRate3, incomingData + 16, 4);
memcpy(&module2TotalFlow1, incomingData + 20, 4);
memcpy(&module2TotalFlow2, incomingData + 24, 4);
memcpy(&module2TotalFlow3, incomingData + 28, 4);
module2Active = true;
lastModule2Time = millis();
Serial.printf(" 💧 Modul 2: Flow1=%.2f L/min, Flow2=%.2f L/min, Flow3=%.2f L/min\n",
module2FlowRate1, module2FlowRate2, module2FlowRate3);
}
return;
}
// ===== MODUL 3 - SENČENJE =====
if (moduleId == 3 || moduleType == 3) {
if (len >= 20) {
memcpy(&shadeCurrentPosition, incomingData + 8, 4);
memcpy(&shadeTargetPosition, incomingData + 12, 4);
shadeIsMoving = (incomingData[16] != 0);
shadeIsCalibrated = (incomingData[17] != 0);
module3Active = true;
lastModule3Time = millis();
Serial.printf(" 🎬 Modul 3: Pozicija=%d%%, Cilj=%d%%, Premikanje=%s, Kalibriran=%s\n",
shadeCurrentPosition, shadeTargetPosition,
shadeIsMoving ? "DA" : "NE",
shadeIsCalibrated ? "DA" : "NE");
}
return;
}
// ===== Če pridemo sem, pomeni da smo prejeli neznan format =====
Serial.print(" ⚠️ Neznan format podatkov: ");
for(int i = 0; i < min(len, 16); i++) {
Serial.printf("%02X ", incomingData[i]);
}
Serial.println();
}
void checkModulesTimeout() {
unsigned long now = millis();
if (now - lastModule1Time > MODULE1_TIMEOUT) {
if (module1Active) {
module1Active = false;
externalTemperature = 0;
externalHumidity = 0;
externalPressure = 1013.25;
externalLux = 0;
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
}
}
if (now - lastModule4Time > MODULE4_TIMEOUT) {
if (module4Active) {
module4Active = false;
Serial.println("⚠️ Modul 4 timeout - ni več aktiven!");
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
}
}
if (now - lastModule2Time > MODULE2_TIMEOUT) {
if (module2Active) {
module2Active = false;
module2FlowRate1 = 0;
module2FlowRate2 = 0;
module2FlowRate3 = 0;
module2TotalFlow1 = 0;
module2TotalFlow2 = 0;
module2TotalFlow3 = 0;
module2Relay1State = false;
module2Relay2State = false;
module2Relay3State = false;
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
}
}
if (now - lastModule3Time > MODULE3_TIMEOUT) {
if (module3Active) {
module3Active = false;
shadeCurrentPosition = 0;
shadeTargetPosition = 0;
shadeIsMoving = false;
shadeIsCalibrated = false;
shadeLimitOpen = false;
shadeLimitClosed = false;
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
}
}
}
float calculateVPD(float temperature, float humidity) {
float es = 0.6108 * exp((17.27 * temperature) / (temperature + 237.3));
float ea = es * (humidity / 100.0);
return es - ea;
}
String getVPDDescription(float vpd) {
if (vpd < 0.2) return "Izjemno vlazno (megla)";
if (vpd < 0.4) return "Zelo vlazno (tropski gozd)";
if (vpd < 0.6) return "Vlazno (idealno za Anthuriume)";
if (vpd < 0.8) return "Zmerno vlazno (idealno za Philodendrone)";
if (vpd < 1.0) return "Zmerno suho (pogojno OK)";
if (vpd < 1.2) return "Suho (listi lahko trpijo)";
if (vpd < 1.5) return "Zelo suho (skodljivo)";
return "Ekstremno suho (NEVARNOST!)";
}
uint16_t getVPDColor(float vpd) {
if (vpd < 0.6) return rgbTo565(0, 150, 255);
if (vpd < 0.9) return rgbTo565(0, 255, 0);
if (vpd < 1.2) return rgbTo565(255, 255, 0);
return rgbTo565(255, 0, 0);
}
int findPlantSpeciesIndex(String species) {
for (int i = 0; i < tropicalSpeciesCount; i++) {
if (tropicalSpecies[i].species == species) {
return i;
}
}
return -1;
}
void updateVPD() {
unsigned long now = millis();
if (now - lastVPDUpdate > VPD_UPDATE_INTERVAL) {
currentVPD = calculateVPD(internalTemperature, internalHumidity);
lastVPDUpdate = now;
}
}
int addPlantToCollection(String name, String species, bool isVariegated, String location) {
if (plantCount >= MAX_PLANTS) return -1;
int speciesIndex = findPlantSpeciesIndex(species);
if (speciesIndex == -1) return -1;
myPlants[plantCount].id = plantCount;
myPlants[plantCount].name = name;
myPlants[plantCount].species = species;
myPlants[plantCount].currentHeight = 0;
myPlants[plantCount].leafCount = 0;
myPlants[plantCount].lastWatered = 0;
myPlants[plantCount].lastFertilized = 0;
myPlants[plantCount].lastMisted = 0;
myPlants[plantCount].personalVPDFactor = 1.0;
myPlants[plantCount].isVariegated = isVariegated;
myPlants[plantCount].location = location;
myPlants[plantCount].imageFile = "";
plantCount++;
savePlantCollection();
return plantCount - 1;
}
void autoConfigureForPlant(int speciesIndex) {
if (speciesIndex < 0 || speciesIndex >= tropicalSpeciesCount) return;
TropicalPlantProfile* plant = &tropicalSpecies[speciesIndex];
targetTempMin = plant->tempMin;
targetTempMax = plant->tempMax;
targetHumMin = plant->humMin;
targetHumMax = plant->humMax;
lightOnThreshold = plant->lightMin * 0.8;
if (lightOnThreshold < 10) lightOnThreshold = 10;
lightOffThreshold = plant->lightMax * 1.2;
if (lightOffThreshold > 30000) lightOffThreshold = 30000;
shadeMinLux = plant->lightMin;
shadeMaxLux = plant->lightMax;
if (plant->soilMoistureMin > 0 && plant->soilMoistureMax > 0) {
SOIL_DRY_VALUE = map(plant->soilMoistureMin, 0, 100, 4095, 0);
SOIL_WET_VALUE = map(plant->soilMoistureMax, 0, 100, 4095, 0);
if (SOIL_DRY_VALUE < SOIL_WET_VALUE) {
int temp = SOIL_DRY_VALUE;
SOIL_DRY_VALUE = SOIL_WET_VALUE;
SOIL_WET_VALUE = temp;
}
} else {
if (plant->needsBottomWatering) {
SOIL_DRY_VALUE = 3500;
SOIL_WET_VALUE = 1800;
} else {
SOIL_DRY_VALUE = 4000;
SOIL_WET_VALUE = 1200;
}
}
if (plant->needsAirMovement) {
ventilationAutoMode = true;
ventilationDayMinRuntime = 10;
ventilationNightMinRuntime = 5;
} else {
ventilationAutoMode = false;
}
lightAutoMode = true;
lightTimeControlEnabled = true;
lightStartHour = 6;
lightStartMinute = 0;
lightEndHour = 18;
lightEndMinute = 0;
markSettingsChanged();
saveAllSettings();
}
void showMessage(String message, uint16_t color) {
tft.fillRect(100, 150, 280, 60, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 60, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(color);
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds(message, 0, 0, &textX, &textY, &textW, &textH);
int textPosX = 100 + (280 - textW) / 2 - textX;
int textPosY = 150 + (60 - textH) / 2 - textY + 5;
tft.setCursor(textPosX, textPosY);
tft.println(message);
delay(2000);
}
void savePlantCollection() {
if (!sdInitialized || !useSDCard) return;
File collectionFile = SD.open("/plant_collection.csv", FILE_WRITE);
if (!collectionFile) return;
collectionFile.println("ID,Ime,Vrsta,Visina (cm),Listi,Zadnje zalivanje,Zadnje gnojenje,Variegated,Lokacija");
for (int i = 0; i < plantCount; i++) {
collectionFile.print(myPlants[i].id);
collectionFile.print(",");
collectionFile.print(myPlants[i].name);
collectionFile.print(",");
collectionFile.print(myPlants[i].species);
collectionFile.print(",");
collectionFile.print(myPlants[i].currentHeight);
collectionFile.print(",");
collectionFile.print(myPlants[i].leafCount);
collectionFile.print(",");
collectionFile.print(myPlants[i].lastWatered);
collectionFile.print(",");
collectionFile.print(myPlants[i].lastFertilized);
collectionFile.print(",");
collectionFile.print(myPlants[i].isVariegated ? "1" : "0");
collectionFile.print(",");
collectionFile.println(myPlants[i].location);
}
collectionFile.close();
}
void loadPlantCollection() {
if (!sdInitialized || !useSDCard) return;
if (!SD.exists("/plant_collection.csv")) return;
File collectionFile = SD.open("/plant_collection.csv", FILE_READ);
if (!collectionFile) return;
collectionFile.readStringUntil('\n');
plantCount = 0;
while (collectionFile.available() && plantCount < MAX_PLANTS) {
String line = collectionFile.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int commas[10], commaCount = 0;
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) == ',') commas[commaCount++] = i;
}
if (commaCount >= 8) {
myPlants[plantCount].id = line.substring(0, commas[0]).toInt();
myPlants[plantCount].name = line.substring(commas[0] + 1, commas[1]);
myPlants[plantCount].species = line.substring(commas[1] + 1, commas[2]);
myPlants[plantCount].currentHeight = line.substring(commas[2] + 1, commas[3]).toFloat();
myPlants[plantCount].leafCount = line.substring(commas[3] + 1, commas[4]).toInt();
myPlants[plantCount].lastWatered = line.substring(commas[4] + 1, commas[5]).toInt();
myPlants[plantCount].lastFertilized = line.substring(commas[5] + 1, commas[6]).toInt();
myPlants[plantCount].isVariegated = line.substring(commas[6] + 1, commas[7]).toInt() == 1;
myPlants[plantCount].location = line.substring(commas[7] + 1);
myPlants[plantCount].personalVPDFactor = 1.0;
plantCount++;
}
}
collectionFile.close();
}
void controlVPDForPlants() {
if (plantCount == 0) return;
updateVPD();
for (int i = 0; i < plantCount; i++) {
int speciesIndex = findPlantSpeciesIndex(myPlants[i].species);
if (speciesIndex == -1) continue;
TropicalPlantProfile profile = tropicalSpecies[speciesIndex];
float adjustedVPDMin = profile.vpdMin * myPlants[i].personalVPDFactor;
float adjustedVPDMax = profile.vpdMax * myPlants[i].personalVPDFactor;
if (internalTemperature < profile.tempMin) {
if (relayControlEnabled) setRelay(0, true);
} else if (internalTemperature > profile.tempMax) {
if (relayControlEnabled) setRelay(1, true);
}
if (internalHumidity < profile.humMin) {
if (profile.needsMisting && (millis() - myPlants[i].lastMisted > 3600000)) {
startMistingForPlant(i);
}
}
if (currentVPD < adjustedVPDMin) {
if (profile.needsAirMovement) setRelay(VENTILATION_RELAY, true);
} else if (currentVPD > adjustedVPDMax) {
setRelay(VENTILATION_RELAY, false);
if (profile.needsMisting) startMistingForPlant(i);
}
}
for (int i = 0; i < plantCount; i++) {
int speciesIndex = findPlantSpeciesIndex(myPlants[i].species);
if (speciesIndex == -1) continue;
if (tropicalSpecies[speciesIndex].needsBottomWatering) {
if (soilMoisturePercent < 40 && (millis() - myPlants[i].lastWatered > 3 * 24 * 3600000)) {
startBottomWatering(i);
}
}
}
}
void startMistingForPlant(int plantId) {
if (plantId < 0 || plantId >= plantCount) return;
setRelay(MISTING_RELAY, true);
myPlants[plantId].lastMisted = millis();
isMisting = true;
mistingStartTime = millis();
}
void startBottomWatering(int plantId) {
if (plantId < 0 || plantId >= plantCount) return;
setRelay(BOTTOM_WATERING_RELAY, true);
myPlants[plantId].lastWatered = millis();
delay(30000);
setRelay(BOTTOM_WATERING_RELAY, false);
}
void updateAirMovement() {
unsigned long now = millis();
bool needsAir = false;
for (int i = 0; i < plantCount; i++) {
int speciesIndex = findPlantSpeciesIndex(myPlants[i].species);
if (speciesIndex != -1 && tropicalSpecies[speciesIndex].needsAirMovement) {
needsAir = true;
break;
}
}
if (!needsAir) return;
if (!airMoving && (now - lastAirMovement > AIR_MOVEMENT_INTERVAL)) {
setRelay(AIR_FLOW_RELAY, true);
airMoving = true;
lastAirMovement = now;
}
if (airMoving && (now - lastAirMovement > AIR_MOVEMENT_DURATION)) {
setRelay(AIR_FLOW_RELAY, false);
airMoving = false;
}
}
void checkMistingTimeout() {
if (isMisting && (millis() - mistingStartTime > MISTING_DURATION)) {
setRelay(MISTING_RELAY, false);
isMisting = false;
}
}
String getPlantCareAdvice(int plantId) {
if (plantId < 0 || plantId >= plantCount) return "Rastlina ne obstaja";
int speciesIndex = findPlantSpeciesIndex(myPlants[plantId].species);
if (speciesIndex == -1) return "Vrsta ni v bazi";
TropicalPlantProfile profile = tropicalSpecies[speciesIndex];
String advice = "";
advice += "🌿 " + myPlants[plantId].name + " (" + profile.commonName + ")\n";
advice += "Tezavnost: " + profile.careLevel + "\n";
advice += "Temperatura: " + String(profile.tempMin) + "-" + String(profile.tempMax) + "°C\n";
advice += "Vlaga: " + String(profile.humMin) + "-" + String(profile.humMax) + "%\n";
advice += "VPD: " + String(profile.vpdMin) + "-" + String(profile.vpdMax) + " kPa\n";
if (myPlants[plantId].isVariegated) advice += "⚠️ PESTRA - potrebuje vec svetlobe!\n";
advice += "\nTrenutno:\n";
advice += "Temp: " + String(internalTemperature, 1) + "°C ";
if (internalTemperature < profile.tempMin) advice += "❄️ PREHLADNO";
else if (internalTemperature > profile.tempMax) advice += "🔥 PREVROCE";
else advice += "✅ OK";
advice += "\n";
advice += "Vlaga: " + String(internalHumidity, 1) + "% ";
if (internalHumidity < profile.humMin) advice += "💧 PRESUHO";
else if (internalHumidity > profile.humMax) advice += "💧 PREVEC";
else advice += "✅ OK";
advice += "\n";
advice += "VPD: " + String(currentVPD, 2) + " kPa ";
if (currentVPD < profile.vpdMin) advice += "💨 PREVEC VLAZNO";
else if (currentVPD > profile.vpdMax) advice += "💨 PRESUHO";
else advice += "✅ IDEALNO";
if (profile.needsMisting && (millis() - myPlants[plantId].lastMisted > 24 * 3600000)) {
advice += "\n🌫️ Priporocam meglenje!";
}
advice += "\n📝 " + profile.notes;
return advice;
}
void showTropicalPlantsMenu() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(0, 200, 100));
tft.setCursor(150, 35);
tft.println("TROPSKE RASTLINE");
tft.drawRoundRect(10, 50, 460, 180, 10, rgbTo565(0, 200, 100));
tft.fillRoundRect(11, 51, 458, 178, 10, rgbTo565(20, 40, 30));
int yOffset = 65;
int lineHeight = 16;
tft.setTextSize(1);
tft.setCursor(20, yOffset);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Trenutni VPD: ");
tft.setTextColor(getVPDColor(currentVPD));
tft.printf("%.2f kPa", currentVPD);
tft.setCursor(20, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Stanje: ");
tft.setTextColor(getVPDColor(currentVPD));
tft.println(getVPDDescription(currentVPD));
tft.setCursor(20, yOffset + 2 * lineHeight + 5);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.printf("Moje rastline (%d/%d):", plantCount, MAX_PLANTS);
int listY = yOffset + 3 * lineHeight + 10;
if (plantCount == 0) {
tft.setCursor(40, listY);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.println("Ni dodanih rastlin");
} else {
int displayCount = min(plantCount, 5);
for (int i = 0; i < displayCount; i++) {
int speciesIndex = findPlantSpeciesIndex(myPlants[i].species);
uint16_t statusColor = rgbTo565(100, 100, 100);
if (speciesIndex != -1) {
float vpd = currentVPD;
if (vpd >= tropicalSpecies[speciesIndex].vpdMin && vpd <= tropicalSpecies[speciesIndex].vpdMax) {
statusColor = rgbTo565(0, 255, 0);
} else {
statusColor = rgbTo565(255, 100, 0);
}
}
tft.fillCircle(25, listY + i * 20, 4, statusColor);
String displayName = myPlants[i].name;
if (displayName.length() > 18) {
displayName = displayName.substring(0, 15) + "...";
}
tft.setCursor(35, listY + i * 20 - 3);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.printf("%d. %s", i + 1, displayName.c_str());
}
if (plantCount > 5) {
tft.setCursor(400, listY + 80);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("▼ Vec");
}
}
int buttonWidth = 140;
int buttonHeight = 35;
int buttonSpacing = 20;
int buttonY1 = 240;
drawMenuButton(40, buttonY1, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "DODAJ RASTLINO");
drawMenuButton(40 + buttonWidth + buttonSpacing, buttonY1, buttonWidth, buttonHeight,
rgbTo565(66, 135, 245), "PREGLEJ");
int buttonY2 = buttonY1 + buttonHeight + 10;
drawMenuButton(40, buttonY2, buttonWidth, buttonHeight,
rgbTo565(255, 150, 0), "NASVETI");
drawMenuButton(40 + buttonWidth + buttonSpacing, buttonY2, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_TROPICAL_PLANTS;
}
void showSelectPlantForViewScreen() {
if (plantCount == 0) {
showTemporaryMessage("Ni dodanih rastlin!\nDodajte novo rastlino", rgbTo565(255, 150, 50), 2000);
showTropicalPlantsMenu();
return;
}
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(0, 200, 100));
tft.setCursor(150, 40);
tft.println("IZBERI RASTLINO");
tft.drawRoundRect(10, 60, 460, 180, 10, rgbTo565(0, 200, 100));
tft.fillRoundRect(11, 61, 458, 178, 10, rgbTo565(20, 40, 30));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(20, 75);
tft.printf("Izberi rastlino za pregled (%d):", plantCount);
int startY = 95;
int buttonHeight = 28;
int buttonSpacing = 4;
int maxButtons = 5;
int startIndex = 0;
int endIndex = min(plantCount, startIndex + maxButtons);
for (int i = startIndex; i < endIndex; i++) {
int yPos = startY + (i - startIndex) * (buttonHeight + buttonSpacing);
uint16_t buttonColor = rgbTo565(66, 135, 245);
tft.fillRoundRect(30, yPos, 420, buttonHeight, 8, buttonColor);
tft.drawRoundRect(30, yPos, 420, buttonHeight, 8, ST77XX_WHITE);
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
int16_t textX, textY;
uint16_t textW, textH;
String displayText = String(i + 1) + ". " + myPlants[i].name;
if (displayText.length() > 30) {
displayText = displayText.substring(0, 27) + "...";
}
tft.getTextBounds(displayText, 0, 0, &textX, &textY, &textW, &textH);
int textPosY = yPos + (buttonHeight - textH) / 2 - textY;
tft.setCursor(45, textPosY);
tft.print(displayText);
if (myPlants[i].isVariegated) {
tft.setCursor(380, textPosY);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print("★");
}
}
int buttonY = startY + maxButtons * (buttonHeight + buttonSpacing) + 15;
tft.fillRoundRect(30, buttonY, 200, 30, 8, rgbTo565(76, 175, 80));
tft.drawRoundRect(30, buttonY, 200, 30, 8, ST77XX_WHITE);
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds("AVTO NASTAVITVE", 0, 0, &textX, &textY, &textW, &textH);
int textPosX = 30 + (200 - textW) / 2 - textX;
int textPosY = buttonY + (30 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.setTextColor(ST77XX_WHITE);
tft.print("AVTO NASTAVITVE");
tft.fillRoundRect(250, buttonY, 200, 30, 8, rgbTo565(245, 67, 54));
tft.drawRoundRect(250, buttonY, 200, 30, 8, ST77XX_WHITE);
tft.getTextBounds("NAZAJ", 0, 0, &textX, &textY, &textW, &textH);
textPosX = 250 + (200 - textW) / 2 - textX;
textPosY = buttonY + (30 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print("NAZAJ");
currentState = STATE_SELECT_PLANT_FOR_VIEW;
}
void showSelectPlantForAdviceScreen() {
if (plantCount == 0) {
showTemporaryMessage("Ni dodanih rastlin!\nDodajte novo rastlino", rgbTo565(255, 150, 50), 2000);
showTropicalPlantsMenu();
return;
}
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 0));
tft.setCursor(150, 40);
tft.println("NASVET ZA RASTLINO");
tft.drawRoundRect(10, 60, 460, 180, 10, rgbTo565(255, 150, 0));
tft.fillRoundRect(11, 61, 458, 178, 10, rgbTo565(40, 30, 20));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(20, 75);
tft.printf("Izberi rastlino za nasvet (%d):", plantCount);
int startY = 95;
int buttonHeight = 28;
int buttonSpacing = 4;
int maxButtons = 5;
int startIndex = 0;
int endIndex = min(plantCount, startIndex + maxButtons);
for (int i = startIndex; i < endIndex; i++) {
int yPos = startY + (i - startIndex) * (buttonHeight + buttonSpacing);
uint16_t buttonColor = rgbTo565(255, 150, 0);
tft.fillRoundRect(30, yPos, 420, buttonHeight, 8, buttonColor);
tft.drawRoundRect(30, yPos, 420, buttonHeight, 8, ST77XX_WHITE);
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
int16_t textX, textY;
uint16_t textW, textH;
String displayText = String(i + 1) + ". " + myPlants[i].name;
if (displayText.length() > 30) {
displayText = displayText.substring(0, 27) + "...";
}
tft.getTextBounds(displayText, 0, 0, &textX, &textY, &textW, &textH);
int textPosY = yPos + (buttonHeight - textH) / 2 - textY;
tft.setCursor(45, textPosY);
tft.print(displayText);
int speciesIndex = findPlantSpeciesIndex(myPlants[i].species);
if (speciesIndex != -1) {
float vpd = currentVPD;
if (vpd >= tropicalSpecies[speciesIndex].vpdMin && vpd <= tropicalSpecies[speciesIndex].vpdMax) {
tft.setCursor(380, textPosY);
tft.setTextColor(rgbTo565(0, 255, 0));
tft.print("✓");
} else {
tft.setCursor(380, textPosY);
tft.setTextColor(rgbTo565(255, 255, 0));
tft.print("!");
}
}
}
int buttonY = startY + maxButtons * (buttonHeight + buttonSpacing) + 15;
drawMenuButton(150, buttonY, 180, 30, rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_SELECT_PLANT_FOR_ADVICE;
}
void showCurrentSettings() {
Serial.println("\n=== TRENUTNE NASTAVITVE ===");
Serial.printf("Temperatura: %.1f-%.1f°C\n", targetTempMin, targetTempMax);
Serial.printf("Vlaznost: %.1f-%.1f%%\n", targetHumMin, targetHumMax);
Serial.printf("Svetloba (vklop/izklop): %.0f/%.0f lx\n", lightOnThreshold, lightOffThreshold);
Serial.printf("Sencenje (min/max): %.0f/%.0f lx\n", shadeMinLux, shadeMaxLux);
Serial.printf("Vlaznost tal (suho/mokro): %d/%d ADC\n", SOIL_DRY_VALUE, SOIL_WET_VALUE);
Serial.printf("Ventilacija avtomatska: %s\n", ventilationAutoMode ? "DA" : "NE");
Serial.printf("Razsvetljava avtomatska: %s\n", lightAutoMode ? "DA" : "NE");
Serial.println("============================\n");
}
void showAddPlantScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(0, 200, 100));
tft.setCursor(140, 30);
tft.println("DODAJ RASTLINO");
tft.drawRoundRect(10, 55, 460, 200, 10, rgbTo565(0, 200, 100));
tft.fillRoundRect(11, 56, 458, 198, 10, rgbTo565(20, 40, 30));
tft.setTextSize(1);
int leftColX = 30;
int rightColX = 250;
int yStart = 70;
int lineHeight = 25;
int buttonHeight = 25;
tft.setCursor(leftColX, yStart);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("IME RASTLINE:");
tft.setCursor(leftColX, yStart + 20);
tft.setTextColor(rgbTo565(200, 220, 255));
String displayName = (tempPlantName.length() > 0) ? tempPlantName : "[klikni za vnos]";
if (displayName.length() > 20) displayName = displayName.substring(0, 17) + "...";
tft.print(displayName);
tft.setCursor(leftColX, yStart + 50);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("VRSTA:");
String speciesName = tropicalSpecies[tempSpeciesIndex].commonName;
if (speciesName.length() > 25) speciesName = speciesName.substring(0, 22) + "...";
tft.setCursor(leftColX, yStart + 70);
tft.setTextColor(rgbTo565(100, 255, 100));
tft.println(speciesName);
tft.fillRoundRect(leftColX, yStart + 90, 45, buttonHeight, 5, rgbTo565(66, 135, 245));
tft.drawRoundRect(leftColX, yStart + 90, 45, buttonHeight, 5, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds("◀", 0, 0, &textX, &textY, &textW, &textH);
int textPosX = leftColX + (45 - textW) / 2 - textX;
int textPosY = yStart + 90 + (buttonHeight - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print("◀");
tft.fillRoundRect(leftColX + 60, yStart + 90, 45, buttonHeight, 5, rgbTo565(66, 135, 245));
tft.drawRoundRect(leftColX + 60, yStart + 90, 45, buttonHeight, 5, ST77XX_WHITE);
tft.getTextBounds("▶", 0, 0, &textX, &textY, &textW, &textH);
textPosX = leftColX + 60 + (45 - textW) / 2 - textX;
textPosY = yStart + 90 + (buttonHeight - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print("▶");
tft.setCursor(leftColX, yStart + 130);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print("PESTROST:");
int pestrostButtonX = leftColX + 100;
int pestrostButtonY = yStart + 125;
tft.fillRoundRect(pestrostButtonX, pestrostButtonY, 70, buttonHeight, 5,
tempIsVariegated ? rgbTo565(255, 100, 100) : rgbTo565(100, 100, 100));
tft.drawRoundRect(pestrostButtonX, pestrostButtonY, 70, buttonHeight, 5, ST77XX_WHITE);
String buttonText = tempIsVariegated ? "DA" : "NE";
tft.getTextBounds(buttonText, 0, 0, &textX, &textY, &textW, &textH);
textPosX = pestrostButtonX + (70 - textW) / 2 - textX;
textPosY = pestrostButtonY + (buttonHeight - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.setTextColor(ST77XX_WHITE);
tft.print(buttonText);
tft.setCursor(rightColX, yStart);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("LOKACIJA:");
tft.setCursor(rightColX, yStart + 20);
tft.setTextColor(rgbTo565(200, 220, 255));
String locationStr = "Cona " + String(tempLocationZone);
if (tempLocationZone == 1) locationStr += " (spodaj)";
else if (tempLocationZone == 2) locationStr += " (sredina)";
else locationStr += " (zgoraj)";
tft.println(locationStr);
tft.fillRoundRect(rightColX, yStart + 45, 45, buttonHeight, 5, rgbTo565(66, 135, 245));
tft.drawRoundRect(rightColX, yStart + 45, 45, buttonHeight, 5, ST77XX_WHITE);
tft.getTextBounds("-", 0, 0, &textX, &textY, &textW, &textH);
textPosX = rightColX + (45 - textW) / 2 - textX;
textPosY = yStart + 45 + (buttonHeight - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.setTextColor(ST77XX_WHITE);
tft.print("-");
tft.fillRoundRect(rightColX + 60, yStart + 45, 45, buttonHeight, 5, rgbTo565(66, 135, 245));
tft.drawRoundRect(rightColX + 60, yStart + 45, 45, buttonHeight, 5, ST77XX_WHITE);
tft.getTextBounds("+", 0, 0, &textX, &textY, &textW, &textH);
textPosX = rightColX + 60 + (45 - textW) / 2 - textX;
textPosY = yStart + 45 + (buttonHeight - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print("+");
tft.setCursor(rightColX, yStart + 90);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("PODROBNOSTI:");
TropicalPlantProfile* profile = &tropicalSpecies[tempSpeciesIndex];
tft.setCursor(rightColX, yStart + 110);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Tezavnost: ");
tft.setTextColor(profile->careLevel == "easy" ? rgbTo565(0, 255, 0) : (profile->careLevel == "moderate" ? rgbTo565(255, 255, 0) : rgbTo565(255, 100, 0)));
tft.println(profile->careLevel);
tft.setCursor(rightColX, yStart + 130);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.printf("VPD: %.1f-%.1f kPa", profile->vpdMin, profile->vpdMax);
tft.setCursor(rightColX, yStart + 150);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.printf("Temp: %.0f-%.0f°C", profile->tempMin, profile->tempMax);
int buttonY = 265;
int buttonWidth = 140;
int buttonHeight2 = 35;
tft.fillRoundRect(40, buttonY, buttonWidth, buttonHeight2, 8, rgbTo565(76, 175, 80));
tft.drawRoundRect(40, buttonY, buttonWidth, buttonHeight2, 8, ST77XX_WHITE);
tft.getTextBounds("SHRANI", 0, 0, &textX, &textY, &textW, &textH);
textPosX = 40 + (buttonWidth - textW) / 2 - textX;
textPosY = buttonY + (buttonHeight2 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.setTextColor(ST77XX_WHITE);
tft.print("SHRANI");
tft.fillRoundRect(40 + buttonWidth + 20, buttonY, buttonWidth, buttonHeight2, 8, rgbTo565(245, 67, 54));
tft.drawRoundRect(40 + buttonWidth + 20, buttonY, buttonWidth, buttonHeight2, 8, ST77XX_WHITE);
tft.getTextBounds("NAZAJ", 0, 0, &textX, &textY, &textW, &textH);
textPosX = 40 + buttonWidth + 20 + (buttonWidth - textW) / 2 - textX;
textPosY = buttonY + (buttonHeight2 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print("NAZAJ");
tft.fillRoundRect(40 + 2 * (buttonWidth + 20), buttonY, buttonWidth, buttonHeight2, 8, rgbTo565(255, 150, 0));
tft.drawRoundRect(40 + 2 * (buttonWidth + 20), buttonY, buttonWidth, buttonHeight2, 8, ST77XX_WHITE);
tft.getTextBounds("VNOS IMENA", 0, 0, &textX, &textY, &textW, &textH);
textPosX = 40 + 2 * (buttonWidth + 20) + (buttonWidth - textW) / 2 - textX;
textPosY = buttonY + (buttonHeight2 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print("VNOS IMENA");
tft.setTextSize(1);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.setCursor(40, 310);
tft.printf("Ime: %s | Vrsta: %d/%d | Pestrost: %s | Cona: %d",
(tempPlantName.length() > 0 ? tempPlantName.c_str() : "(prazno)"),
tempSpeciesIndex + 1, tropicalSpeciesCount,
tempIsVariegated ? "DA" : "NE",
tempLocationZone);
currentState = STATE_ADD_PLANT;
}
void showPlantDetailsScreen(int plantId) {
if (plantId < 0 || plantId >= plantCount) return;
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
IndividualPlant plant = myPlants[plantId];
int speciesIndex = findPlantSpeciesIndex(plant.species);
tft.setTextSize(1);
tft.setTextColor(rgbTo565(0, 200, 100));
tft.setCursor(120, 30);
tft.println(plant.name);
tft.drawRoundRect(10, 50, 460, 190, 10, rgbTo565(0, 200, 100));
tft.fillRoundRect(11, 51, 458, 188, 10, rgbTo565(20, 40, 30));
int yOffset = 65;
int lineHeight = 15;
tft.setTextSize(1);
if (speciesIndex >= 0) {
TropicalPlantProfile profile = tropicalSpecies[speciesIndex];
tft.setCursor(20, yOffset);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println(profile.commonName);
tft.setCursor(20, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Tezavnost: ");
tft.setTextColor(profile.careLevel == "easy" ? rgbTo565(0, 255, 0) : (profile.careLevel == "moderate" ? rgbTo565(255, 255, 0) : rgbTo565(255, 100, 0)));
tft.println(profile.careLevel);
tft.setCursor(20, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Temperatura: ");
tft.setTextColor(TEMP_COLOR);
tft.printf("%.0f-%.0f°C", profile.tempMin, profile.tempMax);
tft.setCursor(20, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Vlaga: ");
tft.setTextColor(HUMIDITY_COLOR);
tft.printf("%.0f-%.0f%%", profile.humMin, profile.humMax);
tft.setCursor(20, yOffset + 4 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("VPD: ");
tft.setTextColor(getVPDColor(currentVPD));
tft.printf("%.1f-%.1f kPa", profile.vpdMin, profile.vpdMax);
tft.setCursor(250, yOffset);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("TRENUTNO");
tft.setCursor(250, yOffset + lineHeight);
tft.setTextColor(internalTemperature < profile.tempMin ? TOO_LOW_COLOR : (internalTemperature > profile.tempMax ? TOO_HIGH_COLOR : NORMAL_COLOR));
tft.printf("%.1f°C", internalTemperature);
tft.setCursor(250, yOffset + 2 * lineHeight);
tft.setTextColor(internalHumidity < profile.humMin ? TOO_LOW_COLOR : (internalHumidity > profile.humMax ? TOO_HIGH_COLOR : NORMAL_COLOR));
tft.printf("%.1f%%", internalHumidity);
tft.setCursor(250, yOffset + 3 * lineHeight);
tft.setTextColor(getVPDColor(currentVPD));
tft.printf("%.2f kPa", currentVPD);
tft.setCursor(20, yOffset + 6 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Zadnje meglenje: ");
unsigned long hoursSinceMist = (millis() - plant.lastMisted) / 3600000;
tft.setTextColor(hoursSinceMist < 24 ? NORMAL_COLOR : WARNING_COLOR);
tft.printf("%lu h", hoursSinceMist);
tft.setCursor(20, yOffset + 7 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Zalivanje: ");
unsigned long daysSinceWater = (millis() - plant.lastWatered) / 86400000;
tft.setTextColor(daysSinceWater < 5 ? NORMAL_COLOR : (daysSinceWater < 7 ? WARNING_COLOR : TOO_HIGH_COLOR));
tft.printf("%lu dni", daysSinceWater);
}
int buttonY = 260;
int buttonWidth = 105;
int buttonSpacing = 10;
drawMenuButton(15, buttonY, buttonWidth, 35, rgbTo565(76, 175, 80), "MEGLENJE");
drawMenuButton(15 + buttonWidth + buttonSpacing, buttonY, buttonWidth, 35,
rgbTo565(66, 135, 245), "NASVET");
drawMenuButton(15 + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, 35,
rgbTo565(245, 67, 54), "NAZAJ");
drawMenuButton(15 + 3 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, 35,
rgbTo565(255, 0, 0), "IZBRISI");
currentState = STATE_PLANT_DETAILS;
selectedPlantId = plantId;
}
void deletePlant(int plantId) {
if (plantId < 0 || plantId >= plantCount) return;
String deletedName = myPlants[plantId].name;
tft.fillRect(50, 100, 380, 150, rgbTo565(20, 40, 70));
tft.drawRect(50, 100, 380, 150, rgbTo565(255, 80, 80));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 255, 255));
tft.setCursor(150, 120);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("BRISANJE RASTLINE");
tft.drawFastHLine(60, 135, 360, rgbTo565(100, 100, 100));
tft.setCursor(80, 150);
tft.setTextColor(rgbTo565(255, 255, 255));
tft.print("Ste prepricani, da zelite izbrisati ");
tft.setCursor(80, 170);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.print(deletedName);
tft.setTextColor(rgbTo565(255, 255, 255));
tft.print("?");
tft.fillRoundRect(120, 200, 100, 35, 8, rgbTo565(255, 80, 80));
tft.drawRoundRect(120, 200, 100, 35, 8, ST77XX_WHITE);
tft.setCursor(155, 210);
tft.setTextColor(ST77XX_WHITE);
tft.println("DA");
tft.fillRoundRect(260, 200, 100, 35, 8, rgbTo565(76, 175, 80));
tft.drawRoundRect(260, 200, 100, 35, 8, ST77XX_WHITE);
tft.setCursor(295, 210);
tft.setTextColor(ST77XX_WHITE);
tft.println("NE");
unsigned long confirmStart = millis();
bool waiting = true;
while (waiting && (millis() - confirmStart < 10000)) {
if (ts.touched()) {
TS_Point p = ts.getPoint();
int touchX = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int touchY = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
if (touchX > 120 && touchX < 220 && touchY > 200 && touchY < 235) {
for (int i = plantId; i < plantCount - 1; i++) {
myPlants[i] = myPlants[i + 1];
}
plantCount--;
savePlantCollection();
showTemporaryMessage("Rastlina " + deletedName + " izbrisana!", rgbTo565(80, 220, 100), 2000);
waiting = false;
showTropicalPlantsMenu();
return;
}
if (touchX > 260 && touchX < 360 && touchY > 200 && touchY < 235) {
waiting = false;
if (currentState == STATE_SELECT_PLANT_FOR_VIEW) {
showSelectPlantForViewScreen();
} else {
showPlantDetailsScreen(plantId);
}
return;
}
}
delay(10);
}
if (currentState == STATE_SELECT_PLANT_FOR_VIEW) {
showSelectPlantForViewScreen();
} else {
showPlantDetailsScreen(plantId);
}
}
void showAdvicePopup(String advice) {
AppState previousState = currentState;
int previousPlantId = selectedPlantId;
tft.fillRect(0, STATUS_BAR_HEIGHT, tft.width(), tft.height() - STATUS_BAR_HEIGHT, rgbTo565(10, 20, 40));
int winX = 20;
int winY = 40;
int winWidth = 440;
int winHeight = 240;
tft.fillRoundRect(winX, winY, winWidth, winHeight, 15, rgbTo565(255, 255, 255));
tft.drawRoundRect(winX, winY, winWidth, winHeight, 15, rgbTo565(0, 200, 100));
tft.drawRoundRect(winX + 2, winY + 2, winWidth - 4, winHeight - 4, 12, rgbTo565(200, 200, 200));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(0, 150, 0));
tft.setCursor(winX + 190, winY + 12);
tft.println("NASVET");
if (previousPlantId >= 0 && previousPlantId < plantCount) {
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 100, 100));
tft.setCursor(winX + 150, winY + 30);
tft.print("za ");
tft.setTextColor(rgbTo565(0, 100, 200));
tft.print(myPlants[previousPlantId].name);
}
tft.drawFastHLine(winX + 20, winY + 45, winWidth - 40, rgbTo565(0, 200, 100));
tft.setTextSize(1);
int y = winY + 60;
int lineHeight = 16;
int leftMargin = winX + 20;
int rightMargin = winX + winWidth - 20;
int maxWidth = rightMargin - leftMargin;
String currentLine = "";
for (int i = 0; i < advice.length(); i++) {
char c = advice.charAt(i);
if (c == '\n') {
if (currentLine.length() > 0) {
tft.setCursor(leftMargin, y);
if (currentLine.startsWith("🌿")) {
tft.setTextColor(rgbTo565(0, 150, 0));
} else if (currentLine.startsWith("Tezavnost:")) {
tft.setTextColor(rgbTo565(150, 100, 0));
} else if (currentLine.startsWith("⚠️")) {
tft.setTextColor(rgbTo565(255, 0, 0));
} else if (currentLine.startsWith("Trenutno:")) {
tft.setTextColor(rgbTo565(0, 100, 200));
} else if (currentLine.endsWith("OK")) {
tft.setTextColor(rgbTo565(0, 150, 0));
} else if (currentLine.endsWith("PREHLADNO") || currentLine.endsWith("PREVROCE") || currentLine.endsWith("PRESUHO") || currentLine.endsWith("PREVEC") || currentLine.endsWith("PREVEC VLAZNO")) {
tft.setTextColor(rgbTo565(200, 0, 0));
} else if (currentLine.endsWith("IDEALNO")) {
tft.setTextColor(rgbTo565(0, 200, 0));
} else if (currentLine.startsWith("📝")) {
tft.setTextColor(rgbTo565(100, 100, 200));
} else {
tft.setTextColor(rgbTo565(50, 50, 50));
}
tft.println(currentLine);
y += lineHeight;
currentLine = "";
if (y > winY + winHeight - 30) {
leftMargin = winX + 240;
y = winY + 60;
}
}
} else {
currentLine += c;
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(currentLine.c_str(), 0, 0, &x1, &y1, &w, &h);
if (w > maxWidth - 20) {
int lastSpace = currentLine.lastIndexOf(' ');
if (lastSpace > 0) {
String firstPart = currentLine.substring(0, lastSpace);
tft.setCursor(leftMargin, y);
tft.setTextColor(rgbTo565(50, 50, 50));
tft.println(firstPart);
y += lineHeight;
currentLine = currentLine.substring(lastSpace + 1);
if (y > winY + winHeight - 30) {
leftMargin = winX + 240;
y = winY + 60;
}
}
}
}
}
if (currentLine.length() > 0) {
tft.setCursor(leftMargin, y);
tft.setTextColor(rgbTo565(50, 50, 50));
tft.println(currentLine);
}
tft.setTextSize(1);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.setCursor(winX + 120, winY + winHeight - 15);
tft.println("Dotik kjerkoli za vrnitev");
unsigned long popupStart = millis();
bool popupActive = true;
while (popupActive && (millis() - popupStart < 30000)) {
handleTouch();
if (ts.touched()) {
TS_Point p = ts.getPoint();
popupActive = false;
delay(100);
}
delay(20);
}
if (previousPlantId >= 0) {
showPlantDetailsScreen(previousPlantId);
} else {
showTropicalPlantsMenu();
}
}
void showInternalGraph48hScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(INTERNAL_COLOR);
tft.setCursor(150, 25);
tft.println("48-URNI GRAF - NOTRANJI SENZORJI");
int graphX = 30;
int graphY = 60;
int graphWidth = 420;
int graphHeight = 160;
tft.fillRoundRect(graphX, graphY, graphWidth, graphHeight, 6, rgbTo565(20, 30, 40));
tft.drawRoundRect(graphX, graphY, graphWidth, graphHeight, 6, INTERNAL_COLOR);
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 120, 140));
for (int hour = 0; hour <= 48; hour += 6) {
int xPos = graphX + (hour * (graphWidth / 48));
for (int y = graphY; y < graphY + graphHeight; y += 4) {
tft.drawPixel(xPos, y, rgbTo565(60, 70, 80));
}
tft.setCursor(xPos - 10, graphY + graphHeight + 5);
if (hour == 0) {
tft.print("zdaj");
} else if (hour == 48) {
tft.print("-48h");
} else {
tft.print("-");
tft.print(48 - hour);
tft.print("h");
}
}
for (int temp = -10; temp <= 40; temp += 10) {
int yPos = graphY + graphHeight - ((temp + 10) * (graphHeight / 50));
for (int x = graphX; x < graphX + graphWidth; x += 4) {
tft.drawPixel(x, yPos, rgbTo565(60, 70, 80));
}
tft.setCursor(graphX - 25, yPos - 4);
tft.setTextColor(rgbTo565(150, 150, 180));
tft.print(temp);
tft.print("°C");
}
for (int hum = 0; hum <= 100; hum += 20) {
int yPos = graphY + graphHeight - (hum * (graphHeight / 100));
tft.setCursor(graphX + graphWidth + 5, yPos - 4);
tft.setTextColor(rgbTo565(150, 150, 180));
tft.print(hum);
tft.print("%");
}
int validCount = 0;
for (int i = 0; i < GRAPH_HISTORY_SIZE; i++) {
if (!isnan(tempHistory48h[i]) && tempHistory48h[i] != 0) {
validCount++;
}
}
if (validCount < 10) {
tft.setCursor(graphX + graphWidth/2 - 80, graphY + graphHeight/2);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.print("Zbiranje podatkov...");
tft.setCursor(graphX + graphWidth/2 - 60, graphY + graphHeight/2 + 20);
tft.printf("%d/%d meritev", validCount, GRAPH_HISTORY_SIZE);
} else {
if (validCount > 1) {
float prevTempX = -1, prevTempY = -1;
float prevHumX = -1, prevHumY = -1;
float prevLuxX = -1, prevLuxY = -1;
for (int i = 0; i < GRAPH_HISTORY_SIZE; i++) {
int historyIdx = (graphHistoryIndex - i - 1 + GRAPH_HISTORY_SIZE) % GRAPH_HISTORY_SIZE;
if (isnan(tempHistory48h[historyIdx]) && isnan(humHistory48h[historyIdx]) &&
(luxHistory48h[historyIdx] <= 0 || luxHistory48h[historyIdx] > 20000)) {
continue;
}
float xPos = graphX + (i * (graphWidth / (float)GRAPH_HISTORY_SIZE));
xPos = constrain(xPos, graphX, graphX + graphWidth);
if (!isnan(tempHistory48h[historyIdx]) && tempHistory48h[historyIdx] != 0) {
float tempY = graphY + graphHeight - ((tempHistory48h[historyIdx] + 10) * (graphHeight / 50.0));
tempY = constrain(tempY, graphY + 2, graphY + graphHeight - 2);
if (prevTempX != -1) {
tft.drawLine(prevTempX, prevTempY, xPos, tempY, TEMP_COLOR);
}
if (i % 3 == 0 || i == GRAPH_HISTORY_SIZE - 1) {
tft.fillCircle(xPos, tempY, 2, TEMP_COLOR);
}
prevTempX = xPos;
prevTempY = tempY;
}
if (!isnan(humHistory48h[historyIdx]) && humHistory48h[historyIdx] != 0) {
float humY = graphY + graphHeight - (humHistory48h[historyIdx] * (graphHeight / 100.0));
humY = constrain(humY, graphY + 2, graphY + graphHeight - 2);
if (prevHumX != -1) {
tft.drawLine(prevHumX, prevHumY, xPos, humY, HUMIDITY_COLOR);
}
if (i % 3 == 0 || i == GRAPH_HISTORY_SIZE - 1) {
tft.fillCircle(xPos, humY, 2, HUMIDITY_COLOR);
}
prevHumX = xPos;
prevHumY = humY;
}
if (luxHistory48h[historyIdx] > 0 && luxHistory48h[historyIdx] < 20000) {
float luxPercent = (luxHistory48h[historyIdx] / 20000.0) * 100.0;
luxPercent = constrain(luxPercent, 0, 100);
float luxY = graphY + graphHeight - (luxPercent * (graphHeight / 100.0));
luxY = constrain(luxY, graphY + 2, graphY + graphHeight - 2);
if (prevLuxX != -1) {
tft.drawLine(prevLuxX, prevLuxY, xPos, luxY, LIGHT_COLOR);
}
if (i % 3 == 0 || i == GRAPH_HISTORY_SIZE - 1) {
tft.fillCircle(xPos, luxY, 2, LIGHT_COLOR);
}
prevLuxX = xPos;
prevLuxY = luxY;
}
}
tft.setTextSize(1);
tft.setTextColor(rgbTo565(200, 200, 255));
tft.setCursor(graphX + 10, graphY + 10);
tft.printf("Tock: %d", validCount);
}
}
int legendY = graphY + graphHeight + 20;
tft.fillRect(50, legendY, 8, 8, TEMP_COLOR);
tft.setCursor(62, legendY);
tft.setTextColor(TEMP_COLOR);
tft.print("Temp: ");
tft.setTextColor(rgbTo565(200, 200, 255));
if (!isnan(internalTemperature)) {
tft.print(internalTemperature, 1);
tft.print("°C");
} else {
tft.print("--.-°C");
}
tft.fillRect(180, legendY, 8, 8, HUMIDITY_COLOR);
tft.setCursor(192, legendY);
tft.setTextColor(HUMIDITY_COLOR);
tft.print("Vlaga: ");
tft.setTextColor(rgbTo565(200, 200, 255));
if (!isnan(internalHumidity)) {
tft.print(internalHumidity, 1);
tft.print("%");
} else {
tft.print("--.-%");
}
tft.fillRect(310, legendY, 8, 8, LIGHT_COLOR);
tft.setCursor(322, legendY);
tft.setTextColor(LIGHT_COLOR);
tft.print("Svet: ");
tft.setTextColor(rgbTo565(200, 200, 255));
if (ldrInternalLux > 0) {
if (ldrInternalLux < 10) {
tft.print(ldrInternalLux, 1);
} else if (ldrInternalLux < 1000) {
tft.print((int)ldrInternalLux);
} else {
tft.print(ldrInternalLux / 1000.0, 1);
tft.print("k");
}
tft.print("lx");
} else {
tft.print("-- lx");
}
if (validCount > 0) {
unsigned long newestTime = 0;
for (int i = 0; i < GRAPH_HISTORY_SIZE; i++) {
if (timeStamps48h[i] > newestTime && !isnan(tempHistory48h[i])) {
newestTime = timeStamps48h[i];
}
}
if (newestTime > 0) {
unsigned long ageSeconds = (millis() - newestTime) / 1000;
tft.setCursor(50, legendY + 20);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.printf("Zadnja meritev: pred %lu s", ageSeconds);
}
}
int buttonWidth = 120;
int buttonHeight = 30;
int buttonY = legendY + 35;
drawMenuButton(40, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "OSVEZI");
drawMenuButton(180, buttonY, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_INTERNAL_GRAPH_48H;
}
void drawThickLine(int x1, int y1, int x2, int y2, int thickness, uint16_t color) {
if (thickness == 1) {
tft.drawLine(x1, y1, x2, y2, color);
} else if (thickness == 2) {
tft.drawLine(x1, y1, x2, y2, color);
tft.drawLine(x1 + 1, y1, x2 + 1, y2, color);
tft.drawLine(x1, y1 + 1, x2, y2 + 1, color);
} else {
for (int dx = -thickness / 2; dx <= thickness / 2; dx++) {
for (int dy = -thickness / 2; dy <= thickness / 2; dy++) {
tft.drawLine(x1 + dx, y1 + dy, x2 + dx, y2 + dy, color);
}
}
}
}
void showExternalGraph48hScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(EXTERNAL_COLOR);
tft.setCursor(150, 25);
tft.println("48-URNI GRAF - ZUNANJI SENZORJI");
int graphX = 30;
int graphY = 60;
int graphWidth = 420;
int graphHeight = 160;
tft.fillRoundRect(graphX, graphY, graphWidth, graphHeight, 6, rgbTo565(20, 30, 40));
tft.drawRoundRect(graphX, graphY, graphWidth, graphHeight, 6, EXTERNAL_COLOR);
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 120, 140));
for (int hour = 0; hour <= 48; hour += 6) {
int xPos = graphX + (hour * (graphWidth / 48));
for (int y = graphY; y < graphY + graphHeight; y += 4) {
tft.drawPixel(xPos, y, rgbTo565(60, 70, 80));
}
tft.setCursor(xPos - 10, graphY + graphHeight + 5);
if (hour == 0) {
tft.print("zdaj");
} else if (hour == 48) {
tft.print("-48h");
} else {
tft.print("-");
tft.print(48 - hour);
tft.print("h");
}
}
for (int temp = -10; temp <= 40; temp += 10) {
int yPos = graphY + graphHeight - ((temp + 10) * (graphHeight / 50));
for (int x = graphX; x < graphX + graphWidth; x += 4) {
tft.drawPixel(x, yPos, rgbTo565(60, 70, 80));
}
tft.setCursor(graphX - 25, yPos - 4);
tft.setTextColor(rgbTo565(150, 150, 180));
tft.print(temp);
tft.print("°C");
}
for (int hum = 0; hum <= 100; hum += 20) {
int yPos = graphY + graphHeight - (hum * (graphHeight / 100));
tft.setCursor(graphX + graphWidth + 5, yPos - 4);
tft.setTextColor(rgbTo565(150, 150, 180));
tft.print(hum);
tft.print("%");
}
int validCount = 0;
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
if (!isnan(extTempHistory48h[i]) && extTempHistory48h[i] != 0) {
validCount++;
}
}
if (validCount > 1) {
float prevTempX = -1, prevTempY = -1;
float prevHumX = -1, prevHumY = -1;
float prevLuxX = -1, prevLuxY = -1;
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
int historyIdx = (extGraphHistoryIndex - i - 1 + EXTERNAL_GRAPH_HISTORY_SIZE) % EXTERNAL_GRAPH_HISTORY_SIZE;
if (isnan(extTempHistory48h[historyIdx]) && isnan(extHumHistory48h[historyIdx]) &&
(extLuxHistory48h[historyIdx] <= 0 || extLuxHistory48h[historyIdx] > 20000)) {
continue;
}
float xPos = graphX + (i * (graphWidth / (float)EXTERNAL_GRAPH_HISTORY_SIZE));
xPos = constrain(xPos, graphX, graphX + graphWidth);
if (!isnan(extTempHistory48h[historyIdx]) && extTempHistory48h[historyIdx] != 0) {
float tempY = graphY + graphHeight - ((extTempHistory48h[historyIdx] + 10) * (graphHeight / 50.0));
tempY = constrain(tempY, graphY + 2, graphY + graphHeight - 2);
if (prevTempX != -1) {
tft.drawLine(prevTempX, prevTempY, xPos, tempY, TEMP_COLOR);
}
if (i % 3 == 0 || i == EXTERNAL_GRAPH_HISTORY_SIZE - 1) {
tft.fillCircle(xPos, tempY, 2, TEMP_COLOR);
}
prevTempX = xPos;
prevTempY = tempY;
}
if (!isnan(extHumHistory48h[historyIdx]) && extHumHistory48h[historyIdx] != 0) {
float humY = graphY + graphHeight - (extHumHistory48h[historyIdx] * (graphHeight / 100.0));
humY = constrain(humY, graphY + 2, graphY + graphHeight - 2);
if (prevHumX != -1) {
tft.drawLine(prevHumX, prevHumY, xPos, humY, HUMIDITY_COLOR);
}
if (i % 3 == 0 || i == EXTERNAL_GRAPH_HISTORY_SIZE - 1) {
tft.fillCircle(xPos, humY, 2, HUMIDITY_COLOR);
}
prevHumX = xPos;
prevHumY = humY;
}
if (extLuxHistory48h[historyIdx] > 0 && extLuxHistory48h[historyIdx] < 20000) {
float luxPercent = (extLuxHistory48h[historyIdx] / 20000.0) * 100.0;
luxPercent = constrain(luxPercent, 0, 100);
float luxY = graphY + graphHeight - (luxPercent * (graphHeight / 100.0));
luxY = constrain(luxY, graphY + 2, graphY + graphHeight - 2);
if (prevLuxX != -1) {
tft.drawLine(prevLuxX, prevLuxY, xPos, luxY, LIGHT_COLOR);
}
if (i % 3 == 0 || i == EXTERNAL_GRAPH_HISTORY_SIZE - 1) {
tft.fillCircle(xPos, luxY, 2, LIGHT_COLOR);
}
prevLuxX = xPos;
prevLuxY = luxY;
}
}
tft.setTextSize(1);
tft.setTextColor(rgbTo565(200, 200, 255));
tft.setCursor(graphX + 10, graphY + 10);
tft.printf("Tock: %d", validCount);
} else {
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(graphX + graphWidth / 2 - 60, graphY + graphHeight / 2 - 20);
tft.println("Premalo podatkov za graf");
tft.setCursor(graphX + graphWidth / 2 - 50, graphY + graphHeight / 2);
tft.println("Pocakajte vsaj 30 minut");
tft.setCursor(graphX + graphWidth / 2 - 40, graphY + graphHeight / 2 + 20);
tft.printf("Veljavnih: %d/%d", validCount, EXTERNAL_GRAPH_HISTORY_SIZE);
if (!module1Active) {
tft.setCursor(graphX + graphWidth / 2 - 70, graphY + graphHeight / 2 + 45);
tft.setTextColor(rgbTo565(255, 80, 80));
tft.println("⚠️ Modul 1 ni aktiven!");
tft.setCursor(graphX + graphWidth / 2 - 50, graphY + graphHeight / 2 + 60);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Preverite povezavo");
}
}
int legendY = graphY + graphHeight + 20;
tft.fillRect(50, legendY, 8, 8, TEMP_COLOR);
tft.setCursor(62, legendY);
tft.setTextColor(TEMP_COLOR);
tft.print("Temp: ");
tft.setTextColor(rgbTo565(200, 200, 255));
if (module1Active && !isnan(externalTemperature)) {
tft.print(externalTemperature, 1);
tft.print("°C");
} else {
tft.print("--.-°C");
}
tft.fillRect(180, legendY, 8, 8, HUMIDITY_COLOR);
tft.setCursor(192, legendY);
tft.setTextColor(HUMIDITY_COLOR);
tft.print("Vlaga: ");
tft.setTextColor(rgbTo565(200, 200, 255));
if (module1Active && !isnan(externalHumidity)) {
tft.print(externalHumidity, 1);
tft.print("%");
} else {
tft.print("--.-%");
}
tft.fillRect(310, legendY, 8, 8, LIGHT_COLOR);
tft.setCursor(322, legendY);
tft.setTextColor(LIGHT_COLOR);
tft.print("Svet: ");
tft.setTextColor(rgbTo565(200, 200, 255));
if (module1Active && externalLux > 0) {
if (externalLux < 10) {
tft.print(externalLux, 1);
} else if (externalLux < 1000) {
tft.print((int)externalLux);
} else {
tft.print(externalLux / 1000.0, 1);
tft.print("k");
}
tft.print("lx");
} else {
tft.print("-- lx");
}
if (validCount > 0) {
unsigned long newestTime = 0;
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
if (extTimeStamps48h[i] > newestTime && !isnan(extTempHistory48h[i])) {
newestTime = extTimeStamps48h[i];
}
}
if (newestTime > 0) {
unsigned long ageSeconds = (millis() - newestTime) / 1000;
tft.setCursor(50, legendY + 20);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.printf("Zadnja meritev: pred %lu s", ageSeconds);
}
}
int buttonWidth = 120;
int buttonHeight = 30;
int buttonY = legendY + 35;
drawMenuButton(40, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "OSVEZI");
drawMenuButton(180, buttonY, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_EXTERNAL_GRAPH_48H;
}
void showExternalSensorsInfo() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(EXTERNAL_COLOR);
tft.setCursor(150, 35);
tft.println("ZUNANJI SENZORJI");
tft.setCursor(350, 35);
tft.setTextColor(module1Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.print("●");
tft.setTextColor(ST77XX_WHITE);
tft.print(module1Active ? " Modul 1" : " Modul nedosegljiv");
tft.drawRoundRect(10, 48, 460, 170, 10, EXTERNAL_COLOR);
tft.fillRoundRect(11, 49, 458, 168, 10, rgbTo565(40, 20, 50));
int leftColumnX = 30;
int rightColumnX = 260;
int yOffset = 58;
int lineHeight = 14;
tft.setTextSize(1);
tft.setCursor(leftColumnX, yOffset);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("AHT20: ");
tft.setTextColor(module1Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(module1Active ? "MODUL 1" : "NI PODATKOV");
if (module1Active) {
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Temperatura:");
int tempBarWidth = map(constrain(externalTemperature, -10, 40), -10, 40, 0, 150);
tft.fillRect(leftColumnX, yOffset + lineHeight + 4, 150, 12, rgbTo565(80, 60, 60));
tft.fillRect(leftColumnX, yOffset + lineHeight + 4, tempBarWidth, 12, TEMP_COLOR);
tft.drawRect(leftColumnX, yOffset + lineHeight + 4, 150, 12, ST77XX_WHITE);
tft.setCursor(leftColumnX, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Vlaznost:");
int humidBarWidth = map(constrain(externalHumidity, 0, 100), 0, 100, 0, 150);
tft.fillRect(leftColumnX, yOffset + 3 * lineHeight + 4, 150, 12, rgbTo565(60, 60, 80));
tft.fillRect(leftColumnX, yOffset + 3 * lineHeight + 4, humidBarWidth, 12, HUMIDITY_COLOR);
tft.drawRect(leftColumnX, yOffset + 3 * lineHeight + 4, 150, 12, ST77XX_WHITE);
} else {
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.println("Modul 1 ni dosegljiv!");
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Preverite povezavo");
}
tft.setCursor(rightColumnX, yOffset);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("BMP280: ");
if (module1Active) {
tft.setTextColor(rgbTo565(80, 220, 100));
tft.println("MODUL 1");
} else {
tft.setTextColor(rgbTo565(255, 80, 80));
tft.println("NI PODATKOV");
}
if (module1Active) {
tft.setCursor(rightColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Tlak:");
int pressureBarWidth = map(constrain(externalPressure, 950, 1050), 950, 1050, 0, 150);
tft.fillRect(rightColumnX, yOffset + lineHeight + 4, 150, 12, rgbTo565(60, 80, 60));
tft.fillRect(rightColumnX, yOffset + lineHeight + 4, pressureBarWidth, 12, PRESSURE_COLOR);
tft.drawRect(rightColumnX, yOffset + lineHeight + 4, 150, 12, ST77XX_WHITE);
}
tft.setCursor(rightColumnX, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Svetloba:");
int lightBarWidth = map(constrain(externalLux, 0, 20000), 0, 20000, 0, 150);
tft.fillRect(rightColumnX, yOffset + 3 * lineHeight + 4, 150, 12, rgbTo565(60, 60, 60));
tft.fillRect(rightColumnX, yOffset + 3 * lineHeight + 4, lightBarWidth, 12, LIGHT_COLOR);
tft.drawRect(rightColumnX, yOffset + 3 * lineHeight + 4, 150, 12, ST77XX_WHITE);
int measurementY = yOffset + 5 * lineHeight + 5;
if (module1Active) {
tft.setCursor(leftColumnX, measurementY);
tft.setTextColor(TEMP_COLOR);
tft.print("T: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.printf("%.1f°C", externalTemperature);
tft.setCursor(leftColumnX + 85, measurementY);
tft.setTextColor(HUMIDITY_COLOR);
tft.print("V: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.printf("%.1f%%", externalHumidity);
tft.setCursor(rightColumnX, measurementY);
tft.setTextColor(PRESSURE_COLOR);
tft.print("P: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.printf("%.1f hPa", externalPressure);
} else {
tft.setCursor(leftColumnX, measurementY);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.print("Modul ni dosegljiv");
}
int measurementY2 = measurementY + lineHeight;
if (module1Active) {
tft.setCursor(leftColumnX, measurementY2);
tft.setTextColor(LIGHT_COLOR);
tft.print("Sv: ");
tft.setTextColor(rgbTo565(200, 200, 255));
if (externalLux < 10) {
tft.printf("%.1f lx", externalLux);
} else if (externalLux < 1000) {
tft.printf("%.0f lx", externalLux);
} else {
tft.printf("%.1f klx", externalLux / 1000.0);
}
}
int buttonY = 228;
int buttonWidth = 135;
int buttonHeight = 32;
int buttonSpacing = 10;
int row1X = (tft.width() - (2 * buttonWidth + buttonSpacing)) / 2;
drawIconButton(row1X, buttonY, buttonWidth, buttonHeight, EXTERNAL_COLOR, "OSVEZI", "refresh");
drawMenuButton(row1X + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "48H GRAF");
int row2Y = buttonY + buttonHeight + 8;
int btnW2 = 110;
int spacing2 = 8;
int totalWidth = 3 * btnW2 + 2 * spacing2;
int row2X = (tft.width() - totalWidth) / 2;
drawMenuButton(row2X, row2Y, btnW2, buttonHeight, rgbTo565(76, 175, 80), "SHRANI");
drawMenuButton(row2X + btnW2 + spacing2, row2Y, btnW2, buttonHeight,
EXTERNAL_COLOR, "NAZAJ");
if (!module1Active) {
tft.fillRect(100, 150, 280, 40, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 40, rgbTo565(255, 80, 80));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(130, 165);
tft.println("MODUL 1 NI DOSEGLJIV!");
tft.setCursor(140, 180);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Preverite povezavo");
}
currentState = STATE_EXTERNAL_SENSORS_INFO;
}
void handleExternalGraph48hTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int buttonWidth = 120;
int buttonHeight = 30;
int graphY = 60;
int graphHeight = 160;
int legendY = graphY + graphHeight + 20;
int buttonY = legendY + 35;
if (x > 40 && x < 40 + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
showExternalGraph48hScreen();
return;
}
if (x > 180 && x < 180 + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
showExternalSensorsInfo();
return;
}
}
void handleExternalSensorsTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int buttonWidth = 135;
int buttonHeight = 32;
int buttonSpacing = 10;
int buttonY1 = 228;
int buttonY2 = buttonY1 + buttonHeight + 8;
int row1X = (tft.width() - (2 * buttonWidth + buttonSpacing)) / 2;
if (x > row1X && x < row1X + buttonWidth && y > buttonY1 && y < buttonY1 + buttonHeight) {
Serial.println("Gumb OSVEZI pritisnjen");
showExternalSensorsInfo();
return;
}
if (x > row1X + buttonWidth + buttonSpacing && x < row1X + 2 * buttonWidth + buttonSpacing &&
y > buttonY1 && y < buttonY1 + buttonHeight) {
Serial.println("Gumb 48H GRAF pritisnjen");
showExternalGraph48hScreen();
return;
}
int btnW2 = 110;
int spacing2 = 8;
int totalWidth = 2 * btnW2 + spacing2;
int row2X = (tft.width() - totalWidth) / 2;
if (x > row2X && x < row2X + btnW2 && y > buttonY2 && y < buttonY2 + buttonHeight) {
Serial.println("Gumb SHRANI pritisnjen");
if (sdInitialized && useSDCard) {
saveSettingsToSD();
showTemporaryMessage("Nastavitve shranjene\nna SD kartico!", rgbTo565(80, 220, 100), 2000);
} else {
if (!sdInitialized) {
showTemporaryMessage("SD kartica ni inicializirana!\nPoskusite znova", rgbTo565(255, 80, 80), 2000);
initializeSDCard();
} else if (!useSDCard) {
showTemporaryMessage("Shranjevanje ni na SD!\nPreklopite nacin", rgbTo565(255, 150, 50), 2000);
}
}
showExternalSensorsInfo();
return;
}
if (x > row2X + btnW2 + spacing2 && x < row2X + 2 * btnW2 + spacing2 &&
y > buttonY2 && y < buttonY2 + buttonHeight) {
Serial.println("Gumb NAZAJ pritisnjen");
showMainMenu();
return;
}
}
void setupWiFi() {
Serial.println("\n=== KONFIGURACIJA WIFI OMREŽIJ ===");
// Počisti obstoječe nastavitve
WiFi.disconnect(true);
delay(100);
// Nastavi način
WiFi.mode(WIFI_STA);
delay(100);
// Izpiši MAC naslov
Serial.printf(" MAC naslov: %s\n", WiFi.macAddress().c_str());
int networksAdded = 0;
for (int i = 0; i < MAX_WIFI_NETWORKS; i++) {
String ssid = String(wifiNetworks[i][0]);
String pass = String(wifiNetworks[i][1]);
if (ssid.length() > 0 && ssid != "" && pass.length() > 0) {
wifiMulti.addAP(ssid.c_str(), pass.c_str());
networksAdded++;
Serial.printf(" Dodano omrezje %d: %s\n", i+1, ssid.c_str());
}
}
if (networksAdded == 0) {
Serial.println(" OPOZORILO: Ni dodanih WiFi omrezij!");
} else {
Serial.printf(" Skupaj dodanih omrezij: %d\n", networksAdded);
}
Serial.println("=== KONEC KONFIGURACIJE ===\n");
}
void testWiFiConnection() {
Serial.println("\n╔══════════════════════════════════════════════════════════════╗");
Serial.println("║ TEST WIFI POVEZAVE ║");
Serial.println("╚══════════════════════════════════════════════════════════════╝");
bool wasConnected = wifiConnected;
WiFi.disconnect(true);
delay(500);
Serial.println("\n📡 1. Poskusam povezavo BREZ GESLA...");
WiFi.begin("WiFi-192");
int attempts = 0;
bool connected = false;
while (attempts < 30 && WiFi.status() != WL_CONNECTED) {
delay(200);
attempts++;
if (attempts % 10 == 0) {
Serial.printf(" Status: %d (1=OMREZJE NI NAJDENO, 6=POVEZAVA NEUSPELA)\n", WiFi.status());
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(" ✅ Povezano BREZ GESLA! Omrezje je odprto!");
Serial.printf(" IP naslov: %s\n", WiFi.localIP().toString());
connected = true;
} else {
Serial.println(" ❌ Povezava brez gesla ni uspela - omrezje zahteva geslo");
}
if (connected) {
WiFi.disconnect();
delay(500);
}
const char* commonPasswords[] = {"", "password", "12345678", "admin", "sijuan5768", "sijuan5768", "WiFi-192", "1234567890"};
int numPasswords = sizeof(commonPasswords) / sizeof(commonPasswords[0]);
Serial.println("\n📡 2. Poskusam z razlicnimi gesli...");
for (int i = 0; i < numPasswords; i++) {
String password = String(commonPasswords[i]);
if (password.length() == 0) continue;
Serial.printf(" Geslo: '%s' ", password.c_str());
WiFi.begin("WiFi-192", password.c_str());
attempts = 0;
bool passConnected = false;
while (attempts < 25 && WiFi.status() != WL_CONNECTED) {
delay(200);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(" ✅ POVEZANO!");
Serial.printf(" IP naslov: %s\n", WiFi.localIP().toString());
Serial.printf(" Kanal: %d\n", WiFi.channel());
Serial.printf(" RSSI: %d dBm\n", WiFi.RSSI());
connected = true;
break;
} else {
Serial.println(" ❌");
}
WiFi.disconnect();
delay(300);
}
if (!connected) {
Serial.println("\n📡 3. Dodatne informacije za debug:");
Serial.printf(" MAC naslov ESP32: %s\n", WiFi.macAddress().c_str());
Serial.println(" Ponovno skeniram omrezja...");
int n = WiFi.scanNetworks();
bool found = false;
for (int i = 0; i < n; i++) {
if (WiFi.SSID(i) == "WiFi-192") {
found = true;
Serial.printf(" ✅ Omrezje 'WiFi-192' NAJDENO!\n");
Serial.printf(" Kanal: %d\n", WiFi.channel(i));
Serial.printf(" RSSI: %d dBm\n", WiFi.RSSI(i));
Serial.printf(" Enkripcija: %d (0=ODPRTO, 2=WPA, 3=WPA2, 4=WPA/WPA2)\n", WiFi.encryptionType(i));
break;
}
}
WiFi.scanDelete();
if (!found) {
Serial.println(" ❌ Omrezje 'WiFi-192' NI NAJDENO!");
Serial.println(" Preverite, ali je omrezje vidno na drugih napravah.");
}
Serial.println("\n Možni vzroki:");
Serial.println(" 1. Napačno geslo - preverite na drugi napravi");
Serial.println(" 2. MAC filtriranje na routerju - dodajte MAC naslov ESP32");
Serial.println(" 3. DHCP ni omogočen - router ne dodeli IP naslova");
Serial.println(" 4. Preveč naprav povezanih - router ima omejitev");
}
if (wasConnected && !connected) {
Serial.println("\n📡 Obnavljam prejsnjo WiFi povezavo...");
WiFi.begin("WiFi-192", "sijuan5768");
attempts = 0;
while (attempts < 30 && WiFi.status() != WL_CONNECTED) {
delay(200);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnected = true;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
Serial.println(" ✅ Prejsnja povezava obnovljena!");
}
}
Serial.println("\n╔══════════════════════════════════════════════════════════════╗");
Serial.printf("║ KONEC TESTA - Povezava: %s\n", connected ? "USPEŠNA ✅" : "NEUSPEŠNA ❌");
Serial.println("╚══════════════════════════════════════════════════════════════╝\n");
}
void autoConnectToWiFi() {
Serial.println("\n=== POVEZAVA NA WIFI ===");
if (WiFi.status() == WL_CONNECTED) {
Serial.println("✅ Že povezan na WiFi!");
wifiConnected = true;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
return;
}
// Počisti stare nastavitve
WiFi.disconnect(true);
delay(100);
// Nastavi način
WiFi.mode(WIFI_STA);
delay(100);
// Poskusi povezavo DIREKTNO (brez WiFiMulti)
Serial.println(" Poskušam povezavo na WiFi-192...");
WiFi.begin("WiFi-192", "sijuan5768");
int attempts = 0;
while (attempts < 40 && WiFi.status() != WL_CONNECTED) {
delay(500);
attempts++;
Serial.printf(" Čakam... (%d/40), status: %d\n", attempts, WiFi.status());
if (attempts == 20) {
Serial.println(" Ponovni poskus povezave...");
WiFi.begin("WiFi-192", "sijuan5768");
}
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnected = true;
wifiSSID = "WiFi-192";
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
Serial.println("\n✅ WiFi POVEZAN!");
Serial.printf(" IP: %s\n", wifiIP.c_str());
Serial.printf(" RSSI: %d dBm\n", wifiRSSI);
Serial.printf(" Kanal: %d\n", WiFi.channel());
// Nastavi kanal na 1
WiFi.setChannel(1);
if (!ntpSynchronized) {
syncTimeWithNTP();
}
} else {
Serial.printf("\n❌ Povezava neuspešna (status: %d)\n", WiFi.status());
Serial.println(" Preverite:");
Serial.println(" 1. Ali je geslo 'sijuan5768' pravilno?");
Serial.println(" 2. Ali router oddaja SSID 'WiFi-192'?");
Serial.println(" 3. Ali je DHCP omogočen?");
wifiConnected = false;
}
}
void checkWiFiStatus() {
unsigned long currentTime = millis();
static unsigned long lastConnectAttempt = 0;
if (currentTime - lastWifiCheck > 2000) {
uint8_t currentStatus = WiFi.status();
static uint8_t lastStatus = 255;
if (currentStatus != lastStatus) {
Serial.printf("WiFi status spremenjen: %d -> %d\n", lastStatus, currentStatus);
lastStatus = currentStatus;
}
if (currentStatus == WL_CONNECTED) {
if (!wifiConnected) {
wifiConnected = true;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
autoConnecting = false;
WiFi.setChannel(1);
Serial.printf("✅ WiFi povezan, kanal: %d\n", WiFi.channel());
if (!ntpSynchronized) {
syncTimeWithNTP();
}
drawStatusBar();
}
} else {
if (wifiConnected) {
wifiConnected = false;
wifiSSID = "Brez mreze";
wifiIP = "N/A";
wifiRSSI = 0;
Serial.println("⚠️ WiFi povezava prekinjena!");
drawStatusBar();
}
if (!autoConnecting && currentState != STATE_WIFI_CONNECTING &&
(currentTime - lastConnectAttempt > 30000)) {
lastConnectAttempt = currentTime;
Serial.println("🔄 Poskus ponovne povezave WiFi...");
autoConnectToWiFi();
}
}
lastWifiCheck = currentTime;
}
}
void connectToWiFi() {
Serial.println("\n=== ROCNO POVEZOVANJE NA WIFI ===");
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.setCursor(120, 100);
tft.println("POVEZUJEM...");
for (int i = 0; i < 3; i++) {
int centerX = tft.width() / 2;
int centerY = 160;
for (int r = 5; r <= 15; r += 5) {
uint16_t color = blendColor(rgbTo565(255, 200, 50), rgbTo565(30, 30, 50), r * 3);
for (int a = 225 + i * 30; a <= 315 + i * 30; a += 15) {
int px = centerX + (r * cos(a * 3.14159 / 180));
int py = centerY + (r * sin(a * 3.14159 / 180));
tft.drawPixel(px, py, color);
}
}
tft.fillCircle(centerX, centerY, 3, rgbTo565(255, 200, 50));
delay(300);
}
tft.setCursor(80, 200);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.setTextSize(1);
tft.println("Iskanje omrezij in povezovanje...");
currentState = STATE_WIFI_CONNECTING;
autoConnecting = true;
lastWifiCheck = millis();
uint8_t status = wifiMulti.run();
if (status == WL_CONNECTED) {
wifiConnected = true;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
autoConnecting = false;
WiFi.setChannel(1);
showWiFiConnectedScreen();
currentState = STATE_WIFI_CONNECTED;
}
}
void handleWiFiConnecting() {
static int dotCount = 0;
static unsigned long lastDotTime = 0;
unsigned long currentTime = millis();
if (currentState != STATE_WIFI_CONNECTING && !autoConnecting) {
return;
}
if (currentTime - lastDotTime > 500) {
tft.fillRect(180, 160, 120, 20, ST77XX_BLACK);
tft.setCursor(180, 160);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.setTextSize(1);
tft.print("Povezujem");
for (int i = 0; i < dotCount; i++) {
tft.print(".");
}
dotCount = (dotCount + 1) % 4;
lastDotTime = currentTime;
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnected = true;
autoConnecting = false;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
WiFi.setChannel(1);
drawStatusBar();
if (currentState == STATE_WIFI_CONNECTING) {
showWiFiConnectedScreen();
currentState = STATE_WIFI_CONNECTED;
}
if (!ntpSynchronized) {
syncTimeWithNTP();
}
}
if (currentTime - lastWifiCheck > 60000) {
if (currentState == STATE_WIFI_CONNECTING) {
autoConnecting = false;
showWiFiFailedScreen();
currentState = STATE_MAIN_MENU;
}
}
}
void showWiFiConnectedScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(80, 220, 100));
tft.setCursor(120, 30);
tft.println("WI-FI POVEZAN");
tft.drawRoundRect(20, 50, 440, 150, 10, rgbTo565(0, 150, 255));
tft.fillRoundRect(21, 51, 438, 148, 10, rgbTo565(20, 40, 70));
tft.setTextSize(1);
tft.setCursor(30, 65);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Omrezje: ");
tft.setTextColor(rgbTo565(150, 255, 150));
tft.println(wifiSSID);
tft.setCursor(30, 85);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("IP naslov: ");
tft.setTextColor(rgbTo565(150, 200, 255));
tft.println(wifiIP);
tft.setCursor(30, 105);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Jakost signala: ");
tft.setTextColor(rgbTo565(80, 220, 100));
tft.print(wifiRSSI);
tft.println(" dBm");
tft.setCursor(30, 125);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("MAC naslov: ");
tft.setTextColor(rgbTo565(150, 200, 255));
tft.println(WiFi.macAddress());
tft.setCursor(30, 145);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Kanal: ");
tft.setTextColor(rgbTo565(255, 255, 100));
tft.println(WiFi.channel());
drawIconButton(50, 210, 180, 40, rgbTo565(66, 135, 245), "TEST PING", "wifi");
drawIconButton(250, 210, 180, 40, rgbTo565(76, 175, 80), "SCAN OMREZIJ", "display");
drawIconButton(50, 260, 180, 40, rgbTo565(245, 67, 54), "NAZAJ", "info");
drawIconButton(250, 260, 180, 40, rgbTo565(156, 39, 176), "TEST POVEZAVE", "touch");
}
void showWiFiFailedScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(255, 80, 80));
tft.setCursor(120, 100);
tft.println("NI OMREZJA!");
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.setCursor(80, 140);
tft.println("Preverite WiFi nastavitve");
tft.setCursor(100, 160);
tft.println("in poskusite ponovno.");
drawIconButton(150, 200, 180, 40, rgbTo565(66, 135, 245), "NAZAJ", "info");
bool waiting = true;
unsigned long startTime = millis();
while (waiting && (millis() - startTime < 10000)) {
handleTouch();
if (ts.touched()) {
TS_Point p = ts.getPoint();
int x = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int y = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
if (x > 150 && x < 330 && y > 200 && y < 240) {
waiting = false;
}
}
delay(10);
}
showMainMenu();
}
void handleWiFiConnectedTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
if (x > 50 && x < 230 && y > 260 && y < 300) {
showMainMenu();
}
if (x > 250 && x < 430 && y > 210 && y < 250) {
scanWiFiNetworks();
}
}
void scanWiFiNetworks() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(0, 200, 255));
tft.setCursor(140, 40);
tft.println("WI-FI SCAN");
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.setCursor(80, 80);
tft.println("Iskanje omrezij...");
for (int i = 0; i < 3; i++) {
int centerX = tft.width() / 2;
int centerY = 120;
for (int r = 5; r <= 15; r += 5) {
uint16_t color = blendColor(rgbTo565(255, 200, 50), rgbTo565(30, 30, 50), r * 3);
for (int a = 225 + i * 30; a <= 315 + i * 30; a += 15) {
int px = centerX + (r * cos(a * 3.14159 / 180));
int py = centerY + (r * sin(a * 3.14159 / 180));
tft.drawPixel(px, py, color);
}
}
tft.fillCircle(centerX, centerY, 3, rgbTo565(255, 200, 50));
delay(300);
}
WiFi.scanDelete();
int n = WiFi.scanNetworks();
tft.fillRect(0, 80, 480, 40, ST77XX_BLACK);
tft.fillRect(0, 120, 480, 180, rgbTo565(20, 40, 70));
tft.drawRoundRect(20, 80, 440, 180, 10, rgbTo565(0, 150, 255));
if (n == 0) {
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 80, 80));
tft.setCursor(80, 100);
tft.println("Ni najdenih omrezij!");
} else {
tft.setTextSize(1);
tft.setTextColor(rgbTo565(80, 220, 100));
tft.setCursor(50, 90);
tft.printf("Najdenih: %d omrezij", n);
int maxDisplay = min(n, 5);
for (int i = 0; i < maxDisplay; i++) {
int yPos = 110 + (i * 30);
tft.fillRect(30, yPos - 5, 420, 25, rgbTo565(30, 60, 100));
tft.drawRect(30, yPos - 5, 420, 25, rgbTo565(0, 150, 255));
tft.setCursor(35, yPos);
tft.setTextColor(rgbTo565(220, 240, 255));
tft.printf("%d. %s", i + 1, WiFi.SSID(i).c_str());
tft.setCursor(300, yPos);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.printf("%d dBm", WiFi.RSSI(i));
int strengthBars = map(WiFi.RSSI(i), -100, -50, 0, 4);
strengthBars = constrain(strengthBars, 0, 4);
for (int b = 0; b < 4; b++) {
int barX = 380 + b * 8;
int barHeight = (b < strengthBars) ? (b + 1) * 3 : 3;
uint16_t barColor;
if (b < strengthBars) {
if (strengthBars > 2) barColor = rgbTo565(80, 220, 100);
else if (strengthBars > 1) barColor = rgbTo565(255, 200, 50);
else barColor = rgbTo565(255, 100, 50);
} else {
barColor = rgbTo565(100, 100, 120);
}
tft.fillRect(barX, yPos + 10 - barHeight, 5, barHeight, barColor);
}
tft.setCursor(420, yPos);
if (WiFi.encryptionType(i) == WIFI_AUTH_OPEN) {
tft.setTextColor(rgbTo565(255, 100, 100));
tft.print("ODPRTO");
} else {
tft.setTextColor(rgbTo565(100, 255, 100));
tft.print("ZAKL.");
}
}
}
WiFi.scanDelete();
delay(5000);
showWiFiConnectedScreen();
}
void checkWarnings() {
unsigned long currentTime = millis();
if (module4Active && module4SoilMoisture > 0) {
float soilWarningMin = 20;
float soilWarningMax = 80;
if (module4SoilMoisture < soilWarningMin || module4SoilMoisture > soilWarningMax) {
if (!warnings.soilWarning) {
warnings.soilWarning = true;
warnings.soilWarningStart = currentTime;
warnings.soilValue = module4SoilMoisture;
warnings.soilMin = 20;
warnings.soilMax = 80;
}
} else {
warnings.soilWarning = false;
}
}
if (module4Active && !isnan(module4AirTemp)) {
float tempRange = targetTempMax - targetTempMin;
float tempWarningMin = targetTempMin - (tempRange * WARNING_THRESHOLD_PERCENT / 100.0);
float tempWarningMax = targetTempMax + (tempRange * WARNING_THRESHOLD_PERCENT / 100.0);
if (module4AirTemp < tempWarningMin || module4AirTemp > tempWarningMax) {
if (!warnings.tempWarning) {
warnings.tempWarning = true;
warnings.tempWarningStart = currentTime;
warnings.tempValue = module4AirTemp;
warnings.tempMin = targetTempMin;
warnings.tempMax = targetTempMax;
}
} else {
warnings.tempWarning = false;
}
}
if (module4Active && !isnan(module4AirHum)) {
float humRange = targetHumMax - targetHumMin;
float humWarningMin = targetHumMin - (humRange * WARNING_THRESHOLD_PERCENT / 100.0);
float humWarningMax = targetHumMax + (humRange * WARNING_THRESHOLD_PERCENT / 100.0);
if (module4AirHum < humWarningMin || module4AirHum > humWarningMax) {
if (!warnings.humWarning) {
warnings.humWarning = true;
warnings.humWarningStart = currentTime;
warnings.humValue = module4AirHum;
warnings.humMin = targetHumMin;
warnings.humMax = targetHumMax;
}
} else {
warnings.humWarning = false;
}
}
if (warnings.soilWarning && currentTime - warnings.soilWarningStart > WARNING_DISPLAY_TIME) {
warnings.soilWarning = false;
}
if (warnings.tempWarning && currentTime - warnings.tempWarningStart > WARNING_DISPLAY_TIME) {
warnings.tempWarning = false;
}
if (warnings.humWarning && currentTime - warnings.humWarningStart > WARNING_DISPLAY_TIME) {
warnings.humWarning = false;
}
}
void drawWarningBar() {
if (!warnings.soilWarning && !warnings.tempWarning && !warnings.humWarning) {
return;
}
unsigned long currentTime = millis();
if (currentTime - lastWarningBlink > WARNING_BLINK_INTERVAL) {
warningBlinkState = !warningBlinkState;
lastWarningBlink = currentTime;
}
if (!warningBlinkState) {
return;
}
int barY = 288;
int barHeight = 32;
String warningText = "";
uint16_t warningColor = rgbTo565(255, 0, 0);
if (warnings.soilWarning && warnings.tempWarning && warnings.humWarning) {
warningText = "⚠️ OPOZORILO: Temperatura, vlaga in vlaga tal! ⚠️";
} else if (warnings.soilWarning && warnings.tempWarning) {
warningText = "⚠️ OPOZORILO: Temperatura in vlaga tal! ⚠️";
} else if (warnings.soilWarning && warnings.humWarning) {
warningText = "⚠️ OPOZORILO: Vlaga in vlaga tal! ⚠️";
} else if (warnings.tempWarning && warnings.humWarning) {
warningText = "⚠️ OPOZORILO: Temperatura in vlaga! ⚠️";
} else if (warnings.soilWarning) {
warningText = "⚠️ OPOZORILO: Vlaga tal " + String(warnings.soilValue, 0) + "% (meje: " + String(warnings.soilMin, 0) + "-" + String(warnings.soilMax, 0) + "%) ⚠️";
warningColor = SOIL_COLOR;
} else if (warnings.tempWarning) {
warningText = "⚠️ OPOZORILO: Temperatura " + String(warnings.tempValue, 1) + "°C (meje: " + String(warnings.tempMin, 1) + "-" + String(warnings.tempMax, 1) + "°C) ⚠️";
warningColor = TEMP_COLOR;
} else if (warnings.humWarning) {
warningText = "⚠️ OPOZORILO: Vlaznost " + String(warnings.humValue, 1) + "% (meje: " + String(warnings.humMin, 1) + "-" + String(warnings.humMax, 1) + "%) ⚠️";
warningColor = HUMIDITY_COLOR;
}
if (warningText.length() > 0) {
tft.fillRect(0, barY - 2, tft.width(), barHeight + 4, ST77XX_BLACK);
tft.fillRoundRect(10, barY, tft.width() - 20, barHeight, 8, warningColor);
tft.drawRoundRect(10, barY, tft.width() - 20, barHeight, 8, ST77XX_WHITE);
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds(warningText, 0, 0, &textX, &textY, &textW, &textH);
int textPosX = 10 + ((tft.width() - 20) - textW) / 2 - textX;
int textPosY = barY + (barHeight - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print(warningText);
}
}
bool haveValuesChanged() {
static unsigned long lastCheck = 0;
unsigned long now = millis();
if (now - lastCheck < 1000) {
return false;
}
lastCheck = now;
static float lastTemp = -999, lastHum = -999, lastLux = -999, lastExtLux = -999;
static float lastExtTemp = -999, lastExtHum = -999;
static int lastSoil = -999;
static bool lastModule1 = false, lastModule2 = false, lastModule3 = false;
bool changed = false;
if (abs(lastTemp - internalTemperature) > 0.1) { lastTemp = internalTemperature; changed = true; }
if (abs(lastHum - internalHumidity) > 0.1) { lastHum = internalHumidity; changed = true; }
if (abs(lastLux - ldrInternalLux) > 10) { lastLux = ldrInternalLux; changed = true; }
if (abs(lastExtLux - externalLux) > 5) { lastExtLux = externalLux; changed = true; }
if (abs(lastExtTemp - externalTemperature) > 0.1) { lastExtTemp = externalTemperature; changed = true; }
if (abs(lastExtHum - externalHumidity) > 0.1) { lastExtHum = externalHumidity; changed = true; }
if (lastSoil != soilMoisturePercent) { lastSoil = soilMoisturePercent; changed = true; }
if (lastModule1 != module1Active) { lastModule1 = module1Active; changed = true; }
if (lastModule2 != module2Active) { lastModule2 = module2Active; changed = true; }
if (lastModule3 != module3Active) { lastModule3 = module3Active; changed = true; }
return changed;
}
void handleTouch() {
if (ts.touched()) {
TS_Point p = ts.getPoint();
int x = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int y = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
x = constrain(x, 0, tft.width() - 1);
y = constrain(y, 0, tft.height() - 1);
if (!touchPressed) {
touchPressed = true;
lastX = x;
lastY = y;
switch (currentState) {
case STATE_HOME_SCREEN:
handleHomeScreenTouch(x, y);
break;
case STATE_MAIN_MENU:
handleMainMenuTouch(x, y);
break;
case STATE_WIFI_CONNECTING:
break;
case STATE_WIFI_CONNECTED:
handleWiFiConnectedTouch(x, y);
break;
case STATE_MCP23017_TEST:
handleMCP23017TestTouch(x, y);
break;
case STATE_RTC_INFO:
handleRTCInfoTouch(x, y);
break;
case STATE_INTERNAL_SENSORS_INFO:
handleInternalSensorsTouch(x, y);
break;
case STATE_EXTERNAL_SENSORS_INFO:
handleExternalSensorsTouch(x, y);
break;
case STATE_INTERNAL_GRAPH_48H:
handleInternalGraph48hTouch(x, y);
break;
case STATE_EXTERNAL_GRAPH_48H:
handleExternalGraph48hTouch(x, y);
break;
case STATE_SOIL_MOISTURE_INFO:
handleSoilMoistureTouch(x, y);
break;
case STATE_FLOW_SENSORS_INFO:
handleFlowSensorsTouch(x, y);
break;
case STATE_RELAY_CONTROL:
handleRelayControlTouch(x, y);
break;
case STATE_BOOTING:
break;
case STATE_TEMP_HUM_CONTROL:
handleTempHumControlTouch(x, y);
break;
case STATE_SHADE_CONTROL:
handleShadeControlTouch(x, y);
break;
case STATE_LIGHTING_CONTROL:
handleLightingControlTouch(x, y);
break;
case STATE_EDIT_LIGHTING_BLOCK:
handleEditLightingBlockTouch(x, y);
break;
case STATE_LIGHT_AUTO_CONTROL:
handleLightAutoControlTouch(x, y);
break;
case STATE_EDIT_LIGHT_TIME:
handleEditLightTimeTouch(x, y);
break;
case STATE_VENTILATION_CONTROL:
handleVentilationControlTouch(x, y);
break;
case STATE_SD_MANAGEMENT:
handleSDCardManagementTouch(x, y);
break;
case STATE_MODULE2_INFO:
handleModule2InfoTouch(x, y);
break;
case STATE_TROPICAL_PLANTS:
if (x > 40 && x < 180 && y > 240 && y < 275) {
showAddPlantScreen();
}
else if (x > 200 && x < 340 && y > 240 && y < 275) {
if (plantCount > 0) {
showSelectPlantForViewScreen();
} else {
tft.fillRect(100, 150, 280, 60, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 60, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(130, 170);
tft.println("Ni dodanih rastlin!");
tft.setCursor(110, 190);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Dodajte novo rastlino");
delay(2000);
showTropicalPlantsMenu();
}
}
else if (x > 40 && x < 180 && y > 285 && y < 320) {
if (plantCount > 0) {
showSelectPlantForAdviceScreen();
} else {
tft.fillRect(100, 150, 280, 60, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 60, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(140, 170);
tft.println("Ni rastlin!");
tft.setCursor(120, 190);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Dodajte vsaj eno");
delay(2000);
showTropicalPlantsMenu();
}
}
else if (x > 200 && x < 340 && y > 285 && y < 320) {
showMainMenu();
}
break;
case STATE_SELECT_PLANT_FOR_VIEW:
{
int startY = 95;
int buttonHeight = 28;
int buttonSpacing = 4;
int maxButtons = 5;
for (int i = 0; i < min(plantCount, maxButtons); i++) {
int yPos = startY + i * (buttonHeight + buttonSpacing);
if (x > 30 && x < 450 && y > yPos && y < yPos + buttonHeight) {
showPlantDetailsScreen(i);
return;
}
}
int buttonY = startY + maxButtons * (buttonHeight + buttonSpacing) + 15;
if (x > 30 && x < 230 && y > buttonY && y < buttonY + 30) {
tft.fillRect(100, 150, 280, 80, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 80, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 255, 255));
tft.setCursor(130, 165);
tft.println("Za katero rastlino?");
int optionY = 190;
for (int i = 0; i < min(3, plantCount); i++) {
tft.fillRoundRect(120, optionY + i * 25, 240, 22, 5, rgbTo565(66, 135, 245));
tft.drawRoundRect(120, optionY + i * 25, 240, 22, 5, ST77XX_WHITE);
String optionText = String(i + 1) + ". " + myPlants[i].name;
if (optionText.length() > 20) optionText = optionText.substring(0, 17) + "...";
tft.setCursor(130, optionY + i * 25 + 5);
tft.setTextColor(ST77XX_WHITE);
tft.print(optionText);
}
unsigned long waitStart = millis();
bool waiting = true;
while (waiting && millis() - waitStart < 8000) {
if (ts.touched()) {
TS_Point p = ts.getPoint();
int touchX = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int touchY = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
for (int i = 0; i < min(3, plantCount); i++) {
if (touchX > 120 && touchX < 360 && touchY > optionY + i * 25 && touchY < optionY + i * 25 + 22) {
int speciesIndex = findPlantSpeciesIndex(myPlants[i].species);
if (speciesIndex >= 0) {
autoConfigureForPlant(speciesIndex);
showMessage("Nastavitve za " + myPlants[i].name, rgbTo565(80, 220, 100));
}
waiting = false;
break;
}
}
}
delay(10);
}
showSelectPlantForViewScreen();
return;
}
if (x > 250 && x < 450 && y > buttonY && y < buttonY + 30) {
showTropicalPlantsMenu();
return;
}
break;
}
case STATE_SELECT_PLANT_FOR_ADVICE:
{
int startY = 85;
int buttonHeight = 35;
int buttonSpacing = 5;
int maxButtons = 5;
for (int i = 0; i < min(plantCount, maxButtons); i++) {
int yPos = startY + i * (buttonHeight + buttonSpacing);
if (x > 30 && x < 450 && y > yPos && y < yPos + buttonHeight) {
String advice = getPlantCareAdvice(i);
showAdvicePopup(advice);
return;
}
}
int buttonY = startY + maxButtons * (buttonHeight + buttonSpacing) + 10;
if (x > 150 && x < 330 && y > buttonY && y < buttonY + 35) {
showTropicalPlantsMenu();
}
break;
}
case STATE_ADD_PLANT:
if (x > 30 && x < 75 && y > 160 && y < 185) {
tempSpeciesIndex = (tempSpeciesIndex - 1 + tropicalSpeciesCount) % tropicalSpeciesCount;
showAddPlantScreen();
}
else if (x > 90 && x < 135 && y > 160 && y < 185) {
tempSpeciesIndex = (tempSpeciesIndex + 1) % tropicalSpeciesCount;
showAddPlantScreen();
}
else if (x > 130 && x < 200 && y > 195 && y < 220) {
tempIsVariegated = !tempIsVariegated;
showAddPlantScreen();
}
else if (x > 250 && x < 295 && y > 115 && y < 140) {
tempLocationZone = max(1, tempLocationZone - 1);
showAddPlantScreen();
}
else if (x > 310 && x < 355 && y > 115 && y < 140) {
tempLocationZone = min(3, tempLocationZone + 1);
showAddPlantScreen();
}
else if (x > 40 && x < 180 && y > 265 && y < 300) {
if (tempPlantName.length() == 0) {
tempPlantName = "Rastlina " + String(plantCount + 1);
}
addPlantToCollection(tempPlantName, tropicalSpecies[tempSpeciesIndex].species,
tempIsVariegated, "cona" + String(tempLocationZone));
tempPlantName = "";
tempSpeciesIndex = 0;
tempIsVariegated = false;
tempLocationZone = 1;
showTropicalPlantsMenu();
}
else if (x > 200 && x < 340 && y > 265 && y < 300) {
tempPlantName = "";
tempSpeciesIndex = 0;
tempIsVariegated = false;
tempLocationZone = 1;
showTropicalPlantsMenu();
}
else if (x > 360 && x < 500 && y > 265 && y < 300) {
if (tempPlantName.length() == 0) {
tempPlantName = "Moja " + tropicalSpecies[tempSpeciesIndex].commonName;
} else {
tempPlantName = "";
}
showAddPlantScreen();
}
break;
case STATE_PLANT_DETAILS:
{
int buttonWidth = 105;
int buttonSpacing = 10;
int buttonY = 260;
if (x > 15 && x < 15 + buttonWidth && y > buttonY && y < buttonY + 35) {
startMistingForPlant(selectedPlantId);
showPlantDetailsScreen(selectedPlantId);
}
else if (x > 15 + buttonWidth + buttonSpacing && x < 15 + 2 * buttonWidth + buttonSpacing && y > buttonY && y < buttonY + 35) {
String advice = getPlantCareAdvice(selectedPlantId);
showAdvicePopup(advice);
}
else if (x > 15 + 2 * (buttonWidth + buttonSpacing) && x < 15 + 3 * buttonWidth + 2 * buttonSpacing && y > buttonY && y < buttonY + 35) {
showSelectPlantForViewScreen();
}
else if (x > 15 + 3 * (buttonWidth + buttonSpacing) && x < 15 + 4 * buttonWidth + 3 * buttonSpacing && y > buttonY && y < buttonY + 35) {
deletePlant(selectedPlantId);
}
}
break;
case STATE_YEAR_ARCHIVE:
if (x > 150 && x < 330 && y > 270 && y < 305) {
showSoilMoistureInfo();
}
break;
case STATE_SOIL_MOISTURE_GRAPH:
if (x > 40 && x < 220 && y > 295 && y < 330) {
showYearArchiveScreen();
}
else if (x > 260 && x < 440 && y > 295 && y < 330) {
showSoilMoistureInfo();
}
break;
case STATE_AIR_QUALITY:
if (x > 150 && x < 330 && y > 260 && y < 295) {
showInternalSensorsInfo();
}
break;
case STATE_MODULE4_INFO:
handleModule4InfoTouch(x, y);
break;
case STATE_LDR_CALIBRATION:
handleLDRCalibrationTouch(x, y);
break;
default:
break;
}
} else if (abs(x - lastX) > 2 || abs(y - lastY) > 2) {
lastX = x;
lastY = y;
}
} else {
if (touchPressed) {
touchPressed = false;
}
}
}
void showMCP23017Test() {
tft.fillScreen(ST77XX_BLACK);
tft.fillRect(0, 0, tft.width(), tft.height(), ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(rgbTo565(0, 200, 255));
tft.setCursor(150, 10);
tft.println("MCP23017 TEST");
int frameHeight = 140;
int frameY = 30;
tft.drawRoundRect(20, frameY, 440, frameHeight, 10, rgbTo565(0, 150, 255));
tft.fillRoundRect(21, frameY + 1, 438, frameHeight - 1, 10, rgbTo565(20, 40, 70));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(220, 240, 255));
tft.setCursor(40, frameY + 10);
tft.println("RELEJI (pin 0-7):");
tft.setCursor(260, frameY + 10);
tft.println("VHODI (pin 8-15):");
if (!mcpInitialized) {
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 80, 80));
tft.setCursor(100, frameY + 40);
tft.println("MCP23017 NI INICIALIZIRAN!");
tft.setCursor(80, frameY + 60);
tft.println("Preverite povezave in naslov.");
} else {
drawAllMCPPins();
}
lastActivePinsCount = -1;
int buttonY1 = 180;
int buttonY2 = 230;
int buttonWidth = 180;
int buttonHeight = 40;
drawIconButton(50, buttonY1, buttonWidth, buttonHeight, rgbTo565(66, 135, 245), "PREBERI PINOVE", "wifi");
drawIconButton(250, buttonY1, buttonWidth, buttonHeight, rgbTo565(76, 175, 80), "TEST RELEJEV", "display");
drawIconButton(50, buttonY2, buttonWidth, buttonHeight, rgbTo565(245, 67, 54), "NAZAJ", "info");
drawIconButton(250, buttonY2, buttonWidth, buttonHeight, rgbTo565(156, 39, 176), "PONOVNA INIC.", "touch");
currentState = STATE_MCP23017_TEST;
}
void drawAllMCPPins() {
if (!mcpInitialized) return;
uint16_t currentState = mcp.readGPIOAB();
int boxSize = 35;
int startX = 40;
int startY = 60;
for (int i = 0; i < 8; i++) {
bool state = (currentState >> i) & 1;
int x = startX + (i % 4) * 50;
int y = startY + (i / 4) * 45;
bool relayOn = !state;
tft.fillRect(x, y, boxSize, boxSize, relayOn ? RELAY_ON_COLOR : RELAY_OFF_COLOR);
tft.drawRect(x, y, boxSize, boxSize, ST77XX_WHITE);
tft.setCursor(x + 12, y + 8);
tft.setTextColor(ST77XX_WHITE);
tft.print(i);
tft.setCursor(x + 14, y + 20);
tft.setTextColor(relayOn ? ST77XX_BLACK : ST77XX_WHITE);
tft.print(relayOn ? "ON" : "OFF");
}
for (int i = 8; i < 16; i++) {
bool state = (currentState >> i) & 1;
int x = 260 + ((i - 8) % 4) * 50;
int y = startY + ((i - 8) / 4) * 45;
tft.fillRect(x, y, boxSize, boxSize, state ? rgbTo565(60, 60, 80) : rgbTo565(100, 200, 100));
tft.drawRect(x, y, boxSize, boxSize, ST77XX_WHITE);
tft.setCursor(x + 10, y + 8);
tft.setTextColor(ST77XX_WHITE);
tft.print(i);
tft.setCursor(x + 14, y + 20);
tft.setTextColor(state ? rgbTo565(200, 200, 200) : ST77XX_BLACK);
tft.print(state ? "H" : "L");
}
updateActivePinsCount(currentState);
}
void displayMCP23017Pins() {
if (!mcpInitialized) return;
uint16_t currentState = mcp.readGPIOAB();
int boxSize = 35;
int startX = 40;
int startY = 60;
for (int i = 0; i < 8; i++) {
bool state = (currentState >> i) & 1;
int x = startX + (i % 4) * 50;
int y = startY + (i / 4) * 45;
bool relayOn = !state;
tft.fillRect(x, y, boxSize, boxSize, relayOn ? RELAY_ON_COLOR : RELAY_OFF_COLOR);
tft.drawRect(x, y, boxSize, boxSize, ST77XX_WHITE);
tft.setCursor(x + 12, y + 8);
tft.setTextColor(ST77XX_WHITE);
tft.print(i);
tft.setCursor(x + 14, y + 20);
tft.setTextColor(relayOn ? ST77XX_BLACK : ST77XX_WHITE);
tft.print(relayOn ? "ON" : "OFF");
}
for (int i = 8; i < 16; i++) {
bool state = (currentState >> i) & 1;
int x = 260 + ((i - 8) % 4) * 50;
int y = startY + ((i - 8) / 4) * 45;
tft.fillRect(x, y, boxSize, boxSize, state ? rgbTo565(60, 60, 80) : rgbTo565(100, 200, 100));
tft.drawRect(x, y, boxSize, boxSize, ST77XX_WHITE);
tft.setCursor(x + 10, y + 8);
tft.setTextColor(ST77XX_WHITE);
tft.print(i);
tft.setCursor(x + 14, y + 20);
tft.setTextColor(state ? rgbTo565(200, 200, 200) : ST77XX_BLACK);
tft.print(state ? "H" : "L");
}
updateActivePinsCount(currentState);
}
void updateActivePinsCount(uint16_t gpioState) {
int activePins = 0;
for (int i = 8; i < 16; i++) {
if (!((gpioState >> i) & 1)) activePins++;
}
int frameY = 30;
int frameHeight = 140;
tft.fillRect(40, frameY + frameHeight - 15, 200, 15, rgbTo565(20, 40, 70));
tft.setCursor(40, frameY + frameHeight - 15);
tft.setTextColor(rgbTo565(220, 240, 255));
tft.print("Aktivni vhodi (LOW): ");
tft.setTextColor(rgbTo565(255, 200, 50));
tft.print(activePins);
tft.print("/8");
}
void handleMCP23017Test() {
if (mcpInitialized && currentState == STATE_MCP23017_TEST) {
static unsigned long lastUpdate = 0;
unsigned long currentTime = millis();
if (currentTime - lastUpdate > MCP_DISPLAY_MIN_INTERVAL) {
displayMCP23017Pins();
lastUpdate = currentTime;
}
}
}
void handleMCP23017TestTouch(int x, int y) {
int boxSize = 35;
int startX = 40;
int startY = 60;
for (int i = 0; i < 8; i++) {
int relX = startX + (i % 4) * 50;
int relY = startY + (i / 4) * 45;
if (x > relX && x < relX + boxSize && y > relY && y < relY + boxSize) {
toggleRelay(i);
displayMCP23017Pins();
return;
}
}
if (x > 50 && x < 230 && y > 230 && y < 270) {
showMainMenu();
}
if (x > 250 && x < 430 && y > 230 && y < 270) {
initializeMCP23017();
initializeRelays();
drawAllMCPPins();
}
}
void showInternalSensorsInfo() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(INTERNAL_COLOR);
tft.setCursor(150, 30);
tft.println("VITRINA (MODUL 4)");
tft.drawRoundRect(10, 45, 460, 210, 10, INTERNAL_COLOR);
tft.fillRoundRect(11, 46, 458, 208, 10, rgbTo565(40, 20, 50));
int leftColumnX = 30;
int rightColumnX = 260;
int yOffset = 60;
int lineHeight = 18;
tft.setTextSize(1);
tft.setCursor(leftColumnX, yOffset);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Modul 4: ");
tft.setTextColor(module4Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(module4Active ? "AKTIVEN" : "NEDOSEGLJIV");
if (!module4Active) {
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.println("Modul 4 ni dosegljiv!");
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Preverite povezavo in napajanje");
// Gumb NAZAJ
drawMenuButton(150, 260, 180, 40, rgbTo565(245, 67, 54), "NAZAJ");
} else {
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Temp. zraka: ");
tft.setTextColor(TEMP_COLOR);
tft.printf("%.1f °C", module4AirTemp);
int tempBarWidth = map(constrain(module4AirTemp, 0, 50), 0, 50, 0, 180);
tft.fillRect(leftColumnX, yOffset + 2 * lineHeight + 2, 180, 10, rgbTo565(80, 60, 60));
tft.fillRect(leftColumnX, yOffset + 2 * lineHeight + 2, tempBarWidth, 10, TEMP_COLOR);
tft.drawRect(leftColumnX, yOffset + 2 * lineHeight + 2, 180, 10, ST77XX_WHITE);
tft.setCursor(leftColumnX, yOffset + 3 * lineHeight + 8);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Vlaga zraka: ");
tft.setTextColor(HUMIDITY_COLOR);
tft.printf("%.1f %%", module4AirHum);
int humidBarWidth = map(constrain(module4AirHum, 0, 100), 0, 100, 0, 180);
tft.fillRect(leftColumnX, yOffset + 4 * lineHeight + 10, 180, 10, rgbTo565(60, 60, 80));
tft.fillRect(leftColumnX, yOffset + 4 * lineHeight + 10, humidBarWidth, 10, HUMIDITY_COLOR);
tft.drawRect(leftColumnX, yOffset + 4 * lineHeight + 10, 180, 10, ST77XX_WHITE);
tft.setCursor(leftColumnX, yOffset + 5 * lineHeight + 18);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Svetloba: ");
tft.setTextColor(LIGHT_COLOR);
tft.printf("%d %%", module4LightPercent);
int lightBarWidth = map(module4LightPercent, 0, 100, 0, 180);
tft.fillRect(leftColumnX, yOffset + 6 * lineHeight + 20, 180, 10, rgbTo565(60, 60, 60));
tft.fillRect(leftColumnX, yOffset + 6 * lineHeight + 20, lightBarWidth, 10, LIGHT_COLOR);
tft.drawRect(leftColumnX, yOffset + 6 * lineHeight + 20, 180, 10, ST77XX_WHITE);
tft.setCursor(rightColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Temp. zemlje: ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%.1f °C", module4SoilTemp);
int soilTempBarWidth = map(constrain(module4SoilTemp, 0, 40), 0, 40, 0, 180);
tft.fillRect(rightColumnX, yOffset + 2 * lineHeight + 2, 180, 10, rgbTo565(60, 60, 60));
tft.fillRect(rightColumnX, yOffset + 2 * lineHeight + 2, soilTempBarWidth, 10, rgbTo565(255, 150, 100));
tft.drawRect(rightColumnX, yOffset + 2 * lineHeight + 2, 180, 10, ST77XX_WHITE);
tft.setCursor(rightColumnX, yOffset + 3 * lineHeight + 8);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Vlaga tal: ");
uint16_t soilColor;
if (module4SoilMoisture < 30) soilColor = rgbTo565(255, 100, 100);
else if (module4SoilMoisture < 60) soilColor = rgbTo565(255, 200, 50);
else soilColor = rgbTo565(100, 150, 255);
tft.setTextColor(soilColor);
tft.printf("%d %%", module4SoilMoisture);
int soilBarWidth = map(module4SoilMoisture, 0, 100, 0, 180);
tft.fillRect(rightColumnX, yOffset + 4 * lineHeight + 10, 180, 10, rgbTo565(60, 60, 60));
tft.fillRect(rightColumnX, yOffset + 4 * lineHeight + 10, soilBarWidth, 10, soilColor);
tft.drawRect(rightColumnX, yOffset + 4 * lineHeight + 10, 180, 10, ST77XX_WHITE);
tft.setCursor(rightColumnX, yOffset + 5 * lineHeight + 18);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Zracni tlak: ");
tft.setTextColor(PRESSURE_COLOR);
tft.printf("%.1f hPa", module4Pressure);
tft.setCursor(rightColumnX, yOffset + 6 * lineHeight + 20);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("TVOC: ");
uint16_t tvocColor = (module4TVOC < 400) ? rgbTo565(80, 220, 100) :
(module4TVOC < 800) ? rgbTo565(255, 200, 50) : rgbTo565(255, 80, 80);
tft.setTextColor(tvocColor);
tft.printf("%.0f ppb", module4TVOC);
}
// ========== 4 GUMBI: NAZAJ, GRAFI, KAK. ZRAKA, OSVEZI ==========
int buttonY = 270;
int buttonWidth = 100;
int buttonHeight = 38;
int buttonSpacing = 10;
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2; // = 25
// Gumb 1: NAZAJ (x = 25)
drawMenuButton(startX, buttonY, buttonWidth, buttonHeight, rgbTo565(245, 67, 54), "NAZAJ");
// Gumb 2: GRAFI (x = 135)
drawMenuButton(startX + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "GRAFI");
// Gumb 3: KAK. ZRAKA (x = 245)
drawMenuButton(startX + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(66, 135, 245), "KAK. ZRAKA");
// Gumb 4: OSVEZI (x = 355)
drawMenuButton(startX + 3 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(100, 200, 100), "OSVEZI");
currentState = STATE_INTERNAL_SENSORS_INFO;
}
void handleInternalSensorsTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
// ========== ISTE KOORDINATE KOT PRI RISANJU ==========
int buttonY = 270;
int buttonWidth = 100;
int buttonHeight = 38;
int buttonSpacing = 10;
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2; // = 25
// Gumb 1: NAZAJ (x = 25 do 125)
if (x > startX && x < startX + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → NAZAJ - nazaj na GLAVNI MENI");
showMainMenu();
return;
}
// Gumb 2: GRAFI (x = 135 do 235)
if (x > startX + buttonWidth + buttonSpacing &&
x < startX + 2 * buttonWidth + buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → GRAFI - 48-urni graf");
showInternalGraph48hScreen();
return;
}
// Gumb 3: KAK. ZRAKA (x = 245 do 345)
if (x > startX + 2 * (buttonWidth + buttonSpacing) &&
x < startX + 3 * buttonWidth + 2 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → KAKOVOST ZRAKA");
showAirQualityScreen();
return;
}
// Gumb 4: OSVEZI (x = 355 do 455)
if (x > startX + 3 * (buttonWidth + buttonSpacing) &&
x < startX + 4 * buttonWidth + 3 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → OSVEZI");
showInternalSensorsInfo();
return;
}
}
void handleInternalGraph48hTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int buttonWidth = 120;
int buttonHeight = 30;
int graphY = 60;
int graphHeight = 160;
int legendY = graphY + graphHeight + 20;
int buttonY = legendY + 35;
if (x > 40 && x < 40 + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
updateGraphHistory();
showInternalGraph48hScreen();
return;
}
if (x > 180 && x < 180 + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
showInternalSensorsInfo();
return;
}
}
void showFlowSensorsInfo() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(200, 100, 200));
tft.setCursor(150, 30);
tft.println("FLOW SENZORJI");
tft.drawRoundRect(20, 50, 440, 180, 10, rgbTo565(200, 100, 200));
tft.fillRoundRect(21, 51, 438, 178, 10, rgbTo565(40, 20, 40));
int leftColumnX = 30;
int middleColumnX = 170;
int rightColumnX = 310;
int yOffset = 70;
int lineHeight = 15;
tft.setTextSize(1);
tft.setCursor(leftColumnX, yOffset);
tft.setTextColor(FLOW_COLOR_1);
tft.println("FLOW 1:");
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Pretok: ");
tft.setTextColor(FLOW_COLOR_1);
tft.print(String(flowRate1, 2));
tft.println(" L/min");
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Skupaj: ");
tft.setTextColor(FLOW_COLOR_1);
tft.print(String(totalFlow1, 2));
tft.println(" L");
int flowBarWidth1 = map(constrain(flowRate1, 0, 10), 0, 10, 0, 120);
tft.fillRect(leftColumnX, yOffset + 3 * lineHeight, 120, 10, rgbTo565(80, 40, 40));
tft.fillRect(leftColumnX, yOffset + 3 * lineHeight, flowBarWidth1, 10, FLOW_COLOR_1);
tft.drawRect(leftColumnX, yOffset + 3 * lineHeight, 120, 10, ST77XX_WHITE);
tft.setCursor(middleColumnX, yOffset);
tft.setTextColor(FLOW_COLOR_2);
tft.println("FLOW 2:");
tft.setCursor(middleColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Pretok: ");
tft.setTextColor(FLOW_COLOR_2);
tft.print(String(flowRate2, 2));
tft.println(" L/min");
tft.setCursor(middleColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Skupaj: ");
tft.setTextColor(FLOW_COLOR_2);
tft.print(String(totalFlow2, 2));
tft.println(" L");
int flowBarWidth2 = map(constrain(flowRate2, 0, 10), 0, 10, 0, 120);
tft.fillRect(middleColumnX, yOffset + 3 * lineHeight, 120, 10, rgbTo565(40, 80, 40));
tft.fillRect(middleColumnX, yOffset + 3 * lineHeight, flowBarWidth2, 10, FLOW_COLOR_2);
tft.drawRect(middleColumnX, yOffset + 3 * lineHeight, 120, 10, ST77XX_WHITE);
tft.setCursor(rightColumnX, yOffset);
tft.setTextColor(FLOW_COLOR_3);
tft.println("FLOW 3:");
tft.setCursor(rightColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Pretok: ");
tft.setTextColor(FLOW_COLOR_3);
tft.print(String(flowRate3, 2));
tft.println(" L/min");
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Skupaj: ");
tft.setTextColor(FLOW_COLOR_3);
tft.print(String(totalFlow3, 2));
tft.println(" L");
int flowBarWidth3 = map(constrain(flowRate3, 0, 10), 0, 10, 0, 120);
tft.fillRect(rightColumnX, yOffset + 3 * lineHeight, 120, 10, rgbTo565(40, 40, 80));
tft.fillRect(rightColumnX, yOffset + 3 * lineHeight, flowBarWidth3, 10, FLOW_COLOR_3);
tft.drawRect(rightColumnX, yOffset + 3 * lineHeight, 120, 10, ST77XX_WHITE);
tft.setCursor(leftColumnX, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Pulses/L: ");
tft.setCursor(leftColumnX + 60, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(255, 255, 150));
tft.print(PULSES_PER_LITER);
tft.setCursor(middleColumnX, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Skupaj vseh: ");
tft.setCursor(middleColumnX + 70, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(255, 255, 150));
tft.print(String(totalFlow1 + totalFlow2 + totalFlow3, 2));
tft.println(" L");
tft.setCursor(rightColumnX, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Status: ");
tft.setCursor(rightColumnX + 50, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(80, 220, 100));
tft.println("AKTIVEN");
tft.setCursor(leftColumnX, yOffset + 7 * lineHeight);
tft.fillRect(leftColumnX, yOffset + 6 * lineHeight, 10, 10, FLOW_COLOR_1);
tft.setCursor(leftColumnX + 15, yOffset + 7 * lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("Flow 1");
tft.setCursor(middleColumnX, yOffset + 7 * lineHeight);
tft.fillRect(middleColumnX, yOffset + 6 * lineHeight, 10, 10, FLOW_COLOR_2);
tft.setCursor(middleColumnX + 15, yOffset + 7 * lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("Flow 2");
tft.setCursor(rightColumnX, yOffset + 7 * lineHeight);
tft.fillRect(rightColumnX, yOffset + 6 * lineHeight, 10, 10, FLOW_COLOR_3);
tft.setCursor(rightColumnX + 15, yOffset + 7 * lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("Flow 3");
drawIconButton(50, 240, 180, 40, rgbTo565(200, 100, 200), "OSVEZI", "info");
drawIconButton(250, 240, 180, 40, rgbTo565(76, 175, 80), "GRAF", "display");
drawIconButton(50, 290, 180, 40, rgbTo565(245, 67, 54), "NAZAJ", "info");
drawIconButton(250, 290, 180, 40, rgbTo565(156, 39, 176), "RESETIRAJ", "touch");
currentState = STATE_FLOW_SENSORS_INFO;
}
void handleFlowSensorsTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
if (x > 50 && x < 230 && y > 240 && y < 280) {
updateFlowSensors();
showFlowSensorsInfo();
}
if (x > 250 && x < 430 && y > 240 && y < 280) {
showFlowGraphScreen();
}
if (x > 50 && x < 230 && y > 290 && y < 330) {
showMainMenu();
}
if (x > 250 && x < 430 && y > 290 && y < 330) {
resetFlowTotals();
showFlowSensorsInfo();
}
}
void showFlowGraphScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(200, 100, 200));
tft.setCursor(160, 30);
tft.println("FLOW GRAF");
tft.drawRoundRect(40, 60, 400, 150, 10, rgbTo565(200, 100, 200));
tft.fillRoundRect(41, 61, 398, 148, 10, rgbTo565(40, 20, 40));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(200, 200, 255));
for (int i = 0; i <= 10; i += 2) {
int y = 190 - (i * 12);
tft.setCursor(20, y - 5);
tft.print(i);
tft.setCursor(30, y);
tft.print("-");
}
tft.setCursor(60, 220);
tft.print("Cas (zadnjih 10 meritev)");
int prevX = 60;
int prevFlow1Y = 190 - (int)(flowRate1 * 12);
int prevFlow2Y = 190 - (int)(flowRate2 * 12);
int prevFlow3Y = 190 - (int)(flowRate3 * 12);
for (int i = 1; i <= 10; i++) {
int x = 60 + (i * 30);
int flow1Y = 190 - (int)((flowRate1 + random(-0.5, 0.5)) * 12);
int flow2Y = 190 - (int)((flowRate2 + random(-0.5, 0.5)) * 12);
int flow3Y = 190 - (int)((flowRate3 + random(-0.5, 0.5)) * 12);
tft.drawLine(prevX, prevFlow1Y, x, flow1Y, FLOW_COLOR_1);
tft.drawLine(prevX, prevFlow2Y, x, flow2Y, FLOW_COLOR_2);
tft.drawLine(prevX, prevFlow3Y, x, flow3Y, FLOW_COLOR_3);
tft.fillCircle(x, flow1Y, 2, FLOW_COLOR_1);
tft.fillCircle(x, flow2Y, 2, FLOW_COLOR_2);
tft.fillCircle(x, flow3Y, 2, FLOW_COLOR_3);
prevX = x;
prevFlow1Y = flow1Y;
prevFlow2Y = flow2Y;
prevFlow3Y = flow3Y;
}
tft.fillRect(60, 240, 10, 10, FLOW_COLOR_1);
tft.setCursor(75, 240);
tft.setTextColor(rgbTo565(200, 200, 255));
tft.print("Flow 1");
tft.fillRect(200, 240, 10, 10, FLOW_COLOR_2);
tft.setCursor(215, 240);
tft.print("Flow 2");
tft.fillRect(340, 240, 10, 10, FLOW_COLOR_3);
tft.setCursor(355, 240);
tft.print("Flow 3");
drawIconButton(180, 270, 120, 40, rgbTo565(245, 67, 54), "NAZAJ", "info");
unsigned long graphStartTime = millis();
while (millis() - graphStartTime < 10000) {
handleTouch();
if (ts.touched()) {
TS_Point p = ts.getPoint();
int touchX = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int touchY = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
if (touchX > 180 && touchX < 300 && touchY > 270 && touchY < 310) {
break;
}
}
delay(10);
}
showFlowSensorsInfo();
}
void showSoilMoistureInfo() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(SOIL_COLOR);
tft.setCursor(150, 30);
tft.println("VLAGA TAL (MODUL 4)");
tft.drawRoundRect(10, 45, 460, 200, 10, SOIL_COLOR);
tft.fillRoundRect(11, 46, 458, 198, 10, rgbTo565(40, 20, 50));
int leftColumnX = 30;
int rightColumnX = 260;
int yOffset = 60;
int lineHeight = 18;
tft.setTextSize(1);
tft.setCursor(leftColumnX, yOffset);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Modul 4: ");
tft.setTextColor(module4Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(module4Active ? "AKTIVEN" : "NEDOSEGLJIV");
if (!module4Active) {
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.println("Modul 4 ni dosegljiv!");
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Preverite povezavo in napajanje");
// Gumb NAZAJ ko modul ni aktiven
drawMenuButton(150, 260, 180, 40, rgbTo565(245, 67, 54), "NAZAJ");
} else {
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Temp. zemlje: ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%.1f °C", module4SoilTemp);
tft.setCursor(leftColumnX, yOffset + 2 * lineHeight + 5);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Vlaga tal: ");
uint16_t soilColor;
if (module4SoilMoisture < 30) soilColor = rgbTo565(255, 100, 100);
else if (module4SoilMoisture < 60) soilColor = rgbTo565(255, 200, 50);
else soilColor = rgbTo565(100, 150, 255);
tft.setTextColor(soilColor);
tft.printf("%d %%", module4SoilMoisture);
int moistureBarWidth = map(constrain(module4SoilMoisture, 0, 100), 0, 100, 0, 180);
tft.fillRect(leftColumnX, yOffset + 3 * lineHeight + 7, 180, 15, rgbTo565(60, 60, 80));
tft.fillRect(leftColumnX, yOffset + 3 * lineHeight + 7, moistureBarWidth, 15, soilColor);
tft.drawRect(leftColumnX, yOffset + 3 * lineHeight + 7, 180, 15, ST77XX_WHITE);
tft.setCursor(rightColumnX, yOffset);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.println("Interpretacija:");
tft.setCursor(rightColumnX, yOffset + lineHeight);
if (module4SoilMoisture < 20) {
tft.setTextColor(rgbTo565(255, 80, 80));
tft.println("Zelo suho");
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 180));
tft.println("Potrebna zalivanje!");
} else if (module4SoilMoisture < 40) {
tft.setTextColor(rgbTo565(255, 200, 50));
tft.println("Suho");
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 180));
tft.println("Kmalu zalijte");
} else if (module4SoilMoisture < 60) {
tft.setTextColor(rgbTo565(150, 255, 150));
tft.println("Optimalno");
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 180));
tft.println("Dobro stanje");
} else if (module4SoilMoisture < 80) {
tft.setTextColor(rgbTo565(100, 150, 255));
tft.println("Vlazno");
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 180));
tft.println("Ne zalivajte");
} else {
tft.setTextColor(rgbTo565(255, 50, 50));
tft.println("Prevec mokro");
tft.setCursor(rightColumnX, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 200, 180));
tft.println("Izsusite tla!");
}
}
// ========== 4 GUMBI: NAZAJ, GRAFI, KALIBRACIJA, OSVEZI ==========
int buttonY = 265;
int buttonWidth = 100;
int buttonHeight = 38;
int buttonSpacing = 10;
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2; // = 25
// Gumb 1: NAZAJ (x = 25) - nazaj na GLAVNI MENI
drawMenuButton(startX, buttonY, buttonWidth, buttonHeight, rgbTo565(245, 67, 54), "NAZAJ");
// Gumb 2: GRAFI (x = 135) - 30-dnevni graf
drawMenuButton(startX + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "GRAFI");
// Gumb 3: KALIBRACIJA (x = 245)
drawMenuButton(startX + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(255, 165, 0), "KALIBRACIJA");
// Gumb 4: OSVEZI (x = 355)
drawMenuButton(startX + 3 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(100, 200, 100), "OSVEZI");
currentState = STATE_SOIL_MOISTURE_INFO;
}
void handleSoilMoistureTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
// ========== ISTE KOORDINATE KOT PRI RISANJU ==========
int buttonY = 265;
int buttonWidth = 100;
int buttonHeight = 38;
int buttonSpacing = 10;
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2; // = 25
// Gumb 1: NAZAJ (x = 25 do 125) - nazaj na GLAVNI MENI
if (x > startX && x < startX + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → NAZAJ - nazaj na GLAVNI MENI");
showMainMenu();
return;
}
// Gumb 2: GRAFI (x = 135 do 235) - 30-dnevni graf
if (x > startX + buttonWidth + buttonSpacing &&
x < startX + 2 * buttonWidth + buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → GRAFI - 30-dnevni graf");
showSoilMoistureGraphScreen();
return;
}
// Gumb 3: KALIBRACIJA (x = 245 do 345)
if (x > startX + 2 * (buttonWidth + buttonSpacing) &&
x < startX + 3 * buttonWidth + 2 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → KALIBRACIJA");
calibrateSoilSensor();
return;
}
// Gumb 4: OSVEZI (x = 355 do 455)
if (x > startX + 3 * (buttonWidth + buttonSpacing) &&
x < startX + 4 * buttonWidth + 3 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → OSVEZI");
showSoilMoistureInfo();
return;
}
}
void showSoilMoistureGraphScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(SOIL_COLOR);
tft.setCursor(130, 25);
tft.println("30-DNEVNI GRAF - VLAGA TAL");
tft.setTextSize(1);
tft.setTextColor(rgbTo565(200, 200, 255));
tft.setCursor(30, 45);
int validCount = 0;
for (int i = 0; i < SCREEN_HISTORY_SIZE; i++) {
if (screenSoilHistory[i] > 0) validCount++;
}
tft.print("Meritev: ");
tft.print(validCount);
tft.print("/");
tft.print(SCREEN_HISTORY_SIZE);
int estimatedDays = validCount / 8;
tft.print(" (");
tft.print(estimatedDays);
tft.println(" dni)");
int graphX = 30;
int graphY = 60;
int graphWidth = 420;
int graphHeight = 160;
tft.fillRoundRect(graphX, graphY, graphWidth, graphHeight, 6, rgbTo565(20, 30, 40));
tft.drawRoundRect(graphX, graphY, graphWidth, graphHeight, 6, SOIL_COLOR);
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 120, 140));
for (int day = 0; day <= 30; day += 5) {
int xPos = graphX + (day * (graphWidth / 30));
for (int y = graphY; y < graphY + graphHeight; y += 4) {
tft.drawPixel(xPos, y, rgbTo565(60, 70, 80));
}
tft.setCursor(xPos - 10, graphY + graphHeight + 5);
if (day == 0) {
tft.print("zdaj");
} else {
tft.print("-");
tft.print(day);
tft.print("d");
}
}
for (int percent = 0; percent <= 100; percent += 20) {
int yPos = graphY + graphHeight - (percent * (graphHeight / 100));
for (int x = graphX; x < graphX + graphWidth; x += 4) {
tft.drawPixel(x, yPos, rgbTo565(60, 70, 80));
}
tft.setCursor(graphX - 25, yPos - 4);
tft.setTextColor(rgbTo565(150, 150, 180));
tft.print(percent);
tft.print("%");
}
if (validCount > 1) {
float validValues[SCREEN_HISTORY_SIZE];
int indices[SCREEN_HISTORY_SIZE];
int validIdx = 0;
for (int i = 0; i < SCREEN_HISTORY_SIZE; i++) {
if (screenSoilHistory[i] > 0) {
validValues[validIdx] = screenSoilHistory[i];
indices[validIdx] = i;
validIdx++;
}
}
for (int i = 0; i < validIdx - 1; i++) {
for (int j = i + 1; j < validIdx; j++) {
if (indices[i] > indices[j]) {
int tempIdx = indices[i];
indices[i] = indices[j];
indices[j] = tempIdx;
float tempVal = validValues[i];
validValues[i] = validValues[j];
validValues[j] = tempVal;
}
}
}
int oldestIdx = indices[0];
int newestIdx = indices[validIdx - 1];
int timeSpan = newestIdx - oldestIdx;
if (timeSpan <= 0) timeSpan = validIdx;
float prevX = -1, prevY = -1;
for (int i = 0; i < validIdx; i++) {
float age;
if (timeSpan > 0) {
age = (float)(newestIdx - indices[i]) / timeSpan;
} else {
age = (float)(validIdx - 1 - i) / (validIdx - 1);
}
int x = graphX + (int)(age * graphWidth);
x = constrain(x, graphX, graphX + graphWidth);
int y = graphY + graphHeight - (int)((validValues[i] / 100.0) * graphHeight);
y = constrain(y, graphY, graphY + graphHeight);
if (prevX != -1) {
tft.drawLine(prevX, prevY, x, y, SOIL_COLOR);
tft.drawLine(prevX + 1, prevY, x + 1, y, SOIL_COLOR);
}
tft.fillCircle(x, y, 2, SOIL_COLOR);
prevX = x;
prevY = y;
}
float sum = 0, minVal = 100, maxVal = 0;
for (int i = 0; i < validIdx; i++) {
sum += validValues[i];
if (validValues[i] < minVal) minVal = validValues[i];
if (validValues[i] > maxVal) maxVal = validValues[i];
}
tft.setTextSize(1);
tft.setCursor(graphX + 10, graphY + 10);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.printf("Povp: %.1f%%", sum/validIdx);
tft.setCursor(graphX + 150, graphY + 10);
tft.setTextColor(rgbTo565(100, 150, 255));
tft.printf("Min: %.0f%%", minVal);
tft.setCursor(graphX + 250, graphY + 10);
tft.setTextColor(SOIL_COLOR);
tft.printf("Max: %.0f%%", maxVal);
} else {
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(graphX + graphWidth / 2 - 60, graphY + graphHeight / 2 - 20);
tft.println("Premalo podatkov za graf");
tft.setCursor(graphX + graphWidth / 2 - 50, graphY + graphHeight / 2);
tft.println("Pocakajte na meritve");
tft.setCursor(graphX + graphWidth / 2 - 40, graphY + graphHeight / 2 + 20);
tft.printf("Veljavnih: %d/%d", validCount, SCREEN_HISTORY_SIZE);
}
int legendY = graphY + graphHeight + 20;
tft.fillRect(50, legendY, 8, 8, SOIL_COLOR);
tft.setCursor(62, legendY);
tft.setTextColor(SOIL_COLOR);
tft.print("Vlaga tal: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.print(soilMoisturePercent);
tft.print("%");
tft.setCursor(250, legendY);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("Suho/Mokro: ");
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print(SOIL_DRY_VALUE);
tft.print("/");
tft.setTextColor(rgbTo565(100, 150, 255));
tft.print(SOIL_WET_VALUE);
int buttonY = legendY + 35;
drawMenuButton(40, buttonY, 180, 35, SOIL_COLOR, "LETNI ARHIV");
drawMenuButton(260, buttonY, 180, 35, rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_SOIL_MOISTURE_GRAPH;
}
void showRelayControl() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(200, 40);
tft.println("RELEJI");
int frameY = 50;
int frameHeight = 180;
tft.drawRoundRect(20, frameY, 440, frameHeight, 10, rgbTo565(255, 150, 50));
tft.fillRoundRect(21, frameY + 1, 438, frameHeight - 1, 10, rgbTo565(40, 30, 20));
int leftColumnX = 40;
int rightColumnX = 260;
int startY = frameY + 12;
int buttonWidth = 180;
int buttonHeight = 32;
int buttonSpacing = 5;
String leftLabels[4] = { "GRETJE", "VLAZENJE", "RAZSVETLJAVA", "ZALIVANJE" };
int leftRelays[4] = { 0, 2, 4, 6 };
for (int i = 0; i < 4; i++) {
int x = leftColumnX;
int y = startY + i * (buttonHeight + buttonSpacing);
uint16_t bgColor;
String stateText;
if (!relayControlEnabled) {
bgColor = RELAY_DISABLED_COLOR;
stateText = "ONEMOGOCEN";
} else if (relayStates[leftRelays[i]]) {
bgColor = RELAY_ON_COLOR;
stateText = "VKLOPLJEN";
} else {
bgColor = RELAY_OFF_COLOR;
stateText = "IZKLOPLJEN";
}
tft.fillRoundRect(x, y, buttonWidth, buttonHeight, 6, bgColor);
tft.drawRoundRect(x, y, buttonWidth, buttonHeight, 6, ST77XX_WHITE);
tft.setTextSize(1);
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds(leftLabels[i], 0, 0, &textX, &textY, &textW, &textH);
int textPosX = x + (buttonWidth - textW) / 2 - textX;
int textPosY = y + (buttonHeight / 2 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.setTextColor(bgColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.print(leftLabels[i]);
tft.getTextBounds(stateText, 0, 0, &textX, &textY, &textW, &textH);
textPosX = x + (buttonWidth - textW) / 2 - textX;
textPosY = y + buttonHeight / 2 + (buttonHeight / 2 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print(stateText);
}
String rightLabels[4] = { "HLAJENJE", "SUSENJE", "AVT. RAZSVETLJ.", "ROSENJE" };
int rightRelays[4] = { 1, 3, 5, 7 };
for (int i = 0; i < 4; i++) {
int x = rightColumnX;
int y = startY + i * (buttonHeight + buttonSpacing);
uint16_t bgColor;
String stateText;
if (!relayControlEnabled) {
bgColor = RELAY_DISABLED_COLOR;
stateText = "ONEMOGOCEN";
} else if (relayStates[rightRelays[i]]) {
bgColor = RELAY_ON_COLOR;
stateText = "VKLOPLJEN";
} else {
bgColor = RELAY_OFF_COLOR;
stateText = "IZKLOPLJEN";
}
tft.fillRoundRect(x, y, buttonWidth, buttonHeight, 6, bgColor);
tft.drawRoundRect(x, y, buttonWidth, buttonHeight, 6, ST77XX_WHITE);
tft.setTextSize(1);
int16_t textX, textY;
uint16_t textW, textH;
tft.getTextBounds(rightLabels[i], 0, 0, &textX, &textY, &textW, &textH);
int textPosX = x + (buttonWidth - textW) / 2 - textX;
int textPosY = y + (buttonHeight / 2 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.setTextColor(bgColor == RELAY_ON_COLOR ? ST77XX_BLACK : ST77XX_WHITE);
tft.print(rightLabels[i]);
tft.getTextBounds(stateText, 0, 0, &textX, &textY, &textW, &textH);
textPosX = x + (buttonWidth - textW) / 2 - textX;
textPosY = y + buttonHeight / 2 + (buttonHeight / 2 - textH) / 2 - textY;
tft.setCursor(textPosX, textPosY);
tft.print(stateText);
}
int statusY = frameY + frameHeight + 5;
tft.setTextSize(1);
tft.setCursor(leftColumnX, statusY);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Nadzor: ");
tft.setTextColor(relayControlEnabled ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(relayControlEnabled ? "OMOGOCEN" : "ONEMOGOCEN");
int activeCount = 0;
for (int i = 0; i < RELAY_COUNT; i++) {
if (relayStates[i] && relayControlEnabled) activeCount++;
}
tft.setCursor(rightColumnX, statusY);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Aktivni: ");
tft.setTextColor(activeCount > 0 ? RELAY_ON_COLOR : rgbTo565(200, 200, 200));
tft.printf("%d/%d", activeCount, RELAY_COUNT);
int buttonY = statusY + 25;
int controlButtonWidth = 140;
int controlButtonHeight = 40;
int controlSpacing = 10;
int totalWidth = 3 * controlButtonWidth + 2 * controlSpacing;
int startX = (tft.width() - totalWidth) / 2;
drawMenuButton(startX, buttonY, controlButtonWidth, controlButtonHeight,
rgbTo565(76, 175, 80), "VSE VKLOPI");
drawMenuButton(startX + controlButtonWidth + controlSpacing, buttonY,
controlButtonWidth, controlButtonHeight,
rgbTo565(245, 67, 54), "VSE IZKLOPI");
drawMenuButton(startX + 2 * (controlButtonWidth + controlSpacing), buttonY,
controlButtonWidth, controlButtonHeight,
rgbTo565(156, 39, 176), "NAZAJ");
currentState = STATE_RELAY_CONTROL;
}
void handleRelayControlTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int frameY = 50;
int leftColumnX = 40;
int rightColumnX = 260;
int startY = frameY + 12;
int buttonWidth = 180;
int buttonHeight = 32;
int buttonSpacing = 5;
int leftRelays[4] = { 0, 2, 4, 6 };
int rightRelays[4] = { 1, 3, 5, 7 };
for (int i = 0; i < 4; i++) {
int relX = leftColumnX;
int relY = startY + i * (buttonHeight + buttonSpacing);
if (x > relX && x < relX + buttonWidth && y > relY && y < relY + buttonHeight) {
if (relayControlEnabled) {
toggleRelay(leftRelays[i]);
showRelayControl();
}
return;
}
}
for (int i = 0; i < 4; i++) {
int relX = rightColumnX;
int relY = startY + i * (buttonHeight + buttonSpacing);
if (x > relX && x < relX + buttonWidth && y > relY && y < relY + buttonHeight) {
if (relayControlEnabled) {
toggleRelay(rightRelays[i]);
showRelayControl();
}
return;
}
}
int statusY = frameY + 180 + 5;
int controlButtonWidth = 140;
int controlButtonHeight = 40;
int controlSpacing = 10;
int totalWidth = 3 * controlButtonWidth + 2 * controlSpacing;
int startX = (tft.width() - totalWidth) / 2;
int buttonY = statusY + 25;
if (x > startX && x < startX + controlButtonWidth && y > buttonY && y < buttonY + controlButtonHeight) {
setAllRelays(true);
showRelayControl();
return;
}
if (x > startX + controlButtonWidth + controlSpacing && x < startX + 2 * controlButtonWidth + controlSpacing && y > buttonY && y < buttonY + controlButtonHeight) {
setAllRelays(false);
showRelayControl();
return;
}
if (x > startX + 2 * (controlButtonWidth + controlSpacing) && x < startX + 3 * controlButtonWidth + 2 * controlSpacing && y > buttonY && y < buttonY + controlButtonHeight) {
showMainMenu();
return;
}
}
void showMainMenu() {
tft.fillScreen(ST77XX_BLACK);
drawStatusBar();
drawGradientBackground();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(0, 150, 255));
tft.setCursor(200, 30);
tft.println("GLAVNI MENI");
int buttonWidth = 140;
int buttonHeight = 28;
int col1X = 20;
int col2X = 170;
int col3X = 320;
int startY = 55;
int buttonSpacing = 30;
// ===== PRVI STOLPEC (col1X) - 7 gumbov =====
drawMenuButton(col1X, startY, buttonWidth, buttonHeight,
rgbTo565(66, 135, 245), "DOMOV");
drawMenuButton(col1X, startY + buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(255, 100, 100), "NOTRANJI");
drawMenuButton(col1X, startY + 2 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(139, 69, 19), "VLAGA TAL");
drawMenuButton(col1X, startY + 3 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(255, 150, 50), "RELEJI");
drawMenuButton(col1X, startY + 4 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(66, 135, 245), "NADZOR");
drawMenuButton(col1X, startY + 5 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(255, 215, 0), "RAZ. REL5");
drawMenuButton(col1X, startY + 6 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(100, 200, 100), "SD KARTICA");
// ===== DRUGI STOLPEC (col2X) - 7 gumbov =====
drawMenuButton(col2X, startY, buttonWidth, buttonHeight,
rgbTo565(156, 39, 176), "MCP TEST");
drawMenuButton(col2X, startY + buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(0, 200, 255), "VENTILACIJA");
drawMenuButton(col2X, startY + 2 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(0, 200, 255), "RTC INFO");
drawMenuButton(col2X, startY + 3 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(100, 150, 255), "ZUNANJI");
drawMenuButton(col2X, startY + 4 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(200, 100, 200), "FLOW");
drawMenuButton(col2X, startY + 5 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(0, 150, 255), "SENCENJE");
drawMenuButton(col2X, startY + 6 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(150, 150, 255), "WI-FI");
// ===== TRETJI STOLPEC (col3X) - 8 gumbov =====
drawMenuButton(col3X, startY, buttonWidth, buttonHeight,
rgbTo565(0, 200, 100), "TROPSKE");
drawMenuButton(col3X, startY + buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(200, 165, 0), "AVTO REL6");
drawMenuButton(col3X, startY + 2 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(100, 200, 255), "MODUL 2");
drawMenuButton(col3X, startY + 3 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "GRAFI 48h");
drawMenuButton(col3X, startY + 4 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(255, 150, 0), "LETNI ARHIV");
drawMenuButton(col3X, startY + 5 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(100, 200, 255), "TEST M3");
drawMenuButton(col3X, startY + 6 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(100, 200, 255), "KAK. ZRAKA");
drawMenuButton(col3X, startY + 7 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(255, 165, 0), "LDR KALIB");
tft.drawFastHLine(50, 40, 380, rgbTo565(0, 100, 200));
currentState = STATE_MAIN_MENU;
lastState = STATE_MAIN_MENU;
}
void handleMainMenuTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int buttonWidth = 140;
int buttonHeight = 28;
int col1X = 20;
int col2X = 170;
int col3X = 320;
int startY = 55;
int buttonSpacing = 30;
int touchTolerance = 8;
// ===== PRVI STOLPEC (col1X) =====
if (x >= col1X - touchTolerance && x <= col1X + buttonWidth + touchTolerance) {
// Gumb 0: DOMOV
int btnTop = startY - touchTolerance;
int btnBottom = startY + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
forceFullHomeScreenRedraw();
return;
}
// Gumb 1: NOTRANJI
btnTop = startY + buttonSpacing - touchTolerance;
btnBottom = startY + buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showInternalSensorsInfo();
return;
}
// Gumb 2: VLAGA TAL
btnTop = startY + 2 * buttonSpacing - touchTolerance;
btnBottom = startY + 2 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showSoilMoistureInfo();
return;
}
// Gumb 3: RELEJI
btnTop = startY + 3 * buttonSpacing - touchTolerance;
btnBottom = startY + 3 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showRelayControl();
return;
}
// Gumb 4: NADZOR
btnTop = startY + 4 * buttonSpacing - touchTolerance;
btnBottom = startY + 4 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showTempHumControlScreen();
return;
}
// Gumb 5: RAZ. REL5
btnTop = startY + 5 * buttonSpacing - touchTolerance;
btnBottom = startY + 5 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showLightingControlScreen();
return;
}
// Gumb 6: SD KARTICA
btnTop = startY + 6 * buttonSpacing - touchTolerance;
btnBottom = startY + 6 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showSDCardManagementScreen();
return;
}
}
// ===== DRUGI STOLPEC (col2X) =====
else if (x >= col2X - touchTolerance && x <= col2X + buttonWidth + touchTolerance) {
// Gumb 0: MCP TEST
int btnTop = startY - touchTolerance;
int btnBottom = startY + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showMCP23017Test();
return;
}
// Gumb 1: VENTILACIJA
btnTop = startY + buttonSpacing - touchTolerance;
btnBottom = startY + buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showVentilationControlScreen();
return;
}
// Gumb 2: RTC INFO
btnTop = startY + 2 * buttonSpacing - touchTolerance;
btnBottom = startY + 2 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showRTCInfo();
return;
}
// Gumb 3: ZUNANJI
btnTop = startY + 3 * buttonSpacing - touchTolerance;
btnBottom = startY + 3 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showExternalSensorsInfo();
return;
}
// Gumb 4: FLOW
btnTop = startY + 4 * buttonSpacing - touchTolerance;
btnBottom = startY + 4 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showFlowSensorsInfo();
return;
}
// Gumb 5: SENCENJE
btnTop = startY + 5 * buttonSpacing - touchTolerance;
btnBottom = startY + 5 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showShadeControlScreen();
return;
}
// Gumb 6: WI-FI
btnTop = startY + 6 * buttonSpacing - touchTolerance;
btnBottom = startY + 6 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showWiFiConnectedScreen();
return;
}
}
// ===== TRETJI STOLPEC (col3X) =====
else if (x >= col3X - touchTolerance && x <= col3X + buttonWidth + touchTolerance) {
// Gumb 0: TROPSKE
int btnTop = startY - touchTolerance;
int btnBottom = startY + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showTropicalPlantsMenu();
return;
}
// Gumb 1: AVTO REL6
btnTop = startY + buttonSpacing - touchTolerance;
btnBottom = startY + buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showLightAutoControlScreen();
return;
}
// Gumb 2: MODUL 2
btnTop = startY + 2 * buttonSpacing - touchTolerance;
btnBottom = startY + 2 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showModule2Info();
return;
}
// Gumb 3: GRAFI 48h
btnTop = startY + 3 * buttonSpacing - touchTolerance;
btnBottom = startY + 3 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showInternalGraph48hScreen();
return;
}
// Gumb 4: LETNI ARHIV
btnTop = startY + 4 * buttonSpacing - touchTolerance;
btnBottom = startY + 4 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showYearArchiveScreen();
return;
}
// Gumb 5: TEST M3
btnTop = startY + 5 * buttonSpacing - touchTolerance;
btnBottom = startY + 5 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
testSendToModule3();
return;
}
// Gumb 6: KAK. ZRAKA
btnTop = startY + 6 * buttonSpacing - touchTolerance;
btnBottom = startY + 6 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showAirQualityScreen();
return;
}
// Gumb 7: LDR KALIB (NOVI GUMB)
btnTop = startY + 7 * buttonSpacing - touchTolerance;
btnBottom = startY + 7 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showLDRCalibrationScreen();
return;
}
}
}
void showYearArchiveScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(SOIL_COLOR);
tft.setCursor(150, 25);
tft.println("LETNI ARHIV - VLAGA TAL");
int validDays = 0;
int newestIdx = -1;
unsigned long newestDate = 0;
for (int i = 0; i < ARCHIVE_HISTORY_SIZE; i++) {
if (yearArchive[i].samples > 0) {
validDays++;
if (yearArchive[i].date > newestDate) {
newestDate = yearArchive[i].date;
newestIdx = i;
}
}
}
tft.setCursor(30, 45);
tft.setTextColor(rgbTo565(200, 200, 255));
tft.printf("Dni v arhivu: %d\n", validDays);
if (sdInitialized && useSDCard) {
tft.setCursor(30, 60);
tft.setTextColor(rgbTo565(80, 220, 100));
tft.println("Shranjeno na SD kartici");
}
if (validDays > 0) {
int startY = 85;
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(30, startY);
tft.println("Zadnji vnosi:");
for (int i = 0; i < min(8, validDays); i++) {
int idx = (newestIdx - i + ARCHIVE_HISTORY_SIZE) % ARCHIVE_HISTORY_SIZE;
if (yearArchive[idx].samples > 0) {
int yPos = startY + 20 + i * 18;
uint32_t date = yearArchive[idx].date;
int year = date / 10000;
int month = (date / 100) % 100;
int day = date % 100;
tft.setCursor(50, yPos);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.printf("%02d.%02d.%d: ", day, month, year);
float avg = yearArchive[idx].avgMoisture;
if (avg < 20) tft.setTextColor(rgbTo565(255, 80, 80));
else if (avg < 40) tft.setTextColor(rgbTo565(255, 200, 50));
else if (avg < 60) tft.setTextColor(rgbTo565(150, 255, 150));
else if (avg < 80) tft.setTextColor(rgbTo565(100, 150, 255));
else tft.setTextColor(rgbTo565(255, 50, 50));
tft.printf("%.1f%% (min %.0f%%, max %.0f%%)",
avg,
yearArchive[idx].minMoisture,
yearArchive[idx].maxMoisture);
}
}
} else {
tft.setCursor(80, 120);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Arhiv je prazen");
tft.setCursor(60, 140);
tft.println("Pocakajte na prve meritve");
}
drawMenuButton(150, 270, 180, 35, rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_YEAR_ARCHIVE;
}
void showAirQualityScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 200, 255));
tft.setCursor(140, 30);
tft.println("KAKOVOST ZRAKA");
tft.drawRoundRect(10, 45, 460, 200, 10, rgbTo565(100, 200, 255));
tft.fillRoundRect(11, 46, 458, 198, 10, rgbTo565(20, 40, 70));
int yOffset = 60;
int lineHeight = 22;
tft.setTextSize(1);
tft.setCursor(20, yOffset);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Modul 4: ");
tft.setTextColor(module4Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(module4Active ? "AKTIVEN" : "NEDOSEGLJIV");
if (!module4Active) {
tft.setCursor(100, 120);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Modul 4 ni dosegljiv!");
tft.setCursor(80, 145);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Preverite povezavo in napajanje");
} else {
tft.setCursor(20, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("TVOC: ");
uint16_t tvocColor = (module4TVOC < 400) ? rgbTo565(80, 220, 100) :
(module4TVOC < 800) ? rgbTo565(255, 200, 50) : rgbTo565(255, 80, 80);
tft.setTextColor(tvocColor);
tft.printf("%.0f ppb", module4TVOC);
tft.setCursor(20, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("eCO2: ");
uint16_t co2Color = (module4ECO2 < 600) ? rgbTo565(80, 150, 255) :
(module4ECO2 < 1500) ? rgbTo565(80, 220, 100) : rgbTo565(255, 150, 50);
tft.setTextColor(co2Color);
tft.printf("%.0f ppm", module4ECO2);
tft.setCursor(20, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("eCH2O: ");
tft.setTextColor(module4ECH2O > 300 ? rgbTo565(255, 150, 50) : rgbTo565(100, 200, 100));
tft.printf("%.0f ppb", module4ECH2O);
tft.setCursor(20, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("INTERPRETACIJA:");
tft.setCursor(20, yOffset + 6 * lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
if (module4TVOC > 800) {
tft.println("⚠️ Visok TVOC - Rastline pod stresom! Prezracite.");
} else if (module4ECO2 < 600) {
tft.println("🌱 Nizek CO2 - Zmanjsajte prezracevanje.");
} else if (module4ECO2 > 1500) {
tft.println("🌿 Visok CO2 - Vklopite ventilator.");
} else {
tft.println("✅ Kakovost zraka ODLICNA!");
}
int barX = 250;
int barY = yOffset + lineHeight;
int barWidth = 200;
int barHeight = 15;
tft.fillRect(barX, barY, barWidth, barHeight, rgbTo565(60, 60, 60));
int tvocWidth = map(constrain(module4TVOC, 0, 2000), 0, 2000, 0, barWidth);
if (tvocWidth > 0) {
tft.fillRect(barX, barY, tvocWidth, barHeight, tvocColor);
}
tft.drawRect(barX, barY, barWidth, barHeight, ST77XX_WHITE);
barY = yOffset + 2 * lineHeight;
tft.fillRect(barX, barY, barWidth, barHeight, rgbTo565(60, 60, 60));
int co2Width = map(constrain(module4ECO2, 0, 2500), 0, 2500, 0, barWidth);
if (co2Width > 0) {
tft.fillRect(barX, barY, co2Width, barHeight, co2Color);
}
tft.drawRect(barX, barY, barWidth, barHeight, ST77XX_WHITE);
}
drawMenuButton(150, 260, 180, 35, rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_AIR_QUALITY;
}
void showModule4Info() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(150, 200, 100));
tft.setCursor(150, 30);
tft.println("VITRINA (MODUL 4)");
tft.drawRoundRect(10, 45, 460, 210, 10, rgbTo565(150, 200, 100));
tft.fillRoundRect(11, 46, 458, 208, 10, rgbTo565(40, 50, 30));
int leftX = 20;
int rightX = 260;
int yOffset = 60;
int lineHeight = 20;
tft.setTextSize(1);
tft.setCursor(leftX, yOffset);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Status: ");
tft.setTextColor(module4Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(module4Active ? "AKTIVEN" : "NEDOSEGLJIV");
if (!module4Active) {
tft.setCursor(100, 120);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("Modul 4 ni dosegljiv!");
tft.setCursor(80, 145);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Preverite povezavo");
// Gumb NAZAJ ko modul ni aktiven
drawMenuButton(150, 260, 180, 40, rgbTo565(245, 67, 54), "NAZAJ");
} else {
tft.setCursor(leftX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Temp. zraka: ");
tft.setTextColor(TEMP_COLOR);
tft.printf("%.1f °C", module4AirTemp);
int tempBarWidth = map(constrain(module4AirTemp, 0, 50), 0, 50, 0, 150);
tft.fillRect(leftX + 130, yOffset + lineHeight - 2, 150, 8, rgbTo565(60, 60, 60));
tft.fillRect(leftX + 130, yOffset + lineHeight - 2, tempBarWidth, 8, TEMP_COLOR);
tft.drawRect(leftX + 130, yOffset + lineHeight - 2, 150, 8, ST77XX_WHITE);
tft.setCursor(leftX, yOffset + 2 * lineHeight + 5);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Vlaga zraka: ");
tft.setTextColor(HUMIDITY_COLOR);
tft.printf("%.1f %%", module4AirHum);
int humBarWidth = map(constrain(module4AirHum, 0, 100), 0, 100, 0, 150);
tft.fillRect(leftX + 130, yOffset + 2 * lineHeight + 3, 150, 8, rgbTo565(60, 60, 60));
tft.fillRect(leftX + 130, yOffset + 2 * lineHeight + 3, humBarWidth, 8, HUMIDITY_COLOR);
tft.drawRect(leftX + 130, yOffset + 2 * lineHeight + 3, 150, 8, ST77XX_WHITE);
tft.setCursor(leftX, yOffset + 3 * lineHeight + 10);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Zracni tlak: ");
tft.setTextColor(PRESSURE_COLOR);
tft.printf("%.1f hPa", module4Pressure);
tft.setCursor(leftX, yOffset + 4 * lineHeight + 15);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Svetloba: ");
tft.setTextColor(LIGHT_COLOR);
tft.printf("%d %%", module4LightPercent);
int lightBarWidth = map(module4LightPercent, 0, 100, 0, 150);
tft.fillRect(leftX + 100, yOffset + 4 * lineHeight + 13, 150, 8, rgbTo565(60, 60, 60));
tft.fillRect(leftX + 100, yOffset + 4 * lineHeight + 13, lightBarWidth, 8, LIGHT_COLOR);
tft.drawRect(leftX + 100, yOffset + 4 * lineHeight + 13, 150, 8, ST77XX_WHITE);
tft.setCursor(rightX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Temp. zemlje: ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%.1f °C", module4SoilTemp);
int soilTempBarWidth = map(constrain(module4SoilTemp, 0, 40), 0, 40, 0, 150);
tft.fillRect(rightX + 130, yOffset + lineHeight - 2, 150, 8, rgbTo565(60, 60, 60));
tft.fillRect(rightX + 130, yOffset + lineHeight - 2, soilTempBarWidth, 8, rgbTo565(255, 150, 100));
tft.drawRect(rightX + 130, yOffset + lineHeight - 2, 150, 8, ST77XX_WHITE);
tft.setCursor(rightX, yOffset + 2 * lineHeight + 5);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("Vlaga tal: ");
uint16_t soilColor;
if (module4SoilMoisture < 30) soilColor = rgbTo565(255, 100, 100);
else if (module4SoilMoisture < 60) soilColor = rgbTo565(255, 200, 50);
else soilColor = rgbTo565(100, 150, 255);
tft.setTextColor(soilColor);
tft.printf("%d %%", module4SoilMoisture);
int soilBarWidth = map(module4SoilMoisture, 0, 100, 0, 150);
tft.fillRect(rightX + 100, yOffset + 2 * lineHeight + 3, 150, 8, rgbTo565(60, 60, 60));
tft.fillRect(rightX + 100, yOffset + 2 * lineHeight + 3, soilBarWidth, 8, soilColor);
tft.drawRect(rightX + 100, yOffset + 2 * lineHeight + 3, 150, 8, ST77XX_WHITE);
tft.setCursor(rightX, yOffset + 3 * lineHeight + 10);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("TVOC: ");
uint16_t tvocColor = (module4TVOC < 400) ? rgbTo565(80, 220, 100) :
(module4TVOC < 800) ? rgbTo565(255, 200, 50) : rgbTo565(255, 80, 80);
tft.setTextColor(tvocColor);
tft.printf("%.0f ppb", module4TVOC);
tft.setCursor(rightX, yOffset + 4 * lineHeight + 15);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("eCO2: ");
uint16_t co2Color = (module4ECO2 < 600) ? rgbTo565(80, 150, 255) :
(module4ECO2 < 1500) ? rgbTo565(80, 220, 100) : rgbTo565(255, 150, 50);
tft.setTextColor(co2Color);
tft.printf("%.0f ppm", module4ECO2);
}
// ========== 4 GUMBI: NAZAJ, GRAFI, KAK. ZRAKA, OSVEZI ==========
int buttonY = 270;
int buttonWidth = 100;
int buttonHeight = 40;
int buttonSpacing = 10;
// 4 * 100 + 3 * 10 = 400 + 30 = 430
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2; // (480 - 430) / 2 = 25
// Gumb 1: NAZAJ (x = 25)
drawMenuButton(startX, buttonY, buttonWidth, buttonHeight, rgbTo565(245, 67, 54), "NAZAJ");
// Gumb 2: GRAFI (x = 25 + 100 + 10 = 135)
drawMenuButton(startX + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "GRAFI");
// Gumb 3: KAK. ZRAKA (x = 135 + 100 + 10 = 245)
drawMenuButton(startX + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(66, 135, 245), "KAK. ZRAKA");
// Gumb 4: OSVEZI (x = 245 + 100 + 10 = 355)
drawMenuButton(startX + 3 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(100, 200, 100), "OSVEZI");
currentState = STATE_MODULE4_INFO;
}
void handleModule4InfoTouch(int x, int y) {
Serial.printf("🔘 VITRINA DOTIK: x=%d, y=%d\n", x, y);
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
if (!module4Active) {
// Gumb NAZAJ ko modul ni aktiven (centriran)
if (x > 150 && x < 330 && y > 260 && y < 300) {
Serial.println(" → NAZAJ - nazaj na GLAVNI MENI");
showMainMenu();
return;
}
return;
}
// ========== ISTE KOORDINATE KOT PRI RISANJU ==========
int buttonY = 270;
int buttonWidth = 100;
int buttonHeight = 40;
int buttonSpacing = 10;
int totalWidth = 4 * buttonWidth + 3 * buttonSpacing;
int startX = (tft.width() - totalWidth) / 2; // = 25
// Gumb 1: NAZAJ (x = 25 do 125)
if (x > startX && x < startX + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → ✅ NAZAJ - nazaj na GLAVNI MENI");
showMainMenu();
return;
}
// Gumb 2: GRAFI (x = 135 do 235)
if (x > startX + buttonWidth + buttonSpacing &&
x < startX + 2 * buttonWidth + buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → ✅ GRAFI - 48-urni graf");
showInternalGraph48hScreen();
return;
}
// Gumb 3: KAK. ZRAKA (x = 245 do 345)
if (x > startX + 2 * (buttonWidth + buttonSpacing) &&
x < startX + 3 * buttonWidth + 2 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → ✅ KAKOVOST ZRAKA");
showAirQualityScreen();
return;
}
// Gumb 4: OSVEZI (x = 355 do 455)
if (x > startX + 3 * (buttonWidth + buttonSpacing) &&
x < startX + 4 * buttonWidth + 3 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
Serial.println(" → ✅ OSVEZI");
showModule4Info();
return;
}
Serial.println(" → ❌ Noben gumb ni bil prepoznan");
}
void showRTCInfo() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(2);
tft.setTextColor(rgbTo565(0, 200, 255));
tft.setCursor(180, 30);
tft.println("RTC INFO");
tft.drawRoundRect(20, 50, 440, 120, 10, rgbTo565(0, 150, 255));
tft.fillRoundRect(21, 51, 438, 118, 10, rgbTo565(20, 40, 70));
int yOffset = 70;
int lineHeight = 15;
tft.setTextSize(1);
tft.setCursor(30, yOffset);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Status RTC: ");
tft.setTextColor(rtcInitialized ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.println(rtcInitialized ? "PRISOTEN" : "ODSOTEN");
if (rtcInitialized) {
DateTime now = rtc.now();
tft.setCursor(30, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Datum: ");
tft.setTextColor(rgbTo565(150, 255, 150));
tft.println(currentDate);
tft.setCursor(30, yOffset + 2 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Cas: ");
tft.setTextColor(rgbTo565(150, 200, 255));
tft.println(currentTime);
tft.setCursor(30, yOffset + 3 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Temperatura: ");
tft.setTextColor(rgbTo565(255, 200, 50));
tft.println(getRTCTemperature());
tft.setCursor(30, yOffset + 4 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("NTP sinhronizacija: ");
tft.setTextColor(ntpSynchronized ? rgbTo565(80, 220, 100) : rgbTo565(255, 200, 50));
tft.println(ntpSynchronized ? "DA" : "NE");
if (ntpSynchronized) {
tft.setCursor(30, yOffset + 5 * lineHeight);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.print("Zadnja sinhronizacija: ");
unsigned long secs = (millis() - lastNTPSync) / 1000;
if (secs < 60) {
tft.setTextColor(rgbTo565(150, 255, 150));
tft.printf("%lu s", secs);
} else if (secs < 3600) {
tft.setTextColor(rgbTo565(255, 255, 150));
tft.printf("%lu min", secs / 60);
} else {
tft.setTextColor(rgbTo565(255, 200, 50));
tft.printf("%lu h", secs / 3600);
}
}
} else {
tft.setCursor(30, yOffset + lineHeight);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.println("RTC modul ni najden!");
}
drawIconButton(50, 180, 180, 40, rgbTo565(66, 135, 245), "SINHRONIZIRAJ", "refresh");
drawIconButton(250, 180, 180, 40, rgbTo565(76, 175, 80), "OSVEZI", "display");
drawIconButton(50, 230, 180, 40, rgbTo565(245, 67, 54), "NAZAJ", "info");
drawIconButton(250, 230, 180, 40, rgbTo565(156, 39, 176), "PONOVNA INIC.", "touch");
currentState = STATE_RTC_INFO;
}
void handleRTCInfoTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
if (x > 50 && x < 230 && y > 180 && y < 220) {
if (wifiConnected) {
syncTimeWithNTP();
} else {
showTemporaryMessage("Ni WiFi povezave!\nPovezite se na WiFi", rgbTo565(255, 80, 80), 2000);
}
showRTCInfo();
}
if (x > 250 && x < 430 && y > 180 && y < 220) {
updateRTC();
showRTCInfo();
}
if (x > 50 && x < 230 && y > 230 && y < 270) {
showMainMenu();
}
if (x > 250 && x < 430 && y > 230 && y < 270) {
initializeRTC();
showRTCInfo();
}
}
void saveSettingsOnShutdown() {
saveAllSettings(true);
}
void loadSettingsOnBoot() {
loadAllSettings(true);
}
void saveHistoryToSD() {
if (!sdInitialized || !useSDCard) return;
File histFile = SD.open(HISTORY_FILE, FILE_APPEND);
if (!histFile) return;
unsigned long timestamp = rtcInitialized ? rtc.now().unixtime() : millis() / 1000;
for (int i = 0; i < 10; i++) {
int idx = (graphHistoryIndex - i - 1 + GRAPH_HISTORY_SIZE) % GRAPH_HISTORY_SIZE;
if (!isnan(tempHistory48h[idx]) && tempHistory48h[idx] != 0) {
histFile.printf("%lu,temp_in,%.1f\n", timestamp - (i * 1800), tempHistory48h[idx]);
}
if (!isnan(humHistory48h[idx]) && humHistory48h[idx] != 0) {
histFile.printf("%lu,hum_in,%.1f\n", timestamp - (i * 1800), humHistory48h[idx]);
}
}
histFile.close();
}
void showSDCardInfo() {
if (!sdInitialized) return;
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
uint64_t usedSpace = SD.usedBytes() / (1024 * 1024);
uint64_t freeSpace = cardSize - usedSpace;
Serial.println("\n=== SD KARTICA INFO ===");
Serial.printf("Velikost: %llu MB\n", cardSize);
Serial.printf("Zasedeno: %llu MB\n", usedSpace);
Serial.printf("Prostor: %llu MB\n", freeSpace);
File root = SD.open("/");
Serial.println("Datoteke na SD:");
File file = root.openNextFile();
while (file) {
if (!file.isDirectory()) {
Serial.printf(" %s (%d bytes)\n", file.name(), file.size());
}
file = root.openNextFile();
}
root.close();
}
void emergencySaveSettings() {
Serial.println("\n=== VARNOSTNO SHRANJEVANJE OB IZKLOPU ===");
if (!preferences.begin("emergency-save", false)) {
return;
}
preferences.putBool("emergencySaved", true);
preferences.putULong("emergencyTime", millis());
for (int i = 0; i < min(4, RELAY_COUNT); i++) {
preferences.putBool(("emerRelay" + String(i)).c_str(), relayStates[i]);
}
preferences.putFloat("emerTempMin", targetTempMin);
preferences.putFloat("emerTempMax", targetTempMax);
preferences.putFloat("emerHumMin", targetHumMin);
preferences.putFloat("emerHumMax", targetHumMax);
preferences.end();
}
void saveRelayStates() {
preferences.begin("system-settings", false);
for (int i = 0; i < RELAY_COUNT; i++) {
preferences.putBool(("relay" + String(i)).c_str(), relayStates[i]);
}
preferences.putULong("lastRelayChange", millis());
preferences.end();
}
void redrawCurrentScreen() {
unsigned long now = millis();
if (now - lastScreenDraw < MIN_DRAW_INTERVAL) {
return;
}
lastScreenDraw = now;
AppState stateToDraw = currentState;
switch (stateToDraw) {
case STATE_HOME_SCREEN:
showHomeScreen();
break;
case STATE_MAIN_MENU:
showMainMenu();
break;
case STATE_WIFI_CONNECTED:
showWiFiConnectedScreen();
break;
case STATE_MCP23017_TEST:
showMCP23017Test();
break;
case STATE_RTC_INFO:
showRTCInfo();
break;
case STATE_INTERNAL_SENSORS_INFO:
showInternalSensorsInfo();
break;
case STATE_EXTERNAL_SENSORS_INFO:
showExternalSensorsInfo();
break;
case STATE_SOIL_MOISTURE_INFO:
showSoilMoistureInfo();
break;
case STATE_FLOW_SENSORS_INFO:
showFlowSensorsInfo();
break;
case STATE_RELAY_CONTROL:
showRelayControl();
break;
case STATE_TEMP_HUM_CONTROL:
showTempHumControlScreen();
break;
case STATE_SHADE_CONTROL:
showShadeControlScreen();
break;
case STATE_LIGHTING_CONTROL:
showLightingControlScreen();
break;
case STATE_LIGHT_AUTO_CONTROL:
showLightAutoControlScreen();
break;
case STATE_EDIT_LIGHT_TIME:
showEditLightTimeScreen();
break;
case STATE_EXTERNAL_GRAPH_48H:
showExternalGraph48hScreen();
break;
case STATE_MODULE2_INFO:
showModule2Info();
break;
case STATE_TROPICAL_PLANTS:
showTropicalPlantsMenu();
break;
case STATE_SELECT_PLANT_FOR_VIEW:
showSelectPlantForViewScreen();
break;
case STATE_SELECT_PLANT_FOR_ADVICE:
showSelectPlantForAdviceScreen();
break;
case STATE_ADD_PLANT:
showAddPlantScreen();
break;
case STATE_PLANT_DETAILS:
if (selectedPlantId >= 0) showPlantDetailsScreen(selectedPlantId);
else showTropicalPlantsMenu();
break;
case STATE_YEAR_ARCHIVE:
showYearArchiveScreen();
break;
case STATE_SOIL_MOISTURE_GRAPH:
showSoilMoistureGraphScreen();
break;
case STATE_VENTILATION_CONTROL:
showVentilationControlScreen();
break;
case STATE_SD_MANAGEMENT:
showSDCardManagementScreen();
break;
case STATE_INTERNAL_GRAPH_48H:
showInternalGraph48hScreen();
break;
case STATE_AIR_QUALITY:
showAirQualityScreen();
break;
case STATE_MODULE4_INFO:
showModule4Info();
break;
case STATE_LDR_CALIBRATION: // NOVO
showLDRCalibrationScreen();
break;
default:
break;
}
lastState = stateToDraw;
}
void updateTrendHistory() {
unsigned long currentTime = millis();
if (currentTime - lastHistoryUpdate > HISTORY_UPDATE_INTERVAL && !isnan(internalTemperature) && !isnan(internalHumidity)) {
tempHistory[historyIndex] = internalTemperature;
humHistory[historyIndex] = internalHumidity;
historyIndex = (historyIndex + 1) % TREND_HISTORY_SIZE;
lastHistoryUpdate = currentTime;
static unsigned long lastTrendCheck = 0;
if (currentTime - lastTrendCheck > 30000) {
calculateTrends();
lastTrendCheck = currentTime;
}
}
}
void calculateTrends() {
if (historyIndex < 10) return;
int recentCount = min(300, historyIndex);
int oldCount = min(900, historyIndex);
if (recentCount < 10 || oldCount < 30) return;
float recentTempAvg = 0, oldTempAvg = 0;
float recentHumAvg = 0, oldHumAvg = 0;
int recentStart = (historyIndex - recentCount + TREND_HISTORY_SIZE) % TREND_HISTORY_SIZE;
for (int i = 0; i < recentCount; i++) {
int idx = (recentStart + i) % TREND_HISTORY_SIZE;
recentTempAvg += tempHistory[idx];
recentHumAvg += humHistory[idx];
}
recentTempAvg /= recentCount;
recentHumAvg /= recentCount;
int oldStart = (historyIndex - oldCount + TREND_HISTORY_SIZE) % TREND_HISTORY_SIZE;
for (int i = 0; i < oldCount; i++) {
int idx = (oldStart + i) % TREND_HISTORY_SIZE;
oldTempAvg += tempHistory[idx];
oldHumAvg += humHistory[idx];
}
oldTempAvg /= oldCount;
oldHumAvg /= oldCount;
float tempDiff = recentTempAvg - oldTempAvg;
if (abs(tempDiff) > TREND_THRESHOLD) {
tempTrend = (tempDiff > 0) ? TREND_UP : TREND_DOWN;
lastTrendChangeTime = millis();
lastTrendTemp = internalTemperature;
} else if (abs(internalTemperature - lastTrendTemp) < TREND_THRESHOLD) {
if (millis() - lastTrendChangeTime > TREND_TIMEOUT) {
tempTrend = TREND_NONE;
}
}
float humDiff = recentHumAvg - oldHumAvg;
if (abs(humDiff) > TREND_THRESHOLD) {
humTrend = (humDiff > 0) ? TREND_UP : TREND_DOWN;
lastTrendChangeTime = millis();
lastTrendHum = internalHumidity;
} else if (abs(internalHumidity - lastTrendHum) < TREND_THRESHOLD) {
if (millis() - lastTrendChangeTime > TREND_TIMEOUT) {
humTrend = TREND_NONE;
}
}
}
void drawTrendArrow(int x, int y, int direction, uint16_t color) {
int arrowSize = 16;
switch (direction) {
case TREND_UP:
tft.fillTriangle(x - arrowSize / 2 - 2, y + arrowSize / 2 + 2,
x, y - arrowSize / 2 - 2,
x + arrowSize / 2 + 2, y + arrowSize / 2 + 2,
ST77XX_WHITE);
tft.fillTriangle(x - arrowSize / 2, y + arrowSize / 2,
x, y - arrowSize / 2,
x + arrowSize / 2, y + arrowSize / 2,
color);
break;
case TREND_DOWN:
tft.fillTriangle(x - arrowSize / 2 - 2, y - arrowSize / 2 - 2,
x, y + arrowSize / 2 + 2,
x + arrowSize / 2 + 2, y - arrowSize / 2 - 2,
ST77XX_WHITE);
tft.fillTriangle(x - arrowSize / 2, y - arrowSize / 2,
x, y + arrowSize / 2,
x + arrowSize / 2, y - arrowSize / 2,
color);
break;
case TREND_STEADY:
tft.fillRect(x - arrowSize / 2 - 2, y - 3 - 2, arrowSize + 4, 4 + 4, ST77XX_WHITE);
tft.fillRect(x - arrowSize / 2 - 2, y + 3 - 2, arrowSize + 4, 4 + 4, ST77XX_WHITE);
tft.fillRect(x - arrowSize / 2, y - 3, arrowSize, 4, color);
tft.fillRect(x - arrowSize / 2, y + 3, arrowSize, 4, color);
break;
}
}
void initializeMCP23017() {
Serial.println("Inicializacija MCP23017...");
delay(50);
for (int attempt = 0; attempt < 3; attempt++) {
if (attempt > 0) {
Serial.printf("Ponovni poskus MCP23017 (%d/3)...\n", attempt + 1);
delay(100);
}
if (mcp.begin_I2C(MCP_ADDR)) {
Serial.println("MCP23017 uspesno inicializiran!");
for (int i = 0; i < 8; i++) {
mcp.pinMode(i, OUTPUT);
mcp.digitalWrite(i, HIGH);
}
for (int i = 8; i < 16; i++) {
mcp.pinMode(i, INPUT_PULLUP);
}
mcpInitialized = true;
lastGPIOState = mcp.readGPIOAB();
return;
}
delay(50);
}
Serial.println("MCP23017 NI najden! Preverite povezave in naslov.");
mcpInitialized = false;
}
void updateMCP23017() {
if (!mcpInitialized) return;
unsigned long currentTime = millis();
if (currentTime - lastMCPUpdate > MCP_UPDATE_INTERVAL) {
uint16_t currentState = mcp.readGPIOAB();
if (currentState != lastGPIOState) {
Serial.print("MCP23017 sprememba stanja: 0x");
Serial.println(currentState, HEX);
for (int i = 0; i < 16; i++) {
bool oldState = (lastGPIOState >> i) & 1;
bool newState = (currentState >> i) & 1;
if (oldState != newState) {
Serial.printf(" Pin %d: %s -> %s\n",
i,
oldState ? "HIGH" : "LOW",
newState ? "HIGH" : "LOW");
}
}
lastGPIOState = currentState;
}
lastMCPUpdate = currentTime;
}
}
void showBootScreen() {
tft.fillScreen(ST77XX_BLACK);
for (int y = 0; y < tft.height(); y++) {
int progress = map(y, 0, tft.height(), 0, 100);
uint8_t r = 10 + (progress * 2 / 100);
uint8_t g = 20 + (progress * 3 / 100);
uint8_t b = 40 + (progress * 6 / 100);
tft.drawFastHLine(0, y, tft.width(), rgbTo565(r, g, b));
}
for (int i = 0; i < 30; i++) {
int x = random(0, tft.width());
int y = random(50, tft.height() - 50);
int size = random(1, 4);
tft.fillCircle(x, y, size, rgbTo565(60, 100, 150));
}
int centerX = tft.width() / 2;
int centerY = tft.height() / 2 - 40;
for (int r = 30; r > 0; r -= 2) {
uint16_t color = rgbTo565(0, 150 - r * 3, 200 - r * 2);
tft.drawCircle(centerX, centerY, r, color);
}
tft.fillCircle(centerX, centerY, 15, rgbTo565(0, 180, 240));
drawHighlightedTitle(centerX - 150, centerY + 50, "Smart VitroGrov", 3);
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(centerX - 100, centerY + 140);
tft.println("ESP32-S3 TFT Kontrolnik");
tft.setTextSize(1);
tft.setTextColor(rgbTo565(200, 200, 100));
tft.setCursor(centerX - 40, centerY + 160);
tft.println("Ver 3.0 + KLIMA NADZOR");
tft.setCursor(centerX - 70, centerY + 190);
tft.setTextColor(ST77XX_GREEN);
tft.println("Inicializiram sisteme...");
static int bootDot = 0;
for (int i = 0; i < 5; i++) {
tft.setCursor(centerX - 40 + i * 10, centerY + 210);
tft.setTextColor(i <= bootDot ? ST77XX_YELLOW : rgbTo565(40, 40, 60));
tft.print("●");
delay(200);
bootDot++;
}
currentState = STATE_BOOTING;
}
void drawHighlightedTitle(int x, int y, const char* text, int textSize) {
tft.setTextSize(textSize);
int charWidth = 6 * textSize;
int currentX = x;
for (int i = 0; i < strlen(text); i++) {
char c = text[i];
if (c == 'S' || c == 'V' || c == 'G') {
tft.setTextColor(HIGHLIGHT_GREEN);
} else {
tft.setTextColor(ST77XX_WHITE);
}
tft.setCursor(currentX, y);
tft.print(c);
currentX += charWidth;
}
}
void drawStatusBar() {
if (WiFi.status() == WL_CONNECTED) {
if (!wifiConnected) {
wifiConnected = true;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
}
} else {
if (wifiConnected) {
wifiConnected = false;
wifiSSID = "Brez mreze";
wifiIP = "N/A";
wifiRSSI = 0;
}
}
if (settingsChangedFlag) {
tft.fillCircle(465, 10, 4, rgbTo565(255, 200, 0));
}
if (sdInitialized && useSDCard) {
tft.fillCircle(475, 10, 3, rgbTo565(80, 220, 100));
}
for (int y = 0; y < STATUS_BAR_HEIGHT; y++) {
uint16_t color = blendColor(STATUS_BAR_COLOR, rgbTo565(0, 60, 120), y * 100 / STATUS_BAR_HEIGHT);
tft.drawFastHLine(0, y, tft.width(), color);
}
tft.drawFastHLine(0, STATUS_BAR_HEIGHT, tft.width(), rgbTo565(0, 150, 255));
int textBaselineY = STATUS_BAR_HEIGHT / 2 + 4;
int leftSectionX = 5;
drawCompactWiFiIcon(leftSectionX, 2);
tft.setFont(&FreeSansBold9pt7b);
String displaySSID = wifiSSID;
if (displaySSID.length() > 10) {
displaySSID = displaySSID.substring(0, 8) + "..";
}
tft.setCursor(leftSectionX + 28, textBaselineY);
if (wifiConnected) {
tft.setTextColor(rgbTo565(200, 240, 255));
} else {
tft.setTextColor(rgbTo565(255, 150, 150));
}
tft.print(displaySSID);
bool isDST = false;
if (rtcInitialized) {
DateTime now = rtc.now();
int lastSundayMarch = 31;
for (int day = 31; day >= 25; day--) {
DateTime testDate(now.year(), 3, day, 0, 0, 0);
if (testDate.dayOfTheWeek() == 0) {
lastSundayMarch = day;
break;
}
}
int lastSundayOctober = 31;
for (int day = 31; day >= 25; day--) {
DateTime testDate(now.year(), 10, day, 0, 0, 0);
if (testDate.dayOfTheWeek() == 0) {
lastSundayOctober = day;
break;
}
}
if ((now.month() > 3 && now.month() < 10) ||
(now.month() == 3 && (now.day() > lastSundayMarch ||
(now.day() == lastSundayMarch && now.hour() >= 2))) ||
(now.month() == 10 && (now.day() < lastSundayOctober ||
(now.day() == lastSundayOctober && now.hour() < 3)))) {
isDST = true;
}
}
tft.setFont(&FreeSansBold9pt7b);
String timeStr = "";
String dateStr = "";
if (rtcInitialized) {
timeStr = currentTime.substring(0, 5);
dateStr = currentDate.substring(0, 5);
} else {
timeStr = "--:--";
dateStr = "--/--";
}
String fullDateTimeStr = timeStr + " " + dateStr;
int16_t dummyX, dummyY;
uint16_t dateTimeWidth, dateTimeHeight;
tft.getTextBounds(fullDateTimeStr, 0, 0, &dummyX, &dummyY, &dateTimeWidth, &dateTimeHeight);
int symbolWidth = 18;
int blockStartX = 130;
int symbolX = blockStartX + 9;
int symbolY = STATUS_BAR_HEIGHT / 2;
if (rtcInitialized) {
if (isDST) {
tft.fillCircle(symbolX, symbolY, 4, rgbTo565(255, 200, 50));
tft.fillCircle(symbolX, symbolY, 2, rgbTo565(255, 255, 100));
for (int i = 0; i < 8; i++) {
float angle = i * 45 * PI / 180;
int x1 = symbolX + 6 * cos(angle);
int y1 = symbolY + 6 * sin(angle);
int x2 = symbolX + 9 * cos(angle);
int y2 = symbolY + 9 * sin(angle);
tft.drawLine(x1, y1, x2, y2, rgbTo565(255, 200, 50));
}
} else {
tft.fillCircle(symbolX, symbolY, 4, rgbTo565(200, 220, 255));
tft.fillCircle(symbolX + 2, symbolY - 1, 3, STATUS_BAR_COLOR);
tft.fillCircle(symbolX + 1, symbolY, 1, rgbTo565(180, 200, 240));
tft.drawPixel(symbolX - 5, symbolY - 3, rgbTo565(255, 255, 200));
tft.drawPixel(symbolX - 3, symbolY - 5, rgbTo565(255, 255, 200));
tft.drawPixel(symbolX + 6, symbolY - 4, rgbTo565(255, 255, 200));
}
} else {
tft.setCursor(symbolX - 3, textBaselineY);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.print("?");
}
int textStartX = blockStartX + symbolWidth + 5;
tft.setCursor(textStartX, textBaselineY);
if (rtcInitialized) {
tft.setTextColor(rgbTo565(150, 255, 150));
tft.print(timeStr);
tft.setTextColor(rgbTo565(150, 200, 255));
tft.print(" ");
tft.print(dateStr);
} else {
tft.setTextColor(rgbTo565(255, 200, 50));
tft.print(fullDateTimeStr);
}
int moduleStartX = tft.width() - 220;
tft.setFont(&FreeSansBold9pt7b);
tft.setCursor(moduleStartX, textBaselineY);
tft.setTextColor(module1Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M1");
tft.setCursor(moduleStartX + 26, textBaselineY);
tft.setTextColor(module2Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M2");
tft.setCursor(moduleStartX + 52, textBaselineY);
tft.setTextColor(module3Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M3");
tft.setCursor(moduleStartX + 78, textBaselineY);
tft.setTextColor(module4Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M4");
int signalTextX = moduleStartX + 106;
String signalString = "N/A";
if (wifiConnected && wifiRSSI != 0) {
signalString = String(wifiRSSI) + "dBm";
} else if (autoConnecting) {
signalString = "...";
}
int16_t signalX, signalY;
uint16_t signalWidth, signalHeight;
tft.getTextBounds(signalString, 0, 0, &signalX, &signalY, &signalWidth, &signalHeight);
tft.setCursor(signalTextX, textBaselineY);
if (wifiConnected && wifiRSSI != 0) {
tft.setTextColor(getSignalColor(wifiRSSI));
} else {
tft.setTextColor(rgbTo565(150, 150, 150));
}
tft.print(signalString);
int barsX = signalTextX + signalWidth + 5;
if (barsX + 35 < tft.width()) {
drawWiFiBars(barsX, 3, wifiRSSI);
}
tft.setFont();
}
void drawCompactWiFiIcon(int x, int y) {
int iconHeight = 22;
int centerX = x + 12;
int centerY = STATUS_BAR_HEIGHT / 2;
if (WiFi.status() == WL_CONNECTED) {
for (int i = 0; i < 3; i++) {
int radius = 5 + i * 4;
uint16_t color = blendColor(WIFI_STRONG_COLOR, STATUS_BAR_COLOR, i * 40);
for (int angle = 180; angle < 360; angle += 10) {
int px = centerX + (radius * cos(angle * 3.14159 / 180));
int py = centerY + (radius * sin(angle * 3.14159 / 180));
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, color);
}
}
}
tft.fillCircle(centerX, centerY, 3, WIFI_STRONG_COLOR);
} else if (autoConnecting) {
tft.setFont(&FreeSansBold9pt7b);
tft.setTextSize(1);
tft.setCursor(x, centerY - 4);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.print("...");
tft.setFont();
} else {
tft.fillCircle(centerX, centerY, 5, WIFI_NONE_COLOR);
tft.drawCircle(centerX, centerY, 5, ST77XX_WHITE);
}
}
void drawWiFiBars(int x, int y, int rssi) {
int barWidth = 5;
int barSpacing = 1;
int barHeights[4] = { 5, 9, 13, 17 };
int totalBarsHeight = barHeights[3] + 2;
int startY = (STATUS_BAR_HEIGHT - totalBarsHeight) / 2;
int signalStrength = map(constrain(rssi, -100, -50), -100, -50, 0, 4);
for (int i = 0; i < 4; i++) {
int barX = x + i * (barWidth + barSpacing);
int barHeight = barHeights[i];
if (i < signalStrength) {
uint16_t barColor = getSignalColor(rssi);
tft.fillRect(barX, startY + (barHeights[3] - barHeight), barWidth, barHeight, barColor);
tft.drawRect(barX, startY + (barHeights[3] - barHeight), barWidth, barHeight, blendColor(barColor, ST77XX_WHITE, 30));
} else {
tft.fillRect(barX, startY + (barHeights[3] - barHeight), barWidth, barHeight, rgbTo565(60, 60, 60));
tft.drawRect(barX, startY + (barHeights[3] - barHeight), barWidth, barHeight, rgbTo565(100, 100, 100));
}
}
}
uint16_t getSignalColor(int rssi) {
if (rssi >= -50) return rgbTo565(80, 220, 100);
if (rssi >= -60) return rgbTo565(150, 220, 80);
if (rssi >= -70) return rgbTo565(255, 200, 50);
if (rssi >= -80) return rgbTo565(255, 150, 50);
return rgbTo565(255, 80, 80);
}
void updateStatusBar() {
unsigned long currentTime = millis();
if (currentTime - lastStatusUpdate > STATUS_UPDATE_INTERVAL) {
updateRTC();
if (WiFi.status() == WL_CONNECTED && !wifiConnected) {
wifiConnected = true;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
} else if (WiFi.status() != WL_CONNECTED && wifiConnected) {
wifiConnected = false;
wifiRSSI = 0;
}
drawStatusBar();
lastStatusUpdate = currentTime;
}
}
void drawGradientBackground() {
for (int y = STATUS_BAR_HEIGHT; y < tft.height(); y++) {
int progress = map(y, STATUS_BAR_HEIGHT, tft.height(), 0, 100);
uint8_t r = 10 + (progress * 2 / 100);
uint8_t g = 20 + (progress * 3 / 100);
uint8_t b = 40 + (progress * 6 / 100);
tft.drawFastHLine(0, y, tft.width(), rgbTo565(r, g, b));
}
for (int i = 0; i < 20; i++) {
int x = random(0, tft.width());
int y = random(STATUS_BAR_HEIGHT + 50, tft.height() - 50);
int size = random(1, 3);
uint16_t color = rgbTo565(40, 40, 60);
tft.fillCircle(x, y, size, color);
}
}
uint16_t blendColor(uint16_t color1, uint16_t color2, uint8_t ratio) {
uint8_t r1 = (color1 >> 11) & 0x1F;
uint8_t g1 = (color1 >> 5) & 0x3F;
uint8_t b1 = color1 & 0x1F;
uint8_t r2 = (color2 >> 11) & 0x1F;
uint8_t g2 = (color2 >> 5) & 0x3F;
uint8_t b2 = color2 & 0x1F;
uint8_t r = r1 + ((r2 - r1) * ratio / 100);
uint8_t g = g1 + ((g2 - g1) * ratio / 100);
uint8_t b = b1 + ((b2 - b1) * ratio / 100);
return (r << 11) | (g << 5) | b;
}
void highlightButton(int x, int y, int w, int h, bool highlight) {
if (highlight) {
for (int i = 0; i < 3; i++) {
tft.drawRoundRect(x - i, y - i, w + 2 * i, h + 2 * i, 12 + i,
blendColor(ST77XX_YELLOW, ST77XX_WHITE, i * 30));
}
} else {
for (int i = 0; i < 3; i++) {
tft.drawRoundRect(x - i, y - i, w + 2 * i, h + 2 * i, 12 + i,
rgbTo565(10, 20, 40));
}
}
}
void drawHamburgerMenu(int x, int y, int size) {
int barWidth = size;
int barHeight = 3;
int spacing = 5;
tft.fillRoundRect(x - 5, y - 5, size + 10, 3 * (barHeight + spacing) - spacing + 10,
6, rgbTo565(20, 20, 40));
tft.drawRoundRect(x - 4, y - 4, size + 8, 3 * (barHeight + spacing) - spacing + 8,
5, rgbTo565(100, 150, 255));
for (int i = 0; i < 3; i++) {
int barY = y + i * (barHeight + spacing);
tft.fillRoundRect(x, barY, barWidth, barHeight, 2, rgbTo565(255, 255, 255));
tft.drawRoundRect(x, barY, barWidth, barHeight, 2, rgbTo565(200, 200, 255));
}
if (millis() % 2000 < 1000) {
tft.fillCircle(x + size + 3, y + size/2 - 2, 2, rgbTo565(0, 255, 100));
}
}
void showWiFiQuickInfo() {
tft.fillRect(0, STATUS_BAR_HEIGHT, tft.width(), 60, rgbTo565(30, 60, 100));
tft.drawRect(0, STATUS_BAR_HEIGHT, tft.width(), 60, rgbTo565(0, 150, 255));
tft.setCursor(10, STATUS_BAR_HEIGHT + 10);
tft.setTextColor(rgbTo565(220, 240, 255));
tft.setTextSize(1);
if (wifiConnected) {
tft.print("SSID: ");
tft.setTextColor(rgbTo565(150, 255, 150));
tft.println(wifiSSID);
tft.setCursor(10, STATUS_BAR_HEIGHT + 25);
tft.setTextColor(rgbTo565(220, 240, 255));
tft.print("IP: ");
tft.setTextColor(rgbTo565(150, 200, 255));
tft.println(wifiIP);
tft.setCursor(10, STATUS_BAR_HEIGHT + 40);
tft.setTextColor(rgbTo565(220, 240, 255));
tft.print("Signal: ");
tft.setTextColor(rgbTo565(255, 255, 150));
tft.print(wifiRSSI);
tft.println(" dBm");
} else {
tft.setTextColor(rgbTo565(255, 150, 150));
tft.println("WiFi NI POVEZAN");
tft.setCursor(10, STATUS_BAR_HEIGHT + 25);
tft.setTextColor(rgbTo565(255, 255, 150));
tft.println("Pritisni 'WiFi CONNECT'");
}
delay(3000);
redrawCurrentScreen();
}
void drawSimpleButton(int x, int y, int w, int h, uint16_t color, const char* label) {
drawNormalButton(x, y, w, h, color, label);
}
void drawWideButton(int x, int y, int w, int h, uint16_t color, const char* label) {
drawMenuButton(x, y, w, h, color, label);
}
void drawCenteredButtonWithText(int x, int y, int w, int h, uint16_t color, const char* text) {
drawMenuButton(x, y, w, h, color, text);
}
void diagnoseESPNow() {
Serial.println("\n=== DIAGNOSTIKA ESP-NOW ===");
wifi_mode_t currentMode = WiFi.getMode();
Serial.printf("WiFi nacin: %d (1=STA, 2=AP, 3=AP+STA)\n", currentMode);
Serial.print("MAC naslov glavnega sistema: ");
Serial.println(WiFi.macAddress());
Serial.printf("WiFi status: %d\n", WiFi.status());
Serial.printf("WiFi kanal: %d\n", WiFi.channel());
Serial.print("MAC naslov modula 1: ");
for (int i = 0; i < 6; i++) {
Serial.printf("%02X", module1MAC[i]);
if (i < 5) Serial.print(":");
}
Serial.println();
Serial.print("MAC naslov modula 2: ");
for (int i = 0; i < 6; i++) {
Serial.printf("%02X", module2MAC[i]);
if (i < 5) Serial.print(":");
}
Serial.println();
Serial.print("MAC naslov modula 3: ");
for (int i = 0; i < 6; i++) {
Serial.printf("%02X", module3MAC[i]);
if (i < 5) Serial.print(":");
}
Serial.println();
bool peer1Exists = esp_now_is_peer_exist(module1MAC);
bool peer2Exists = esp_now_is_peer_exist(module2MAC);
bool peer3Exists = esp_now_is_peer_exist(module3MAC);
Serial.printf("Peer (modul 1) obstaja: %s\n", peer1Exists ? "DA" : "NE");
Serial.printf("Peer (modul 2) obstaja: %s\n", peer2Exists ? "DA" : "NE");
Serial.printf("Peer (modul 3) obstaja: %s\n", peer3Exists ? "DA" : "NE");
esp_now_peer_num_t peerNum;
esp_err_t err = esp_now_get_peer_num(&peerNum);
if (err == ESP_OK) {
Serial.printf("Stevilo peerjev: %d\n", peerNum.total_num);
}
Serial.printf("Modul 1 aktiven: %s (zadnji prejem pred %lu ms)\n",
module1Active ? "DA" : "NE",
module1Active ? (millis() - lastModule1Time) : 0);
Serial.printf("Modul 2 aktiven: %s (zadnji prejem pred %lu ms)\n",
module2Active ? "DA" : "NE",
module2Active ? (millis() - lastModule2Time) : 0);
Serial.printf("Modul 3 aktiven: %s (zadnji prejem pred %lu ms)\n",
module3Active ? "DA" : "NE",
module3Active ? (millis() - lastModule3Time) : 0);
Serial.println("=== KONEC DIAGNOSTIKE ===\n");
}
void diagnoseSDCard() {
Serial.println("\n=== DIAGNOSTIKA SD KARTICE ===");
Serial.printf("CS pin %d: ", SD_CS_PIN);
pinMode(SD_CS_PIN, OUTPUT);
digitalWrite(SD_CS_PIN, LOW);
delay(10);
Serial.println("OK");
Serial.printf("SCK pin %d: ", SD_SCK_PIN);
pinMode(SD_SCK_PIN, OUTPUT);
digitalWrite(SD_SCK_PIN, HIGH);
delay(10);
digitalWrite(SD_SCK_PIN, LOW);
Serial.println("OK");
Serial.printf("MOSI pin %d: ", SD_MOSI_PIN);
pinMode(SD_MOSI_PIN, OUTPUT);
digitalWrite(SD_MOSI_PIN, HIGH);
delay(10);
digitalWrite(SD_MOSI_PIN, LOW);
Serial.println("OK");
Serial.printf("MISO pin %d: ", SD_MISO_PIN);
pinMode(SD_MISO_PIN, INPUT_PULLUP);
int misoValue = digitalRead(SD_MISO_PIN);
Serial.printf("%d (1=HIGH, 0=LOW)\n", misoValue);
Serial.println("\nPoskusam inicializirati z nizko hitrostjo...");
digitalWrite(TFT_CS, HIGH);
delay(10);
SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
if (SD.begin(SD_CS_PIN, SPI, 400000)) {
Serial.println("✓ SD kartica inicializirana pri 400kHz!");
File root = SD.open("/");
if (root) {
Serial.println(" Vsebina korenske mape:");
File file = root.openNextFile();
while (file) {
if (!file.isDirectory()) {
Serial.printf(" %s (%d bytes)\n", file.name(), file.size());
}
file = root.openNextFile();
}
root.close();
}
SD.end();
} else {
Serial.println("✗ SD kartice ni mogoce inicializirati tudi pri 400kHz!");
}
digitalWrite(TFT_CS, LOW);
Serial.println("=== KONEC DIAGNOSTIKE ===\n");
}
void diagnoseSDPins() {
Serial.println("\n=== DIAGNOSTIKA SD PINOV ===");
int sdPins[] = { SD_CS_PIN, SD_SCK_PIN, SD_MOSI_PIN, SD_MISO_PIN };
const char* pinNames[] = { "CS", "SCK", "MOSI", "MISO" };
for (int i = 0; i < 4; i++) {
int pin = sdPins[i];
Serial.printf("%s (pin %d): ", pinNames[i], pin);
if (pin >= 0 && pin <= 48) {
if ((pin >= 22 && pin <= 25) || pin == 30 || pin == 31) {
Serial.println("⚠ OPOZORILO: Pin je rezerviran!");
} else {
Serial.println("OK (veljaven pin)");
}
} else {
Serial.println("❌ NEVELJAVEN PIN!");
}
}
pinMode(SD_MISO_PIN, INPUT_PULLUP);
delay(10);
int misoValue = digitalRead(SD_MISO_PIN);
Serial.printf("\nMISO pin %d stanje: %d (1=HIGH, 0=LOW)\n",
SD_MISO_PIN, misoValue);
Serial.printf("Ce je 1: SD kartica se ne odziva\n");
Serial.printf("Ce je 0: SD kartica je morda prisotna\n");
Serial.println("=== KONEC DIAGNOSTIKE ===\n");
}
void testSDCard() {
Serial.println("\n=== TEST SD KARTICE ===");
if (!sdInitialized) {
Serial.println("SD kartica ni inicializirana!");
if (initializeSDCard()) {
} else {
return;
}
}
File testFile = SD.open("/test.txt", FILE_WRITE);
if (testFile) {
testFile.println("Test pisanja na SD kartico");
testFile.println(String("Cas: ") + millis());
testFile.close();
Serial.println("✓ Test pisanja USPESEN");
testFile = SD.open("/test.txt", FILE_READ);
if (testFile) {
Serial.println("Vsebina test.txt:");
while (testFile.available()) {
Serial.write(testFile.read());
}
testFile.close();
SD.remove("/test.txt");
}
} else {
Serial.println("✗ Test pisanja NEUSPESEN");
}
}
void testSDHardware() {
Serial.println("\n=== TEST SD HARDWARE ===");
int pins[] = { SD_CS_PIN, SD_SCK_PIN, SD_MOSI_PIN, SD_MISO_PIN };
const char* names[] = { "CS", "SCK", "MOSI", "MISO" };
for (int i = 0; i < 4; i++) {
pinMode(pins[i], INPUT_PULLUP);
delay(10);
int val = digitalRead(pins[i]);
Serial.printf("%s (pin %d): %d\n", names[i], pins[i], val);
}
Serial.println("=== KONEC TESTA ===\n");
}
bool syncTimeWithNTP() {
if (!wifiConnected) {
Serial.println("NTP: WiFi ni povezan, ne morem sinhronizirati");
return false;
}
Serial.println("NTP: Sinhroniziram čas...");
configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
struct tm timeinfo;
int attempts = 0;
bool timeSet = false;
while (!getLocalTime(&timeinfo, 500) && attempts < 20) {
attempts++;
Serial.printf("NTP: Poskus %d/20\n", attempts);
delay(500);
}
if (attempts >= 20) {
Serial.println("NTP: Časovna sinhronizacija ni uspela!");
return false;
}
if (rtcInitialized) {
int offsetHours = GMT_OFFSET_SEC / 3600;
if (timeinfo.tm_isdst > 0) {
offsetHours += 1;
}
DateTime ntpTime(
timeinfo.tm_year + 1900,
timeinfo.tm_mon + 1,
timeinfo.tm_mday,
timeinfo.tm_hour,
timeinfo.tm_min,
timeinfo.tm_sec);
rtc.adjust(ntpTime);
ntpSynchronized = true;
lastNTPSync = millis();
Serial.printf("NTP: Čas nastavljen na %02d:%02d:%02d %02d.%02d.%04d\n",
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900);
Serial.printf("NTP: Poletni čas: %s\n", timeinfo.tm_isdst ? "DA (CEST)" : "NE (CET)");
return true;
}
return false;
}
void checkWiFiChannel() {
Serial.println("\n=== PREVERJANJE WIFI KANALA ===");
if (WiFi.status() == WL_CONNECTED) {
int currentChannel = WiFi.channel();
Serial.printf("Trenutni WiFi kanal: %d\n", currentChannel);
if (currentChannel != 1) {
Serial.printf("⚠️ WiFi je na kanalu %d, moduli pa na kanalu 1\n", currentChannel);
Serial.println(" Za ESP-NOW komunikacijo morata biti kanala enaka!");
Serial.println(" Priporocilo: Spremenite kanal routerja na 1");
} else {
Serial.println("✅ Kanal je pravilno nastavljen na 1");
}
} else {
Serial.println("WiFi ni povezan, kanal ni določen");
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
Serial.println("📡 Kanal nastavljen na 1 za ESP-NOW (WiFi ni povezan)");
}
Serial.println("================================\n");
}
void checkNTPSync() {
if (wifiConnected && !ntpSynchronized) {
syncTimeWithNTP();
}
if (wifiConnected && ntpSynchronized) {
unsigned long currentTime = millis();
if (currentTime - lastNTPSync > 6 * 3600000UL) {
syncTimeWithNTP();
}
}
}
void initializeRTC() {
Serial.println("Inicializacija RTC (DS3231)...");
Wire.beginTransmission(0x68);
byte error = Wire.endTransmission();
if (error != 0) {
rtcInitialized = false;
return;
}
if (!rtc.begin()) {
rtcInitialized = false;
return;
}
if (rtc.lostPower()) {
}
rtcInitialized = true;
updateRTC();
}
void updateRTC() {
if (!rtcInitialized) return;
unsigned long currentTimeMillis = millis();
if (currentTimeMillis - lastRTCUpdate > RTC_UPDATE_INTERVAL) {
DateTime now = rtc.now();
char timeBuffer[9];
snprintf(timeBuffer, sizeof(timeBuffer), "%02d:%02d:%02d",
now.hour(), now.minute(), now.second());
currentTime = String(timeBuffer);
char dateBuffer[11];
snprintf(dateBuffer, sizeof(dateBuffer), "%02d/%02d/%04d",
now.day(), now.month(), now.year());
currentDate = String(dateBuffer);
lastRTCUpdate = currentTimeMillis;
}
}
void checkDaylightSavingTime() {
if (!rtcInitialized) return;
DateTime now = rtc.now();
bool isDST = false;
int lastSundayMarch = 31;
for (int day = 31; day >= 25; day--) {
DateTime testDate(now.year(), 3, day, 0, 0, 0);
if (testDate.dayOfTheWeek() == 0) {
lastSundayMarch = day;
break;
}
}
int lastSundayOctober = 31;
for (int day = 31; day >= 25; day--) {
DateTime testDate(now.year(), 10, day, 0, 0, 0);
if (testDate.dayOfTheWeek() == 0) {
lastSundayOctober = day;
break;
}
}
if ((now.month() > 3 && now.month() < 10) ||
(now.month() == 3 && (now.day() > lastSundayMarch ||
(now.day() == lastSundayMarch && now.hour() >= 2))) ||
(now.month() == 10 && (now.day() < lastSundayOctober ||
(now.day() == lastSundayOctober && now.hour() < 3)))) {
isDST = true;
}
static bool lastDSTStatus = false;
if (isDST != lastDSTStatus) {
Serial.printf("🌞 Poletni čas: %s\n", isDST ? "AKTIVEN (CEST)" : "NEAKTIVEN (CET)");
lastDSTStatus = isDST;
}
}
String getRTCTemperature() {
if (!rtcInitialized) return "N/A";
float temp = rtc.getTemperature();
return String(temp, 1) + " C";
}
String getSoilMoistureString() {
return String(soilMoisturePercent) + "%";
}
void calibrateSoilSensor() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.setCursor(160, 25);
tft.println("KALIBRACIJA SENZORJA");
tft.drawRoundRect(20, 50, 440, 180, 10, rgbTo565(255, 200, 50));
tft.fillRoundRect(21, 51, 438, 178, 10, rgbTo565(40, 30, 20));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(220, 200, 180));
tft.setCursor(40, 70);
tft.println("Navodila za kalibracijo:");
tft.setCursor(40, 90);
tft.println("1. Senzor postavite v SUH zrak");
tft.setCursor(40, 110);
tft.println("2. Pritisnite 'SUHO'");
tft.setCursor(40, 130);
tft.println("3. Senzor potopite v VODO");
tft.setCursor(40, 150);
tft.println("4. Pritisnite 'MOKRO'");
tft.setCursor(40, 170);
tft.println("5. Pritisnite 'SHARNI' za konec");
tft.setCursor(40, 190);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print("Trenutna vlaga: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.print(soilMoisturePercent);
tft.println("%");
drawMenuButton(50, 220, 120, 40, rgbTo565(255, 100, 100), "SUHO");
drawMenuButton(190, 220, 120, 40, rgbTo565(100, 100, 255), "MOKRO");
drawMenuButton(330, 220, 120, 40, rgbTo565(76, 175, 80), "SHARNI");
drawMenuButton(190, 270, 120, 40, rgbTo565(245, 67, 54), "NAZAJ");
bool calibrating = true;
int dryValue = SOIL_DRY_VALUE;
int wetValue = SOIL_WET_VALUE;
bool drySet = false;
bool wetSet = false;
while (calibrating) {
tft.fillRect(40, 190, 200, 15, rgbTo565(40, 30, 20));
tft.setCursor(40, 190);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print("Trenutna vlaga: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.print(soilMoisturePercent);
tft.println("%");
if (drySet) {
tft.fillRect(50, 205, 120, 15, rgbTo565(40, 30, 20));
tft.setCursor(50, 205);
tft.setTextColor(rgbTo565(255, 100, 100));
tft.print("Suho: ");
tft.println(dryValue);
}
if (wetSet) {
tft.fillRect(190, 205, 120, 15, rgbTo565(40, 30, 20));
tft.setCursor(190, 205);
tft.setTextColor(rgbTo565(100, 100, 255));
tft.print("Mokro: ");
tft.println(wetValue);
}
if (ts.touched()) {
TS_Point p = ts.getPoint();
int x = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int y = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
if (y > 220 && y < 260) {
if (x > 50 && x < 170) {
dryValue = map(soilMoisturePercent, 0, 100, SOIL_DRY_VALUE, SOIL_WET_VALUE);
drySet = true;
showTemporaryMessage("Suha vrednost shranjena!", rgbTo565(80, 220, 100), 1000);
} else if (x > 190 && x < 310) {
wetValue = map(soilMoisturePercent, 0, 100, SOIL_DRY_VALUE, SOIL_WET_VALUE);
wetSet = true;
showTemporaryMessage("Mokra vrednost shranjena!", rgbTo565(80, 220, 100), 1000);
} else if (x > 330 && x < 450) {
if (!drySet || !wetSet) {
showTemporaryMessage("Najprej izmerite obe vrednosti!", rgbTo565(255, 80, 80), 2000);
} else if (dryValue <= wetValue) {
showTemporaryMessage("Suha vrednost mora biti visja od mokre!", rgbTo565(255, 80, 80), 2000);
} else {
preferences.begin("soil-calib", false);
preferences.putInt("dryValue", dryValue);
preferences.putInt("wetValue", wetValue);
preferences.end();
SOIL_DRY_VALUE = dryValue;
SOIL_WET_VALUE = wetValue;
saveAllSettings();
showTemporaryMessage("Kalibracija shranjena!", rgbTo565(80, 220, 100), 2000);
calibrating = false;
}
}
} else if (y > 270 && y < 310 && x > 190 && x < 310) {
calibrating = false;
}
}
delay(100);
}
showSoilMoistureInfo();
}
// ==================== LDR KALIBRACIJSKI ZASLON ====================
void showLDRCalibrationScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 215, 0));
tft.setCursor(130, 25);
tft.println("KALIBRACIJA LDR SENZORJEV");
// Okvir - zmanjšana višina
tft.drawRoundRect(10, 40, 460, 210, 10, rgbTo565(255, 215, 0));
tft.fillRoundRect(11, 41, 458, 208, 10, rgbTo565(30, 30, 50));
int yOffset = 55;
int lineHeight = 16;
// ========== NOTRANJI LDR (Modul 4) ==========
tft.setTextSize(1);
tft.setTextColor(rgbTo565(100, 200, 255));
tft.setCursor(20, yOffset);
tft.println("NOTRANJI LDR (Vitrina - Modul 4)");
tft.setCursor(20, yOffset + lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("Trenutna vrednost: ");
tft.setTextColor(LIGHT_COLOR);
tft.printf("%d %% (%d lx)", module4LightPercent, (int)ldrInternalLux);
tft.setCursor(20, yOffset + lineHeight + 10);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("Kalibracija: 0% = ");
tft.setTextColor(rgbTo565(100, 200, 100));
tft.print((int)internalLuxMin);
tft.print(" lx, 100% = ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.print((int)internalLuxMax);
tft.println(" lx");
// Gumbi za notranji LDR
int btnW = 100;
int btnH = 26;
int btnY1 = yOffset + lineHeight + 28;
drawMenuButton(20, btnY1, btnW, btnH, rgbTo565(80, 80, 100), "TEMNA");
drawMenuButton(130, btnY1, btnW, btnH, rgbTo565(255, 200, 50), "SVETLA");
drawMenuButton(240, btnY1, 80, btnH, rgbTo565(76, 175, 80), "RESET");
// ========== ZUNANJI LDR (Modul 1) ==========
int yOffset2 = btnY1 + btnH + 20;
tft.setTextColor(rgbTo565(100, 200, 255));
tft.setCursor(20, yOffset2);
tft.println("ZUNANJI LDR (Modul 1)");
tft.setCursor(20, yOffset2 + lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("Trenutna vrednost: ");
tft.setTextColor(LIGHT_COLOR);
if (module1Active) {
if (externalLux < 10) tft.printf("%.1f lx", externalLux);
else if (externalLux < 1000) tft.printf("%.0f lx", externalLux);
else tft.printf("%.1f klx", externalLux / 1000.0);
} else {
tft.print("-- lx (modul nedosegljiv)");
}
tft.setCursor(20, yOffset2 + lineHeight + 10);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.print("Kalibracija: izhod = (vhod + ");
tft.setTextColor(rgbTo565(100, 200, 100));
tft.print((int)externalLuxOffset);
tft.print(") × ");
tft.setTextColor(rgbTo565(255, 150, 100));
tft.printf("%.2f", externalLuxScale);
// Gumbi za zunanji LDR
int btnY2 = yOffset2 + lineHeight + 28;
drawMenuButton(20, btnY2, btnW, btnH, rgbTo565(80, 80, 100), "TEMNA");
drawMenuButton(130, btnY2, btnW, btnH, rgbTo565(255, 200, 50), "SVETLA");
drawMenuButton(240, btnY2, 80, btnH, rgbTo565(76, 175, 80), "RESET");
// ========== Spodnji gumbi - premaknjeni višje ==========
int bottomY = btnY2 + btnH + 15;
drawMenuButton(80, bottomY, 140, 35, rgbTo565(76, 175, 80), "SHRANI");
drawMenuButton(260, bottomY, 140, 35, rgbTo565(245, 67, 54), "NAZAJ");
// Prikaz statusa kalibracije - če je aktiven
if (ldrCalibStep != CALIB_NONE) {
tft.fillRect(20, bottomY - 12, 440, 15, rgbTo565(30, 30, 50));
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(30, bottomY - 10);
switch(ldrCalibStep) {
case CALIB_INTERNAL_DARK:
tft.print("📷 NOTRANJI: Zaprite vitrino (popolna tema) in pritisnite TEMNA");
break;
case CALIB_INTERNAL_BRIGHT:
tft.print("☀️ NOTRANJI: Osvetlite vitrino (največ svetlobe) in pritisnite SVETLA");
break;
case CALIB_EXTERNAL_DARK:
tft.print("🌙 ZUNANJI: Pokrijte zunanji senzor (popolna tema) in pritisnite TEMNA");
break;
case CALIB_EXTERNAL_BRIGHT:
tft.print("☀️ ZUNANJI: Osvetlite zunanji senzor (sonce) in pritisnite SVETLA");
break;
}
}
currentState = STATE_LDR_CALIBRATION;
}
// ========== FUNKCIJA ZA POSODOBITEV PRETVORBE ==========
void updateLDRConversion() {
// Notranji LDR: procenti -> luks (Modul 4)
if (module4Active && module4LightPercent >= 0 && module4LightPercent <= 100) {
float newLux = internalLuxMin + (module4LightPercent / 100.0) * (internalLuxMax - internalLuxMin);
if (newLux < 0) newLux = 0;
if (newLux != ldrInternalLux) {
ldrInternalLux = newLux;
Serial.printf(" 🔆 Notranji LDR: %d%% → %.0f lx (kalibrirano: min=%.0f, max=%.0f)\n",
module4LightPercent, ldrInternalLux, internalLuxMin, internalLuxMax);
}
}
// Zunanji LDR: (surova vrednost + offset) * scale (Modul 1)
if (module1Active && externalLux > 0) {
float rawLux = externalLux;
float calibratedLux = (rawLux + externalLuxOffset) * externalLuxScale;
if (calibratedLux < 0) calibratedLux = 0;
if (abs(calibratedLux - rawLux) > 1) { // Če je razlika večja od 1 lx
externalLux = calibratedLux;
Serial.printf(" 🔆 Zunanji LDR: surovo=%.0f lx → kalibrirano=%.0f lx (offset=%.1f, scale=%.3f)\n",
rawLux, externalLux, externalLuxOffset, externalLuxScale);
}
}
}
// ========== FUNKCIJA ZA SHRANJEVANJE LDR KALIBRACIJE ==========
void saveLDRCalibration() {
preferences.begin("ldr-calib", false);
preferences.putFloat("intLuxMin", internalLuxMin);
preferences.putFloat("intLuxMax", internalLuxMax);
preferences.putFloat("extLuxOff", externalLuxOffset);
preferences.putFloat("extLuxScale", externalLuxScale);
preferences.end();
Serial.println("✅ LDR kalibracija shranjena!");
Serial.printf(" Notranji: 0%%=%d lx, 100%%=%d lx\n", (int)internalLuxMin, (int)internalLuxMax);
Serial.printf(" Zunanji: offset=%.1f, scale=%.2f\n", externalLuxOffset, externalLuxScale);
showTemporaryMessage("LDR kalibracija shranjena!", rgbTo565(80, 220, 100), 2000);
}
// ========== FUNKCIJA ZA NALAGANJE LDR KALIBRACIJE ==========
void loadLDRCalibration() {
preferences.begin("ldr-calib", true);
internalLuxMin = preferences.getFloat("intLuxMin", 0.0);
internalLuxMax = preferences.getFloat("intLuxMax", 20000.0);
externalLuxOffset = preferences.getFloat("extLuxOff", 0.0);
externalLuxScale = preferences.getFloat("extLuxScale", 1.0);
preferences.end();
// Preveri veljavnost vrednosti
if (internalLuxMin < 0) internalLuxMin = 0;
if (internalLuxMax <= internalLuxMin) internalLuxMax = internalLuxMin + 1000;
if (externalLuxScale <= 0) externalLuxScale = 1.0;
Serial.println("✅ LDR kalibracija naložena!");
Serial.printf(" Notranji: 0%%=%d lx, 100%%=%d lx\n", (int)internalLuxMin, (int)internalLuxMax);
Serial.printf(" Zunanji: offset=%.1f, scale=%.2f\n", externalLuxOffset, externalLuxScale);
updateLDRConversion();
}
// ========== FUNKCIJA ZA RESET LDR KALIBRACIJE ==========
void resetLDRCalibration() {
internalLuxMin = 0.0;
internalLuxMax = 20000.0;
externalLuxOffset = 0.0;
externalLuxScale = 1.0;
saveLDRCalibration();
updateLDRConversion();
showTemporaryMessage("LDR kalibracija ponastavljena\nna privzete vrednosti!", rgbTo565(255, 200, 50), 2000);
}
// ========== HANDLER ZA DOTIK NA KALIBRACIJSKEM ZASLONU ==========
void handleLDRCalibrationTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int btnW = 100;
int btnH = 26;
// Koordinate za notranji LDR
int yOffset = 55;
int lineHeight = 16;
int internalBtnY = yOffset + lineHeight + 28;
// Koordinate za zunanji LDR
int externalBtnY = internalBtnY + btnH + 20 + lineHeight + 28;
// Spodnji gumbi - premaknjeni višje
int bottomY = externalBtnY + btnH + 15;
int bottomW = 140;
int bottomH = 35;
// ===== NOTRANJI LDR GUMBI =====
if (y > internalBtnY && y < internalBtnY + btnH) {
// Gumb TEMNA za notranji (x=20, width=100)
if (x > 20 && x < 20 + btnW) {
if (ldrCalibStep == CALIB_INTERNAL_DARK || ldrCalibStep == CALIB_NONE) {
calibInternalDarkPercent = module4LightPercent;
internalLuxMin = 0; // Predpostavimo da je tema 0 lx
ldrCalibStep = CALIB_NONE;
showTemporaryMessage("Notranji TEMNA shranjena!\n(0% = 0 lx)", rgbTo565(80, 220, 100), 1500);
showLDRCalibrationScreen();
} else {
showTemporaryMessage("Najprej dokončajte trenutno kalibracijo!", rgbTo565(255, 80, 80), 1500);
}
return;
}
// Gumb SVETLA za notranji (x=130, width=100)
if (x > 130 && x < 130 + btnW) {
if (ldrCalibStep == CALIB_INTERNAL_BRIGHT || ldrCalibStep == CALIB_NONE) {
if (calibInternalDarkPercent > 0) {
// Izračunamo luks pri 100% glede na znano svetlobo
showLuxInputPopup(true); // true = notranji LDR
} else {
showTemporaryMessage("Najprej shranite TEMNO vrednost!", rgbTo565(255, 150, 50), 1500);
}
} else {
showTemporaryMessage("Najprej dokončajte trenutno kalibracijo!", rgbTo565(255, 80, 80), 1500);
}
return;
}
// Gumb RESET za notranji (x=240, width=80)
if (x > 240 && x < 240 + 80) {
internalLuxMin = 0;
internalLuxMax = 20000;
showTemporaryMessage("Notranji LDR ponastavljen!", rgbTo565(255, 200, 50), 1500);
showLDRCalibrationScreen();
return;
}
}
// ===== ZUNANJI LDR GUMBI =====
if (y > externalBtnY && y < externalBtnY + btnH) {
// Gumb TEMNA za zunanji (x=20, width=100)
if (x > 20 && x < 20 + btnW) {
if (module1Active) {
calibExternalDarkLux = externalLux;
externalLuxOffset = -calibExternalDarkLux; // Zamaknemo tako, da tema postane 0
showTemporaryMessage("Zunanji TEMNA shranjena!\nTema = 0 lx", rgbTo565(80, 220, 100), 1500);
showLDRCalibrationScreen();
} else {
showTemporaryMessage("Modul 1 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
}
return;
}
// Gumb SVETLA za zunanji (x=130, width=100)
if (x > 130 && x < 130 + btnW) {
if (module1Active && calibExternalDarkLux > 0) {
showLuxInputPopup(false); // false = zunanji LDR
} else if (!module1Active) {
showTemporaryMessage("Modul 1 ni dosegljiv!", rgbTo565(255, 80, 80), 1500);
} else {
showTemporaryMessage("Najprej shranite TEMNO vrednost!", rgbTo565(255, 150, 50), 1500);
}
return;
}
// Gumb RESET za zunanji (x=240, width=80)
if (x > 240 && x < 240 + 80) {
externalLuxOffset = 0;
externalLuxScale = 1.0;
showTemporaryMessage("Zunanji LDR ponastavljen!", rgbTo565(255, 200, 50), 1500);
showLDRCalibrationScreen();
return;
}
}
// ===== SPODNJI GUMBI =====
if (y > bottomY && y < bottomY + bottomH) {
// Gumb SHRANI (x=80, width=140)
if (x > 80 && x < 80 + bottomW) {
saveLDRCalibration();
updateLDRConversion();
showLDRCalibrationScreen();
return;
}
// Gumb NAZAJ (x=260, width=140)
if (x > 260 && x < 260 + bottomW) {
showMainMenu();
return;
}
}
}
// ========== POPUP ZA VNOS LUKS VREDNOSTI ==========
void showLuxInputPopup(bool isInternal) {
tft.fillRect(80, 150, 320, 120, rgbTo565(20, 40, 70));
tft.drawRect(80, 150, 320, 120, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(100, 170);
if (isInternal) {
tft.println("Vnesite dejansko vrednost svetlobe");
tft.setCursor(100, 190);
tft.print("pri 100% (npr. 15000 lx):");
} else {
tft.println("Vnesite dejansko vrednost svetlobe");
tft.setCursor(100, 190);
tft.print("pri močni svetlobi (npr. 80000 lx):");
}
// Preprost numerični vnos
String inputValue = "";
bool entering = true;
unsigned long startTime = millis();
// Gumbi za vnos
int btnW = 35;
int btnH = 25;
int numStartX = 100;
int numStartY = 215;
const char* numbers[] = {"0","1","2","3","4","5","6","7","8","9","⌫","✓"};
while (entering && (millis() - startTime < 30000)) {
// Nariši gumbe
for (int i = 0; i < 12; i++) {
int row = i / 6;
int col = i % 6;
int btnX = numStartX + col * (btnW + 5);
int btnY = numStartY + row * (btnH + 5);
tft.fillRoundRect(btnX, btnY, btnW, btnH, 5, rgbTo565(66, 135, 245));
tft.drawRoundRect(btnX, btnY, btnW, btnH, 5, ST77XX_WHITE);
tft.setCursor(btnX + 12, btnY + 8);
tft.setTextColor(ST77XX_WHITE);
tft.print(numbers[i]);
}
// Prikaz vnosa
tft.fillRect(180, 205, 100, 15, rgbTo565(20, 40, 70));
tft.setCursor(185, 208);
tft.setTextColor(rgbTo565(255, 255, 100));
tft.print(inputValue);
if (ts.touched()) {
TS_Point p = ts.getPoint();
int touchX = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int touchY = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
for (int i = 0; i < 12; i++) {
int row = i / 6;
int col = i % 6;
int btnX = numStartX + col * (btnW + 5);
int btnY = numStartY + row * (btnH + 5);
if (touchX > btnX && touchX < btnX + btnW && touchY > btnY && touchY < btnY + btnH) {
if (i == 10) { // ⌫
if (inputValue.length() > 0) {
inputValue.remove(inputValue.length() - 1);
}
} else if (i == 11) { // ✓
if (inputValue.length() > 0) {
float luxValue = inputValue.toFloat();
if (luxValue > 0) {
if (isInternal) {
internalLuxMax = luxValue;
showTemporaryMessage("Notranji SVETLA shranjena!\n100% = " + String((int)luxValue) + " lx", rgbTo565(80, 220, 100), 1500);
} else {
// Izračunamo scale faktor: želimo da izmerjena vrednost postane vnesena
// Trenutna izmerjena vrednost (po offsetu) je calibExternalBrightLux
float measuredAfterOffset = (calibExternalBrightLux + externalLuxOffset);
if (measuredAfterOffset > 0) {
externalLuxScale = luxValue / measuredAfterOffset;
}
showTemporaryMessage("Zunanji SVETLA shranjena!\nFaktor: " + String(externalLuxScale, 3), rgbTo565(80, 220, 100), 1500);
}
saveLDRCalibration();
updateLDRConversion();
}
entering = false;
}
} else {
inputValue += String(numbers[i]);
if (inputValue.length() > 6) inputValue = inputValue.substring(0, 6);
}
delay(200);
break;
}
}
}
delay(50);
}
showLDRCalibrationScreen();
}
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n\n=========================================");
Serial.println("ESP32-S3 ST7796 TFT s Touch - GLAVNI SISTEM");
Serial.println("=========================================\n");
diagnoseSDPins();
Serial.println("Inicializacija zaslona...");
tft.init(320, 480, 0, 0, ST7796S_BGR);
tft.setRotation(3);
tft.invertDisplay(true);
tft.fillScreen(ST77XX_BLACK);
showBootScreen();
Serial.println("Inicializacija touch...");
touchSPI.begin(T_CLK, T_DO, T_DIN, T_CS);
delay(100);
if (!ts.begin(touchSPI)) {
Serial.println("Napaka pri inicializaciji touch!");
} else {
ts.setRotation(3);
Serial.println("Touch inicializiran!");
}
Serial.println("Inicializacija I2C...");
Wire.begin(MCP_SDA, MCP_SCL);
Wire.setClock(100000);
delay(250);
scanI2CDevices();
Serial.println("Inicializacija Preferences...");
preferences.begin("system-settings", false);
preferences.end();
initializeTimeBlocks();
initializeGraphHistory();
initializeExternalGraphHistory();
loadGraphHistoryFromFlash();
initializeMCP23017();
delay(200);
initializeRelays();
initializeRTC();
delay(200);
initializeFlowSensors();
initializeMotor();
loadAllSettings(true);
loadPlantCollection();
loadLDRCalibration();
if (mcpInitialized) {
initializeRelays();
}
// ==================== INICIALIZACIJA SD KARTICE ====================
Serial.println("\n=== INICIALIZACIJA SD KARTICE (SAMODEJNO) ===");
pinMode(SD_CS_PIN, OUTPUT);
pinMode(SD_SCK_PIN, OUTPUT);
pinMode(SD_MOSI_PIN, OUTPUT);
pinMode(SD_MISO_PIN, INPUT_PULLUP);
digitalWrite(SD_CS_PIN, HIGH);
delay(50);
SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
SPIClass* sdSPI = new SPIClass(FSPI);
sdSPI->begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
digitalWrite(TFT_CS, HIGH);
delay(10);
int speeds[] = { 400000, 200000, 100000, 50000 };
bool sdOk = false;
for (int s = 0; s < 4; s++) {
Serial.printf("Poskus %d: %d Hz\n", s + 1, speeds[s]);
if (SD.begin(SD_CS_PIN, *sdSPI, speeds[s])) {
uint8_t cardType = SD.cardType();
if (cardType != CARD_NONE) {
Serial.println("✓ SD kartica USPESNO inicializirana!");
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf(" Velikost: %llu MB\n", cardSize);
sdInitialized = true;
useSDCard = true;
sdOk = true;
createSDStructure();
Serial.println(" Nalagam nastavitve s SD kartice...");
loadSettingsFromSD();
Serial.println(" Nalagam letni arhiv vlage tal s SD kartice...");
loadYearArchiveFromSD();
break;
}
}
delay(100);
}
if (sdInitialized && useSDCard) {
loadAllGraphsFromSD();
} else {
loadGraphHistoryFromFlash();
}
if (!sdOk) {
Serial.println("\n❌ SD kartica NI inicializirana!");
sdInitialized = false;
useSDCard = false;
Serial.println(" Nalagam nastavitve iz FLASH pomnilnika...");
loadAllSettings(true);
for (int i = 0; i < ARCHIVE_HISTORY_SIZE; i++) {
yearArchive[i].samples = 0;
}
archiveIndex = 0;
currentDay = 0;
dailySum = 0;
dailyMin = 100;
dailyMax = 0;
dailyCount = 0;
}
digitalWrite(TFT_CS, LOW);
Serial.println("=== KONEC SD INICIALIZACIJE ===\n");
Serial.println("Inicializiram 30-dnevno zgodovino za zaslon...");
for (int i = 0; i < SCREEN_HISTORY_SIZE; i++) {
screenSoilHistory[i] = 0;
}
screenHistoryIndex = 0;
lastScreenHistoryUpdate = 0;
// ==================== NASTAVI PRAVILNE MAC NASLOVE (POPRAVLJENI) ====================
Serial.println("\n=== NASTAVLJAM PRAVILNE MAC NASLOVE MODULOV ===");
// PRAVILNI MAC NASLOVI (pridobljeni iz logov modulov)
uint8_t fixedModule1MAC[] = {0x80, 0xB5, 0x4E, 0xC6, 0x0A, 0x04}; // Modul 1 - vremenski
uint8_t fixedModule2MAC[] = {0xB8, 0xF8, 0x62, 0xF8, 0x65, 0xC0}; // Modul 2 - namakalni
uint8_t fixedModule3MAC[] = {0x58, 0x8C, 0x81, 0xCB, 0xDC, 0x80}; // Modul 3 - senčenje
uint8_t fixedModule4MAC[] = {0x1A, 0xD8, 0xCB, 0x3F, 0x14, 0xD8}; // Modul 4 - vitrina
memcpy(module4MAC, fixedModule4MAC, 6);
memcpy(module1MAC, fixedModule1MAC, 6);
memcpy(module2MAC, fixedModule2MAC, 6);
memcpy(module3MAC, fixedModule3MAC, 6);
memcpy(module4MAC, fixedModule4MAC, 6);
Serial.print(" Modul 1 MAC: ");
for(int i = 0; i < 6; i++) Serial.printf("%02X%s", module1MAC[i], i<5 ? ":" : "");
Serial.println();
Serial.print(" Modul 2 MAC: ");
for(int i = 0; i < 6; i++) Serial.printf("%02X%s", module2MAC[i], i<5 ? ":" : "");
Serial.println();
Serial.print(" Modul 3 MAC: ");
for(int i = 0; i < 6; i++) Serial.printf("%02X%s", module3MAC[i], i<5 ? ":" : "");
Serial.println();
Serial.print(" Modul 4 MAC: ");
for(int i = 0; i < 6; i++) Serial.printf("%02X%s", module4MAC[i], i<5 ? ":" : "");
Serial.println();
Serial.println("================================================\n");
// ==================== POMEMBNO: NAJPREJ PRAVILNO INICIALIZIRAJ WIFI ====================
Serial.println("\n=== INICIALIZACIJA WIFI (OBVEZNO PRED ESP-NOW) ===");
// 1. Počisti stare nastavitve
WiFi.disconnect(true);
delay(100);
// 2. Nastavi način na STA
WiFi.mode(WIFI_STA);
delay(100);
// 3. Počakaj, da se WiFi inicializira
delay(500);
// 4. Preveri, ali je WiFi pravilno inicializiran
Serial.printf(" MAC naslov ESP32: %s\n", WiFi.macAddress().c_str());
if (WiFi.macAddress() == "00:00:00:00:00:00") {
Serial.println(" ⚠️ WiFi ni pravilno inicializiran! Poskusim znova...");
WiFi.mode(WIFI_AP_STA);
delay(100);
WiFi.mode(WIFI_STA);
delay(500);
Serial.printf(" Nov MAC naslov: %s\n", WiFi.macAddress().c_str());
}
// 5. Nastavi kanal na 1
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
Serial.println(" 📡 Kanal nastavljen na 1");
// 6. Dodaj omrežja za povezavo
setupWiFi();
// 7. Poskusi povezavo
autoConnectToWiFi();
// 8. Še enkrat preveri MAC naslov
Serial.printf(" Končni MAC naslov: %s\n", WiFi.macAddress().c_str());
Serial.printf(" WiFi status: %d\n", WiFi.status());
Serial.printf(" WiFi kanal: %d\n", WiFi.channel());
// ==================== INICIALIZACIJA ESP-NOW ====================
Serial.println("\n=== INICIALIZACIJA ESP-NOW ===");
// Počakaj, da se WiFi stabilizira
delay(500);
if (esp_now_init() != ESP_OK) {
Serial.println(" ❌ ESP-NOW napaka pri inicializaciji!");
// Poskusi ponovno
delay(500);
if (esp_now_init() != ESP_OK) {
Serial.println(" ❌ ESP-NOW ponovna inicializacija prav tako ni uspela!");
} else {
Serial.println(" ✅ ESP-NOW inicializiran (poskus 2)!");
}
} else {
Serial.println(" ✅ ESP-NOW inicializiran!");
}
// Registriraj callback za prejemanje
esp_now_register_recv_cb(esp_now_recv_cb_t(onModuleDataRecv));
// Dodaj peerje
esp_now_peer_info_t peerInfo;
// Modul 1
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module1MAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.printf(" ✅ Peer za modul 1 dodan\n");
}
// Modul 2
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module2MAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
esp_now_add_peer(&peerInfo);
// Modul 3
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module3MAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
esp_now_add_peer(&peerInfo);
// Modul 4
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module4MAC, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
esp_now_add_peer(&peerInfo);
Serial.println("=== KONEC INICIALIZACIJE ESP-NOW ===\n");
// ==================== TESTNI UKAZ ZA MODUL 3 ====================
delay(1000);
Serial.println("\n=== POŠILJAM TESTNI UKAZ MODULU 3 ===");
sendShadeCommand(CMD_STOP, 0, 0);
delay(500);
sendShadeCommand(CMD_MOVE_TO_POSITION, 50, 0);
delay(500);
sendShadeCommand(CMD_MOVE_TO_POSITION, 0, 0);
Serial.println("=== KONEC TESTNIH UKAZOV ===\n");
checkWiFiChannel();
// ==================== ZAKLJUČEK SETUP ====================
homeScreenBackgroundDrawn = false;
homeScreenStaticElementsDrawn = false;
showHomeScreen();
Serial.println("\n=========================================");
Serial.println("=== SETUP ZAKLJUCEN - SISTEM DELUJE ===");
Serial.println("=========================================\n");
Serial.println("=== TRENUTNO STANJE MODULOV ===");
Serial.printf("Modul 1: %s\n", module1Active ? "AKTIVEN" : "NEAKTIVEN");
Serial.printf("Modul 2: %s\n", module2Active ? "AKTIVEN" : "NEAKTIVEN");
Serial.printf("Modul 3: %s\n", module3Active ? "AKTIVEN" : "NEAKTIVEN");
Serial.printf("Modul 4: %s\n", module4Active ? "AKTIVEN" : "NEAKTIVEN");
Serial.println("===============================\n");
}
void loop() {
unsigned long now = millis();
handleTouch();
static unsigned long lastDataUpdate = 0;
if (now - lastDataUpdate > 500) {
updateFlowSensors();
updateRTC();
updateMCP23017();
lastDataUpdate = now;
}
static unsigned long lastDSTCheck = 0;
if (now - lastDSTCheck > 60000) {
checkDaylightSavingTime();
lastDSTCheck = now;
}
static unsigned long lastControlUpdate = 0;
if (now - lastControlUpdate > 2000) {
if (!isnan(internalTemperature) && !isnan(internalHumidity)) {
autoControlRelays();
}
updateVentilationControl();
lastControlUpdate = now;
}
static unsigned long lastShadeAuto = 0;
if (now - lastShadeAuto > 2000) {
autoControlShade();
lastShadeAuto = now;
}
static unsigned long lastLightingCheck = 0;
if (now - lastLightingCheck > 1000) {
if (lightingAutoMode && !lightingManualOverride) {
bool shouldBeOn = shouldRelaysBeOn();
if (shouldBeOn != relayStates[LIGHTING_RELAY_1]) {
setRelay(LIGHTING_RELAY_1, shouldBeOn);
Serial.printf("⏰ Časovna razsvetljava (rele 4): %s\n", shouldBeOn ? "VKLOP" : "IZKLOP");
}
}
lastLightingCheck = now;
}
static unsigned long lastAutoLightCheck = 0;
if (now - lastAutoLightCheck > 1000) {
checkAndControlLighting();
lastAutoLightCheck = now;
}
static unsigned long lastHistoryUpdate = 0;
if (now - lastHistoryUpdate > 1800000) {
updateGraphHistory();
updateExternalGraphHistory();
updateTrendHistory();
calculateTrends();
lastHistoryUpdate = now;
}
static unsigned long lastStatusCheck = 0;
if (now - lastStatusCheck > 5000) {
checkWiFiStatus();
checkModulesTimeout();
checkWarnings();
lastStatusCheck = now;
}
static unsigned long lastSaveCheck = 0;
if (now - lastSaveCheck > 5000) {
autoSaveSettings();
lastSaveCheck = now;
}
if (currentState == STATE_HOME_SCREEN) {
static unsigned long lastHomeScreenUpdate = 0;
if (now - lastHomeScreenUpdate > 1000) {
updateHomeScreenDynamicValues();
lastHomeScreenUpdate = now;
}
}
static unsigned long lastStatusDraw = 0;
if (now - lastStatusDraw > 2000) {
drawStatusBar();
lastStatusDraw = now;
}
if (plantCount > 0) {
static unsigned long lastPlantControl = 0;
if (now - lastPlantControl > 10000) {
controlVPDForPlants();
updateAirMovement();
checkMistingTimeout();
lastPlantControl = now;
}
}
static unsigned long lastGraphSave = 0;
if (now - lastGraphSave > 3600000) {
if (sdInitialized && useSDCard) {
saveAllGraphsToSD();
}
lastGraphSave = now;
}
static unsigned long lastNTPSyncCheck = 0;
if (wifiConnected && (now - lastNTPSyncCheck > 3600000)) {
syncTimeWithNTP();
lastNTPSyncCheck = now;
}
delay(10);
}
// Ali je možno da se gumb na začetnem zaslonu za releje GRET gretje pomakne v veliki krog za notranjo temperaturo tik nad številčnim izpisom za notranjo temperaturo in za HLAJ hlajenje tik pod oznako za C stopinje in po vertikali centrirani v krogu. Prav tako pa bi naredil z gumbi VLAZ in SUS, ki bi jih pomaknil po istem sistemu v veliki krog za notranjo vlago. Vse ostalo naj ostane kot je. Prosim če spremenjene funkcije izpišeš cele in ne samo spremenjenih delov vsako posebaj. Še enkrat bi povdaril, da razen gumbov za gretje, hlajenje, vlaženje in sušenje ne premikaj ničesar!Loading
ili9341-cap-touch
ili9341-cap-touch
T_CLK
T_CS
T_DIN
T_DO
T_IRQ
GRETJE
HLAJENJE
SUŠENJE
LUČI 1
VLAŽENJE
ČRPALKA
LUČI 2
ČRPALKA