// Imam kodo za ESP32-S3 MCN32R16V in TFT ST7796 4-palčni (480x320). Koda:
#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 <DHT.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_BMP280.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, // Običajen gumb s senco
BUTTON_COMPACT = 1, // Manjši gumb za +/-
BUTTON_WIDE = 2, // Širok gumb za glavne akcije
BUTTON_HIGHLIGHT = 3, // Poudarjen gumb (npr. aktiven)
BUTTON_WARNING = 4, // Opozorilni gumb (rdeč)
BUTTON_SUCCESS = 5, // Uspešni gumb (zelen)
BUTTON_INFO = 6, // Informacijski gumb ( moder)
BUTTON_DISABLED = 7 // Onemogočen gumb
};
// Struktura za konfiguracijo gumba
struct ButtonConfig {
int x, y; // Pozicija
int width, height; // Dimenzije
uint16_t baseColor; // Osnovna barva
const char* label; // Besedilo (lahko z \n za novo vrstico)
ButtonStyle style; // Stil gumba
bool isActive; // Ali je aktiven (za BUTTON_HIGHLIGHT)
const char* icon; // Ikona (opcijsko: "wifi", "home", "back", itd.)
int textSize; // Velikost teksta (1-3, 0 za privzeto)
};
#define WHITE_COLOR 0xFFFF
#define BLACK_COLOR 0x0000
#define RED_COLOR 0xF800
#define GREEN_COLOR 0x07E0
#define BLUE_COLOR 0x001F
// ==================== MINIMALNI SISTEM ZA PREPREČEVANJE UTREPANJA ====================
unsigned long lastScreenDraw = 0;
#define MIN_DRAW_INTERVAL 2000 // SAMO 1x na 2 SEKUNDI
// ==================== 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,
// Kalibracijske kode za zunanji LDR (ujemajo se z modulom 1)
CALIB_DARK = 100, // Kalibracijska vrednost za temo
CALIB_BRIGHT = 101, // Kalibracijska vrednost za svetlo
CALIB_CONFIRM = 102 // Potrditev kalibracije
};
// ==================== UKAZI ZA MODUL 3 - SENČENJE ====================
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;
ModuleType moduleType;
unsigned long timestamp;
ErrorCode errorCode;
float batteryVoltage;
union {
// MODUL 1 - Vreme
struct {
float temperature;
float humidity;
float pressure;
float lux;
float bmpTemperature;
} weather;
// MODUL 2 - Namakanje
struct {
float flowRate1, flowRate2, flowRate3;
float totalFlow1, totalFlow2, totalFlow3;
bool relay1State, relay2State, relay3State;
} irrigation;
// MODUL 3 - Senčenje
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;
bool module1Active = false;
unsigned long lastModule1Time = 0;
#define MODULE1_TIMEOUT 60000
uint8_t module1MAC[] = { 0x3A, 0x2E, 0xCB, 0x3F, 0x34, 0x2E };
// ==================== 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;
// ==================== 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[] = {0xD6, 0x4B, 0xCC, 0x3F, 0xD0, 0x4B}; // MAC naslov Modula 4
bool module4Active = false;
unsigned long lastModule4Time = 0;
#define MODULE4_TIMEOUT 30000 // 30 sekund timeout
// Podatki iz Modula 4 (vitrina)
float module4AirTemp = 0; // Temperatura zraka v vitrini
float module4AirHum = 0; // Vlaga zraka v vitrini
float module4Pressure = 0; // Zračni tlak
int module4LightPercent = 0; // Svetloba v vitrini (%)
float module4SoilTemp = 0; // Temperatura zemlje
int module4SoilMoisture = 0; // Vlaga tal (%)
float module4TVOC = 0; // TVOC v ppb
float module4ECO2 = 0; // eCO2 v ppm
float module4ECH2O = 0; // eCH2O v ppb
String module4AirQuality = "---";
float module4Battery = 0;
// Za sledenje sprememb na domačem zaslonu (za Modul 4)
float lastDrawnModule4Temp = -999;
float lastDrawnModule4Hum = -999;
int lastDrawnModule4Light = -999;
int lastDrawnModule4SoilM = -999;
float lastDrawnModule4TVOC = -999;
float lastDrawnModule4ECO2 = -999;
// ==================== PIN DEFINICIJE ====================
// Pini za DISPLAY
#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
// Pin za DHT22 - notranje meritve
#define DHTPIN 14
#define DHTTYPE DHT22
// I2C za AHT20 in BMP280 - zunanje meritve
#define EXTERNAL_SDA MCP_SDA
#define EXTERNAL_SCL MCP_SCL
// Pini za LDR (svetloba) - SAMO NOTRANJI!
#define LDR_INTERNAL_PIN 15
// Pin za Soil Moisture Sensor
#define SOIL_MOISTURE_PIN 1
// 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 // CET (Central European Time) - zimska ura
#define DAYLIGHT_OFFSET_SEC 3600 // CEST (Central European Summer Time) - poletna ura +1
// 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 - OPTIMIZIRANA VELIKOST
#define HAMBURGER_X 440
#define HAMBURGER_Y STATUS_BAR_HEIGHT + 10
#define HAMBURGER_SIZE 22 // Zmanjšano s 30 na 22
#define HAMBURGER_TOUCH_PADDING 15 // Območje za dotik ostaja enako
// Povečana območja za dotik
#define TOUCH_PADDING_RELAYS 12 // Povečano z 8 na 12
#define TOUCH_PADDING_CIRCLES 20 // Povečano s 15 na 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
; // DODAJTE TO VRSTICO ZA TEST
#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;
};
void showTemporaryMessage(String message, uint16_t color, int durationMs);
// ==================== 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
int SOIL_DRY_VALUE = 4095;
int SOIL_WET_VALUE = 1500;
#define SOIL_UPDATE_INTERVAL 2000
// 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;
// Inicializacija DHT22 - notranje
DHT dht(DHTPIN, DHTTYPE);
// Inicializacija AHT20 - zunanje
Adafruit_AHTX0 aht;
// Inicializacija BMP280 - zunanje
Adafruit_BMP280 bmp;
// 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;
// Za kalibracijo LDR
float ldrInternalLuxAtDark = 0;
float ldrInternalLuxAtBright = 1000;
// ==================== SPREMENLJIVKE ZA ČASOVNO KRMILJENJE RAZSVETLJAVE ====================
#define MAX_TIME_BLOCKS 4 // Spremenite iz #define MAX_TIME_BLOCKS 6 na:
#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
bool dhtInitialized = false;
float internalTemperature = 0.0;
float internalHumidity = 0.0;
unsigned long lastDHTUpdate = 0;
#define DHT_UPDATE_INTERVAL 2000
bool ahtInitialized = false;
float externalTemperature = 0;
float externalHumidity = 0;
unsigned long lastAHTUpdate = 0;
#define AHT_UPDATE_INTERVAL 2000
bool bmpInitialized = false;
bool bmpFound = false;
uint8_t bmpAddress = 0;
float externalPressure = 1013.25;
float bmpTemperature = 0.0;
unsigned long lastBMPUpdate = 0;
#define BMP_UPDATE_INTERVAL 2000
int ldrInternalValue = 0;
int ldrExternalValue = 0;
float ldrInternalLux = 0.0;
float ldrExternalLux = 0.0;
int ldrInternalPercent = 0;
int ldrExternalPercent = 0;
unsigned long lastLDRUpdate = 0;
#define LDR_UPDATE_INTERVAL 1000
float ldrInternalLuxDark = 0.0;
float ldrInternalLuxBright = 1000.0;
float ldrExternalLuxDark = 0.0;
float ldrExternalLuxBright = 1000.0;
float ldrLuxB = 6.5;
float ldrLuxM = -0.7;
float R_fixed = 10000.0;
float VCC = 3.3;
int ldrInternalMin = 500;
int ldrInternalMax = 3000;
// Za kalibracijo zunanjega LDR
int ldrExternalMin = 500; // Privzeta vrednost za svetlo (NIZKA ADC)
int ldrExternalMax = 3000; // Privzeta vrednost za temno (VISOKA ADC)
int soilMoistureValue = 0;
int soilMoisturePercent = 0;
unsigned long lastSoilUpdate = 0;
SPIClass* sdSPI = nullptr;
// ==================== 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_LDR_CALIBRATION,
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_EXTERNAL_LDR_CALIBRATION,
STATE_AIR_QUALITY, // NOVO!
STATE_MODULE4_INFO // NOVO!
};
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
bool ldrCalibrating = false;
int ldrCalibrationStep = 0;
int ldrCalibrationType = 0;
unsigned long ldrCalibrationStartTime = 0;
#define LDR_CALIBRATION_TIMEOUT 30000
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 ====================
// To je KLJUČNO za preprečevanje utripanja!
// Plast 1: Ozadje (se nariše samo enkrat)
bool homeScreenBackgroundDrawn = false;
// Plast 2: Statični elementi (napisi, okvirji - se narišejo samo enkrat)
bool homeScreenStaticElementsDrawn = false;
// Plast 3: Dinamične vrednosti (zadnje narisane vrednosti)
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;
// Poišči ta del v kodi (tam so definirane vse barve, npr. DARK_GRAY, HIGHLIGHT_GREEN...)
#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 // Rumeno v RGB565 formatu
// ==================== 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 loadLDRCalibration();
void resetLDRCalibration();
void updateLDR();
String getLDRInternalLuxString();
String getLDRInternalString();
void showLDRCalibrationScreen();
void handleLDRCalibrationTouch(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 initializeDHT();
void updateDHT();
String getInternalTemperatureString();
String getInternalHumidityString();
bool syncTimeWithNTP();
void checkNTPSync();
void initializeRTC();
void updateRTC();
String getRTCTemperature();
void showRTCInfo();
void handleRTCInfoTouch(int x, int y);
void saveSettingsOnShutdown();
void loadSettingsOnBoot();
void calibrateLDRToLux(int sensorType, float knownLux);
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 diagnoseDHT22();
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();
// ==================== 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 (ldrExternalLux > 0 && ldrExternalLux < 100000) {
hasValidData = true;
}
if (hasValidData) {
extTempHistory48h[extGraphHistoryIndex] = isnan(externalTemperature) ? 0 : externalTemperature;
extHumHistory48h[extGraphHistoryIndex] = isnan(externalHumidity) ? 0 : externalHumidity;
extLuxHistory48h[extGraphHistoryIndex] = (ldrExternalLux < 0 || ldrExternalLux > 100000) ? 0 : ldrExternalLux;
if (bmpInitialized && !isnan(externalPressure)) {
extPressureHistory48h[extGraphHistoryIndex] = externalPressure;
} else {
extPressureHistory48h[extGraphHistoryIndex] = 0;
}
extTimeStamps48h[extGraphHistoryIndex] = currentTime;
// DODAJTE DEBUG IZPIS ZA PREVERJANJE
static unsigned long lastDebugSave = 0;
if (currentTime - lastDebugSave > 30000) { // Vsakih 30 sekund
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 {
// DODAJTE IZPIS, KO NI PODATKOV
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;
// DODAJTE DEBUG IZPIS
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;
// Shrani v FLASH vsakih 10 meritev
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.putInt("ldrExtMin", ldrExternalMin);
preferences.putInt("ldrExtMax", ldrExternalMax);
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;
// POPRAVEK: Privzete vrednosti - MIN (svetlo) < MAX (temno)
ldrInternalMin = 500; // Svetloba (NIZKA ADC)
ldrInternalMax = 3000; // Tema (VISOKA ADC)
ldrExternalMin = 500;
ldrExternalMax = 3000;
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);
// POPRAVEK: Pravilne privzete vrednosti
// V TEMI: ADC je VISOK (npr. 3000) - to je MAX
// NA SVETLOBI: ADC je NIZAK (npr. 500) - to je MIN
ldrInternalMin = preferences.getInt("ldrIntMin", 500); // Svetlo = MIN
ldrInternalMax = preferences.getInt("ldrIntMax", 3000); // Temno = MAX
ldrExternalMin = preferences.getInt("ldrExtMin", 500);
ldrExternalMax = preferences.getInt("ldrExtMax", 3000);
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;
// Zunanji krogi (prosojni)
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);
}
// Glavni krog
tft.drawCircle(x, y, size, isOn ? rgbTo565(0, 200, 255) : rgbTo565(120, 120, 140));
// Rotirajoče lopatice (samo če je vklopljen)
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);
}
// Center ventilatorja
int centerSize = size / 4;
tft.fillCircle(x, y, centerSize, isOn ? rgbTo565(0, 180, 240) : rgbTo565(100, 100, 120));
// Svetlejša pika v centru (samo za lepši izgled)
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");
}
void updateExternalLDR() {
// Ta funkcija bi morala prebrati vrednost iz modula 1 preko ESP-NOW
// Ker je zunanji LDR na modulu 1, se vrednost posodobi v onModuleDataRecv()
// Za namene kalibracije pa potrebujemo trenutno vrednost
// Vrednost externalLux se posodobi preko ESP-NOW, vendar za kalibracijo
// potrebujemo ADC vrednost, ki je na voljo samo na modulu 1
// Zato bo kalibracija potekala tako, da pošljemo ukaz modulu 1,
// ta izvede kalibracijo in nam pošlje nazaj rezultat
}
// ========== POENOTENE FUNKCIJE ZA GUMBE ==========
// GLAVNA FUNKCIJA ZA RISANJE GUMBOV
void drawButton(ButtonConfig btn) {
// Privzete vrednosti
if (btn.textSize == 0) btn.textSize = 1;
if (btn.style == BUTTON_DISABLED) btn.isActive = false;
// Določi barvo glede na stil
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); // Svetlo rumen za aktiven
textColor = ST77XX_BLACK;
}
break;
case BUTTON_WARNING:
finalColor = rgbTo565(245, 67, 54); // Rdeča
break;
case BUTTON_SUCCESS:
finalColor = rgbTo565(76, 175, 80); // Zelena
break;
case BUTTON_INFO:
finalColor = rgbTo565(66, 135, 245); // Modra
break;
case BUTTON_DISABLED:
finalColor = rgbTo565(100, 100, 100); // Siva
textColor = rgbTo565(180, 180, 180);
borderColor = rgbTo565(150, 150, 150);
break;
}
// Nariši senco (zamaknjen pravokotnik)
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);
}
// Nariši glavni gumb
tft.fillRoundRect(btn.x, btn.y, btn.width, btn.height, cornerRadius, finalColor);
tft.drawRoundRect(btn.x, btn.y, btn.width, btn.height, cornerRadius, borderColor);
// Nastavi font glede na velikost
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);
// Preveri, če besedilo vsebuje newline
String labelStr = String(btn.label);
int newlinePos = labelStr.indexOf('\n');
if (newlinePos > 0 && btn.style != BUTTON_COMPACT) {
// Dvovrstični tekst
String line1 = labelStr.substring(0, newlinePos);
String line2 = labelStr.substring(newlinePos + 1);
int16_t x1, y1;
uint16_t w1, h1, w2, h2;
tft.getTextBounds(line1, 0, 0, &x1, &y1, &w1, &h1);
tft.getTextBounds(line2, 0, 0, &x1, &y1, &w2, &h2);
int line1X = btn.x + (btn.width - w1) / 2 - x1;
int line1Y = btn.y + (btn.height / 2 - h1) / 2 - y1;
int line2X = btn.x + (btn.width - w2) / 2 - x1;
int line2Y = btn.y + btn.height / 2 + (btn.height / 2 - h2) / 2 - y1;
tft.setCursor(line1X, line1Y);
tft.print(line1);
tft.setCursor(line2X, line2Y);
tft.print(line2);
} else {
// Enovrstični tekst
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);
}
// Nariši ikono, če je podana
if (btn.icon != nullptr && strlen(btn.icon) > 0) {
drawButtonIcon(btn.x, btn.y, btn.width, btn.height, btn.icon, textColor);
}
// Ponastavi font na privzetega
tft.setFont();
tft.setTextSize(1);
}
// Pomožna funkcija za risanje ikon
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);
}
}
// ========== POMOŽNE FUNKCIJE ZA HITRO USTVARJANJE GUMBOV ==========
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);
}
// === Funkcijo za prikaz kalibracije zunanjega LDR ===
void showExternalLDRCalibrationScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
// ===== NASLOV =====
tft.setTextSize(1);
tft.setTextColor(EXTERNAL_COLOR);
tft.setCursor(120, 28);
tft.println("KALIBRACIJA ZUNANJI LDR");
// ===== OKVIR ZA NAVODILA =====
tft.drawRoundRect(10, 45, 460, 105, 8, EXTERNAL_COLOR);
tft.fillRoundRect(11, 46, 458, 103, 8, rgbTo565(40, 30, 20));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(220, 200, 180));
// NAVODILA (5 vrstic)
tft.setCursor(20, 58);
tft.println("1. Postavite senzor v POPOLNO TEMO");
tft.setCursor(20, 74);
tft.println("2. Pritisnite 'TEMNO'");
tft.setCursor(20, 90);
tft.println("3. Postavite senzor na MOCNO SVETLOBO");
tft.setCursor(20, 106);
tft.println("4. Pritisnite 'SVETLO'");
tft.setCursor(20, 122);
tft.println("5. Pritisnite 'SHRANI'");
// ===== OKVIR ZA TRENUTNE VREDNOSTI =====
tft.drawRoundRect(10, 155, 460, 70, 8, rgbTo565(100, 100, 150));
tft.fillRoundRect(11, 156, 458, 68, 8, rgbTo565(30, 30, 50));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.setCursor(20, 168);
tft.print("Trenutna kalibracija:");
tft.setTextColor(rgbTo565(200, 200, 200));
tft.setCursor(20, 183);
tft.printf("Svetlo (min ADC): %d", ldrExternalMin);
tft.setCursor(20, 198);
tft.printf("Temno (max ADC): %d", ldrExternalMax);
// Status kalibracije
if (ldrExternalMin > 0 && ldrExternalMax > 0 && ldrExternalMin < ldrExternalMax) {
tft.setTextColor(rgbTo565(80, 220, 100));
tft.setCursor(250, 198);
tft.print("✅ VELJAVNO");
} else if (ldrExternalMin > 0 && ldrExternalMax > 0) {
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(250, 198);
tft.print("⚠ NEVELJAVNO");
} else {
tft.setTextColor(rgbTo565(150, 150, 150));
tft.setCursor(250, 198);
tft.print("⭕ NI KALIBRIRANO");
}
// ===== TRENUTNA SVETLOST =====
tft.setTextColor(rgbTo565(150, 150, 150));
tft.setCursor(280, 168);
tft.print("Trenutna svetlost:");
tft.setCursor(280, 183);
if (module1Active && externalLux >= 0) {
tft.setTextColor(LIGHT_COLOR);
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.setTextColor(rgbTo565(255, 80, 80));
tft.print("-- lx");
}
tft.setCursor(280, 198);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("Modul 1: ");
tft.setTextColor(module1Active ? rgbTo565(80, 220, 100) : rgbTo565(255, 80, 80));
tft.print(module1Active ? "AKTIVEN" : "NEDOSEGLJIV");
// ===== GUMBI - VSI ZNOTRAJ VIDNEGA OBMOČJA =====
int buttonY = 235; // Začetek gumbov
// Prva vrsta - 3 gumbi
int btnW = 110;
int btnH = 38;
int spacing = 12;
int totalWidth = 3 * btnW + 2 * spacing;
int startX = (tft.width() - totalWidth) / 2;
// Gumb TEMNO
tft.fillRoundRect(startX, buttonY, btnW, btnH, 8, rgbTo565(50, 50, 150));
tft.drawRoundRect(startX, buttonY, btnW, btnH, 8, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.setCursor(startX + 35, buttonY + 12);
tft.print("TEMNO");
// Gumb SVETLO
int btn2X = startX + btnW + spacing;
tft.fillRoundRect(btn2X, buttonY, btnW, btnH, 8, rgbTo565(255, 200, 50));
tft.drawRoundRect(btn2X, buttonY, btnW, btnH, 8, ST77XX_WHITE);
tft.setTextColor(ST77XX_BLACK);
tft.setCursor(btn2X + 32, buttonY + 12);
tft.print("SVETLO");
// Gumb SHRANI
int btn3X = btn2X + btnW + spacing;
tft.fillRoundRect(btn3X, buttonY, btnW, btnH, 8, rgbTo565(76, 175, 80));
tft.drawRoundRect(btn3X, buttonY, btnW, btnH, 8, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(btn3X + 32, buttonY + 12);
tft.print("SHRANI");
// Druga vrsta - NAZAJ gumb (sredinsko)
int btnY2 = buttonY + btnH + 15;
int backW = 140;
int backX = (tft.width() - backW) / 2;
tft.fillRoundRect(backX, btnY2, backW, btnH, 8, rgbTo565(245, 67, 54));
tft.drawRoundRect(backX, btnY2, backW, btnH, 8, ST77XX_WHITE);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(backX + 50, btnY2 + 12);
tft.print("NAZAJ");
// ===== OPOMBA SPODAJ =====
tft.setTextSize(0);
tft.setTextColor(rgbTo565(100, 100, 100));
tft.setCursor(20, 455);
tft.print("Nasvet: Najprej izmerite temo, nato svetlobo in shranite.");
currentState = STATE_EXTERNAL_LDR_CALIBRATION;
}
void handleExternalLDRCalibrationTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
// Gumb TEMNO
if (x > 50 && x < 170 && y > 225 && y < 265) {
Serial.println("🔘 Gumb TEMNO pritisnjen - kalibracija zunanjega LDR");
if (module1Active) {
// Pošlji ukaz modulu 1 za merjenje TEMNE vrednosti
sendModule1Command(10, 0, 0);
showTemporaryMessage("🌑 Merjenje temne vrednosti...\nČakam na modul 1",
rgbTo565(255, 200, 50), 2000);
// Počakamo, da modul odgovori (max 5 sekund)
unsigned long startWait = millis();
int receivedDarkValue = -1;
int previousDarkValue = ldrExternalMin;
while (millis() - startWait < 5000 && receivedDarkValue < 0) {
// Obdelaj morebitne prejete podatke
handleTouch();
delay(10);
// Če se je vrednost spremenila, smo prejeli odgovor
if (ldrExternalMin != previousDarkValue && ldrExternalMin > 0) {
receivedDarkValue = ldrExternalMin;
Serial.printf("✅ Prejeta temna vrednost: %d\n", receivedDarkValue);
}
}
if (receivedDarkValue > 0) {
showTemporaryMessage("✅ Temna vrednost: " + String(receivedDarkValue) +
"\nPritisnite 'SVETLO' za naslednji korak",
rgbTo565(80, 220, 100), 2500);
} else {
showTemporaryMessage("❌ Napaka: Modul 1 ni odgovoril!\nPreverite povezavo",
rgbTo565(255, 80, 80), 3000);
}
showExternalLDRCalibrationScreen();
} else {
showTemporaryMessage("❌ Modul 1 ni dosegljiv!\nPreverite povezavo",
rgbTo565(255, 80, 80), 2000);
showExternalLDRCalibrationScreen();
}
return;
}
// Gumb SVETLO
if (x > 190 && x < 310 && y > 225 && y < 265) {
Serial.println("🔘 Gumb SVETLO pritisnjen - kalibracija zunanjega LDR");
if (module1Active) {
// Pošlji ukaz modulu 1 za merjenje SVETLE vrednosti
sendModule1Command(11, 0, 0);
showTemporaryMessage("☀️ Merjenje svetle vrednosti...\nČakam na modul 1",
rgbTo565(255, 200, 50), 2000);
// Počakamo, da modul odgovori (max 5 sekund)
unsigned long startWait = millis();
int receivedBrightValue = -1;
int previousBrightValue = ldrExternalMax;
while (millis() - startWait < 5000 && receivedBrightValue < 0) {
// Obdelaj morebitne prejete podatke
handleTouch();
delay(10);
// Če se je vrednost spremenila, smo prejeli odgovor
if (ldrExternalMax != previousBrightValue && ldrExternalMax > 0) {
receivedBrightValue = ldrExternalMax;
Serial.printf("✅ Prejeta svetla vrednost: %d\n", receivedBrightValue);
}
}
if (receivedBrightValue > 0) {
showTemporaryMessage("✅ Svetla vrednost: " + String(receivedBrightValue) +
"\nPritisnite 'SHRANI' za zaključek",
rgbTo565(80, 220, 100), 2500);
} else {
showTemporaryMessage("❌ Napaka: Modul 1 ni odgovoril!\nPreverite povezavo",
rgbTo565(255, 80, 80), 3000);
}
showExternalLDRCalibrationScreen();
} else {
showTemporaryMessage("❌ Modul 1 ni dosegljiv!\nPreverite povezavo",
rgbTo565(255, 80, 80), 2000);
showExternalLDRCalibrationScreen();
}
return;
}
// Gumb SHRANI
if (x > 330 && x < 450 && y > 225 && y < 265) {
Serial.println("🔘 Gumb SHRANI pritisnjen - shranjevanje kalibracije");
// Preveri, ali so kalibracijske vrednosti smiselne
if (ldrExternalMin <= 0 || ldrExternalMax <= 0) {
showTemporaryMessage("❌ Napaka: Izmerite obe vrednosti!\nNajprej temo, nato svetlobo",
rgbTo565(255, 80, 80), 2500);
showExternalLDRCalibrationScreen();
return;
}
if (ldrExternalMin >= ldrExternalMax) {
showTemporaryMessage("❌ Napaka: Svetlo mora imeti nižji ADC kot temno!\nPonovite kalibracijo",
rgbTo565(255, 80, 80), 2500);
showExternalLDRCalibrationScreen();
return;
}
// Pošlji modulu 1 nove kalibracijske vrednosti
if (module1Active) {
showTemporaryMessage("📤 Pošiljanje kalibracije modulu 1...",
rgbTo565(255, 200, 50), 1500);
// Ukaz 12 = shrani kalibracijo, param1 = min (svetlo), param2 = max (temno)
sendModule1Command(12, ldrExternalMin, ldrExternalMax);
// Počakamo na potrditev (max 3 sekunde)
unsigned long startWait = millis();
bool confirmed = false;
while (millis() - startWait < 3000 && !confirmed) {
handleTouch();
delay(10);
// Potrditev bo prišla preko CALIB_CONFIRM v onModuleDataRecv
// in bo samodejno preklopila zaslon
if (currentState != STATE_EXTERNAL_LDR_CALIBRATION) {
confirmed = true;
}
}
if (!confirmed) {
showTemporaryMessage("⚠️ Modul 1 ni potrdil prejema!\nVrednosti so shranjene lokalno",
rgbTo565(255, 150, 50), 2000);
}
}
// Shrani v preferences na glavnem sistemu
preferences.begin("ldr-calib", false);
preferences.putInt("ext_min", ldrExternalMin);
preferences.putInt("ext_max", ldrExternalMax);
preferences.end();
saveAllSettings(); // Shrani tudi v ostale nastavitve
showExternalSensorsInfo(); // Vrni se na zaslon zunanjih senzorjev
return;
}
// Gumb NAZAJ
if (x > 190 && x < 310 && y > 275 && y < 315) {
Serial.println("🔘 Gumb NAZAJ pritisnjen");
showExternalSensorsInfo();
return;
}
}
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();
// Preveri, ali je dan v tednu izbran
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;
// Če je end čas manjši od start časa, gre za čez polnoč
if (endTimeInMinutes <= startTimeInMinutes) {
endTimeInMinutes += 24 * 60;
if (currentTimeInMinutes < startTimeInMinutes) {
currentTimeInMinutes += 24 * 60;
}
}
return (currentTimeInMinutes >= startTimeInMinutes && currentTimeInMinutes < endTimeInMinutes);
}
bool shouldRelaysBeOn() {
if (!lightingAutoMode || lightingManualOverride) {
// Če je ročni način, vrni trenutno stanje releja 4
return lightingManualOverride ? relayStates[LIGHTING_RELAY_1] : false;
}
bool overallState = false;
// Preveri vse časovne bloke
for (int i = 0; i < MAX_TIME_BLOCKS; i++) {
if (!timeBlocks[i].enabled) continue;
if (isTimeInBlock(i)) {
if (timeBlocks[i].relayOn) {
overallState = true; // Vsaj en blok zahteva vklop
} else {
overallState = false; // Blok za izklop ima prednost
break;
}
}
}
return overallState;
}
void checkAndControlLighting() {
if (!rtcInitialized) return;
unsigned long currentTime = millis();
if (currentTime - lastLightingCheck > LIGHTING_CHECK_INTERVAL) {
bool relay6ShouldBeOn = false;
// ===== AVTOMATSKA RAZSVETLJAVA (RELE 5) =====
if (!lightAutoMode || lightManualOverride) {
// Ročni način - upoštevaj ročno stanje
relay6ShouldBeOn = lightManualOverride ? relayStates[LIGHTING_RELAY_2] : false;
} else {
// Avtomatski način
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 {
// Obdrži trenutno stanje
relay6ShouldBeOn = relayStates[LIGHTING_RELAY_2];
}
} else {
relay6ShouldBeOn = false;
Serial.printf("💡 Avto razsvetljava: ZUNAJ ČASOVNEGA OKNA → IZKLOP\n");
}
}
// Spremeni stanje samo, če je potrebno
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");
// ===== OKVIR =====
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);
// ===== LEVI STOLPEC - STATUS RELEJA 6 =====
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);
// ===== DESNI STOLPEC - ČASOVNO OBMOČJE =====
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);
}
// Gumb UREDI CAS
drawMenuButton(rightColumnX, yOffset + 4 * lineHeight, 110, 24,
rgbTo565(66, 135, 245), "UREDI");
// ===== PRAGOVI SVETLOBE =====
int pragoviStartY = yOffset + 7 * lineHeight + 8;
// Povečan okvir za pragove
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));
// ===== VIKLOPNI PRAG (korak 100 lx) =====
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);
// ===== IZKLOPNI PRAG (korak 100 lx) =====
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);
// ===== GUMBI IZVEN OKVIRJA =====
int buttonY = 235;
int buttonWidth = 90;
int buttonHeight = 30;
int buttonSpacing = 10;
// Prva vrsta gumbov (3 gumbi)
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");
// Druga vrsta gumbov (2 gumba)
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;
// ===== GUMBA ZA VIKLOPNI PRAG (korak 100 lx) =====
int vklopX = leftColumnX;
int btnVklopY = pragoviStartY + 22;
int btnHeight = 26;
int btnWidth = 60;
// Gumb "-100" za vklopni prag
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;
}
// Gumb "+100" za vklopni prag
if (x > vklopX + 70 && x < vklopX + 70 + btnWidth && y > btnVklopY && y < btnVklopY + btnHeight) {
lightOnThreshold += 100.0;
if (lightOnThreshold > lightOffThreshold) lightOnThreshold = lightOffThreshold;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
// ===== GUMBA ZA IZKLOPNI PRAG (korak 100 lx) =====
int izklopX = rightColumnX;
int btnIzklopY = pragoviStartY + 22;
// Gumb "-100" za izklopni prag
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;
}
// Gumb "+100" za izklopni prag
if (x > izklopX + 70 && x < izklopX + 70 + btnWidth && y > btnIzklopY && y < btnIzklopY + btnHeight) {
lightOffThreshold += 100.0;
if (lightOffThreshold > 20000) lightOffThreshold = 20000;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
// ===== Gumb UREDI CAS =====
if (x > rightColumnX && x < rightColumnX + 110 && y > yOffset + 4 * lineHeight && y < yOffset + 4 * lineHeight + 24) {
showEditLightTimeScreen();
return;
}
// ===== GUMBI IZVEN OKVIRJA =====
int buttonY = 235;
int buttonWidth = 90;
int buttonHeight = 30;
int buttonSpacing = 10;
// Prva vrsta gumbov
int totalWidth1 = 3 * buttonWidth + 2 * buttonSpacing;
int startX1 = (tft.width() - totalWidth1) / 2;
int row1Y = buttonY;
// Gumb AVTO
if (x > startX1 && x < startX1 + buttonWidth && y > row1Y && y < row1Y + buttonHeight) {
lightAutoMode = !lightAutoMode;
if (lightAutoMode) {
lightManualOverride = false;
}
markSettingsChanged();
showLightAutoControlScreen();
return;
}
// Gumb CAS
if (x > startX1 + buttonWidth + buttonSpacing && x < startX1 + 2 * buttonWidth + buttonSpacing &&
y > row1Y && y < row1Y + buttonHeight) {
lightTimeControlEnabled = !lightTimeControlEnabled;
markSettingsChanged();
showLightAutoControlScreen();
return;
}
// Gumb VKL/IZKL
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;
}
// Druga vrsta gumbov
int row2Y = row1Y + buttonHeight + buttonSpacing;
int buttonWidth2 = 110;
int totalWidth2 = 2 * buttonWidth2 + buttonSpacing;
int startX2 = (tft.width() - totalWidth2) / 2;
// Gumb SHRANI
if (x > startX2 && x < startX2 + buttonWidth2 && y > row2Y && y < row2Y + buttonHeight) {
saveAllSettings();
showLightAutoControlScreen();
return;
}
// Gumb NAZAJ
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;
}
// ===== GUMBI ZA ZACETNI CAS (levi okvir) =====
// Ura - minus
if (x > 50 && x < 110 && y > 115 && y < 145) {
lightStartHour = (lightStartHour - 1 + 24) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// Ura - plus
if (x > 120 && x < 180 && y > 115 && y < 145) {
lightStartHour = (lightStartHour + 1) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// Minuta - minus
if (x > 50 && x < 110 && y > 175 && y < 205) {
lightStartMinute = (lightStartMinute - 5 + 60) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// Minuta - plus
if (x > 120 && x < 180 && y > 175 && y < 205) {
lightStartMinute = (lightStartMinute + 5) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// ===== GUMBI ZA KONCNI CAS (desni okvir) =====
// Ura - minus
if (x > 270 && x < 330 && y > 115 && y < 145) {
lightEndHour = (lightEndHour - 1 + 24) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// Ura - plus
if (x > 340 && x < 400 && y > 115 && y < 145) {
lightEndHour = (lightEndHour + 1) % 24;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// Minuta - minus
if (x > 270 && x < 330 && y > 175 && y < 205) {
lightEndMinute = (lightEndMinute - 5 + 60) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// Minuta - plus
if (x > 340 && x < 400 && y > 175 && y < 205) {
lightEndMinute = (lightEndMinute + 5) % 60;
markSettingsChanged();
showEditLightTimeScreen();
return;
}
// ===== GUMBI SHRANI in NAZAJ (spodaj) =====
int buttonWidth = 180;
int buttonHeight = 40;
int buttonSpacing = 20;
int totalWidth = 2 * buttonWidth + buttonSpacing;
int startX = (tft.width() - totalWidth) / 2;
int buttonY = 260;
// Gumb SHRANI (levi)
if (x > startX && x < startX + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
saveAllSettings();
showLightAutoControlScreen(); // Vrni se na zaslon avtomatske razsvetljave
return;
}
// Gumb NAZAJ (desni)
if (x > startX + buttonWidth + buttonSpacing && x < startX + 2 * buttonWidth + buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
showLightAutoControlScreen(); // Vrni se na zaslon avtomatske razsvetljave
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");
// ===== LEVI OKVIR - ZACETNI CAS =====
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");
// Ura
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);
// Minuta
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);
// ===== DESNI OKVIR - KONCNI CAS =====
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");
// Ura
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);
// Minuta
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);
// ===== GUMBI SHRANI in NAZAJ (spodaj, na sredini) =====
int buttonWidth = 180;
int buttonHeight = 40;
int buttonSpacing = 20;
int totalWidth = 2 * buttonWidth + buttonSpacing;
int startX = (tft.width() - totalWidth) / 2;
int buttonY = 260;
// Gumb SHRANI (levi)
drawMenuButton(startX, buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "SHRANI");
// Gumb NAZAJ (desni)
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();
// ===== NASLOV =====
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 215, 0));
tft.setCursor(110, 28);
tft.println("CASOVNA RAZSVETLJAVA (RELE 4)");
// ===== 4 BLOKI =====
int blockStartY = 48;
int blockWidth = 400;
int blockHeight = 28;
int blockSpacing = 6;
// Informacije so del okvirja
int infoY = blockStartY + 4 * (blockHeight + blockSpacing) + 3;
int infoHeight = 45; // Višina informacij (3 vrstice)
// OKVIR zajema BLOKE + INFORMACIJE
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);
// ===== 4 BLOKI =====
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);
// Ime bloka
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);
// Čas
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);
// Akcija (ON/OFF)
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);
// Aktivno
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);
}
}
// ===== INFORMACIJE (znotraj okvirja) =====
tft.setTextSize(1);
// Nacin
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");
// Stanje releja 4
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");
// Naslednji dogodek
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);
// ===== 4 GUMBI ZUNAJ OKVIRJA (POMAKNJENI NIŽJE) =====
int buttonY = 40 + okvirHeight + 25; // Povečano z 10 na 25 (nižje)
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;
// Gumb AVTO/ROČNO
drawNormalButton(startX, buttonY, buttonWidth, buttonHeight,
lightingAutoMode ? rgbTo565(100, 200, 100) : rgbTo565(255, 150, 50),
lightingAutoMode ? "AVTO" : "ROCNO");
// Gumb AVTO REL6
drawNormalButton(startX + buttonWidth + buttonSpacing, buttonY, buttonWidth, buttonHeight,
rgbTo565(255, 215, 0), "AVTO REL6");
// Gumb SHRANI
drawNormalButton(startX + 2 * (buttonWidth + buttonSpacing), buttonY, buttonWidth, buttonHeight,
rgbTo565(76, 175, 80), "SHRANI");
// Gumb NAZAJ
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;
// Preveri, če je dogodek danes in v prihodnosti
if (d == currentDay && startMinute > currentMinute && startMinute < nextEventTime) {
nextEventTime = startMinute;
eventType = timeBlocks[i].relayOn ? "VKLOP" : "IZKLOP";
eventBlock = i;
}
// Preveri, če je dogodek jutri
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;
}
// ===== KOORDINATE ZA 4 BLOKE =====
int blockStartY = 48;
int blockWidth = 400;
int blockHeight = 28;
int blockSpacing = 6;
// Preveri 4 bloke
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;
}
}
// ===== KOORDINATE ZA OKVIR IN GUMBE =====
int infoY = blockStartY + 4 * (blockHeight + blockSpacing) + 3;
int okvirHeight = (4 * blockHeight + 3 * blockSpacing) + 45 + 15;
// Gumbi so pod okvirjem - POMAKNJENI NIŽJE
int buttonY = 40 + okvirHeight + 25; // Povečano z 10 na 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;
// Gumb AVTO/ROČNO
if (x > startX && x < startX + buttonWidth &&
y > buttonY && y < buttonY + buttonHeight) {
lightingAutoMode = !lightingAutoMode;
if (lightingAutoMode) {
lightingManualOverride = false;
}
markSettingsChanged();
showLightingControlScreen();
return;
}
// Gumb AVTO REL6
if (x > startX + buttonWidth + buttonSpacing &&
x < startX + 2 * buttonWidth + buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
showLightAutoControlScreen();
return;
}
// Gumb SHRANI
if (x > startX + 2 * (buttonWidth + buttonSpacing) &&
x < startX + 3 * buttonWidth + 2 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
saveAllSettings();
showLightingControlScreen();
return;
}
// Gumb NAZAJ
if (x > startX + 3 * (buttonWidth + buttonSpacing) &&
x < startX + 4 * buttonWidth + 3 * buttonSpacing &&
y > buttonY && y < buttonY + buttonHeight) {
showMainMenu(); // NAZAJ v GLAVNI MENI
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();
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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");
// ZMANJŠAN OKVIR
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);
// ===== MODUL STATUS =====
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");
// ===== NOTRANJA SVETLOBA (spremenjeno iz zunanje) =====
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);
}
// Gumbi za nastavitev MIN
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);
// Gumbi za nastavitev MAX
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);
// ===== POZICIJA SENČNIKA =====
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("?");
}
// ===== PROGRESS BAR =====
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("--%");
}
// ===== GUMBI ZA ROČNO KRMILJENJE =====
int buttonStartY = barY + barHeight + 12;
int buttonWidth = 85;
int buttonHeight = 28;
int buttonSpacing = 6;
// Prva vrstica - 5 gumbov
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);
// Druga vrstica
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");
// Tretja vrstica
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;
// Gumbi za MIN svetlobe
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;
}
// Gumbi za MAX svetlobe
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;
}
// ===== POZICIJE GUMBOV =====
int barY = yOffset + 6 * lineHeight;
int barHeight = 22;
int buttonStartY = barY + barHeight + 12;
int buttonWidth = 85;
int buttonHeight = 28;
int buttonSpacing = 6;
// Prva vrstica - 5 gumbov (0%, 25%, 50%, 75%, 100%)
int totalWidth1 = 5 * buttonWidth + 4 * buttonSpacing;
int startX1 = (tft.width() - totalWidth1) / 2;
if (y > buttonStartY && y < buttonStartY + buttonHeight) {
// 0%
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;
}
// 25%
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;
}
// 50%
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;
}
// 75%
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;
}
// 100%
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;
}
}
// Druga vrstica
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) {
// USTAVI
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;
}
// KALIBRIRAJ
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;
}
// AVTO VKL/IZKL
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;
}
// SHRANI
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;
}
}
// Tretja vrstica - NAZAJ
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;
}
}
}
// Funkcija za avtomatsko krmiljenje senčnika - pošlje ukaz modulu 3
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();
// Preverjaj svetlobo vsake 2 sekundi
if (now - lastAutoCommand >= 2000) {
lastAutoCommand = now;
// Izračunaj želeno pozicijo glede na NOTRANJO SVETLOBO
calculateShadePosition();
// Pošlji ukaz samo, če je razlika večja od 2% (da ne pošiljamo preveč ukazov)
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);
// Pošlji ukaz modulu 3 za premik na izračunano pozicijo
sendShadeCommand(CMD_MOVE_TO_POSITION, shadeCalculatedPosition, 0);
lastSentPosition = shadeCalculatedPosition;
}
}
}
// ==================== POPRAVLJENO: Funkcija za risanje progress ringa (poln, rumen, v smeri urinega kazalca) ====================
void drawClockwiseProgressRing(int x, int y, int outerRadius, int innerRadius,
float currentValue, float minValue, float maxValue,
uint16_t ringColor) {
// 1. Izračunaj procent glede na min in max
float progressPercent = 0;
if (maxValue > minValue) {
// Omeji vrednost na min/max obseg
float constrainedValue = constrain(currentValue, minValue, maxValue);
progressPercent = (constrainedValue - minValue) / (maxValue - minValue) * 100.0;
}
// 2. Pretvori procent v kot (0% = 0°, 100% = 360°)
float progressAngle = progressPercent * 3.6; // 3.6 stopinj na procent
// 3. Nariši celotno ozadje (sivo)
tft.fillCircle(x, y, outerRadius, PROGRESS_BG_COLOR);
// 4. Če je progress večji od 0, nariši rumen lok na vrhu
if (progressAngle > 0) {
// Uporabimo lastno funkcijo za risanje polnega loka
// Začnemo pri -90° (12:00) in gremo v smeri urinega kazalca (zato +)
float startAngle = -90.0;
float endAngle = startAngle + progressAngle;
// Pretvori v radiane za matematične funkcije
float startRad = startAngle * PI / 180.0;
float endRad = endAngle * PI / 180.0;
// RIŠEMO POLN LOK (SOLID) - Uporabimo trike s črtami, da zapolnimo območje med notranjim in zunanjim polmerom
for (int r = innerRadius; r <= outerRadius; r++) {
// Za vsak radij narišemo črte od začetnega do končnega kota
// Ta metoda ni popolna, vendar je veliko boljša kot fillArc, ki riše pike.
// Določimo korak glede na radij, da ne bo preveč počasno
float angleStep = 1.0 / r * 5; // Večji radij = manjši korak
if (angleStep < 0.5) angleStep = 0.5; // Omejimo minimalni korak
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);
// Preveri meje (varnost)
if (px >= 0 && px < tft.width() && py >= 0 && py < tft.height()) {
tft.drawPixel(px, py, ringColor);
}
}
}
// Nariši še notranji in zunanji rob za lepši videz (neobvezno)
// tft.drawCircle(x, y, outerRadius, ringColor);
// tft.drawCircle(x, y, innerRadius, ST77XX_BLACK);
}
// 5. Nariši notranji krog (da pokrijemo sredino in ustvarimo "ring" efekt) - vedno črn
tft.fillCircle(x, y, innerRadius, ST77XX_BLACK);
tft.drawCircle(x, y, innerRadius, ST77XX_WHITE); // Bel rob za lepši videz
}
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 (!dhtInitialized) {
Serial.println("AVTO KRMILJENJE: DHT22 ni inicializiran!");
return;
}
if (isnan(internalTemperature) || isnan(internalHumidity)) {
Serial.println("AVTO KRMILJENJE: Podatki DHT22 niso veljavni!");
return;
}
unsigned long currentTime = millis();
if (currentTime - lastControlCheck < CONTROL_CHECK_INTERVAL) {
return; // Še ni čas za novo preverjanje
}
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");
// ===== GRETJE (rele 0) in HLAJENJE (rele 1) =====
if (internalTemperature > targetTempMax) {
// PREVISOKA temperatura - VKLOPI HLAJENJE (rele 1)
if (!relayStates[1]) {
Serial.println("→ UKAZ: PREVISOKA temperatura - VKLOP HLAJENJA (rele 1)");
setRelay(1, true);
} else {
Serial.println("→ HLAJENJE že vklopljeno");
}
// Če je bilo gretje vklopljeno, ga izklopi
if (relayStates[0]) {
Serial.println("→ UKAZ: Izklop GRETJA (rele 0) - ker je temperatura previsoka");
setRelay(0, false);
}
}
else if (internalTemperature < targetTempMin) {
// PRENIZKA temperatura - VKLOPI GRETJE (rele 0)
if (!relayStates[0]) {
Serial.println("→ UKAZ: PRENIZKA temperatura - VKLOP GRETJA (rele 0)");
setRelay(0, true);
} else {
Serial.println("→ GRETJE že vklopljeno");
}
// Če je bilo hlajenje vklopljeno, ga izklopi
if (relayStates[1]) {
Serial.println("→ UKAZ: Izklop HLAJENJA (rele 1) - ker je temperatura prenizka");
setRelay(1, false);
}
}
else {
// TEMPERATURA JE V CILJNEM OBMOČJU - izklopi vse
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");
}
}
// ===== VLAŽENJE (rele 2) in SUŠENJE (rele 3) =====
if (internalHumidity > targetHumMax) {
// PREVISOKA vlaga - VKLOPI SUŠENJE (rele 3)
if (!relayStates[3]) {
Serial.println("→ UKAZ: PREVISOKA vlaga - VKLOP SUŠENJA (rele 3)");
setRelay(3, true);
} else {
Serial.println("→ SUŠENJE že vklopljeno");
}
// Če je bilo vlaženje vklopljeno, ga izklopi
if (relayStates[2]) {
Serial.println("→ UKAZ: Izklop VLAŽENJA (rele 2) - ker je vlaga previsoka");
setRelay(2, false);
}
}
else if (internalHumidity < targetHumMin) {
// PRENIZKA vlaga - VKLOPI VLAŽENJE (rele 2)
if (!relayStates[2]) {
Serial.println("→ UKAZ: PRENIZKA vlaga - VKLOP VLAŽENJA (rele 2)");
setRelay(2, true);
} else {
Serial.println("→ VLAŽENJE že vklopljeno");
}
// Če je bilo sušenje vklopljeno, ga izklopi
if (relayStates[3]) {
Serial.println("→ UKAZ: Izklop SUŠENJA (rele 3) - ker je vlaga prenizka");
setRelay(3, false);
}
}
else {
// VLAGA JE V CILJNEM OBMOČJU - izklopi vse
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");
}
}
// Preveri končno stanje
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);
}
// ==================== VEČPLASTNO RISANJE DOMAČEGA ZASLONA ====================
void drawHomeScreenBackground() {
// Nariši gradient ozadje
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));
}
// Nariši zvezdice
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() {
// Če prihajamo iz drugega stanja (ne iz istega), ponastavi vse
if (lastState != STATE_HOME_SCREEN) {
homeScreenBackgroundDrawn = false;
homeScreenStaticElementsDrawn = false;
lastDrawnExternalLuxBar = -999;
tft.fillScreen(ST77XX_BLACK);
}
// Časovna omejitev - največ 1x na 2 sekundi
unsigned long now = millis();
if (currentState == STATE_HOME_SCREEN && now - lastHomeScreenRedraw < 2000) {
updateHomeScreenDynamicValues();
return;
}
Serial.println("\n=== RISANJE DOMACEGA ZASLONA ===");
// 1. PLAST: Ozadje
if (!homeScreenBackgroundDrawn) {
drawHomeScreenBackground();
}
// 2. PLAST: Statusna vrstica
drawStatusBar();
// 3. PLAST: Statični elementi
if (!homeScreenStaticElementsDrawn) {
drawHomeScreenStaticElements();
}
// Definicije za kroge
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;
// Definicije za progress bare
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;
// Y koordinate za progress bare
int lightBarY = relayStartY + 4 * (relayButtonHeight + relayVerticalSpacing) + 5;
int barSpacing = 10;
int calcBarY = lightBarY + progressBarHeight + barSpacing;
int actualBarY = calcBarY + progressBarHeight + barSpacing;
// ===== ZUNANJA SVETLOBA - MAJHEN PROGRESS BAR ZGORAJ =====
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);
}
// ===== NOTRANJA TEMPERATURA (levi krog) - ZDAJ IZ MODULA 4! =====
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);
// ===== NOTRANJA VLAŽNOST (desni krog) - ZDAJ IZ MODULA 4! =====
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);
// ===== VLAGA TAL (mali krog) - ZDAJ IZ MODULA 4! =====
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);
// ===== ZUNANJA TEMPERATURA (Modul 1) =====
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);
// ===== ZUNANJA VLAŽNOST (Modul 1) =====
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);
// ===== RELEJI (brez sprememb) =====
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);
}
// ===== PROGRESS BAR 1: NOTRANJA SVETLOBA - ZDAJ IZ MODULA 4! =====
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);
// ===== PROGRESS BAR 2: IZRAČUNANA POZICIJA SENČNIKA =====
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);
// ===== PROGRESS BAR 3: DEJANSKA POZICIJA SENČNIKA =====
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);
// ===== OPOZORILA =====
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();
// Posodobi shranjene vrednosti
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;
// ===== DEFINICIJE ZA PROGRESS BAR =====
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;
// Ventilator (statični del - okvir)
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));
// Hamburger meni
drawHamburgerMenu(HAMBURGER_X, HAMBURGER_Y, HAMBURGER_SIZE);
// Okvirji za kroge
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;
}
// ===== SAMO ZUNANJA SVETLOBA (Modul 1) =====
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;
// ===== POSODOBITEV ZUNANJE SVETLOBE =====
if (externalLuxChanged) {
int topProgressBarY = STATUS_BAR_HEIGHT + 5;
int topProgressBarWidth = (extHumCircleX - extTempCircleX) - 70;
int topProgressBarHeight = 20;
int topProgressBarX = extTempCircleX + (extHumCircleX - extTempCircleX - topProgressBarWidth) / 2;
if (topProgressBarWidth > 20) {
int luxPercent = 0;
if (module1Active && externalLux >= 0) {
float constrainedLux = constrain(externalLux, 0, 20000);
luxPercent = (constrainedLux / 20000.0) * 100.0;
}
tft.fillRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, ST77XX_BLACK);
tft.fillRoundRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, 8, PROGRESS_BG_COLOR);
tft.drawRoundRect(topProgressBarX, topProgressBarY, topProgressBarWidth, topProgressBarHeight, 8, ST77XX_WHITE);
if (luxPercent > 0 && module1Active) {
int progressWidth = map(luxPercent, 0, 100, 0, topProgressBarWidth);
if (progressWidth > 0) {
tft.fillRoundRect(topProgressBarX, topProgressBarY, progressWidth, topProgressBarHeight, 8, LIGHT_COLOR);
}
}
tft.setFont();
tft.setTextSize(1);
String luxStr;
if (module1Active && externalLux >= 0) {
if (externalLux < 10) luxStr = String(externalLux, 1) + " lx";
else if (externalLux < 1000) luxStr = String((int)externalLux) + " lx";
else luxStr = String(externalLux / 1000.0, 1) + " klx";
} else {
luxStr = "-- lx";
}
int16_t luxX1, luxY1;
uint16_t luxW, luxH;
tft.getTextBounds(luxStr, 0, 0, &luxX1, &luxY1, &luxW, &luxH);
int textCenterY = topProgressBarY + (topProgressBarHeight / 2);
int luxTextX = topProgressBarX + (topProgressBarWidth - luxW) / 2;
int luxTextY = textCenterY - (luxH / 2) - luxY1;
bool isOverProgress = false;
if (module1Active && luxPercent > 0) {
int progressWidth = map(luxPercent, 0, 100, 0, topProgressBarWidth);
isOverProgress = (luxTextX + luxW / 2) < (topProgressBarX + progressWidth);
}
uint16_t textColor = (isOverProgress && module1Active) ? ST77XX_BLACK : ST77XX_WHITE;
tft.setCursor(luxTextX, luxTextY);
tft.setTextColor(textColor);
tft.print(luxStr);
lastDrawnExternalLuxBar = externalLux;
}
}
// ===== POSODOBITEV NOTRANJE SVETLOBE (progress bar) =====
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);
// Posodobi shranjene vrednosti za Modul 4
lastDrawnModule4Temp = module4AirTemp;
lastDrawnModule4Hum = module4AirHum;
lastDrawnModule4Light = module4LightPercent;
lastDrawnModule4SoilM = module4SoilMoisture;
lastDrawnModule4TVOC = module4TVOC;
lastDrawnModule4ECO2 = module4ECO2;
lastHomeScreenPartialUpdate = now;
}
//=== Funkcijo za popolno osvežitev domačiega zaslona ===
void forceFullHomeScreenRedraw() {
// Ponastavi zastavice za večplastno risanje
homeScreenBackgroundDrawn = false;
homeScreenStaticElementsDrawn = false;
lastDrawnExternalLuxBar = -999; // Ponastavi sledenje zunanje svetlobe
// Povsem počisti celoten zaslon
tft.fillScreen(ST77XX_BLACK);
// Ponastavi vse zadnje narisane vrednosti (da se vse nariše na novo)
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;
}
// Pokliči običajni showHomeScreen, ki bo zdaj narisal vse na novo
showHomeScreen();
}
void handleHomeScreenTouch(int x, int y) {
// Statusna vrstica - vedno prikaže WiFi info
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
// ===== HAMBURGER MENI - MANJŠA VIDNA IKONA, VENDAR ŠE VEDNO VELIKO OBMOČJE ZA DOTIK =====
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;
// Preveri, če je dotik znotraj povečanega območja
if (x >= hamburgerTouchX && x <= hamburgerTouchX + hamburgerTouchWidth &&
y >= hamburgerTouchY && y <= hamburgerTouchY + hamburgerTouchHeight) {
Serial.println("=== HAMBURGER MENI PRITISNJEN ===");
// Vizualni feedback - animacija ob dotiku
animateHamburgerMenu();
// Odpri glavni meni
showMainMenu();
return;
}
// ===== VENTILATOR SIMBOL =====
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;
}
// ===== DEFINICIJE POZICIJ ZA VSE ELEMENTE =====
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;
// ===== SREDNJI KROG - VLAGA TAL (iz Modula 4) =====
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;
}
// ===== ZUNANJA TEMPERATURA (Modul 1) =====
int distanceToExtTempCircle = sqrt(pow(x - extTempCircleX, 2) + pow(y - extTempCircleY, 2));
if (distanceToExtTempCircle < smallCircleRadius + TOUCH_PADDING_CIRCLES) {
Serial.println("=== ZUNANJA TEMPERATURA PRITISNJENA ===");
showExternalSensorsInfo();
return;
}
// ===== ZUNANJA VLAGA (Modul 1) =====
int distanceToExtHumCircle = sqrt(pow(x - extHumCircleX, 2) + pow(y - extHumCircleY, 2));
if (distanceToExtHumCircle < smallCircleRadius + TOUCH_PADDING_CIRCLES) {
Serial.println("=== ZUNANJA VLAGA PRITISNJENA ===");
showExternalSensorsInfo();
return;
}
// ===== LEVI VELIKI KROG - NOTRANJA TEMPERATURA (iz Modula 4) =====
int distanceToLeftCircle = sqrt(pow(x - leftCircleX, 2) + pow(y - circleY, 2));
if (distanceToLeftCircle < circleRadius + 15) {
Serial.println("=== NOTRANJA TEMPERATURA PRITISNJENA ===");
showInternalSensorsInfo(); // Prikaže podatke Modula 4
return;
}
// ===== DESNI VELIKI KROG - NOTRANJA VLAGA (iz Modula 4) =====
int distanceToRightCircle = sqrt(pow(x - rightCircleX, 2) + pow(y - circleY, 2));
if (distanceToRightCircle < circleRadius + 15) {
Serial.println("=== NOTRANJA VLAGA PRITISNJENA ===");
// Preveri, če je slučajno hamburger meni preblizu
if (x > HAMBURGER_X - 30) {
// Če je blizu hamburger menija, ne naredi ničesar
return;
}
showInternalSensorsInfo(); // Prikaže podatke Modula 4
return;
}
// ===== RELEJI - LEVI STOLPEC =====
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;
// Preveri levi stolpec relejev
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);
// Vizualni feedback
tft.fillRoundRect(relayStartX, buttonY, relayColumnWidth, relayButtonHeight, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(relayIndex);
updateHomeScreenRelays();
}
return;
}
}
// Preveri desni stolpec relejev
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);
// Vizualni feedback
tft.fillRoundRect(rightColumnX, buttonY, relayColumnWidth, relayButtonHeight, 3, rgbTo565(255, 255, 0));
delay(50);
toggleRelay(relayIndex);
updateHomeScreenRelays();
}
return;
}
}
// ===== PROGRESS BAR ZA NOTRANJO SVETLOBO (iz Modula 4) =====
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(); // Prikaže podatke Modula 4
return;
}
// ===== PROGRESS BAR ZA IZRAČUNANO POZICIJO SENČNIKA =====
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;
}
// ===== PROGRESS BAR ZA DEJANSKO POZICIJO SENČNIKA =====
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;
}
// ===== GUMB "DETALJI" ZA MODUL 4 (če je okvir viden) =====
// Izračunaj Y pozicijo okvirja Modula 4 (če je prikazan)
// Opomba: Modul 4 okvir je bil odstranjen iz domačega zaslona,
// vendar če ga imate, pustite ta del, sicer ga lahko odstranite
// Če imate še vedno okvir za Modul 4 na domačem zaslonu:
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;
}
// ===== OPOZORILNA VRSTICA =====
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;
}
}
// ===== DESNI ZGORNJI KOT - HITRI DOSTOP DO NASTAVITEV =====
if (x > tft.width() - 60 && y < 60) {
Serial.println("=== HITRI DOSTOP DO NASTAVITEV ===");
showTempHumControlScreen();
return;
}
// ===== LEVI ZGORNJI KOT - HITRI DOSTOP DO MODULOV =====
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;
}
}
// === Animacija za hamburger meni ob dotiku ===
void animateHamburgerMenu() {
int originalX = HAMBURGER_X;
int originalY = HAMBURGER_Y;
int size = HAMBURGER_SIZE;
// Počisti samo območje hamburger menija
tft.fillRect(originalX - 10, originalY - 10, size + 20, 3 * (3 + 5) - 5 + 20, ST77XX_BLACK);
// Hitra animacija (samo 2 koraka)
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);
}
// Hitra povrnitev
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 loadLDRCalibration() {
preferences.begin("ldr-calib", true);
ldrInternalMin = preferences.getInt("int_min", 500);
ldrInternalMax = preferences.getInt("int_max", 3000);
ldrExternalMin = preferences.getInt("ext_min", 500);
ldrExternalMax = preferences.getInt("ext_max", 3000);
ldrInternalLuxDark = preferences.getFloat("int_lux_dark", 0.0);
ldrInternalLuxBright = preferences.getFloat("int_lux_bright", 1000.0);
ldrExternalLuxDark = preferences.getFloat("ext_lux_dark", 0.0);
ldrExternalLuxBright = preferences.getFloat("ext_lux_bright", 1000.0);
preferences.end();
}
void resetLDRCalibration() {
ldrInternalMin = 500;
ldrInternalMax = 3000;
ldrExternalMin = 500;
ldrExternalMax = 3000;
saveAllSettings();
}
void updateLDR() {
unsigned long currentTimeMillis = millis();
if (currentTimeMillis - lastLDRUpdate > LDR_UPDATE_INTERVAL) {
// Preberi ADC vrednost (0-4095)
ldrInternalValue = analogRead(LDR_INTERNAL_PIN);
ldrInternalValue = constrain(ldrInternalValue, 0, 4095);
// ===== POPRAVLJENO: Uporabi KALIBRIRANE ADC vrednosti za procent =====
if (ldrInternalMin > 0 && ldrInternalMax > 0 && ldrInternalMin < ldrInternalMax) {
// Uporabi kalibrirane vrednosti
// ldrInternalMin = ADC na svetlobi (NIZEK)
// ldrInternalMax = ADC v temi (VISOK)
ldrInternalPercent = map(ldrInternalValue, ldrInternalMin, ldrInternalMax, 100, 0);
} else {
// Če ni kalibracije, uporabi približne vrednosti
ldrInternalPercent = map(ldrInternalValue, 500, 3000, 100, 0);
}
// Omeji na 0-100%
ldrInternalPercent = constrain(ldrInternalPercent, 0, 100);
// Lux izračunaj samo informativno iz procenta (ne uporabljaj napačne formule)
// To bo približno pravilno za prikaz
ldrInternalLux = map(ldrInternalPercent, 0, 100, 0, 20000);
// SERIAL DEBUG za notranji LDR
static unsigned long lastDebug = 0;
if (millis() - lastDebug > 2000) {
Serial.println("\n=== LDR DEBUG (ADC KALIBRACIJA) ===");
Serial.printf("ADC: %d\n", ldrInternalValue);
Serial.printf("Kalibracija - Svetlo ADC: %d, Temno ADC: %d\n",
ldrInternalMin, ldrInternalMax);
Serial.printf("Percent: %d%%\n", ldrInternalPercent);
Serial.printf("Lux (pribl.): %.1f\n", ldrInternalLux);
Serial.println("====================================\n");
lastDebug = millis();
}
// DEBUG za zunanjo svetlobo
static unsigned long lastExtDebug = 0;
if (millis() - lastExtDebug > 10000) {
Serial.printf("Zunanja svetloba: %.1f lx (modul1: %s)\n", externalLux, module1Active ? "aktiven" : "neaktiven");
lastExtDebug = millis();
}
lastLDRUpdate = currentTimeMillis;
}
}
String getLDRInternalLuxString() {
if (ldrInternalLux < 1.0) {
return String(ldrInternalLux, 2) + " lx";
} else if (ldrInternalLux < 10.0) {
return String(ldrInternalLux, 1) + " lx";
} else if (ldrInternalLux < 1000.0) {
return String((int)ldrInternalLux) + " lx";
} else {
return String(ldrInternalLux / 1000.0, 1) + " klx";
}
}
String getLDRInternalString() {
return String(ldrInternalPercent) + "% (" + getLDRInternalLuxString() + ")";
}
void showLDRCalibrationScreen() {
tft.fillScreen(ST77XX_BLACK);
drawGradientBackground();
drawStatusBar();
if (ldrCalibrationStep == 0) {
// ===== PRVI KORAK - IZBIRA SENZORJA =====
tft.setTextSize(2);
tft.setTextColor(CALIBRATION_COLOR);
tft.setCursor(120, 30);
tft.println("KALIBRACIJA LDR");
tft.drawRoundRect(20, 60, 440, 150, 10, CALIBRATION_COLOR);
tft.fillRoundRect(21, 61, 438, 148, 10, rgbTo565(40, 30, 20));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(220, 200, 180));
tft.setCursor(50, 80);
tft.println("Izberite senzor za kalibracijo:");
drawIconButton(150, 110, 180, 40, INTERNAL_COLOR, "NOTRANJI LDR", "light");
drawIconButton(150, 170, 180, 40, rgbTo565(100, 100, 100), "NAZAJ", "back");
} else if (ldrCalibrationStep == 1) {
// ===== DRUGI KORAK - KALIBRACIJA NOTRANJEGA LDR =====
tft.setTextSize(2);
tft.setTextColor(CALIBRATION_COLOR);
tft.setCursor(100, 25);
tft.println("KALIBRACIJA LDR");
tft.drawRoundRect(10, 50, 460, 200, 10, CALIBRATION_COLOR);
tft.fillRoundRect(11, 51, 458, 198, 10, rgbTo565(40, 30, 20));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(220, 200, 180));
// ===== NAVODILA =====
tft.setCursor(50, 70);
tft.println("NOTRANJI LDR SENZOR");
tft.drawRoundRect(40, 85, 400, 70, 5, rgbTo565(100, 100, 150));
tft.fillRoundRect(41, 86, 398, 68, 5, rgbTo565(30, 30, 50));
tft.setCursor(60, 95);
tft.println("1. Izmerite TEMO (najnižja svetloba)");
tft.setCursor(60, 110);
tft.println("2. Pritisnite 'IZMERI TEMO'");
tft.setCursor(60, 125);
tft.println("3. Izmerite SVETLOBO (najvišja svetloba)");
tft.setCursor(60, 140);
tft.println("4. Pritisnite 'IZMERI SVETLOBO'");
// ===== PRIKAZ TRENUTNIH VREDNOSTI =====
updateLDR(); // Osveži trenutno vrednost
tft.fillRoundRect(40, 160, 400, 40, 5, rgbTo565(20, 20, 40));
tft.drawRoundRect(40, 160, 400, 40, 5, CALIBRATION_COLOR);
tft.setCursor(50, 170);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print("Trenutna svetloba: ");
tft.setTextColor(rgbTo565(200, 200, 255));
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(50, 185);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("ADC: ");
tft.print(ldrInternalValue);
// ===== GUMBI =====
int buttonY = 210;
// Gumb TEMO
drawIconButton(30, buttonY, 130, 40,
rgbTo565(50, 50, 150), "IZMERI TEMO", "moon");
tft.setCursor(30, buttonY + 42);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("Trenutno: ");
if (ldrInternalLuxAtDark > 0) {
tft.printf("%.1f lx", ldrInternalLuxAtDark);
} else {
tft.print("-");
}
// Gumb SVETLOBA
drawIconButton(180, buttonY, 130, 40,
rgbTo565(255, 200, 50), "IZMERI SVETLOBO", "light");
tft.setCursor(180, buttonY + 42);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("Trenutno: ");
if (ldrInternalLuxAtBright > 0) {
tft.printf("%.1f lx", ldrInternalLuxAtBright);
} else {
tft.print("-");
}
// Gumb SHRANI
drawIconButton(330, buttonY, 120, 40,
rgbTo565(76, 175, 80), "SHRANI", "save");
// Gumb NAZAJ
drawIconButton(190, buttonY + 50, 120, 40,
rgbTo565(245, 67, 54), "NAZAJ", "back");
// ===== GRAFIČNI PRIKAZ =====
int barX = 50;
int barY = 280;
int barWidth = 380;
int barHeight = 20;
// Skala od 0 do 20000 lx
tft.fillRect(barX, barY, barWidth, barHeight, rgbTo565(60, 60, 60));
tft.drawRect(barX, barY, barWidth, barHeight, ST77XX_WHITE);
// Označi trenutno vrednost
int currentPos = map(constrain(ldrInternalLux, 0, 20000), 0, 20000, 0, barWidth);
tft.fillCircle(barX + currentPos, barY + barHeight/2, 5, rgbTo565(255, 255, 0));
// Oznake
tft.setCursor(barX, barY - 15);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("0 lx");
tft.setCursor(barX + barWidth - 40, barY - 15);
tft.print("20000 lx");
tft.setCursor(barX, barY + barHeight + 5);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.println("● Trenutna vrednost");
}
currentState = STATE_LDR_CALIBRATION;
}
void handleLDRCalibrationTouch(int x, int y) {
if (y < STATUS_BAR_HEIGHT) {
showWiFiQuickInfo();
return;
}
if (ldrCalibrationStep == 0) {
// Prvi korak - izbira senzorja
if (x > 150 && x < 330 && y > 110 && y < 150) {
ldrCalibrationType = 0; // Notranji LDR
ldrCalibrationStep = 1;
// Ponastavi kalibracijske vrednosti
ldrInternalMin = 0;
ldrInternalMax = 0;
showLDRCalibrationScreen();
return;
}
else if (x > 150 && x < 330 && y > 170 && y < 210) {
showInternalSensorsInfo();
return;
}
}
else if (ldrCalibrationStep == 1) {
updateLDR(); // Osveži trenutno vrednost
// ===== GUMB "IZMERI TEMO" =====
if (x > 30 && x < 160 && y > 210 && y < 250) {
// V TEMI je ADC VISOK
ldrInternalMax = ldrInternalValue; // Shranimo VISOKO ADC vrednost za temo
Serial.println("\n=== KALIBRACIJA - TEMO ===");
Serial.printf("ADC: %d (VISOK - pravilno za temo)\n", ldrInternalValue);
Serial.println("==========================\n");
showTemporaryMessage("🌑 Tema izmerjena (ADC: " + String(ldrInternalValue) + ")",
rgbTo565(100, 100, 255), 1500);
showLDRCalibrationScreen();
return;
}
// ===== GUMB "IZMERI SVETLOBO" =====
else if (x > 180 && x < 310 && y > 210 && y < 250) {
// NA SVETLOBI je ADC NIZEK
ldrInternalMin = ldrInternalValue; // Shranimo NIZKO ADC vrednost za svetlobo
Serial.println("\n=== KALIBRACIJA - SVETLOBA ===");
Serial.printf("ADC: %d (NIZEK - pravilno za svetlobo)\n", ldrInternalValue);
Serial.println("=============================\n");
showTemporaryMessage("☀️ Svetloba izmerjena (ADC: " + String(ldrInternalValue) + ")",
rgbTo565(255, 255, 100), 1500);
showLDRCalibrationScreen();
return;
}
// ===== GUMB "SHRANI" =====
else if (x > 330 && x < 450 && y > 210 && y < 250) {
// Preveri, ali sta obe vrednosti nastavljeni
if (ldrInternalMin == 0 || ldrInternalMax == 0) {
showTemporaryMessage("❌ Izmerite obe vrednosti!\nNajprej temo, nato svetlobo",
rgbTo565(255, 150, 50), 2500);
showLDRCalibrationScreen();
return;
}
// Preveri, ali so vrednosti smiselne (svetloba ADC < tema ADC)
if (ldrInternalMin >= ldrInternalMax) {
showTemporaryMessage("❌ Napaka: Svetloba ima višji ADC kot tema!\nPonovite kalibracijo",
rgbTo565(255, 80, 80), 3000);
// Ponastavi vrednosti
ldrInternalMin = 0;
ldrInternalMax = 0;
showLDRCalibrationScreen();
return;
}
// Shrani SAMO ADC vrednosti v preferences
preferences.begin("ldr-calib", false);
preferences.putInt("int_min", ldrInternalMin); // ADC za svetlobo (nizek)
preferences.putInt("int_max", ldrInternalMax); // ADC za temo (visok)
preferences.end();
// Shrani tudi v glavne nastavitve
saveAllSettings();
// Potrditev
String message = "✅ Kalibracija shranjena!\n";
message += "Svetlobo ADC: " + String(ldrInternalMin) + "\n";
message += "Temo ADC: " + String(ldrInternalMax);
showTemporaryMessage(message, rgbTo565(80, 220, 100), 2500);
// Izpis v Serial Monitor
Serial.println("\n=== KALIBRACIJA SHRANJENA (ADC) ===");
Serial.printf("Svetloba ADC: %d (NIZEK)\n", ldrInternalMin);
Serial.printf("Tema ADC: %d (VISOK)\n", ldrInternalMax);
Serial.println("===================================\n");
// Testni izpis
updateLDR();
Serial.printf("Trenutni ADC: %d → %d%% svetlobe\n",
ldrInternalValue, ldrInternalPercent);
ldrCalibrationStep = 0;
showInternalSensorsInfo();
return;
}
// ===== GUMB "NAZAJ" =====
else if (x > 190 && x < 310 && y > 260 && y < 300) {
ldrCalibrationStep = 0;
ldrInternalMin = 0;
ldrInternalMax = 0;
showInternalSensorsInfo();
return;
}
// ===== DODATNO: Osveži prikaz =====
else {
showLDRCalibrationScreen();
}
}
}
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;
}
// Preveri, če se stanje dejansko spreminja
if (relayStates[relayNum] == state) {
return; // Ni spremembe, ne delaj ničesar
}
if (relayControlEnabled) {
// Za releje velja: LOW = VKLOP (ker so aktivni na nizki ravni)
mcp.digitalWrite(RELAY_START_PIN + relayNum, state ? LOW : HIGH);
// Posodobi stanje v pomnilniku
relayStates[relayNum] = state;
// Pošlji v serial monitor za debugging
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");
// Če smo na domačem zaslonu, takoj posodobi prikaz relejev
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenRelays(); // Pokličemo posebno funkcijo samo za releje
}
markSettingsChanged();
} else {
Serial.println("OPOZORILO: Krmiljenje relejev je onemogočeno!");
}
}
void updateHomeScreenRelays() {
if (currentState != STATE_HOME_SCREEN) return;
// Definicije pozicij (enake kot v showHomeScreen)
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;
// POPRAVLJENO: Pravilni releji za prikaz
int leftColumnRelays[4] = { 0, 2, LIGHTING_RELAY_1, 6 }; // Rele 4 je zdaj na pravem mestu
String leftLabels[4] = { "GRET", "VLAZ", "CAS", "R7" };
int rightColumnRelays[4] = { 1, 3, LIGHTING_RELAY_2, 7 }; // Rele 5 je zdaj na pravem mestu
String rightLabels[4] = { "HLAJ", "SUS", "AVTO", "R8" };
tft.setFont();
tft.setTextSize(1);
// Posodobi levi stolpec
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);
}
// Posodobi desni stolpec
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);
}
// Posodobi spremenljivke za sledenje
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...");
}
// ==================== POPRAVLJENE FUNKCIJE ZA SENČENJE ====================
// Funkcija za izračun želene pozicije glede na NOTRANJO SVETLOBO
void calculateShadePosition() {
// Uporabi NOTRANJO svetlobo, NE zunanjo!
if (shadeAutoControl) {
if (ldrInternalLux <= shadeMinLux) {
// Zelo temno - senčnik popolnoma odprt (0% zaprtosti)
shadeCalculatedPosition = 0;
Serial.printf("🌑 NOTRANJA SVETLOBA: %.0f lx (pod min %.0f) → ODPRI (0%%)\n",
ldrInternalLux, shadeMinLux);
}
else if (ldrInternalLux >= shadeMaxLux) {
// Zelo svetlo - senčnik popolnoma zaprt (100% zaprtosti)
shadeCalculatedPosition = 100;
Serial.printf("☀️ NOTRANJA SVETLOBA: %.0f lx (nad max %.0f) → ZAPRI (100%%)\n",
ldrInternalLux, shadeMaxLux);
}
else {
// Delno zaprtje glede na svetlobo
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);
}
}
}
// Funkcija za posodobitev stanja motorja (kliče se v loop-u)
void updateMotorPosition() {
unsigned long currentTime = millis();
if (currentTime - lastShadeUpdate > SHADE_UPDATE_INTERVAL) {
// Izračunaj želeno pozicijo glede na NOTRANJO svetlobo
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 == 0x38 || address == 0x39) {
Serial.print(" (AHT20)");
} else if (address == 0x68) {
Serial.print(" (DS3231)");
} else if (address == 0x76 || address == 0x77) {
Serial.print(" (BMP280)");
}
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) {
soilMoistureValue = analogRead(SOIL_MOISTURE_PIN);
if (SOIL_DRY_VALUE != SOIL_WET_VALUE) {
soilMoisturePercent = map(soilMoistureValue, SOIL_DRY_VALUE, SOIL_WET_VALUE, 0, 100);
} else {
soilMoisturePercent = 50;
}
soilMoisturePercent = constrain(soilMoisturePercent, 0, 100);
if (currentTimeMillis - lastScreenHistoryUpdate > SCREEN_HISTORY_INTERVAL) {
screenSoilHistory[screenHistoryIndex] = soilMoisturePercent;
screenHistoryIndex = (screenHistoryIndex + 1) % SCREEN_HISTORY_SIZE;
lastScreenHistoryUpdate = currentTimeMillis;
}
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);
}
// ==================== IZBOLJŠANO SHRANJEVANJE VSEH GRAFOV ====================
void saveAllGraphsToSD() {
if (!sdInitialized || !useSDCard) return;
digitalWrite(TFT_CS, HIGH);
delay(10);
// 1. Shrani 48-urno zgodovino (varnostna kopija na SD)
File graph48File = SD.open("/graph_48h.csv", FILE_WRITE);
if (graph48File) {
graph48File.println("Type,Index,Value1,Value2,Value3,Timestamp");
// Notranji senzorji
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]);
}
}
// Zunanji senzorji
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");
}
// 2. Shrani 30-dnevno zgodovino vlage tal
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);
// 1. Naloži 30-dnevno zgodovino vlage
if (SD.exists("/soil_30d.csv")) {
File soil30File = SD.open("/soil_30d.csv", FILE_READ);
if (soil30File) {
soil30File.readStringUntil('\n'); // Preskoči header
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);
}
}
// 2. Naloži 48-urno zgodovino iz FLASH (že obstaja)
loadGraphHistoryFromFlash();
digitalWrite(TFT_CS, LOW);
}
// Spremenite obstoječo funkcijo updateGraphHistory
void updateGraphHistoryImproved() {
unsigned long currentTime = millis();
if (currentTime - lastGraphUpdate > GRAPH_UPDATE_INTERVAL) {
if (!isnan(internalTemperature) && !isnan(internalHumidity)) {
// Shrani v RAM
tempHistory48h[graphHistoryIndex] = internalTemperature;
humHistory48h[graphHistoryIndex] = internalHumidity;
luxHistory48h[graphHistoryIndex] = ldrInternalLux;
timeStamps48h[graphHistoryIndex] = currentTime;
graphHistoryIndex = (graphHistoryIndex + 1) % GRAPH_HISTORY_SIZE;
// Shrani v FLASH vsakih 5 meritev (pogosteje)
if (graphHistoryIndex % 5 == 0) {
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();
}
// Shrani na SD vsakih 10 meritev
if (sdInitialized && useSDCard && graphHistoryIndex % 10 == 0) {
saveAllGraphsToSD();
}
}
lastGraphUpdate = currentTime;
}
}
// Spremenite obstoječo funkcijo updateSoilMoisture
void updateSoilMoistureImproved() {
unsigned long currentTimeMillis = millis();
if (currentTimeMillis - lastSoilUpdate > SOIL_UPDATE_INTERVAL) {
soilMoistureValue = analogRead(SOIL_MOISTURE_PIN);
if (SOIL_DRY_VALUE != SOIL_WET_VALUE) {
soilMoisturePercent = map(soilMoistureValue, SOIL_DRY_VALUE, SOIL_WET_VALUE, 0, 100);
} else {
soilMoisturePercent = 50;
}
soilMoisturePercent = constrain(soilMoisturePercent, 0, 100);
// Shrani v 30-dnevni graf
if (currentTimeMillis - lastScreenHistoryUpdate > SCREEN_HISTORY_INTERVAL) {
screenSoilHistory[screenHistoryIndex] = soilMoisturePercent;
screenHistoryIndex = (screenHistoryIndex + 1) % SCREEN_HISTORY_SIZE;
lastScreenHistoryUpdate = currentTimeMillis;
// Shrani na SD vsakih 10 meritev
if (sdInitialized && useSDCard && screenHistoryIndex % 10 == 0) {
saveAllGraphsToSD();
}
}
// Letni arhiv (obstaja)
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;
// Shrani letni arhiv vsak dan (spremenjeno iz vsakih 7 dni)
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;
}
}
String getSoilMoistureString() {
return String(soilMoisturePercent) + "%";
}
void initializeDHT() {
Serial.println("Inicializacija DHT22 (notranje)...");
dht.begin();
delay(1000);
float t = dht.readTemperature();
float h = dht.readHumidity();
int attempts = 0;
while ((isnan(t) || isnan(h)) && attempts < 5) {
attempts++;
delay(1000);
t = dht.readTemperature();
h = dht.readHumidity();
}
if (isnan(t) || isnan(h)) {
dhtInitialized = false;
internalTemperature = 0.0;
internalHumidity = 0.0;
} else {
internalTemperature = t;
internalHumidity = h;
dhtInitialized = true;
}
}
void updateDHT() {
if (!dhtInitialized) return;
unsigned long currentTimeMillis = millis();
if (currentTimeMillis - lastDHTUpdate > DHT_UPDATE_INTERVAL) {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
internalTemperature = t;
internalHumidity = h;
}
lastDHTUpdate = currentTimeMillis;
}
}
String getInternalTemperatureString() {
if (!dhtInitialized) return "N/A";
return String(internalTemperature, 1) + "C";
}
String getInternalHumidityString() {
if (!dhtInitialized) return "N/A";
return String(internalHumidity, 1) + "%";
}
bool syncTimeWithNTP() {
if (!wifiConnected) {
Serial.println("NTP: WiFi ni povezan, ne morem sinhronizirati");
return false;
}
Serial.println("NTP: Sinhroniziram čas...");
// Pravilna nastavitev časovnega pasu za Slovenijo/CET/CEST
// GMT_OFFSET_SEC = 3600 (CET - zimska ura)
// DAYLIGHT_OFFSET_SEC = 3600 (CEST - poletna ura, +1 ura)
// Skupaj: zimska 3600, poletna 7200
configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
struct tm timeinfo;
int attempts = 0;
bool timeSet = false;
// Počakaj največ 10 sekund (20 * 500ms)
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) {
// Uporabi timeinfo.tm_isdst za pravilen poletni čas
// tm_isdst = 1 pomeni poletni čas, 0 pomeni zimski čas
int offsetHours = GMT_OFFSET_SEC / 3600;
if (timeinfo.tm_isdst > 0) {
offsetHours += 1; // Dodamo dodatno uro za poletni čas
}
// Ustvari čas z upoštevanim odmikom
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;
}
// === Funkcijo za preverjanje WiFi kanala (pomožna funkcija) ===
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);
// POMEMBNO: Preveri, če je kanal 1
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(" Nastavljam kanal na 1...");
WiFi.setChannel(1);
delay(100);
Serial.printf(" Nov kanal: %d\n", WiFi.channel());
} else {
Serial.println("✅ Kanal je pravilno nastavljen na 1");
}
} else {
Serial.println("WiFi ni povezan, kanal ni določen");
// Če WiFi ni povezan, nastavi kanal na 1 za ESP-NOW
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();
// POPRAVEK: Sinhronizacija vsakih 6 ur namesto 1 ure
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();
// Preprost izračun za Slovenijo (zadnja nedelja v marcu in oktobru)
// Poletni čas: od zadnje nedelje v marcu (2:00) do zadnje nedelje v oktobru (3:00)
bool isDST = false;
// Izračunaj zadnjo nedeljo v marcu
int lastSundayMarch = 31;
for (int day = 31; day >= 25; day--) {
DateTime testDate(now.year(), 3, day, 0, 0, 0);
if (testDate.dayOfTheWeek() == 0) { // Nedelja
lastSundayMarch = day;
break;
}
}
// Izračunaj zadnjo nedeljo v oktobru
int lastSundayOctober = 31;
for (int day = 31; day >= 25; day--) {
DateTime testDate(now.year(), 10, day, 0, 0, 0);
if (testDate.dayOfTheWeek() == 0) { // Nedelja
lastSundayOctober = day;
break;
}
}
// Preveri ali je poletni čas
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;
}
// Izpiši status poletnega časa (samo za debug)
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";
}
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 {
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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 calibrateLDRToLux(int sensorType, float knownLux) {
updateLDR();
int currentADC = (sensorType == 0) ? ldrInternalValue : ldrExternalValue;
if (sensorType == 0) {
if (knownLux < 50) {
ldrInternalLuxDark = knownLux;
} else {
ldrInternalLuxBright = knownLux;
}
} else {
if (knownLux < 50) {
ldrExternalLuxDark = knownLux;
} else {
ldrExternalLuxBright = knownLux;
}
}
}
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) {
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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) {
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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();
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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));
// NAVPIČNE ČRTE (časovne oznake)
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");
}
}
// VODORAVNE ČRTE (temperatura)
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");
}
// OZNAKE ZA VLAGO NA DESNI STRANI
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("%");
}
// PREŠTEJ VELJAVNE MERITVE
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 {
// NARIŠI GRAF, ČE OBSTAJAJO PODATKI
if (validCount > 1) {
float prevTempX = -1, prevTempY = -1;
float prevHumX = -1, prevHumY = -1;
float prevLuxX = -1, prevLuxY = -1;
// Začnemo pri najnovejšem indeksu in gremo nazaj
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;
}
// X pozicija - od leve proti desni (zdaj na levi, -48h na desni)
float xPos = graphX + (i * (graphWidth / (float)GRAPH_HISTORY_SIZE));
xPos = constrain(xPos, graphX, graphX + graphWidth);
// ===== TEMPERATURA =====
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;
}
// ===== VLAŽNOST =====
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;
}
// ===== SVETLOBA (LUX) =====
if (luxHistory48h[historyIdx] > 0 && luxHistory48h[historyIdx] < 20000) {
// Svetlobo prikažemo kot procent od 0-20000 lx
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;
}
}
// IZPIŠI STATISTIKO NA GRAF
tft.setTextSize(1);
tft.setTextColor(rgbTo565(200, 200, 255));
tft.setCursor(graphX + 10, graphY + 10);
tft.printf("Tock: %d", validCount);
}
}
// LEGENDA
int legendY = graphY + graphHeight + 20;
// Temperatura
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 (dhtInitialized && !isnan(internalTemperature)) {
tft.print(internalTemperature, 1);
tft.print("°C");
} else {
tft.print("--.-°C");
}
// Vlažnost
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 (dhtInitialized && !isnan(internalHumidity)) {
tft.print(internalHumidity, 1);
tft.print("%");
} else {
tft.print("--.-%");
}
// Svetloba
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");
}
// DODATNA INFORMACIJA O ČASU ZADNJE MERITVE
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);
}
}
// GUMBI - POPRAVLJENO: PRAVILEN NAZAJ V NOTRANJE SENZORJE
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));
// ===== NAVPIČNE ČRTE (časovne oznake) =====
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");
}
}
// ===== VODORAVNE ČRTE (temperatura) =====
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");
}
// ===== OZNAKE ZA VLAGO NA DESNI STRANI =====
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("%");
}
// ===== PREŠTEJ VELJAVNE MERITVE =====
int validCount = 0;
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
if (!isnan(extTempHistory48h[i]) && extTempHistory48h[i] != 0) {
validCount++;
}
}
// ===== NARIŠI GRAF, ČE OBSTAJAJO PODATKI =====
if (validCount > 1) {
float prevTempX = -1, prevTempY = -1;
float prevHumX = -1, prevHumY = -1;
float prevLuxX = -1, prevLuxY = -1;
// Uporabimo enako logiko kot pri notranjem grafu - prikažemo zadnjih 48 točk
// (od najnovejše do najstarejše)
for (int i = 0; i < EXTERNAL_GRAPH_HISTORY_SIZE; i++) {
// Začnemo pri trenutnem indeksu in gremo nazaj
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;
}
// X pozicija - od leve proti desni (zdaj na levi, -48h na desni)
float xPos = graphX + (i * (graphWidth / (float)EXTERNAL_GRAPH_HISTORY_SIZE));
xPos = constrain(xPos, graphX, graphX + graphWidth);
// ===== TEMPERATURA =====
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;
}
// ===== VLAŽNOST =====
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;
}
// ===== SVETLOBA (LUX) =====
if (extLuxHistory48h[historyIdx] > 0 && extLuxHistory48h[historyIdx] < 20000) {
// Svetlobo prikažemo kot procent od 0-20000 lx
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;
}
}
// ===== IZPIŠI STATISTIKO NA GRAF =====
tft.setTextSize(1);
tft.setTextColor(rgbTo565(200, 200, 255));
tft.setCursor(graphX + 10, graphY + 10);
tft.printf("Tock: %d", validCount);
} else {
// ===== IZPIŠI SPOROČILO, ČE NI PODATKOV =====
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);
// Dodatno sporočilo, če modul ni aktiven
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");
}
}
// ===== LEGENDA =====
int legendY = graphY + graphHeight + 20;
// Temperatura
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");
}
// Vlažnost
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("--.-%");
}
// Svetloba
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");
}
// ===== DODATNA INFORMACIJA O ČASU ZADNJE MERITVE =====
if (validCount > 0) {
// Poišči najnovejši čas meritve
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);
}
}
// ===== GUMBI =====
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 loadGraphHistoryFromFlash() {
Serial.println("=== NALAGANJE 48-URNE ZGODOVINE IZ FLASH ===");
preferences.begin("ext-graph-data", true); // true = read-only
// Naloži indeks
extGraphHistoryIndex = preferences.getInt("extGraphIndex", 0);
// Naloži vse podatke
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);
// Prav tako naloži notranje podatke, če so shranjeni
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);
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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 {
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
showTemporaryMessage("SD kartica ni najdena!\nPreverite povezavo", rgbTo565(255, 80, 80), 2000);
return;
}
}
useSDCard = !useSDCard;
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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 ===");
// Shrani vse grafe
if (sdInitialized && useSDCard) {
saveAllGraphsToSD();
}
// Shrani nastavitve
saveAllSettings(true);
// Shrani stanje relejev
saveRelayStates();
Serial.println("=== VARNOSTNO SHRANJEVANJE KONČANO ===");
}
// Dodajte za preprečevanje prevelikih datotek na SD
void cleanOldGraphData() {
if (!sdInitialized || !useSDCard) return;
File graphFile = SD.open("/graph_48h.csv", FILE_READ);
if (!graphFile) return;
// Preštej vrstice
int lineCount = 0;
while (graphFile.available()) {
graphFile.readStringUntil('\n');
lineCount++;
}
graphFile.close();
// Če je preveč vrstic (>5000), ustvari novo datoteko
if (lineCount > 5000) {
SD.remove("/graph_48h_old.csv");
SD.rename("/graph_48h.csv", "/graph_48h_old.csv");
saveAllGraphsToSD(); // Ustvari novo
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);
}
// Dodajte to funkcijo pred saveSettingsToSD()
void showTemporaryMessage(String message, uint16_t color, int durationMs) {
// Če ni podan durationMs, uporabi 3000
if (durationMs <= 0) durationMs = 3000;
// Shrani trenutno stanje, da ga lahko obnovimo
AppState previousState = currentState;
// Nariši okno za sporočilo - Z DEBELO OKVIRjem, da prekrije morebitne ostanke
int msgX = 80;
int msgY = 140;
int msgWidth = 320;
int msgHeight = 80;
// Najprej nariši DEBELO črno ozadje čez celotno območje (3x3 px večje od okvirja)
tft.fillRect(msgX - 3, msgY - 3, msgWidth + 6, msgHeight + 6, ST77XX_BLACK);
// Nariši okvir
tft.fillRoundRect(msgX, msgY, msgWidth, msgHeight, 10, rgbTo565(20, 40, 70));
tft.drawRoundRect(msgX, msgY, msgWidth, msgHeight, 10, rgbTo565(0, 150, 255));
// Nariši notranje ozadje
tft.fillRoundRect(msgX + 2, msgY + 2, msgWidth - 4, msgHeight - 4, 8, rgbTo565(20, 40, 70));
tft.setTextSize(1);
tft.setTextColor(color);
// Razdeli besedilo v več vrstic, če vsebuje \n
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;
}
// Počakaj določen čas
unsigned long startTime = millis();
while (millis() - startTime < durationMs) {
// Še vedno obdeluj dotike
handleTouch();
delay(10);
}
// POPRAVLJENO: Temeljito počisti območje sporočila pred obnovitvijo zaslona
tft.fillRect(msgX - 5, msgY - 5, msgWidth + 10, msgHeight + 10, ST77XX_BLACK);
// POPRAVLJENO: Namesto redrawCurrentScreen, ki morda ne počisti vsega,
// uporabimo forceFullHomeScreenRedraw za domači zaslon
if (previousState == STATE_HOME_SCREEN) {
// Popolna osvežitev domačega zaslona
forceFullHomeScreenRedraw();
} else {
// Za druge zaslone uporabimo običajno obnovitev
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("[LDR_KALIBRACIJA]");
settingsFile.printf("ldr_int_min:%d\n", ldrInternalMin);
settingsFile.printf("ldr_int_max:%d\n", ldrInternalMax);
settingsFile.printf("ldr_int_lux_dark:%.1f\n", ldrInternalLuxDark);
settingsFile.printf("ldr_int_lux_bright:%.1f\n", ldrInternalLuxBright);
settingsFile.printf("ldr_lux_b:%.2f\n", ldrLuxB);
settingsFile.printf("ldr_lux_m:%.2f\n", ldrLuxM);
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);
// ZAMENJANO: showTemporaryMessage namesto ročnega risanja
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 == "ldr_int_min") {
ldrInternalMin = value.toInt();
loadedCount++;
} else if (key == "ldr_int_max") {
ldrInternalMax = value.toInt();
loadedCount++;
} else if (key == "ldr_ext_min") {
ldrExternalMin = value.toInt();
loadedCount++;
} else if (key == "ldr_ext_max") {
ldrExternalMax = value.toInt();
loadedCount++;
} else if (key == "ldr_int_lux_dark") {
ldrInternalLuxDark = value.toFloat();
loadedCount++;
} else if (key == "ldr_int_lux_bright") {
ldrInternalLuxBright = value.toFloat();
loadedCount++;
} else if (key == "ldr_ext_lux_dark") {
ldrExternalLuxDark = value.toFloat();
loadedCount++;
} else if (key == "ldr_ext_lux_bright") {
ldrExternalLuxBright = value.toFloat();
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");
}
// ==================== INICIALIZACIJA ESP-NOW ====================
void initESPNow() {
Serial.println("\n=== INICIALIZACIJA ESP-NOW ===");
// 1. Nastavi način Wi-Fi
WiFi.mode(WIFI_STA);
// ===== POMEMBNO: Uporabi KANAL 1 (isti kot moduli) =====
int currentChannel = 1; // FIKSNI KANAL 1
// Nastavi kanal na 1
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(currentChannel, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
delay(100);
Serial.print("MAC naslov GLAVNEGA sistema: ");
Serial.println(WiFi.macAddress());
Serial.printf("ESP-NOW kanal: %d\n", currentChannel);
// 2. Inicializacija ESP-NOW
esp_err_t initResult = esp_now_init();
if (initResult != ESP_OK) {
Serial.printf("❌ ESP-NOW napaka pri inicializaciji! Koda: %d\n", initResult);
return;
}
Serial.println("✅ ESP-NOW inicializiran!");
// 3. Registracija callback za prejemanje
esp_now_register_recv_cb(esp_now_recv_cb_t(onModuleDataRecv));
Serial.println("Callback za prejemanje registriran");
// 4. DODAJANJE PEERJEV ZA VSE MODULE NA KANALU 1
Serial.println("\n=== DODAJAM PEERJE ZA MODULE NA KANALU 1 ===");
// Dodaj peer za module 1, 2, 3, 4
uint8_t* allModules[4] = { module1MAC, module2MAC, module3MAC, module4MAC };
const char* moduleNames[4] = { "Modul 1", "Modul 2", "Modul 3", "Modul 4" };
for (int m = 0; m < 4; m++) {
// Preveri, ali MAC naslov ni prazen
bool hasValidMAC = false;
for(int i = 0; i < 6; i++) {
if(allModules[m][i] != 0) {
hasValidMAC = true;
break;
}
}
if (!hasValidMAC) {
Serial.printf(" ⚠ %s nima veljavnega MAC naslova!\n", moduleNames[m]);
continue;
}
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, allModules[m], 6);
peerInfo.channel = 1; // POMEMBNO: KANAL 1!
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (!esp_now_is_peer_exist(allModules[m])) {
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.printf(" ✅ Peer za %s dodan na kanalu 1\n", moduleNames[m]);
} else {
Serial.printf(" ❌ Napaka pri dodajanju peer-ja za %s\n", moduleNames[m]);
}
} else {
Serial.printf(" ✅ Peer za %s že obstaja\n", moduleNames[m]);
}
}
Serial.println("=== KONEC DODAJANJA PEERJEV ===\n");
}
void testModule3Communication() {
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();
Serial.printf("Trenutni kanal: %d\n", WiFi.channel());
// Pošlji ukaz STOP
Serial.println("\n1. Pošiljam ukaz STOP...");
sendShadeCommand(CMD_STOP, 0, 0);
delay(500);
// Pošlji ukaz za premik na 50%
Serial.println("2. Pošiljam ukaz za premik na 50%...");
sendShadeCommand(CMD_MOVE_TO_POSITION, 50, 0);
delay(500);
// Pošlji ukaz za premik na 0%
Serial.println("3. Pošiljam ukaz za premik na 0%...");
sendShadeCommand(CMD_MOVE_TO_POSITION, 0, 0);
Serial.println("\n╔════════════════════════════════════════════════════╗");
Serial.println("║ Preverite Serial Monitor modula 3 za odziv! ║");
Serial.println("╚════════════════════════════════════════════════════╝\n");
}
// Funkcija za pošiljanje ukazov modulu 3
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);
// Izpiši MAC naslov
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();
// Preveri kanal
Serial.printf(" Trenutni kanal: %d\n", WiFi.channel());
CommandData cmd;
cmd.targetModuleId = 3;
cmd.command = command;
cmd.param1 = param1;
cmd.param2 = param2;
// Preveri, ali peer obstaja
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 = 10;
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;
}
}
// Pošlji ukaz
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);
// Poskusi ponovno z drugačnim kanalom
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; // Samodejni kanal
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");
}
// === Testno funkcijo za preverjanje pošiljanja na Modul 3 ====
void testSendToModule3() {
Serial.println("\n╔════════════════════════════════════════════════════╗");
Serial.println("║ TEST POŠILJANJA UKAZA MODULU 3 ║");
Serial.println("╚════════════════════════════════════════════════════╝");
// Izpiši MAC naslov modula 3
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();
// Preveri, ali peer obstaja
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 = 10;
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;
}
}
// Pošlji testni ukaz
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");
}
// Funkcija za premik senčnika na določeno pozicijo (ročno krmiljenje)
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);
}
// Funkcija za kalibracijo senčnika
void calibrateShade() {
Serial.println("\n🔧 KALIBRACIJA: Začenjam kalibracijo senčnika...");
sendShadeCommand(CMD_CALIBRATE, 0, 0);
}
// Funkcija za zaustavitev senčnika
void stopShade() {
Serial.println("\n⏹️ ZAUSTAVITEV: Zaustavljam senčnik");
sendShadeCommand(CMD_STOP, 0, 0);
}
// Funkcija za nujno zaustavitev senčnika
void emergencyStopShade() {
Serial.println("\n🚨 NUJNA ZAUSTAVITEV: Takojšnja zaustavitev motorja!");
sendShadeCommand(CMD_EMERGENCY_STOP, 0, 0);
}
// Funkcija za nastavitev hitrosti motorja
void setShadeSpeed(int speed) {
speed = constrain(speed, 100, 2000);
Serial.printf("\n⚙️ NASTAVITEV HITROSTI: %d\n", speed);
sendShadeCommand(CMD_SET_SPEED, speed, 0);
}
// Funkcija za vklop/izklop avtomatskega načina
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 if (moduleId == 4) {
targetMAC = module4MAC;
moduleName = "Modul 4";
moduleActiveFlag = &module4Active;
} else {
Serial.printf(" ❌ Neznan modul ID: %d\n", moduleId);
return;
}
// Preveri, ali imamo shranjen MAC naslov
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;
// Preveri, ali peer obstaja
if (!esp_now_is_peer_exist(targetMAC)) {
Serial.printf(" ⚠ Peer za %s ne obstaja, ga dodajam na kanalu 1...\n", moduleName.c_str());
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, targetMAC, 6);
peerInfo.channel = 1; // POMEMBNO: KANAL 1!
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.printf(" ❌ Napaka pri dodajanju peer-ja za %s\n", moduleName.c_str());
return;
}
Serial.printf(" ✅ Peer za %s dodan!\n", moduleName.c_str());
}
// Pošlji ukaz
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);
}
}
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 onDataSent(const uint8_t* mac_addr, esp_now_send_status_t status) {
}
// ==================== ESP-NOW PODATKI IZ VSEH MODULOV ====================
void onModuleDataRecv(const uint8_t* mac, const uint8_t* incomingData, int len) {
// DEBUG: Izpiši prejem podatkov
Serial.println("\n📡 ========== PREJET PODATEK PREKO ESP-NOW ==========");
Serial.printf(" Dolžina podatkov: %d bytes\n", len);
Serial.print(" MAC naslov pošiljatelja: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", mac[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
// Izpiši prvih 30 bajtov podatkov (kot HEX)
Serial.print(" Podatki (HEX): ");
for(int i = 0; i < min(len, 30); i++) {
Serial.printf("%02X ", incomingData[i]);
}
Serial.println();
// Izpiši kot string (če so berljivi)
Serial.print(" Podatki (STRING): ");
for(int i = 0; i < min(len, 80); i++) {
if(incomingData[i] >= 32 && incomingData[i] <= 126) {
Serial.print((char)incomingData[i]);
} else {
Serial.print(".");
}
}
Serial.println();
// ==================== PREVERI, ČE JE PODATEK IZ MODULA 4 (STRING) ====================
// Preveri, če je MAC naslov Modula 4
bool isModule4 = true;
for(int i = 0; i < 6; i++) {
if(mac[i] != module4MAC[i]) {
isModule4 = false;
break;
}
}
if (isModule4) {
// Podatki so poslani kot string
String received = String((char*)incomingData);
Serial.printf("📡 Modul 4 surovi podatki: %s\n", received.c_str());
// Odstrani "M4:" če obstaja
if (received.startsWith("M4:")) {
received = received.substring(3);
}
// RAZČLENI PODATKE - format: temp,hum,pressure,light,soilTemp,soilMoisture,TVOC=xx,CO2=xx,CH2O=xx
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();
// Preostali del (TVOC, CO2, CH2O)
String remaining = received.substring(comma6 + 1);
// Poskusi parsirati TVOC, CO2, CH2O
int tvocPos = remaining.indexOf("TVOC=");
int co2Pos = remaining.indexOf("CO2=");
int ch2oPos = remaining.indexOf("CH2O=");
if (tvocPos >= 0) {
int endPos = remaining.indexOf(',', tvocPos);
if (endPos < 0) endPos = remaining.length();
String tvocStr = remaining.substring(tvocPos + 5, endPos);
module4TVOC = tvocStr.toFloat();
}
if (co2Pos >= 0) {
int endPos = remaining.indexOf(',', co2Pos);
if (endPos < 0) endPos = remaining.length();
String co2Str = remaining.substring(co2Pos + 4, endPos);
module4ECO2 = co2Str.toFloat();
}
if (ch2oPos >= 0) {
int endPos = remaining.indexOf(',', ch2oPos);
if (endPos < 0) endPos = remaining.length();
String ch2oStr = remaining.substring(ch2oPos + 5, endPos);
module4ECH2O = ch2oStr.toFloat();
}
// ===== POMEMBNO: Posodobi NOTRANJE spremenljivke iz Modula 4 =====
internalTemperature = module4AirTemp;
internalHumidity = module4AirHum;
soilMoisturePercent = module4SoilMoisture;
ldrInternalPercent = module4LightPercent;
ldrInternalLux = map(module4LightPercent, 0, 100, 0, 20000);
module4Active = true;
lastModule4Time = millis();
Serial.printf("✅ Modul 4 (VITRINA): T=%.1f°C, H=%.1f%%, Light=%d%%, SoilT=%.1f°C, SoilM=%d%%, TVOC=%.0f, CO2=%.0f\n",
module4AirTemp, module4AirHum, module4LightPercent,
module4SoilTemp, module4SoilMoisture, module4TVOC, module4ECO2);
// Posodobi domači zaslon če je potrebno
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
else if (currentState == STATE_MODULE4_INFO) {
showModule4Info();
}
else if (currentState == STATE_AIR_QUALITY) {
showAirQualityScreen();
}
else if (currentState == STATE_INTERNAL_SENSORS_INFO) {
showInternalSensorsInfo();
}
else if (currentState == STATE_SOIL_MOISTURE_INFO) {
showSoilMoistureInfo();
}
} else {
Serial.printf(" ❌ Nepravilen format podatkov Modula 4: %s\n", received.c_str());
}
return; // Konec obdelave Modula 4
}
// ==================== PREVERI, ČE JE PODATEK IZ MODULA 1, 2 ali 3 (struktura) ====================
if (len == sizeof(ModuleData)) {
ModuleData data;
memcpy(&data, incomingData, sizeof(data));
Serial.printf(" Modul ID: %d\n", data.moduleId);
Serial.printf(" Tip modula: %d\n", data.moduleType);
Serial.printf(" Čas: %lu\n", data.timestamp);
Serial.printf(" Napaka: %d\n", data.errorCode);
Serial.printf(" Napetost baterije: %.2f V\n", data.batteryVoltage);
// ==================== MODUL 1 - VREME ====================
if (data.moduleId == 1) {
Serial.println("\n 🌡️ MODUL 1 - VREMENSKI PODATKI:");
Serial.printf(" Zunanja temperatura: %.1f °C\n", data.weather.temperature);
Serial.printf(" Zunanja vlaga: %.1f %%\n", data.weather.humidity);
Serial.printf(" Zračni tlak: %.1f hPa\n", data.weather.pressure);
Serial.printf(" Svetloba (lux): %.1f lx\n", data.weather.lux);
Serial.printf(" Temperatura BMP: %.1f °C\n", data.weather.bmpTemperature);
// Posodobi spremenljivke
externalTemperature = data.weather.temperature;
externalHumidity = data.weather.humidity;
externalPressure = data.weather.pressure;
externalLux = data.weather.lux;
// ===== KALIBRACIJSKI PODATKI ZA ZUNANJI LDR =====
if (data.errorCode == CALIB_DARK) {
ldrExternalMin = (int)data.weather.lux;
Serial.printf(" ✅ Prejeta TEMNA vrednost za zunanji LDR: %d\n", ldrExternalMin);
if (currentState == STATE_EXTERNAL_LDR_CALIBRATION) {
showExternalLDRCalibrationScreen();
}
}
else if (data.errorCode == CALIB_BRIGHT) {
ldrExternalMax = (int)data.weather.lux;
Serial.printf(" ✅ Prejeta SVETLA vrednost za zunanji LDR: %d\n", ldrExternalMax);
if (currentState == STATE_EXTERNAL_LDR_CALIBRATION) {
showExternalLDRCalibrationScreen();
}
}
else if (data.errorCode == CALIB_CONFIRM) {
Serial.println(" ✅ Modul 1 potrdil prejem kalibracijskih vrednosti");
if (currentState == STATE_EXTERNAL_LDR_CALIBRATION) {
showTemporaryMessage("Kalibracija uspešno shranjena\nna modulu 1!",
rgbTo565(80, 220, 100), 2000);
showExternalSensorsInfo();
}
}
if (!module1Active) {
module1Active = true;
Serial.println(" ✅ Modul 1 zdaj AKTIVEN!");
}
lastModule1Time = millis();
// Posodobi zaslone
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
else if (currentState == STATE_EXTERNAL_SENSORS_INFO) {
showExternalSensorsInfo();
}
else if (currentState == STATE_EXTERNAL_GRAPH_48H) {
showExternalGraph48hScreen();
}
}
// ==================== MODUL 2 - NAMAKANJE ====================
else if (data.moduleId == 2) {
Serial.println("\n 💧 MODUL 2 - NAMAKALNI SISTEM:");
Serial.printf(" Pretok 1: %.2f L/min\n", data.irrigation.flowRate1);
Serial.printf(" Pretok 2: %.2f L/min\n", data.irrigation.flowRate2);
Serial.printf(" Pretok 3: %.2f L/min\n", data.irrigation.flowRate3);
Serial.printf(" Skupaj 1: %.1f L\n", data.irrigation.totalFlow1);
Serial.printf(" Skupaj 2: %.1f L\n", data.irrigation.totalFlow2);
Serial.printf(" Skupaj 3: %.1f L\n", data.irrigation.totalFlow3);
Serial.printf(" Rele 1: %s\n", data.irrigation.relay1State ? "VKLOP" : "IZKLOP");
Serial.printf(" Rele 2: %s\n", data.irrigation.relay2State ? "VKLOP" : "IZKLOP");
Serial.printf(" Rele 3: %s\n", data.irrigation.relay3State ? "VKLOP" : "IZKLOP");
module2FlowRate1 = data.irrigation.flowRate1;
module2FlowRate2 = data.irrigation.flowRate2;
module2FlowRate3 = data.irrigation.flowRate3;
module2TotalFlow1 = data.irrigation.totalFlow1;
module2TotalFlow2 = data.irrigation.totalFlow2;
module2TotalFlow3 = data.irrigation.totalFlow3;
module2Relay1State = data.irrigation.relay1State;
module2Relay2State = data.irrigation.relay2State;
module2Relay3State = data.irrigation.relay3State;
module2Battery = data.batteryVoltage;
module2ErrorCode = data.errorCode;
if (!module2Active) {
module2Active = true;
Serial.println(" ✅ Modul 2 zdaj AKTIVEN!");
}
lastModule2Time = millis();
if (currentState == STATE_MODULE2_INFO) {
showModule2Info();
}
}
// ==================== MODUL 3 - SENČENJE ====================
else if (data.moduleId == 3) {
Serial.println("\n 🎯 MODUL 3 - SISTEM SENČENJA:");
Serial.printf(" DEJANSKA POZICIJA: %d %%\n", data.shade.currentPosition);
Serial.printf(" CILJNA POZICIJA: %d %%\n", data.shade.targetPosition);
Serial.printf(" PREMIKANJE: %s\n", data.shade.isMoving ? "DA" : "NE");
Serial.printf(" KALIBRIRAN: %s\n", data.shade.isCalibrated ? "DA" : "NE");
Serial.printf(" LIMIT ODPRT: %s\n", data.shade.limitSwitchOpen ? "DA" : "NE");
Serial.printf(" LIMIT ZAPRT: %s\n", data.shade.limitSwitchClosed ? "DA" : "NE");
Serial.printf(" TOK MOTORJA: %.2f A\n", data.shade.motorCurrent);
Serial.printf(" HITROST: %d\n", data.shade.motorSpeed);
// Posodobi spremenljivke
shadeCurrentPosition = data.shade.currentPosition;
shadeTargetPosition = data.shade.targetPosition;
shadeIsMoving = data.shade.isMoving;
shadeIsCalibrated = data.shade.isCalibrated;
shadeLimitOpen = data.shade.limitSwitchOpen;
shadeLimitClosed = data.shade.limitSwitchClosed;
shadeMotorSpeed = data.shade.motorSpeed;
if (!module3Active) {
module3Active = true;
Serial.println(" ✅ Modul 3 zdaj AKTIVEN!");
}
lastModule3Time = millis();
// Posodobi zaslone
if (currentState == STATE_HOME_SCREEN) {
updateHomeScreenDynamicValues();
}
else if (currentState == STATE_SHADE_CONTROL) {
showShadeControlScreen();
}
}
// ==================== NEZNAN MODUL ====================
else {
Serial.printf("\n ❌ NEZNAN MODUL ID: %d\n", data.moduleId);
}
}
// ==================== ČE NI MODUL 4 IN NI STRUKTURA ====================
else {
Serial.printf(" ❌ Neznan pošiljatelj (dolžina: %d bytes)\n", len);
Serial.print(" Pričakovan MAC Modula 4: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module4MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
}
Serial.println("📡 =================================================\n");
}
void verifyAllMACAddresses() {
Serial.println("\n╔════════════════════════════════════════════════════╗");
Serial.println("║ PREVERJANJE MAC NASLOVOV V KODI ║");
Serial.println("╚════════════════════════════════════════════════════╝");
Serial.print("\n📌 Modul 1 MAC (v kodi): ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module1MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.print("📌 Modul 2 MAC (v kodi): ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module2MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.print("📌 Modul 3 MAC (v kodi): ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module3MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
// Preveri, ali so MAC naslovi veljavni (ne sami 0)
bool valid1 = false, valid2 = false, valid3 = false;
for(int i = 0; i < 6; i++) {
if(module1MAC[i] != 0) valid1 = true;
if(module2MAC[i] != 0) valid2 = true;
if(module3MAC[i] != 0) valid3 = true;
}
Serial.println("\n=== PREVERJANJE VELJAVNOSTI ===");
Serial.printf("Modul 1 MAC: %s\n", valid1 ? "VELJAVEN" : "NEVELJAVEN (sam 0)");
Serial.printf("Modul 2 MAC: %s\n", valid2 ? "VELJAVEN" : "NEVELJAVEN (sam 0)");
Serial.printf("Modul 3 MAC: %s\n", valid3 ? "VELJAVEN" : "NEVELJAVEN (sam 0)");
// Pričakovani MAC naslovi
uint8_t expected1[] = {0x3A, 0x2E, 0xCB, 0x3F, 0x34, 0x2E};
uint8_t expected2[] = {0xB8, 0xF8, 0x62, 0xF8, 0x65, 0xC0};
uint8_t expected3[] = {0x58, 0x8C, 0x81, 0xCB, 0xDC, 0x80};
bool match1 = true, match2 = true, match3 = true;
for(int i = 0; i < 6; i++) {
if(module1MAC[i] != expected1[i]) match1 = false;
if(module2MAC[i] != expected2[i]) match2 = false;
if(module3MAC[i] != expected3[i]) match3 = false;
}
Serial.println("\n=== PRIMERJAVA S PRIČAKOVANIMI ===");
Serial.printf("Modul 1: %s\n", match1 ? "✅ PRAVILEN" : "❌ NAPAČEN");
Serial.printf("Modul 2: %s\n", match2 ? "✅ PRAVILEN" : "❌ NAPAČEN");
Serial.printf("Modul 3: %s\n", match3 ? "✅ PRAVILEN" : "❌ NAPAČEN");
if (!match3) {
Serial.print("\n❗ Popravite MAC naslov modula 3 na: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", expected3[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
}
Serial.println("╚════════════════════════════════════════════════════╝\n");
}
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();
}
}
}
}
void diagnoseDHT22() {
Serial.println("\n=== DIAGNOSTIKA DHT22 ===");
Serial.printf("DHTPIN: %d\n", DHTPIN);
Serial.printf("DHTTYPE: DHT22\n");
// Preveri pin konfiguracijo
pinMode(DHTPIN, INPUT_PULLUP);
delay(100);
int pinValue = digitalRead(DHTPIN);
Serial.printf("Pin %d vrednost (digitalRead): %d\n", DHTPIN, pinValue);
Serial.printf(" (1 = HIGH - senzor ne odziva, 0 = LOW - senzor se odziva)\n");
int adcValue = analogRead(DHTPIN);
Serial.printf("Pin %d ADC vrednost: %d\n", DHTPIN, adcValue);
Serial.printf(" (4095 = popolnoma HIGH, 0 = popolnoma LOW)\n");
// Poskusi večkrat prebrati senzor z daljšimi zamiki
Serial.println("\nPoskusam brati DHT22 10-krat z 2s zamiki:");
int successCount = 0;
for (int i = 0; i < 10; i++) {
Serial.printf("Poskus %d: ", i + 1);
if (i > 0) delay(2000);
// Poskusi prebrati večkrat zaporedoma
float t = NAN;
float h = NAN;
for (int retry = 0; retry < 3; retry++) {
t = dht.readTemperature();
h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) break;
delay(100);
}
if (isnan(t) || isnan(h)) {
Serial.printf("NAPAKA - t=%f, h=%f\n", t, h);
// Preveri stanje pina po branju
pinValue = digitalRead(DHTPIN);
Serial.printf(" Pin %d po branju: %d\n", DHTPIN, pinValue);
if (t == 0.0 && h == 0.0) {
Serial.println(" → Senzor ne odziva - preverite povezave in napajanje");
} else if (isnan(t) && isnan(h)) {
Serial.println(" → Senzor se odziva vendar podatki niso veljavni");
Serial.println(" → Preverite pull-up upor (4.7kΩ - 10kΩ)");
}
} else {
Serial.printf("USPEH - t=%.1f°C, h=%.1f%%\n", t, h);
successCount++;
if (successCount == 1) {
internalTemperature = t;
internalHumidity = h;
dhtInitialized = true;
}
}
}
if (successCount > 0) {
Serial.printf("\n✅ DHT22 USPESNO ZAZNAN! (%d/%d uspesnih branj)\n", successCount, 10);
dhtInitialized = true;
} else {
Serial.println("\n❌ DHT22 NI ZAZNAN!");
Serial.println("Možni vzroki:");
Serial.println(" 1. Senzor ni pravilno priključen");
Serial.println(" 2. Manjka pull-up upor (4.7kΩ - 10kΩ)");
Serial.println(" 3. Senzor je pokvarjen");
Serial.println(" 4. Napačen pin (trenutno pin 14)");
dhtInitialized = false;
internalTemperature = 0.0;
internalHumidity = 0.0;
}
Serial.println("=== KONEC DIAGNOSTIKE DHT22 ===\n");
}
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");
}
void drawHamburgerMenu(int x, int y, int size) {
int barWidth = size;
int barHeight = 3; // Zmanjšano nazaj na 3
int spacing = 5; // Zmanjšano nazaj na 5
// Manjše ozadje
tft.fillRoundRect(x - 5, y - 5, size + 10, 3 * (barHeight + spacing) - spacing + 10,
6, rgbTo565(20, 20, 40));
// Tanjši okvir
tft.drawRoundRect(x - 4, y - 4, size + 8, 3 * (barHeight + spacing) - spacing + 8,
5, rgbTo565(100, 150, 255));
// Tri črte hamburger menija
for (int i = 0; i < 3; i++) {
int barY = y + i * (barHeight + spacing);
// Glavna črta
tft.fillRoundRect(x, barY, barWidth, barHeight, 2, rgbTo565(255, 255, 255));
// Svetlejši zgornji rob
tft.drawRoundRect(x, barY, barWidth, barHeight, 2, rgbTo565(200, 200, 255));
}
// Pika, ki utripa (manjša)
if (millis() % 2000 < 1000) {
tft.fillCircle(x + size + 3, y + size/2 - 2, 2, rgbTo565(0, 255, 100));
}
}
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;
}
// ==================== ZASLON ZA KAKOVOST ZRAKA ====================
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");
// Okvir
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);
// Status modula
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 {
// TVOC
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);
// eCO2
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);
// eCH2O
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);
// Interpretacija
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!");
}
// Progress bar za TVOC
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);
// Progress bar za eCO2
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);
}
// Gumb NAZAJ
drawMenuButton(150, 260, 180, 35, rgbTo565(245, 67, 54), "NAZAJ");
currentState = STATE_AIR_QUALITY;
}
// ==================== ZASLON ZA PODATKE MODULA 4 ====================
// ==================== ZASLON ZA PODATKE MODULA 4 ====================
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");
// Okvir za vse podatke
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);
// ===== STATUS MODULA =====
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");
// Čas od zadnjega prejema
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 {
// ===== LEVI STOLPEC =====
// 1. Temperatura zraka
tft.setCursor(leftX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 220, 255));
tft.print("🌡️ Temp. zraka: ");
tft.setTextColor(TEMP_COLOR);
tft.printf("%.1f °C", module4AirTemp);
// Temperaturni bar (0-50°C)
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);
// 2. Vlaga zraka
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);
// 3. Zračni tlak
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);
// 4. Svetloba
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);
// ===== DESNI STOLPEC =====
// 5. Temperatura zemlje
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);
// 6. Vlaga tal
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);
// 7. Kakovost zraka (TVOC in eCO2)
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);
// 8. eCH2O (če obstaja)
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);
}
// ===== INTERPRETACIJA KAKOVOSTI ZRAKA =====
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");
}
// CO2 interpretacija
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");
}
// ===== GUMBI =====
int buttonY = 280;
// Gumb za podrobnosti kakovosti zraka
drawMenuButton(40, buttonY, 160, 35, rgbTo565(66, 135, 245), "KAKOVOST ZRAKA");
// Gumb za osvežitev
drawMenuButton(210, buttonY, 120, 35, rgbTo565(76, 175, 80), "OSVEZI");
// Gumb NAZAJ
drawMenuButton(340, buttonY, 120, 35, rgbTo565(245, 67, 54), "NAZAJ");
}
currentState = STATE_MODULE4_INFO;
}
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.printf("WiFi kanal glavnega ESP: %d\n", WiFi.channel());
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();
pinMode(LDR_INTERNAL_PIN, INPUT);
pinMode(SOIL_MOISTURE_PIN, INPUT);
Serial.printf("Senzor vlage tal nastavljen na pin %d (ADC1 - ZANESLJIVO)\n", SOIL_MOISTURE_PIN);
Serial.println("Inicializacija Preferences...");
preferences.begin("system-settings", false);
preferences.end();
initializeTimeBlocks();
// Inicializacija zgodovine
initializeGraphHistory();
initializeExternalGraphHistory();
// Naloži shranjeno zgodovino iz FLASH
loadGraphHistoryFromFlash();
initializeMCP23017();
delay(200);
initializeRelays();
initializeRTC();
delay(200);
initializeDHT();
delay(200);
initializeFlowSensors();
initializeMotor();
loadAllSettings(true);
loadPlantCollection();
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);
sdSPI = new SPIClass(FSPI);
sdSPI->begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
digitalWrite(TFT_CS, HIGH);
delay(10);
Serial.println("Poskusam inicializirati SD kartico...");
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);
Serial.printf(" Tip: %d\n", cardType);
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(); // Naloži vse grafe s SD kartice
} else {
loadGraphHistoryFromFlash(); // Sicer iz FLASH
}
if (!sdOk) {
Serial.println("\n❌ SD kartica NI inicializirana!");
Serial.println(" Uporabljam FLASH pomnilnik za shranjevanje.");
sdInitialized = false;
useSDCard = false;
Serial.println(" Nalagam nastavitve iz FLASH pomnilnika...");
loadAllSettings(true);
Serial.println(" Inicializiram prazen arhiv vlage tal...");
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;
setupWiFi();
Serial.println("\n=== ZACETNE MERITVE ===");
diagnoseDHT22();
updateLDR();
updateSoilMoisture();
delay(500);
updateSoilMoisture();
checkSettingsIntegrity();
// ==================== NASTAVI FIKSNE MAC NASLOVE ====================
Serial.println("\n=== NASTAVLJAM FIKSNE MAC NASLOVE ===");
// PRAVILNI MAC naslovi (pridobljeni iz Serial monitorja modulov)
// Modul 1: 3A:2E:CB:3F:34:2E
// Modul 2: B8:F8:62:F8:65:C0
// Modul 3: 58:8C:81:CB:DC:80
// Modul 4: D6:4B:CC:3F:D0:4B
uint8_t fixedModule1MAC[] = {0x3A, 0x2E, 0xCB, 0x3F, 0x34, 0x2E};
uint8_t fixedModule2MAC[] = {0xB8, 0xF8, 0x62, 0xF8, 0x65, 0xC0};
uint8_t fixedModule3MAC[] = {0x58, 0x8C, 0x81, 0xCB, 0xDC, 0x80};
uint8_t fixedModule4MAC[] = {0xD6, 0x4B, 0xCC, 0x3F, 0xD0, 0x4B};
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", module1MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.print(" Modul 2 MAC: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module2MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.print(" Modul 3 MAC: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module3MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.print(" Modul 4 MAC: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", module4MAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.println("================================\n");
// ==================== PREVERI MAC NASLOVE ====================
verifyAllMACAddresses();
// ==================== WIFI POVEZAVA - NASTAVI KANAL NA 1 ====================
Serial.println("\n=== POSKUS WIFI POVEZAVE ===");
autoConnectToWiFi(); // Poskusi povezavo na WiFi
testWiFiConnection(); // Testiraj WiFi povezavo z različnimi gesli
// POČAKAJ, DA SE WIFI STABILIZIRA (če je povezan)
if (wifiConnected) {
Serial.println("WiFi povezan, počakam 2 sekundi za stabilizacijo...");
delay(2000);
// POMEMBNO: NASTAVI KANAL NA 1 (ISTI KOT MODULI!)
WiFi.setChannel(1);
Serial.printf("📡 GLAVNI SISTEM - Wi-Fi kanal: %d\n", WiFi.channel());
} else {
Serial.println("=== WiFi ni povezan, uporabljam kanal 1 za ESP-NOW ===\n");
// Tudi če WiFi ni povezan, nastavi kanal na 1
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
}
delay(500);
// ==================== DIAGNOSTIKA PRED ESP-NOW ====================
diagnoseESPNow();
// ==================== INICIALIZACIJA ESP-NOW ====================
initESPNow();
// Počakaj, da se ESP-NOW inicializira
delay(1000);
// Preveri, ali ESP-NOW deluje
Serial.println("\n=== PREVERJANJE ESP-NOW ===");
esp_err_t checkInit = esp_now_init();
if (checkInit == ESP_ERR_ESPNOW_NOT_INIT) {
Serial.println("⚠️ ESP-NOW ni inicializiran! Ponovni poskus...");
initESPNow();
} else if (checkInit == ESP_OK) {
Serial.println("✅ ESP-NOW deluje pravilno");
} else {
Serial.printf("⚠️ ESP-NOW status: %d\n", checkInit);
}
// Preveri peer za modul 3
bool peerExists = esp_now_is_peer_exist(module3MAC);
Serial.printf(" Peer za modul 3 obstaja: %s\n", peerExists ? "DA" : "NE");
if (!peerExists) {
Serial.println(" Poskušam dodati peer za modul 3...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, module3MAC, 6);
peerInfo.channel = 1; // POMEMBNO: KANAL 1!
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println(" ✅ Peer za modul 3 dodan!");
} else {
Serial.println(" ❌ Napaka pri dodajanju peer-ja za modul 3");
}
}
// Dodaj peer za modul 1
if (!esp_now_is_peer_exist(module1MAC)) {
Serial.println(" Poskušam dodati peer za modul 1...");
esp_now_peer_info_t peerInfo1;
memset(&peerInfo1, 0, sizeof(peerInfo1));
memcpy(peerInfo1.peer_addr, module1MAC, 6);
peerInfo1.channel = 1;
peerInfo1.encrypt = false;
peerInfo1.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo1) == ESP_OK) {
Serial.println(" ✅ Peer za modul 1 dodan!");
}
}
// Dodaj peer za modul 2
if (!esp_now_is_peer_exist(module2MAC)) {
Serial.println(" Poskušam dodati peer za modul 2...");
esp_now_peer_info_t peerInfo2;
memset(&peerInfo2, 0, sizeof(peerInfo2));
memcpy(peerInfo2.peer_addr, module2MAC, 6);
peerInfo2.channel = 1;
peerInfo2.encrypt = false;
peerInfo2.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo2) == ESP_OK) {
Serial.println(" ✅ Peer za modul 2 dodan!");
}
}
// Dodaj peer za modul 4
if (!esp_now_is_peer_exist(module4MAC)) {
Serial.println(" Poskušam dodati peer za modul 4...");
esp_now_peer_info_t peerInfo4;
memset(&peerInfo4, 0, sizeof(peerInfo4));
memcpy(peerInfo4.peer_addr, module4MAC, 6);
peerInfo4.channel = 1;
peerInfo4.encrypt = false;
peerInfo4.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo4) == ESP_OK) {
Serial.println(" ✅ Peer za modul 4 dodan!");
}
}
Serial.println("==========================\n");
// ==================== PONOVNA DIAGNOSTIKA PO ESP-NOW ====================
diagnoseESPNow();
// ==================== TESTNI UKAZ ZA MODUL 3 ====================
Serial.println("\n=== POŠILJAM TESTNI UKAZ MODULU 3 ===");
delay(1000); // Počakaj, da se vse inicializira
// Pošlji testni ukaz STOP
Serial.println("1. Pošiljam ukaz STOP...");
sendShadeCommand(CMD_STOP, 0, 0);
delay(1000);
// Pošlji testni ukaz za premik na 50%
Serial.println("2. Pošiljam ukaz za premik na 50%...");
sendShadeCommand(CMD_MOVE_TO_POSITION, 50, 0);
delay(1000);
// Pošlji testni ukaz za premik na 0%
Serial.println("3. Pošiljam ukaz za premik na 0%...");
sendShadeCommand(CMD_MOVE_TO_POSITION, 0, 0);
Serial.println("=== KONEC TESTNIH UKAZOV ===\n");
// ==================== PREVERI KANAL NA KONCU ====================
checkWiFiChannel();
// ==================== ZAKLJUČEK SETUP ====================
// Ponastavi zastavice za večplastno risanje
homeScreenBackgroundDrawn = false;
homeScreenStaticElementsDrawn = false;
showHomeScreen();
Serial.println("\n=========================================");
Serial.println("=== SETUP ZAKLJUCEN - SISTEM DELUJE ===");
Serial.println("=========================================\n");
// Izpiši trenutno stanje modulov
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");
// Izpiši WiFi status na koncu
if (wifiConnected) {
Serial.printf("WiFi: %s, IP: %s, Kanal: %d, RSSI: %d dBm\n",
wifiSSID.c_str(), wifiIP.c_str(), WiFi.channel(), wifiRSSI);
} else {
Serial.println("WiFi: NI POVEZAN");
}
}
void loop() {
unsigned long now = millis();
// ==================== 1. DOTIKI ====================
handleTouch();
// ==================== 2. POSODABLJANJE PODATKOV ====================
static unsigned long lastDataUpdate = 0;
if (now - lastDataUpdate > 500) {
updateDHT();
updateLDR();
updateSoilMoisture();
updateFlowSensors();
updateRTC();
updateMCP23017();
lastDataUpdate = now;
}
// ==================== PREVERJANJE POLETNEGA ČASA ====================
static unsigned long lastDSTCheck = 0;
if (now - lastDSTCheck > 60000) { // Vsako minuto
checkDaylightSavingTime();
lastDSTCheck = now;
}
// ==================== 3. KRMILJENJE OKOLJA ====================
static unsigned long lastControlUpdate = 0;
if (now - lastControlUpdate > 2000) {
if (dhtInitialized && !isnan(internalTemperature) && !isnan(internalHumidity)) {
autoControlRelays();
}
updateVentilationControl();
lastControlUpdate = now;
}
// ==================== 4. AVTOMATSKO SENCENJE ====================
static unsigned long lastShadeAuto = 0;
if (now - lastShadeAuto > 2000) {
autoControlShade();
lastShadeAuto = now;
}
// ==================== 5. ČASOVNA RAZSVETLJAVA (RELE 4) - POPRAVLJENO! ====================
static unsigned long lastLightingCheck = 0;
if (now - lastLightingCheck > 1000) {
// KRMILJENJE ČASOVNIH BLOKOV ZA RELE 4
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;
}
// ==================== 6. AVTOMATSKA RAZSVETLJAVA (RELE 5) ====================
static unsigned long lastAutoLightCheck = 0;
if (now - lastAutoLightCheck > 1000) {
checkAndControlLighting();
lastAutoLightCheck = now;
}
// ==================== 7. ZGODOVINA MERITEV ====================
static unsigned long lastHistoryUpdate = 0;
if (now - lastHistoryUpdate > 1800000) {
updateGraphHistory();
updateExternalGraphHistory();
updateTrendHistory();
calculateTrends();
lastHistoryUpdate = now;
}
// ==================== 8. PREVERJANJE STANJA MODULOV ====================
static unsigned long lastStatusCheck = 0;
if (now - lastStatusCheck > 5000) {
checkWiFiStatus();
checkModulesTimeout();
checkWarnings();
lastStatusCheck = now;
}
// ==================== 9. SHRANJEVANJE NASTAVITEV ====================
static unsigned long lastSaveCheck = 0;
if (now - lastSaveCheck > 5000) {
autoSaveSettings();
lastSaveCheck = now;
}
// ==================== 10. POSODOBITEV DOMAČEGA ZASLONA ====================
if (currentState == STATE_HOME_SCREEN) {
static unsigned long lastHomeScreenUpdate = 0;
if (now - lastHomeScreenUpdate > 1000) {
updateHomeScreenDynamicValues();
lastHomeScreenUpdate = now;
}
}
// ==================== 11. STATUSNA VRSTICA ====================
static unsigned long lastStatusDraw = 0;
if (now - lastStatusDraw > 2000) {
drawStatusBar();
lastStatusDraw = now;
}
// ==================== 12. TROPSKE RASTLINE ====================
if (plantCount > 0 && dhtInitialized) {
static unsigned long lastPlantControl = 0;
if (now - lastPlantControl > 10000) {
controlVPDForPlants();
updateAirMovement();
checkMistingTimeout();
lastPlantControl = now;
}
}
static unsigned long lastGraphSave = 0;
if (now - lastGraphSave > 3600000) { // Vsako uro
if (sdInitialized && useSDCard) {
saveAllGraphsToSD();
}
lastGraphSave = now;
}
// ==================== 13. SINHRONIZACIJA ČASA ====================
static unsigned long lastNTPSyncCheck = 0;
if (wifiConnected && (now - lastNTPSyncCheck > 3600000)) {
syncTimeWithNTP();
lastNTPSyncCheck = now;
}
delay(10);
}
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;
// Shranimo trenutno stanje, preden kaj narišemo
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_LDR_CALIBRATION:
showLDRCalibrationScreen();
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;
default:
break;
}
lastState = stateToDraw;
}
void updateTrendHistory() {
unsigned long currentTime = millis();
if (currentTime - lastHistoryUpdate > HISTORY_UPDATE_INTERVAL && dhtInitialized) {
if (!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));
}
// Nariši gradient ozadje statusne vrstice
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);
}
// Spodnja črta
tft.drawFastHLine(0, STATUS_BAR_HEIGHT, tft.width(), rgbTo565(0, 150, 255));
// Izračunaj sredino statusne vrstice za vertikalno centriranje
int textBaselineY = STATUS_BAR_HEIGHT / 2 + 4;
// ===== LEVI DEL - WiFi ikona in SSID =====
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);
// ===== SREDINA - DATUM IN ČAS =====
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;
// Ura na fiksnem mestu
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);
}
// ===== DESNI DEL - MODULI M1, M2, M3, M4 (RAZMIK 26 PIKslov) =====
int moduleStartX = tft.width() - 220; // Začetek bloka modulov (premaknjeno bolj levo)
// RAZMIK MED MODULI: 26 pikslov
tft.setFont(&FreeSansBold9pt7b);
// M1
tft.setCursor(moduleStartX, textBaselineY);
tft.setTextColor(module1Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M1");
// M2
tft.setCursor(moduleStartX + 26, textBaselineY);
tft.setTextColor(module2Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M2");
// M3
tft.setCursor(moduleStartX + 52, textBaselineY);
tft.setTextColor(module3Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M3");
// M4
tft.setCursor(moduleStartX + 78, textBaselineY);
tft.setTextColor(module4Active ? rgbTo565(80, 220, 100) : rgbTo565(100, 100, 100));
tft.print("M4");
// ===== RSSI SIGNAL =====
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);
// WiFi bars
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 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;
// ==================== STOLPEC 1 (LEVI) ====================
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");
// ==================== STOLPEC 2 (SREDNJI) ====================
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");
// ==================== STOLPEC 3 (DESNI) ====================
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"); // <-- DODAN GUMB!
drawMenuButton(col3X, startY + 6 * buttonSpacing, buttonWidth, buttonHeight,
rgbTo565(100, 200, 255), "KAK. ZRAKA");
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;
// ========== STOLPEC 1 (LEVI) ==========
if (x >= col1X - touchTolerance && x <= col1X + buttonWidth + touchTolerance) {
// DOMOV gumb
int btnTop = startY - touchTolerance;
int btnBottom = startY + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
forceFullHomeScreenRedraw();
return;
}
// NOTRANJI gumb
btnTop = startY + buttonSpacing - touchTolerance;
btnBottom = startY + buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showInternalSensorsInfo();
return;
}
// VLAGA TAL gumb
btnTop = startY + 2 * buttonSpacing - touchTolerance;
btnBottom = startY + 2 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showSoilMoistureInfo();
return;
}
// RELEJI gumb
btnTop = startY + 3 * buttonSpacing - touchTolerance;
btnBottom = startY + 3 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showRelayControl();
return;
}
// NADZOR gumb
btnTop = startY + 4 * buttonSpacing - touchTolerance;
btnBottom = startY + 4 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showTempHumControlScreen();
return;
}
// RAZ. REL5 gumb
btnTop = startY + 5 * buttonSpacing - touchTolerance;
btnBottom = startY + 5 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showLightingControlScreen();
return;
}
// SD KARTICA gumb
btnTop = startY + 6 * buttonSpacing - touchTolerance;
btnBottom = startY + 6 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showSDCardManagementScreen();
return;
}
}
// ========== STOLPEC 2 (SREDNJI) ==========
else if (x >= col2X - touchTolerance && x <= col2X + buttonWidth + touchTolerance) {
// MCP TEST gumb
int btnTop = startY - touchTolerance;
int btnBottom = startY + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showMCP23017Test();
return;
}
// VENTILACIJA gumb
btnTop = startY + buttonSpacing - touchTolerance;
btnBottom = startY + buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showVentilationControlScreen();
return;
}
// RTC INFO gumb
btnTop = startY + 2 * buttonSpacing - touchTolerance;
btnBottom = startY + 2 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showRTCInfo();
return;
}
// ZUNANJI gumb
btnTop = startY + 3 * buttonSpacing - touchTolerance;
btnBottom = startY + 3 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showExternalSensorsInfo();
return;
}
// FLOW gumb
btnTop = startY + 4 * buttonSpacing - touchTolerance;
btnBottom = startY + 4 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showFlowSensorsInfo();
return;
}
// SENCENJE gumb
btnTop = startY + 5 * buttonSpacing - touchTolerance;
btnBottom = startY + 5 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showShadeControlScreen();
return;
}
// WI-FI gumb
btnTop = startY + 6 * buttonSpacing - touchTolerance;
btnBottom = startY + 6 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showWiFiConnectedScreen();
return;
}
}
// ========== STOLPEC 3 (DESNI) ==========
else if (x >= col3X - touchTolerance && x <= col3X + buttonWidth + touchTolerance) {
// TROPSKE gumb
int btnTop = startY - touchTolerance;
int btnBottom = startY + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showTropicalPlantsMenu();
return;
}
// AVTO REL6 gumb
btnTop = startY + buttonSpacing - touchTolerance;
btnBottom = startY + buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showLightAutoControlScreen();
return;
}
// MODUL 2 gumb
btnTop = startY + 2 * buttonSpacing - touchTolerance;
btnBottom = startY + 2 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showModule2Info();
return;
}
// GRAFI 48h gumb
btnTop = startY + 3 * buttonSpacing - touchTolerance;
btnBottom = startY + 3 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showInternalGraph48hScreen();
return;
}
// LETNI ARHIV gumb
btnTop = startY + 4 * buttonSpacing - touchTolerance;
btnBottom = startY + 4 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showYearArchiveScreen();
return;
}
// TEST M3 gumb
btnTop = startY + 5 * buttonSpacing - touchTolerance;
btnBottom = startY + 5 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
testSendToModule3();
return;
}
// KAK. ZRAKA gumb
btnTop = startY + 6 * buttonSpacing - touchTolerance;
btnBottom = startY + 6 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
showAirQualityScreen();
return;
}
// DODAN GUMB "-------" (zadnji gumb)
btnTop = startY + 6 * buttonSpacing - touchTolerance;
btnBottom = startY + 6 * buttonSpacing + buttonHeight + touchTolerance;
if (y >= btnTop && y <= btnBottom) {
// Ne naredi ničesar, ali pa dodajte funkcionalnost
return;
}
}
}
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 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 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);
// Status Modula 4
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 {
// ===== TEMPERATURA ZEMLJE =====
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);
// ===== VLAGA TAL =====
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);
// Progress bar za vlago tal
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);
// Interpretacija
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!");
}
}
// ===== GUMBI =====
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) {
updateSoilMoisture();
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);
// Preštej veljavne meritve
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);
// Približno število dni (3 ure med meritvami = 8 meritev na dan)
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));
// ===== NAVPIČNE ČRTE (časovne oznake - dnevi) =====
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");
}
}
// ===== VODORAVNE ČRTE (procenti vlage) =====
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("%");
}
// ===== NARIŠI GRAF, ČE OBSTAJAJO PODATKI =====
if (validCount > 1) {
// Zberi vse veljavne vrednosti in jih uredi po času
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++;
}
}
// Uredi po indeksu (času) - od najstarejšega do najnovejšega
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;
}
}
}
// Poišči najstarejši in najnovejši indeks
int oldestIdx = indices[0];
int newestIdx = indices[validIdx - 1];
// Izračunaj časovni razpon v indeksih (število meritev)
int timeSpan = newestIdx - oldestIdx;
if (timeSpan <= 0) timeSpan = validIdx;
float prevX = -1, prevY = -1;
// Nariši črte od najstarejšega (desno) do najnovejšega (levo)
for (int i = 0; i < validIdx; i++) {
// Izračunaj starost meritve kot delež (0 = najnovejša, 1 = najstarejša)
float age;
if (timeSpan > 0) {
age = (float)(newestIdx - indices[i]) / timeSpan;
} else {
age = (float)(validIdx - 1 - i) / (validIdx - 1);
}
// X pozicija - od leve proti desni (zdaj na levi, starejše na desni)
int x = graphX + (int)(age * graphWidth);
x = constrain(x, graphX, graphX + graphWidth);
// Y pozicija glede na procent vlage
int y = graphY + graphHeight - (int)((validValues[i] / 100.0) * graphHeight);
y = constrain(y, graphY, graphY + graphHeight);
// Nariši črto med točkami
if (prevX != -1) {
tft.drawLine(prevX, prevY, x, y, SOIL_COLOR);
tft.drawLine(prevX + 1, prevY, x + 1, y, SOIL_COLOR);
}
// Nariši piko na vsaki točki
tft.fillCircle(x, y, 2, SOIL_COLOR);
prevX = x;
prevY = y;
}
// Izračunaj statistiko
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];
}
// Prikaži statistiko na grafu
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 {
// ===== IZPIŠI SPOROČILO, ČE NI PODATKOV =====
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);
}
// ===== LEGENDA =====
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("%");
// Dodatna informacija o kalibraciji
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);
// ===== GUMBI =====
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 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("Trenutni ADC: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.print(soilMoistureValue);
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) {
updateSoilMoisture();
tft.fillRect(40, 190, 200, 15, rgbTo565(40, 30, 20));
tft.setCursor(40, 190);
tft.setTextColor(rgbTo565(255, 200, 100));
tft.print("Trenutni ADC: ");
tft.setTextColor(rgbTo565(200, 200, 255));
tft.println(soilMoistureValue);
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 = soilMoistureValue;
drySet = true;
} else if (x > 190 && x < 310) {
wetValue = soilMoistureValue;
wetSet = true;
} else if (x > 330 && x < 450) {
if (!drySet || !wetSet) {
tft.fillRect(100, 160, 280, 30, rgbTo565(40, 30, 20));
tft.setCursor(100, 170);
tft.setTextColor(rgbTo565(255, 80, 80));
tft.println("Nastavi obe vrednosti!");
delay(2000);
} else if (dryValue <= wetValue) {
tft.fillRect(100, 160, 280, 30, rgbTo565(40, 30, 20));
tft.setCursor(100, 170);
tft.setTextColor(rgbTo565(255, 80, 80));
tft.println("Suho mora biti > Mokro!");
delay(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();
tft.fillRect(100, 160, 280, 30, rgbTo565(40, 30, 20));
tft.setCursor(120, 170);
tft.setTextColor(rgbTo565(80, 220, 100));
tft.println("Kalibracija shranjena!");
delay(2000);
calibrating = false;
}
}
} else if (y > 270 && y < 310 && x > 190 && x < 310) {
calibrating = false;
}
}
delay(100);
}
showSoilMoistureInfo();
}
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);
// Status Modula 4
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 {
// ===== TEMPERATURA ZRAKA =====
tft.setCursor(leftColumnX, yOffset + lineHeight);
tft.setTextColor(rgbTo565(220, 200, 255));
tft.print("Temp. zraka: ");
tft.setTextColor(TEMP_COLOR);
tft.printf("%.1f °C", module4AirTemp);
// Temperaturni bar (0-50°C)
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);
// ===== VLAGA ZRAKA =====
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);
// ===== SVETLOBA =====
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);
// ===== TEMPERATURA ZEMLJE =====
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);
// ===== VLAGA TAL =====
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);
// ===== ZRAČNI TLAK =====
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);
}
// ===== GUMBI =====
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) {
updateDHT();
updateLDR();
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) {
ldrCalibrationStep = 0;
ldrCalibrationType = 0;
showLDRCalibrationScreen();
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) {
updateDHT();
updateLDR();
updateGraphHistory();
showInternalGraph48hScreen();
return;
}
if (x > 180 && x < 180 + buttonWidth && y > buttonY && y < buttonY + buttonHeight) {
showInternalSensorsInfo();
return;
}
}
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");
// ===== ZMANJŠAN OKVIR - višina 170 namesto 210 =====
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);
// ===== AHT20 =====
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) {
// Temperatura
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);
// Vlažnost
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");
}
// ===== BMP280 =====
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) {
// Tlak
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);
}
// ===== SVETLOBA =====
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);
// ===== NUMERIČNE VREDNOSTI =====
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);
}
// ===== PRIKAZ KALIBRACIJSKIH VREDNOSTI (skrajšan) =====
tft.setCursor(rightColumnX, measurementY2);
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("Kal:");
if (ldrExternalMin > 0 && ldrExternalMax > 0) {
if (ldrExternalMin < ldrExternalMax) {
tft.setTextColor(rgbTo565(80, 220, 100));
tft.printf("%d/%d", ldrExternalMin, ldrExternalMax);
} else {
tft.setTextColor(rgbTo565(255, 150, 50));
tft.print("!NEVELJ");
}
} else {
tft.setTextColor(rgbTo565(150, 150, 150));
tft.print("NI");
}
}
// ===== GUMBI - ZNOTRAJ VIDNEGA OBMOČJA =====
int buttonY = 228; // Gumbi višje (prej 265)
int buttonWidth = 135;
int buttonHeight = 32;
int buttonSpacing = 10;
// VRSTICA 1 - OSVEZI in 48H GRAF
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");
// VRSTICA 2 - SHRANI, KALIB. ZUN. in NAZAJ
int row2Y = buttonY + buttonHeight + 8;
int btnW2 = 110;
int spacing2 = 8;
int totalWidth = 3 * btnW2 + 2 * spacing2;
int row2X = (tft.width() - totalWidth) / 2;
// SHRANI
drawMenuButton(row2X, row2Y, btnW2, buttonHeight, rgbTo565(76, 175, 80), "SHRANI");
// KALIB. ZUN.
drawMenuButton(row2X + btnW2 + spacing2, row2Y, btnW2, buttonHeight,
EXTERNAL_COLOR, "KALIB. ZUN.");
// NAZAJ
drawMenuButton(row2X + 2 * (btnW2 + spacing2), row2Y, btnW2, buttonHeight,
rgbTo565(245, 67, 54), "NAZAJ");
// ===== DODATNO: Prikaz stanja modula =====
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 = 140;
int buttonHeight = 35;
int buttonSpacing = 10; // Enak razmik kot v show funkciji
int buttonY1 = 225; // Prva vrstica gumbov
int buttonY2 = buttonY1 + buttonHeight + 8; // Druga vrstica gumbov
// ===== VRSTICA 1 (buttonY1) =====
// Gumb "OSVEZI" (levi)
if (x > 40 && x < 40 + buttonWidth && y > buttonY1 && y < buttonY1 + buttonHeight) {
Serial.println("Gumb OSVEZI pritisnjen");
showExternalSensorsInfo(); // Osveži zaslon
return;
}
// Gumb "48H GRAF" (desni)
if (x > 40 + buttonWidth + buttonSpacing && x < 40 + 2 * buttonWidth + buttonSpacing &&
y > buttonY1 && y < buttonY1 + buttonHeight) {
Serial.println("Gumb 48H GRAF pritisnjen");
showExternalGraph48hScreen(); // Pokaži 48-urni graf
return;
}
// ===== VRSTICA 2 (buttonY2) - TRIJE GUMBI =====
// Gumb "SHRANI" (prvi - levo)
if (x > 20 && x < 20 + buttonWidth && y > buttonY2 && y < buttonY2 + buttonHeight) {
Serial.println("Gumb SHRANI pritisnjen");
if (sdInitialized && useSDCard) {
saveSettingsToSD(); // Shrani vse nastavitve na SD kartico
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(); // Poskusimo inicializirati SD kartico
} else if (!useSDCard) {
showTemporaryMessage("Shranjevanje ni na SD!\nPreklopite nacin", rgbTo565(255, 150, 50), 2000);
}
}
showExternalSensorsInfo(); // Ponovno prikaži zaslon
return;
}
// Gumb "KALIB. ZUN." (drugi - sredina)
if (x > 20 + buttonWidth + buttonSpacing && x < 20 + 2 * buttonWidth + buttonSpacing &&
y > buttonY2 && y < buttonY2 + buttonHeight) {
Serial.println("Gumb KALIB. ZUN. pritisnjen");
if (module1Active) {
// Preverimo, ali je modul 1 aktiven
ldrCalibrationStep = 0;
ldrCalibrationType = 1; // 1 = zunanji LDR
showExternalLDRCalibrationScreen(); // Pokaži kalibracijski zaslon za zunanji LDR
} else {
showTemporaryMessage("Modul 1 ni dosegljiv!\nPreverite povezavo", rgbTo565(255, 80, 80), 2000);
showExternalSensorsInfo(); // Ponovno prikaži zaslon
}
return;
}
// Gumb "NAZAJ" (tretji - desno)
if (x > 20 + 2 * (buttonWidth + buttonSpacing) && x < 20 + 3 * buttonWidth + 2 * buttonSpacing &&
y > buttonY2 && y < buttonY2 + buttonHeight) {
Serial.println("Gumb NAZAJ pritisnjen");
showMainMenu(); // Nazaj v glavni meni
return;
}
}
void setupWiFi() {
Serial.println("\n=== INICIALIZACIJA WIFI ===");
WiFi.disconnect(true);
delay(100);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.setSleep(false);
// Nastavi kanal na 10 že na začetku
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
Serial.println("📡 Kanal nastavljen na 10");
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 WIFI INICIALIZACIJE ===\n");
}
// === Funkcijo za testiranje WiFi brez gesla ===
void testWiFiConnection() {
Serial.println("\n╔══════════════════════════════════════════════════════════════╗");
Serial.println("║ TEST WIFI POVEZAVE ║");
Serial.println("╚══════════════════════════════════════════════════════════════╝");
// Shrani trenutno stanje
bool wasConnected = wifiConnected;
// Prekini morebitno obstoječo povezavo
WiFi.disconnect(true);
delay(500);
// 1. Poskusi povezavo brez gesla (če je omrežje odprto)
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);
}
// 2. Poskusi s pogostimi gesli
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);
}
// 3. Če ni uspelo, poskusi z dodatnimi informacijami
if (!connected) {
Serial.println("\n📡 3. Dodatne informacije za debug:");
Serial.printf(" MAC naslov ESP32: %s\n", WiFi.macAddress().c_str());
// Ponovno skeniraj omrežja za preverjanje
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");
}
// Obnovi prejšnje stanje
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=== SAMODEJNO POVEZOVANJE WIFI ===");
if (WiFi.status() == WL_CONNECTED) {
if (!wifiConnected) {
wifiConnected = true;
wifiSSID = WiFi.SSID();
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
autoConnecting = false;
Serial.println("✅ Že povezan na WiFi!");
}
return;
}
static unsigned long lastAttempt = 0;
if (autoConnecting && (millis() - lastAttempt < 30000)) {
Serial.println("⏳ Že se povezujem, počakam 30 sekund...");
return;
}
if (autoConnecting) {
autoConnecting = false;
}
lastAttempt = millis();
autoConnecting = true;
wifiConnected = false;
Serial.println("Zacenjam povezovanje na WiFi...");
// Skeniraj omrežja
Serial.println("\n🔍 Skeniram WiFi omrezja...");
int n = WiFi.scanNetworks();
if (n == 0) {
Serial.println(" Ni najdenih omrezij!");
} else {
Serial.printf(" Najdenih %d omrezij:\n", n);
for (int i = 0; i < n; i++) {
String ssid = WiFi.SSID(i);
int channel = WiFi.channel(i);
int rssi = WiFi.RSSI(i);
bool encrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN);
Serial.printf(" %d. %s (kanal: %d, RSSI: %d dBm, %s)\n",
i+1, ssid.c_str(), channel, rssi,
encrypted ? "ZAKLENJENO" : "ODPRTO");
// Preveri, če je to naše omrežje
if (ssid == "WiFi-192") {
Serial.printf(" → NAJDENO! Kanal: %d, RSSI: %d\n", channel, rssi);
}
}
}
WiFi.scanDelete();
// Počisti stare nastavitve
WiFi.disconnect(true);
delay(100);
// Poskusi povezavo
Serial.println("\n🔍 Poskusam povezavo na: WiFi-192");
Serial.println(" Geslo: sijuan5768");
// Poskusi z različnimi načini povezave
WiFi.begin("WiFi-192", "sijuan5768");
// Počakaj do 20 sekund na povezavo
int attempts = 0;
bool connected = false;
while (attempts < 100) { // 100 * 200ms = 20 sekund
delay(200);
attempts++;
wl_status_t status = WiFi.status();
if (status == WL_CONNECTED) {
connected = true;
break;
}
if (attempts % 10 == 0) {
Serial.printf(" Čakam na povezavo... (%d/100), status: %d\n", attempts, status);
if (status == WL_NO_SSID_AVAIL) {
Serial.println(" → Omrežje 'WiFi-192' ni najdeno!");
}
}
}
if (connected) {
wifiConnected = true;
wifiSSID = "WiFi-192";
wifiIP = WiFi.localIP().toString();
wifiRSSI = WiFi.RSSI();
autoConnecting = false;
Serial.println("\n✅ WiFi POVEZAN!");
Serial.printf(" SSID: %s\n", wifiSSID.c_str());
Serial.printf(" IP: %s\n", wifiIP.c_str());
Serial.printf(" RSSI: %d dBm\n", wifiRSSI);
// POMEMBNO: Nastavi kanal na 1 po povezavi
WiFi.setChannel(1);
Serial.printf(" Kanal: %d\n", WiFi.channel());
if (!ntpSynchronized) {
syncTimeWithNTP();
}
drawStatusBar();
if (currentState == STATE_WIFI_CONNECTING) {
showWiFiConnectedScreen();
currentState = STATE_WIFI_CONNECTED;
}
return;
}
Serial.printf("❌ Povezava na WiFi-192 neuspesna (status: %d)\n", WiFi.status());
Serial.println(" Preverite:");
Serial.println(" 1. Ali je geslo pravilno? ('sijuan5768')");
Serial.println(" 2. Ali ima router omogocen DHCP?");
Serial.println(" 3. Ali je MAC naslov ESP32 dovoljen na routerju?");
Serial.printf(" MAC naslov ESP32: %s\n", WiFi.macAddress().c_str());
autoConnecting = false;
drawStatusBar();
}
void checkWiFiStatus() {
unsigned long currentTime = millis();
static unsigned long lastConnectAttempt = 0;
if (currentTime - lastWifiCheck > 2000) {
uint8_t currentStatus = WiFi.status();
// Debug izpis statusa
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;
// POMEMBNO: Nastavi kanal na 10
WiFi.setChannel(1); // iz 10 na 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();
}
// Poveži se samo, če ni že aktivnega povezovanja in če je minilo dovolj časa
if (!autoConnecting && currentState != STATE_WIFI_CONNECTING &&
(currentTime - lastConnectAttempt > 30000)) { // Vsakih 30 sekund
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();
// ===== OPOZORILO ZA VLAGO TAL (iz Modula 4) =====
if (module4Active && module4SoilMoisture > 0) {
float soilWarningMin = 20; // Spodnja meja za opozorilo
float soilWarningMax = 80; // Zgornja meja za opozorilo
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;
}
}
// ===== OPOZORILO ZA TEMPERATURO (iz Modula 4) =====
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;
}
}
// ===== OPOZORILO ZA VLAGO (iz Modula 4) =====
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;
}
}
// Timeout za opozorila (po 5 sekundah izginejo)
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:
// Ne naredi ničesar med povezovanjem
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:
// Ne naredi ničesar med bootanjem
break;
case STATE_LDR_CALIBRATION:
handleLDRCalibrationTouch(x, y);
break;
case STATE_EXTERNAL_LDR_CALIBRATION: // NOVO STANJE!
handleExternalLDRCalibrationTouch(x, y);
break;
case STATE_TEMP_HUM_CONTROL:
handleTempHumControlTouch(x, y);
break;
case STATE_SHADE_CONTROL:
handleShadeControlTouch(x, y);
break;
case STATE_LIGHTING_CONTROL:
handleLightingControlTouch(x, y);
break;
case STATE_EDIT_LIGHTING_BLOCK:
handleEditLightingBlockTouch(x, y);
break;
case STATE_LIGHT_AUTO_CONTROL:
handleLightAutoControlTouch(x, y);
break;
case STATE_EDIT_LIGHT_TIME:
handleEditLightTimeTouch(x, y);
break;
case STATE_VENTILATION_CONTROL:
handleVentilationControlTouch(x, y);
break;
case STATE_SD_MANAGEMENT:
handleSDCardManagementTouch(x, y);
break;
case STATE_MODULE2_INFO:
handleModule2InfoTouch(x, y);
break;
case STATE_TROPICAL_PLANTS:
if (x > 40 && x < 180 && y > 240 && y < 275) {
showAddPlantScreen();
}
else if (x > 200 && x < 340 && y > 240 && y < 275) {
if (plantCount > 0) {
showSelectPlantForViewScreen();
} else {
tft.fillRect(100, 150, 280, 60, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 60, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(130, 170);
tft.println("Ni dodanih rastlin!");
tft.setCursor(110, 190);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Dodajte novo rastlino");
delay(2000);
showTropicalPlantsMenu();
}
}
else if (x > 40 && x < 180 && y > 285 && y < 320) {
if (plantCount > 0) {
showSelectPlantForAdviceScreen();
} else {
tft.fillRect(100, 150, 280, 60, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 60, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 150, 50));
tft.setCursor(140, 170);
tft.println("Ni rastlin!");
tft.setCursor(120, 190);
tft.setTextColor(rgbTo565(200, 220, 255));
tft.println("Dodajte vsaj eno");
delay(2000);
showTropicalPlantsMenu();
}
}
else if (x > 200 && x < 340 && y > 285 && y < 320) {
showMainMenu();
}
break;
case STATE_SELECT_PLANT_FOR_VIEW:
{
int startY = 95;
int buttonHeight = 28;
int buttonSpacing = 4;
int maxButtons = 5;
for (int i = 0; i < min(plantCount, maxButtons); i++) {
int yPos = startY + i * (buttonHeight + buttonSpacing);
if (x > 30 && x < 450 && y > yPos && y < yPos + buttonHeight) {
showPlantDetailsScreen(i);
return;
}
}
int buttonY = startY + maxButtons * (buttonHeight + buttonSpacing) + 15;
if (x > 30 && x < 230 && y > buttonY && y < buttonY + 30) {
tft.fillRect(100, 150, 280, 80, rgbTo565(20, 40, 70));
tft.drawRect(100, 150, 280, 80, rgbTo565(0, 150, 255));
tft.setTextSize(1);
tft.setTextColor(rgbTo565(255, 255, 255));
tft.setCursor(130, 165);
tft.println("Za katero rastlino?");
int optionY = 190;
for (int i = 0; i < min(3, plantCount); i++) {
tft.fillRoundRect(120, optionY + i * 25, 240, 22, 5, rgbTo565(66, 135, 245));
tft.drawRoundRect(120, optionY + i * 25, 240, 22, 5, ST77XX_WHITE);
String optionText = String(i + 1) + ". " + myPlants[i].name;
if (optionText.length() > 20) optionText = optionText.substring(0, 17) + "...";
tft.setCursor(130, optionY + i * 25 + 5);
tft.setTextColor(ST77XX_WHITE);
tft.print(optionText);
}
unsigned long waitStart = millis();
bool waiting = true;
while (waiting && millis() - waitStart < 8000) {
if (ts.touched()) {
TS_Point p = ts.getPoint();
int touchX = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
int touchY = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
for (int i = 0; i < min(3, plantCount); i++) {
if (touchX > 120 && touchX < 360 && touchY > optionY + i * 25 && touchY < optionY + i * 25 + 22) {
int speciesIndex = findPlantSpeciesIndex(myPlants[i].species);
if (speciesIndex >= 0) {
autoConfigureForPlant(speciesIndex);
showMessage("Nastavitve za " + myPlants[i].name, rgbTo565(80, 220, 100));
}
waiting = false;
break;
}
}
}
delay(10);
}
showSelectPlantForViewScreen();
return;
}
if (x > 250 && x < 450 && y > buttonY && y < buttonY + 30) {
showTropicalPlantsMenu();
return;
}
break;
}
case STATE_SELECT_PLANT_FOR_ADVICE:
{
int startY = 85;
int buttonHeight = 35;
int buttonSpacing = 5;
int maxButtons = 5;
for (int i = 0; i < min(plantCount, maxButtons); i++) {
int yPos = startY + i * (buttonHeight + buttonSpacing);
if (x > 30 && x < 450 && y > yPos && y < yPos + buttonHeight) {
String advice = getPlantCareAdvice(i);
showAdvicePopup(advice);
return;
}
}
int buttonY = startY + maxButtons * (buttonHeight + buttonSpacing) + 10;
if (x > 150 && x < 330 && y > buttonY && y < buttonY + 35) {
showTropicalPlantsMenu();
}
break;
}
case STATE_ADD_PLANT:
if (x > 30 && x < 75 && y > 160 && y < 185) {
tempSpeciesIndex = (tempSpeciesIndex - 1 + tropicalSpeciesCount) % tropicalSpeciesCount;
showAddPlantScreen();
}
else if (x > 90 && x < 135 && y > 160 && y < 185) {
tempSpeciesIndex = (tempSpeciesIndex + 1) % tropicalSpeciesCount;
showAddPlantScreen();
}
else if (x > 130 && x < 200 && y > 195 && y < 220) {
tempIsVariegated = !tempIsVariegated;
showAddPlantScreen();
}
else if (x > 250 && x < 295 && y > 115 && y < 140) {
tempLocationZone = max(1, tempLocationZone - 1);
showAddPlantScreen();
}
else if (x > 310 && x < 355 && y > 115 && y < 140) {
tempLocationZone = min(3, tempLocationZone + 1);
showAddPlantScreen();
}
else if (x > 40 && x < 180 && y > 265 && y < 300) {
if (tempPlantName.length() == 0) {
tempPlantName = "Rastlina " + String(plantCount + 1);
}
addPlantToCollection(tempPlantName, tropicalSpecies[tempSpeciesIndex].species,
tempIsVariegated, "cona" + String(tempLocationZone));
tempPlantName = "";
tempSpeciesIndex = 0;
tempIsVariegated = false;
tempLocationZone = 1;
showTropicalPlantsMenu();
}
else if (x > 200 && x < 340 && y > 265 && y < 300) {
tempPlantName = "";
tempSpeciesIndex = 0;
tempIsVariegated = false;
tempLocationZone = 1;
showTropicalPlantsMenu();
}
else if (x > 360 && x < 500 && y > 265 && y < 300) {
if (tempPlantName.length() == 0) {
tempPlantName = "Moja " + tropicalSpecies[tempSpeciesIndex].commonName;
} else {
tempPlantName = "";
}
showAddPlantScreen();
}
break;
case STATE_PLANT_DETAILS:
{
int buttonWidth = 105;
int buttonSpacing = 10;
int buttonY = 260;
if (x > 15 && x < 15 + buttonWidth && y > buttonY && y < buttonY + 35) {
startMistingForPlant(selectedPlantId);
showPlantDetailsScreen(selectedPlantId);
}
else if (x > 15 + buttonWidth + buttonSpacing && x < 15 + 2 * buttonWidth + buttonSpacing && y > buttonY && y < buttonY + 35) {
String advice = getPlantCareAdvice(selectedPlantId);
showAdvicePopup(advice);
}
else if (x > 15 + 2 * (buttonWidth + buttonSpacing) && x < 15 + 3 * buttonWidth + 2 * buttonSpacing && y > buttonY && y < buttonY + 35) {
showSelectPlantForViewScreen();
}
else if (x > 15 + 3 * (buttonWidth + buttonSpacing) && x < 15 + 4 * buttonWidth + 3 * buttonSpacing && y > buttonY && y < buttonY + 35) {
deletePlant(selectedPlantId);
}
}
break;
case STATE_YEAR_ARCHIVE:
if (x > 150 && x < 330 && y > 270 && y < 305) {
showSoilMoistureInfo();
}
break;
case STATE_SOIL_MOISTURE_GRAPH:
if (x > 40 && x < 220 && y > 295 && y < 330) {
showYearArchiveScreen();
}
else if (x > 260 && x < 440 && y > 295 && y < 330) {
showSoilMoistureInfo();
}
break;
default:
break;
}
} else if (abs(x - lastX) > 2 || abs(y - lastY) > 2) {
lastX = x;
lastY = y;
}
} else {
if (touchPressed) {
touchPressed = false;
}
}
}
// Prosim, če vse popravljene funkcije izpišeš vsako posebaj in cele in ne samo spremenjene dele.T_CLK
T_CS
T_DIN
T_DO
T_IRQ
GRETJE
HLAJENJE
SUŠENJE
LUČI 1
VLAŽENJE
ČRPALKA
LUČI 2
ČRPALKA