// 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);
}
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);
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);
drawClockwiseProgressRing(smallCircleX, smallCircleY, smallOuterRadius, smallInnerRadius,
soilMoisturePercent, 0.0, 100.0, SOIL_PROGRESS_COLOR);
tft.fillCircle(smallCircleX, smallCircleY, smallCircleRadius - 4, SOIL_CIRCLE_COLOR);
tft.drawCircle(smallCircleX, smallCircleY, smallCircleRadius - 4, ST77XX_WHITE);
tft.drawCircle(smallCircleX, smallCircleY, smallOuterRadius, ST77XX_WHITE);
tft.setFont(&FreeSansBold12pt7b);
String soilStr = String(soilMoisturePercent);
int16_t soilX1, soilY1;
uint16_t soilW, soilH;
tft.getTextBounds(soilStr, 0, 0, &soilX1, &soilY1, &soilW, &soilH);
int soilTextX = smallCircleX - soilW / 2;
int soilTextY = smallCircleY + soilH / 2 - 5;
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(soilTextX, soilTextY);
tft.print(soilStr);
tft.setFont(&FreeSansBold9pt7b);
String soilSymbolStr = "%";
int16_t soilSymbolX1, soilSymbolY1;
uint16_t soilSymbolW, soilSymbolH;
tft.getTextBounds(soilSymbolStr, 0, 0, &soilSymbolX1, &soilSymbolY1, &soilSymbolW, &soilSymbolH);
int soilSymbolX = smallCircleX - soilSymbolW / 2;
int soilSymbolY = soilTextY + soilH - 2;
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(soilSymbolX, soilSymbolY);
tft.print(soilSymbolStr);
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);
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 leftColumnRelays[4] = { 0, 2, 4, 6 };
String leftLabels[4] = { "GRET", "VLAZ", "R5", "R7" };
tft.setFont();
tft.setTextSize(1);
for (int i = 0; i < 4; 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);
}
int rightColumnX = relayStartX + relayColumnWidth + relayColumnSpacing;
int rightColumnRelays[4] = { 1, 3, 5, 7 };
String rightLabels[4] = { "HLAJ", "SUS", "R6", "R8" };
for (int i = 0; i < 4; 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);
}
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();
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);
for (int r = innerRingRadius; r <= outerRingRadius; r += 2) {
tft.drawCircle(leftCircleX, circleY, r, PROGRESS_BG_COLOR);
}
for (int r = innerRingRadius; r <= outerRingRadius; r += 2) {
tft.drawCircle(rightCircleX, circleY, r, PROGRESS_BG_COLOR);
}
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;
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;
}
}
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 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;
int distanceToSoilCircle = sqrt(pow(x - smallCircleX, 2) + pow(y - smallCircleY, 2));
if (distanceToSoilCircle < smallCircleRadius + TOUCH_PADDING_CIRCLES) {
Serial.println("=== KROG ZA VLAGO 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;
}
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[4] = { 0, 2, 4, 6 };
int rightColumnRelays[4] = { 1, 3, 5, 7 };
int relayTouchPadding = TOUCH_PADDING_RELAYS;
for (int i = 0; i < 4; 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;
}
}
for (int i = 0; i < 4; 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 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[4] = { 0, 2, LIGHTING_RELAY_1, 6 };
String leftLabels[4] = { "GRET", "VLAZ", "CAS", "R7" };
int rightColumnRelays[4] = { 1, 3, LIGHTING_RELAY_2, 7 };
String rightLabels[4] = { "HLAJ", "SUS", "AVTO", "R8" };
tft.setFont();
tft.setTextSize(1);
for (int i = 0; i < 4; 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 < 4; 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_LDR_CALIBRATION:
handleLDRCalibrationTouch(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:
{
int buttonY = 280;
if (x > 40 && x < 200 && y > buttonY && y < buttonY + 35) {
showAirQualityScreen();
}
else if (x > 210 && x < 330 && y > buttonY && y < buttonY + 35) {
showModule4Info();
}
else if (x > 340 && x < 460 && y > buttonY && y < buttonY + 35) {
showInternalSensorsInfo();
}
}
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, 40);
tft.println("VITRINA (MODUL 4)");
tft.drawRoundRect(10, 50, 460, 170, 10, INTERNAL_COLOR);
tft.fillRoundRect(11, 51, 458, 168, 10, rgbTo565(40, 20, 50));
int leftColumnX = 30;
int rightColumnX = 260;
int yOffset = 65;
int lineHeight = 16;
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");
} 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, 12, rgbTo565(80, 60, 60));
tft.fillRect(leftColumnX, yOffset + 2 * lineHeight + 2, tempBarWidth, 12, TEMP_COLOR);
tft.drawRect(leftColumnX, yOffset + 2 * lineHeight + 2, 180, 12, 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, 12, rgbTo565(60, 60, 80));
tft.fillRect(leftColumnX, yOffset + 4 * lineHeight + 10, humidBarWidth, 12, HUMIDITY_COLOR);
tft.drawRect(leftColumnX, yOffset + 4 * lineHeight + 10, 180, 12, 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, 12, rgbTo565(60, 60, 60));
tft.fillRect(leftColumnX, yOffset + 6 * lineHeight + 20, lightBarWidth, 12, LIGHT_COLOR);
tft.drawRect(leftColumnX, yOffset + 6 * lineHeight + 20, 180, 12, 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, 12, rgbTo565(60, 60, 60));
tft.fillRect(rightColumnX, yOffset + 2 * lineHeight + 2, soilTempBarWidth, 12, rgbTo565(255, 150, 100));
tft.drawRect(rightColumnX, yOffset + 2 * lineHeight + 2, 180, 12, 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, 12, rgbTo565(60, 60, 60));
tft.fillRect(rightColumnX, yOffset + 4 * lineHeight + 10, soilBarWidth, 12, soilColor);
tft.drawRect(rightColumnX, yOffset + 4 * lineHeight + 10, 180, 12, 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);
}
int buttonWidth = 140;
int buttonHeight = 35;
int buttonSpacing = 20;
int buttonY1 = 240;
drawIconButton(40, buttonY1, buttonWidth, buttonHeight,
INTERNAL_COLOR, "OSVEZI", "refresh");
drawMenuButton(40 + buttonWidth + buttonSpacing, buttonY1,
buttonWidth, buttonHeight, rgbTo565(76, 175, 80), "KAK. ZRAKA");
int buttonY2 = buttonY1 + buttonHeight + 10;
drawMenuButton(40, buttonY2, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_INTERNAL_SENSORS_INFO;
}
void handleInternalSensorsTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int buttonWidth = 140;
int buttonHeight = 35;
int buttonSpacing = 20;
int buttonY1 = 240;
int buttonY2 = buttonY1 + buttonHeight + 10;
if (x > 40 && x < 40 + buttonWidth && y > buttonY1 && y < buttonY1 + buttonHeight) {
showInternalSensorsInfo();
return;
}
if (x > 40 + buttonWidth + buttonSpacing && x < 40 + 2 * buttonWidth + buttonSpacing && y > buttonY1 && y < buttonY1 + buttonHeight) {
showInternalGraph48hScreen();
return;
}
if (x > 40 && x < 40 + buttonWidth && y > buttonY2 && y < buttonY2 + buttonHeight) {
showInternalSensorsInfo();
return;
}
if (x > 40 + buttonWidth + buttonSpacing && x < 40 + 2 * buttonWidth + buttonSpacing && y > buttonY2 && y < buttonY2 + buttonHeight) {
showMainMenu();
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, 40);
tft.println("VLAGA TAL (MODUL 4)");
tft.drawRoundRect(10, 50, 460, 170, 10, SOIL_COLOR);
tft.fillRoundRect(11, 51, 458, 168, 10, rgbTo565(40, 20, 50));
int leftColumnX = 30;
int rightColumnX = 260;
int yOffset = 65;
int lineHeight = 16;
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");
} 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!");
}
}
int buttonWidth = 140;
int buttonHeight = 35;
int buttonSpacing = 20;
int buttonY1 = 240;
drawIconButton(40, buttonY1, buttonWidth, buttonHeight, SOIL_COLOR, "OSVEZI", "refresh");
drawMenuButton(40 + buttonWidth + buttonSpacing, buttonY1, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "30D GRAF");
int buttonY2 = buttonY1 + buttonHeight + 10;
drawMenuButton(40, buttonY2, buttonWidth, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_SOIL_MOISTURE_INFO;
}
void handleSoilMoistureTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
int buttonWidth = 140;
int buttonHeight = 35;
int buttonSpacing = 20;
int buttonY1 = 240;
int buttonY2 = buttonY1 + buttonHeight + 10;
if (x > 40 && x < 40 + buttonWidth && y > buttonY1 && y < buttonY1 + buttonHeight) {
showSoilMoistureInfo();
return;
}
if (x > 40 + buttonWidth + buttonSpacing && x < 40 + 2 * buttonWidth + buttonSpacing && y > buttonY1 && y < buttonY1 + buttonHeight) {
showSoilMoistureGraphScreen();
return;
}
if (x > 40 && x < 40 + buttonWidth && y > buttonY2 && y < buttonY2 + buttonHeight) {
calibrateSoilSensor();
return;
}
if (x > 40 + buttonWidth + buttonSpacing && x < 40 + 2 * buttonWidth + buttonSpacing && y > buttonY2 && y < buttonY2 + buttonHeight) {
showMainMenu();
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("MODUL 4 - VITRINA");
tft.drawRoundRect(10, 45, 460, 230, 10, rgbTo565(150, 200, 100));
tft.fillRoundRect(11, 46, 458, 228, 10, rgbTo565(40, 50, 30));
int leftX = 20;
int rightX = 260;
int yOffset = 60;
int lineHeight = 22;
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) {
unsigned long timeSinceLast = (millis() - lastModule4Time) / 1000;
tft.setCursor(leftX + 100, yOffset);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.printf("(zadnji prejem: %lu s)", timeSinceLast);
}
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:");
tft.setCursor(80, 165);
tft.setTextColor(rgbTo565(200, 200, 200));
tft.println(" • Napajanje Modula 4");
tft.setCursor(80, 185);
tft.println(" • ESP-NOW povezavo");
tft.setCursor(80, 205);
tft.println(" • MAC naslov v kodi");
} 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, 10, rgbTo565(60, 60, 60));
tft.fillRect(leftX + 130, yOffset + lineHeight - 2, tempBarWidth, 10, TEMP_COLOR);
tft.drawRect(leftX + 130, yOffset + lineHeight - 2, 150, 10, 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, 10, rgbTo565(60, 60, 60));
tft.fillRect(leftX + 130, yOffset + 2 * lineHeight + 3, humBarWidth, 10, HUMIDITY_COLOR);
tft.drawRect(leftX + 130, yOffset + 2 * lineHeight + 3, 150, 10, 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, 10, rgbTo565(60, 60, 60));
tft.fillRect(leftX + 100, yOffset + 4 * lineHeight + 13, lightBarWidth, 10, LIGHT_COLOR);
tft.drawRect(leftX + 100, yOffset + 4 * lineHeight + 13, 150, 10, 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, 10, rgbTo565(60, 60, 60));
tft.fillRect(rightX + 130, yOffset + lineHeight - 2, soilTempBarWidth, 10, rgbTo565(255, 150, 100));
tft.drawRect(rightX + 130, yOffset + lineHeight - 2, 150, 10, 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, 10, rgbTo565(60, 60, 60));
tft.fillRect(rightX + 100, yOffset + 2 * lineHeight + 3, soilBarWidth, 10, soilColor);
tft.drawRect(rightX + 100, yOffset + 2 * lineHeight + 3, 150, 10, 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);
if (module4ECH2O > 0) {
tft.setCursor(rightX, yOffset + 5 * lineHeight + 20);
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);
}
int interpretY = yOffset + 7 * lineHeight + 5;
tft.drawFastHLine(leftX, interpretY - 5, tft.width() - 40, rgbTo565(80, 100, 80));
tft.setCursor(leftX, interpretY);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.println("INTERPRETACIJA:");
tft.setCursor(leftX + 20, interpretY + lineHeight);
tft.setTextColor(rgbTo565(200, 200, 200));
if (module4TVOC > 800) {
tft.println("⚠️ VISOK TVOC - Rastline pod stresom!");
tft.setCursor(leftX + 20, interpretY + lineHeight + 16);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("→ Prezracite prostor, preverite rastline");
} else if (module4TVOC > 400) {
tft.println("🌫️ POVISAN TVOC - Zrak je nekoliko onesnazen");
tft.setCursor(leftX + 20, interpretY + lineHeight + 16);
tft.setTextColor(rgbTo565(255, 200, 50));
tft.println("→ Priporocljivo prezracevanje");
} else {
tft.println("✅ TVOC V REDU - Zrak je cist");
tft.setCursor(leftX + 20, interpretY + lineHeight + 16);
tft.setTextColor(rgbTo565(80, 220, 100));
tft.println("→ Kakovost zraka odlicna");
}
tft.setCursor(leftX + 240, interpretY + lineHeight);
if (module4ECO2 < 600) {
tft.setTextColor(rgbTo565(80, 150, 255));
tft.println("🌱 NIZEK CO2");
tft.setCursor(leftX + 240, interpretY + lineHeight + 16);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.println("→ Zmanjsajte prezracevanje");
} else if (module4ECO2 > 1500) {
tft.setTextColor(rgbTo565(255, 150, 50));
tft.println("🌿 VISOK CO2");
tft.setCursor(leftX + 240, interpretY + lineHeight + 16);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.println("→ Vklopite ventilator");
} else {
tft.setTextColor(rgbTo565(80, 220, 100));
tft.println("✅ OPTIMALEN CO2");
tft.setCursor(leftX + 240, interpretY + lineHeight + 16);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.println("→ Idealno za fotosintezo");
}
int buttonY = 280;
drawMenuButton(40, buttonY, 160, 35, rgbTo565(66, 135, 245), "KAKOVOST ZRAKA");
drawMenuButton(210, buttonY, 120, 35, rgbTo565(76, 175, 80), "OSVEZI");
drawMenuButton(340, buttonY, 120, 35, rgbTo565(245, 67, 54), "NAZAJ");
}
currentState = STATE_MODULE4_INFO;
}
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);
}
// Galavna ESPT_CLK
T_CS
T_DIN
T_DO
T_IRQ
GRETJE
HLAJENJE
SUŠENJE
LUČI 1
VLAŽENJE
ČRPALKA
LUČI 2
ČRPALKA