// 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(10);
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(10, 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 ===");
// ===== NASTAVI FIKSNI KANAL NA 10 =====
setFixedWiFiChannel();
// Preveri trenutni kanal
int currentChannel = WiFi.channel();
if (currentChannel != 10) {
Serial.printf("⚠️ Kanal ni 10! Trenutno: %d, popravljam...\n", currentChannel);
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
delay(100);
}
Serial.printf("ESP-NOW uporablja kanal: %d\n", WiFi.channel());
// 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!");
// Registracija callback za prejemanje
esp_now_register_recv_cb(esp_now_recv_cb_t(onModuleDataRecv));
Serial.println("Callback za prejemanje registriran");
// ===== DODAJANJE PEERJEV ZA MODULE NA KANALU 10 =====
Serial.println("\n=== DODAJAM PEERJE ZA MODULE (KANAL 10) ===");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
peerInfo.channel = 10; // POMEMBNO: ISTI KANAL ZA VSE!
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
// Funkcija za dodajanje peerja
auto addPeer = [&](uint8_t* mac, const char* name) {
memcpy(peerInfo.peer_addr, mac, 6);
// Če peer že obstaja, ga najprej odstrani
if (esp_now_is_peer_exist(mac)) {
esp_now_del_peer(mac);
delay(50);
}
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.printf(" ✅ Peer za %s dodan (kanal %d)\n", name, peerInfo.channel);
} else {
Serial.printf(" ❌ Napaka pri dodajanju peer-ja za %s\n", name);
}
};
// Dodaj vse module
addPeer(module1MAC, "Modul 1");
addPeer(module2MAC, "Modul 2");
addPeer(module3MAC, "Modul 3");
addPeer(module4MAC, "Modul 4");
Serial.println("=== KONEC DODAJANJA PEERJEV ===\n");
}
void setFixedWiFiChannel() {
Serial.println("\n=== NASTAVLJAM FIKSNI KANAL 10 ===");
// Onemogoči samodejno povezovanje in počišči stare nastavitve
WiFi.disconnect(true);
delay(100);
// Nastavi način
WiFi.mode(WIFI_STA);
delay(100);
// POMEMBNO: Ne shranjuj WiFi nastavitev v NVS
esp_wifi_set_storage(WIFI_STORAGE_RAM);
// Nastavi fiksni kanal 10
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
delay(100);
Serial.printf("Kanal nastavljen na: %d\n", WiFi.channel());
Serial.println("================================\n");
}
void checkAndFixWiFiChannel() {
static unsigned long lastChannelCheck = 0;
unsigned long now = millis();
if (now - lastChannelCheck > 30000) { // Vsakih 30 sekund
int currentChannel = WiFi.channel();
if (currentChannel != 10) {
Serial.printf("⚠️ Kanal spremenjen na %d! Popravljam na 10...\n", currentChannel);
// Onemogoči WiFi za trenutek
WiFi.disconnect(true);
delay(100);
// Ponastavi kanal
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
delay(100);
// Ponovno poveži WiFi (če je bil povezan)
if (wifiConnected) {
WiFi.begin(wifiSSID.c_str());
}
Serial.printf("Kanal popravljen na: %d\n", WiFi.channel());
}
lastChannelCheck = now;
}
}
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;
}
// === funkcijo za debug ===
void printESPNowStatus() {
Serial.println("\n=== ESP-NOW STATUS ===");
Serial.printf("WiFi kanal: %d\n", WiFi.channel());
Serial.printf("WiFi status: %d (%s)\n", WiFi.status(),
WiFi.status() == WL_CONNECTED ? "POVEZAN" : "NI POVEZAN");
auto checkPeer = [&](uint8_t* mac, const char* name, bool& active, unsigned long& lastTime) {
bool exists = esp_now_is_peer_exist(mac);
Serial.printf("Peer %s: %s\n", name, exists ? "✅ OBSTAJA" : "❌ NE OBSTAJA");
Serial.printf(" Aktivnost: %s, zadnji podatek: %lu ms\n",
active ? "AKTIVEN" : "NEAKTIVEN",
active ? (millis() - lastTime) : 0);
};
checkPeer(module1MAC, "Modul 1", module1Active, lastModule1Time);
checkPeer(module2MAC, "Modul 2", module2Active, lastModule2Time);
checkPeer(module3MAC, "Modul 3", module3Active, lastModule3Time);
checkPeer(module4MAC, "Modul 4", module4Active, lastModule4Time);
Serial.println("========================\n");
}
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(10);
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(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
}
delay(500);
// ==================== DIAGNOSTIKA PRED ESP-NOW ====================
diagnoseESPNow();
// ===== NASTAVI FIKSNI KANAL PRED ESP-NOW =====
setFixedWiFiChannel();
delay(100);
// ==================== 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() {
// ===== PREVERI IN POPRAVI KANAL =====
checkAndFixWiFiChannel();
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(10);
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(10); // 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(10);
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(10);
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;
}
}
}
// Koda Modul 1:
// ==================== MODUL 1 - VREMENSKI MODUL ZUNANJI ====================
#include <esp_now.h>
#include <esp_wifi.h>
#include <esp_mac.h>
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_BMP280.h>
#include <Preferences.h>
// ==================== PIN DEFINICIJE ====================
#define I2C_SDA 4
#define I2C_SCL 5
#define LDR_PIN 6
#define STATUS_LED 2
#define MODULE_ID 1
#define MODULE_TYPE 1 // MODULE_WEATHER = 1
// MAC naslov glavnega sistema (master ESP32)
uint8_t masterMAC[] = {0xB4, 0x3A, 0x45, 0xF3, 0xEB, 0xF0};
// Interval pošiljanja (5 sekund)
const unsigned long SEND_INTERVAL = 5000;
// ==================== KALIBRACIJSKE KONSTANTE ====================
#define CMD_CALIBRATE_DARK 10 // Kalibriraj temno vrednost
#define CMD_CALIBRATE_BRIGHT 11 // Kalibriraj svetlo vrednost
#define CMD_SAVE_CALIBRATION 12 // Shrani kalibracijo
// ==================== ESP-NOW STRUKTURE (USKLAJENE Z GLAVNIM SISTEMOM) ====================
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,
// Kalibracijske kode
CALIB_DARK = 100,
CALIB_BRIGHT = 101,
CALIB_CONFIRM = 102
};
// GLAVNA STRUKTURA Z UNIJO
struct ModuleData {
uint8_t moduleId;
ModuleType moduleType;
unsigned long timestamp;
ErrorCode errorCode;
float batteryVoltage;
union {
struct {
float temperature;
float humidity;
float pressure;
float lux;
float bmpTemperature;
} weather;
struct {
float flowRate1, flowRate2, flowRate3;
float totalFlow1, totalFlow2, totalFlow3;
bool relay1State, relay2State, relay3State;
} irrigation;
};
};
// Struktura za prejemanje ukazov
struct CommandData {
uint8_t targetModuleId;
uint8_t command;
float param1;
float param2;
};
// ==================== GLOBALNE SPREMENLJIVKE ====================
ModuleData sendData;
unsigned long lastSendTime = 0;
Adafruit_AHTX0 aht;
Adafruit_BMP280 bmp;
// LDR kalibracijski parametri
const float LDR_LUX_B = 6.5;
const float LDR_LUX_M = -0.7;
const float R_FIXED = 10000.0;
const float VCC = 3.3;
// ==================== KALIBRACIJSKE SPREMENLJIVKE ====================
int ldrMinADC = 500; // ADC vrednost pri SVETLOBI (nizek ADC)
int ldrMaxADC = 3000; // ADC vrednost pri TEMI (visok ADC)
Preferences preferences;
// ==================== FUNKCIJE ZA BRANJE SENZORJEV ====================
float readAHT20Temperature() {
sensors_event_t humidity, temp;
if (!aht.getEvent(&humidity, &temp)) return -999.0;
return temp.temperature;
}
float readAHT20Humidity() {
sensors_event_t humidity, temp;
if (!aht.getEvent(&humidity, &temp)) return -999.0;
return humidity.relative_humidity;
}
float readBMP280Pressure() {
float pressure = bmp.readPressure() / 100.0F;
if (isnan(pressure) || pressure < 300 || pressure > 1100) return -999.0;
return pressure;
}
float readBMP280Temperature() {
float temp = bmp.readTemperature();
if (isnan(temp) || temp < -40 || temp > 85) return -999.0;
return temp;
}
// ==================== LDR S KALIBRACIJO ====================
void saveLDCalibration() {
preferences.begin("ldr-calib", false);
preferences.putInt("min_adc", ldrMinADC);
preferences.putInt("max_adc", ldrMaxADC);
preferences.end();
Serial.printf("✓ Kalibracija shranjena: min=%d (svetlo), max=%d (temno)\n", ldrMinADC, ldrMaxADC);
}
void loadLDCalibration() {
preferences.begin("ldr-calib", true);
ldrMinADC = preferences.getInt("min_adc", 500);
ldrMaxADC = preferences.getInt("max_adc", 3000);
preferences.end();
Serial.printf("Nalozena kalibracija: min=%d (svetlo), max=%d (temno)\n", ldrMinADC, ldrMaxADC);
if (ldrMinADC >= ldrMaxADC) {
Serial.println("⚠ OPOZORILO: Kalibracijske vrednosti so neveljavne! Uporabljam privzete.");
ldrMinADC = 500;
ldrMaxADC = 3000;
}
}
float readLuxWithCalibration() {
int adcValue = analogRead(LDR_PIN);
if (ldrMinADC > 0 && ldrMaxADC > 0 && ldrMinADC < ldrMaxADC) {
int percent = map(adcValue, ldrMinADC, ldrMaxADC, 100, 0);
percent = constrain(percent, 0, 100);
float lux = map(percent, 0, 100, 0, 20000);
static unsigned long lastDebug = 0;
if (millis() - lastDebug > 10000) {
Serial.printf("LDR kalibriran: ADC=%d → %d%% → %.0f lx\n", adcValue, percent, lux);
lastDebug = millis();
}
return lux;
}
float voltage = (adcValue / 4095.0) * VCC;
float resistance = 0;
if (voltage > 0.01 && voltage < (VCC - 0.01)) {
resistance = (VCC - voltage) * R_FIXED / voltage;
} else if (voltage <= 0.01) {
resistance = 1000000.0;
} else {
resistance = 10.0;
}
float lux = 0;
if (resistance > 0.1 && resistance < 10000000.0) {
lux = pow(10.0, (log10(resistance) - LDR_LUX_B) / LDR_LUX_M);
lux = constrain(lux, 0.1, 20000.0);
}
return lux;
}
// ==================== INICIALIZACIJA SENZORJEV ====================
bool initSensors() {
bool allOk = true;
sendData.errorCode = ERR_NONE;
Serial.println("\n--- Inicializacija senzorjev ---");
Serial.print("I2C... ");
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000);
delay(100);
Serial.println("OK");
Serial.print("AHT20... ");
if (!aht.begin()) {
Serial.println("NAPAKA");
sendData.errorCode = ERR_SENSOR_AHT;
allOk = false;
} else {
Serial.println("OK");
}
Serial.print("BMP280... ");
if (bmp.begin(0x76) || bmp.begin(0x77)) {
Serial.println("OK");
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
Adafruit_BMP280::SAMPLING_X2,
Adafruit_BMP280::SAMPLING_X16,
Adafruit_BMP280::FILTER_X16,
Adafruit_BMP280::STANDBY_MS_500);
} else {
Serial.println("NAPAKA");
if (sendData.errorCode == ERR_NONE) {
sendData.errorCode = ERR_SENSOR_BMP;
}
allOk = false;
}
pinMode(LDR_PIN, INPUT);
pinMode(STATUS_LED, OUTPUT);
digitalWrite(STATUS_LED, LOW);
return allOk;
}
// ==================== CALLBACK ZA PREJEM UKAZOV ====================
void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len) {
if (len == sizeof(CommandData)) {
CommandData cmd;
memcpy(&cmd, incomingData, sizeof(cmd));
if (cmd.targetModuleId == MODULE_ID) {
Serial.printf("\n--- Prejet ukaz ---\n");
Serial.printf("Ukaz: %d\n", cmd.command);
Serial.printf("Param1: %.1f\n", cmd.param1);
Serial.printf("Param2: %.1f\n", cmd.param2);
// UKAZ 10: Izmeri TEMNO vrednost (visok ADC)
if (cmd.command == CMD_CALIBRATE_DARK) {
int darkADC = analogRead(LDR_PIN);
Serial.printf("Izmerjena TEMNA vrednost: %d\n", darkADC);
ModuleData response;
response.moduleId = MODULE_ID;
response.moduleType = MODULE_WEATHER;
response.timestamp = millis();
response.errorCode = CALIB_DARK;
response.weather.lux = darkADC;
esp_err_t result = esp_now_send(recv_info->src_addr, (uint8_t*)&response, sizeof(response));
if (result == ESP_OK) {
Serial.println("✓ Temna vrednost poslana nazaj");
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
} else {
Serial.printf("✗ Napaka pri pošiljanju: %d\n", result);
for(int i = 0; i < 3; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
delay(50);
}
}
}
// UKAZ 11: Izmeri SVETLO vrednost (nizek ADC)
else if (cmd.command == CMD_CALIBRATE_BRIGHT) {
int brightADC = analogRead(LDR_PIN);
Serial.printf("Izmerjena SVETLA vrednost: %d\n", brightADC);
ModuleData response;
response.moduleId = MODULE_ID;
response.moduleType = MODULE_WEATHER;
response.timestamp = millis();
response.errorCode = CALIB_BRIGHT;
response.weather.lux = brightADC;
esp_err_t result = esp_now_send(recv_info->src_addr, (uint8_t*)&response, sizeof(response));
if (result == ESP_OK) {
Serial.println("✓ Svetla vrednost poslana nazaj");
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
} else {
Serial.printf("✗ Napaka pri pošiljanju: %d\n", result);
for(int i = 0; i < 3; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
delay(50);
}
}
}
// UKAZ 12: Shrani kalibracijske vrednosti
else if (cmd.command == CMD_SAVE_CALIBRATION) {
int newMin = (int)cmd.param1;
int newMax = (int)cmd.param2;
Serial.printf("Shranjujem kalibracijo: min=%d, max=%d\n", newMin, newMax);
if (newMin >= newMax) {
Serial.println("⚠ OPOZORILO: Svetlo >= Temno! Vrednosti bodo zamenjane.");
int temp = newMin;
newMin = newMax;
newMax = temp;
}
if (newMin < 0) newMin = 0;
if (newMax > 4095) newMax = 4095;
if (newMax - newMin < 100) {
Serial.println("⚠ OPOZORILO: Razlika med vrednostmi je premajhna!");
}
ldrMinADC = newMin;
ldrMaxADC = newMax;
saveLDCalibration();
ModuleData response;
response.moduleId = MODULE_ID;
response.moduleType = MODULE_WEATHER;
response.timestamp = millis();
response.errorCode = CALIB_CONFIRM;
response.weather.lux = 0;
esp_err_t result = esp_now_send(recv_info->src_addr, (uint8_t*)&response, sizeof(response));
if (result == ESP_OK) {
Serial.println("✓ Kalibracija shranjena in potrjena");
digitalWrite(STATUS_LED, HIGH);
delay(200);
digitalWrite(STATUS_LED, LOW);
} else {
Serial.printf("✗ Napaka pri pošiljanju potrditve: %d\n", result);
}
}
else {
Serial.printf("Neznan ukaz: %d\n", cmd.command);
}
}
}
}
void diagnoseWiFi() {
Serial.println("\n=== DIAGNOSTIKA WIFI ===");
// Preveri MAC naslov
String mac = WiFi.macAddress();
Serial.printf("MAC naslov: %s\n", mac.c_str());
if (mac == "00:00:00:00:00:00") {
Serial.println("❌ KRITIČNO: MAC naslov je neveljaven!");
Serial.println(" To se lahko zgodi na ESP32-S3, če:");
Serial.println(" 1. WiFi ni pravilno inicializiran");
Serial.println(" 2. Potrebno je počakati dlje časa");
Serial.println(" 3. Težava s knjižnicami");
}
// Preveri kanal
Serial.printf("Trenutni kanal: %d\n", WiFi.channel());
// Preveri način
wifi_mode_t mode = WiFi.getMode();
Serial.printf("WiFi način: %d (1=STA, 2=AP, 3=AP+STA)\n", mode);
// Preveri status
wl_status_t status = WiFi.status();
Serial.printf("WiFi status: %d\n", status);
// Poskusi dobiti IP naslov (čeprav ne rabimo)
Serial.printf("IP naslov: %s\n", WiFi.localIP().toString().c_str());
Serial.println("========================\n");
}
// ==================== SETUP ====================
void setup() {
// Počakaj dlje časa za stabilizacijo
delay(3000);
setCpuFrequencyMhz(80);
Serial.begin(115200);
delay(2000); // Daljši delay za Serial
Serial.println("\n\n=========================================");
Serial.println("MODUL 1 - VREMENSKI MODUL (KANAL 10)");
Serial.println("=========================================");
pinMode(STATUS_LED, OUTPUT);
digitalWrite(STATUS_LED, LOW);
// ========== NALOŽI KALIBRACIJO ==========
Serial.println("\n--- Nalaganje kalibracije LDR ---");
loadLDCalibration();
// ========== WIFI INICIALIZACIJA - KANAL 10 ==========
Serial.println("\n--- WiFi inicializacija ---");
// POMEMBNO: Najprej onemogoči WiFi
WiFi.mode(WIFI_OFF);
delay(500);
// Nato nastavi način STA
WiFi.mode(WIFI_STA);
delay(500);
// Počisti vse obstoječe nastavitve
WiFi.disconnect(true);
delay(500);
// Počakaj, da se WiFi modul stabilizira
for(int i = 0; i < 10; i++) {
delay(100);
Serial.print(".");
}
Serial.println();
// Preveri MAC naslov
String mac = WiFi.macAddress();
Serial.print("MAC naslov: ");
Serial.println(mac);
// Če je MAC neveljaven, poskusi ponovno inicializacijo
if (mac == "00:00:00:00:00:00" || mac == "FF:FF:FF:FF:FF:FF") {
Serial.println("⚠️ Neveljaven MAC naslov! Poskus ponovne inicializacije...");
// Popolna ponastavitev WiFi
WiFi.mode(WIFI_OFF);
delay(1000);
WiFi.mode(WIFI_STA);
delay(1000);
mac = WiFi.macAddress();
Serial.print("Nov MAC naslov: ");
Serial.println(mac);
}
// Nastavi fiksni kanal 10
WiFi.setChannel(10);
delay(200);
Serial.print("Nastavljen kanal: ");
Serial.println(WiFi.channel());
// ========== ESP-NOW INICIALIZACIJA ==========
Serial.println("\n--- ESP-NOW inicializacija ---");
// Počisti morebitne obstoječe peerje
esp_now_deinit();
delay(500);
if (esp_now_init() != ESP_OK) {
Serial.println("NAPAKA pri inicializaciji ESP-NOW!");
// Poskusi ponovno
delay(1000);
if (esp_now_init() != ESP_OK) {
while(1) {
digitalWrite(STATUS_LED, HIGH);
delay(500);
digitalWrite(STATUS_LED, LOW);
delay(500);
}
}
}
Serial.println("ESP-NOW inicializiran");
esp_now_register_recv_cb(esp_now_recv_cb_t(onDataRecv));
// Dodaj master kot peerja - KANAL 10
Serial.print("Dodajanje master peerja... ");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
// Če peer že obstaja, ga najprej odstrani
if (esp_now_is_peer_exist(masterMAC)) {
esp_now_del_peer(masterMAC);
delay(100);
}
esp_err_t addResult = esp_now_add_peer(&peerInfo);
if (addResult == ESP_OK) {
Serial.println("OK");
} else {
Serial.printf("NAPAKA (koda: %d)\n", addResult);
}
// ========== INICIALIZACIJA SENZORJEV ==========
initSensors();
// ========== PRIPRAVI PODATKE ZA POŠILJANJE ==========
sendData.moduleId = MODULE_ID;
sendData.moduleType = MODULE_WEATHER;
sendData.batteryVoltage = 0;
Serial.println("\n=== MODUL 1 PRIPRAVLJEN ===");
Serial.print("MAC naslov: ");
Serial.println(WiFi.macAddress());
Serial.print("Kanal: ");
Serial.println(WiFi.channel());
Serial.println("=========================================\n");
for(int i = 0; i < 3; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(100);
digitalWrite(STATUS_LED, LOW);
delay(100);
}
}
// ==================== GLAVNA ZANKA ====================
void loop() {
unsigned long now = millis();
// ========== 1. PREVERI MAC NASLOV IN KANAL ==========
static unsigned long lastChannelCheck = 0;
if (now - lastChannelCheck > 10000) {
// Preveri MAC naslov
String mac = WiFi.macAddress();
if (mac == "00:00:00:00:00:00" || mac == "FF:FF:FF:FF:FF:FF") {
Serial.println("⚠️ Neveljaven MAC naslov! Poskus ponastavitve...");
WiFi.mode(WIFI_OFF);
delay(500);
WiFi.mode(WIFI_STA);
delay(1000);
WiFi.setChannel(10);
}
// Preveri in popravi kanal
int currentChannel = WiFi.channel();
if (currentChannel != 10) {
Serial.printf("⚠ Kanal je %d, nastavljam na 10...\n", currentChannel);
WiFi.setChannel(10);
delay(100);
if (WiFi.channel() == 10) {
Serial.println("✓ Kanal uspešno nastavljen na 10");
} else {
Serial.printf("✗ Napaka pri nastavljanju kanala (trenutno: %d)\n", WiFi.channel());
}
}
lastChannelCheck = now;
}
// ========== 2. PREVERI ALI PEER OBSTAJA ==========
static unsigned long lastPeerCheck = 0;
if (now - lastPeerCheck > 30000) {
if (!esp_now_is_peer_exist(masterMAC)) {
Serial.println("⚠ Peer ne obstaja - ponovno dodajanje...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println("✓ Peer ponovno dodan");
}
}
lastPeerCheck = now;
}
// ========== 3. BRANJE SENZORJEV ==========
static unsigned long lastSensorRead = 0;
if (now - lastSensorRead >= 2000) {
// Vaša obstoječa koda za branje senzorjev
sendData.weather.temperature = readAHT20Temperature();
sendData.weather.humidity = readAHT20Humidity();
sendData.weather.pressure = readBMP280Pressure();
sendData.weather.bmpTemperature = readBMP280Temperature();
sendData.weather.lux = readLuxWithCalibration();
sendData.timestamp = now;
lastSensorRead = now;
}
// ========== 4. POŠILJANJE PODATKOV ==========
static unsigned long lastSendTime = 0;
if (now - lastSendTime >= 5000) { // SEND_INTERVAL = 5000
Serial.println("\n--- Pošiljanje podatkov ---");
Serial.printf("Temperatura: %.2f°C\n", sendData.weather.temperature);
Serial.printf("Vlaga: %.2f%%\n", sendData.weather.humidity);
Serial.printf("Tlak: %.2f hPa\n", sendData.weather.pressure);
Serial.printf("Svetloba: %.2f lx\n", sendData.weather.lux);
Serial.printf("Kanal: %d\n", WiFi.channel());
Serial.printf("MAC: %s\n", WiFi.macAddress().c_str());
if (esp_now_is_peer_exist(masterMAC)) {
esp_err_t result = esp_now_send(masterMAC, (uint8_t *)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("✓ USPEŠNO poslano");
digitalWrite(STATUS_LED, HIGH);
delay(10);
digitalWrite(STATUS_LED, LOW);
} else {
Serial.printf("✗ NAPAKA pri pošiljanju (koda: %d)\n", result);
for(int i = 0; i < 3; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
delay(50);
}
}
} else {
Serial.println("✗ Peer ne obstaja!");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println(" Peer dodan, poskušam znova...");
esp_err_t retry = esp_now_send(masterMAC, (uint8_t *)&sendData, sizeof(sendData));
if (retry == ESP_OK) {
Serial.println(" ✓ Poslano po ponovnem dodajanju");
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
}
}
}
lastSendTime = now;
}
delay(50);
}
//Modul 1
//Koda Modul 2:
// ==================== MODUL 2 - NAMAKALNI SISTEM ====================
#include <esp_now.h>
#include <esp_wifi.h>
#include <esp_mac.h>
#include <WiFi.h>
// ==================== PIN DEFINICIJE ====================
#define FLOW_SENSOR_1_PIN 18
#define FLOW_SENSOR_2_PIN 19
#define FLOW_SENSOR_3_PIN 21
// Releji - uporabite MOSFET module, ne direktnih relejev!
#define RELAY_1_PIN 4
#define RELAY_2_PIN 5
#define RELAY_3_PIN 6
#define STATUS_LED 2
// ==================== KONFIGURACIJA ====================
#define MODULE_ID 2
#define MODULE_TYPE 2
uint8_t masterMAC[] = {0xB4, 0x3A, 0x45, 0xF3, 0xEB, 0xF0};
const unsigned long SEND_INTERVAL = 10000; // 10 sekund
#define PULSES_PER_LITER 450
#define FLOW_UPDATE_INTERVAL 2000
// ==================== STRUKTURE ====================
enum ModuleType { MODULE_WEATHER = 1, MODULE_IRRIGATION = 2, MODULE_SHADE_MOTOR = 3 };
enum ErrorCode { ERR_NONE = 0, ERR_SENSOR_FLOW_1 = 10, ERR_SENSOR_FLOW_2 = 11, ERR_SENSOR_FLOW_3 = 12 };
struct ModuleData {
uint8_t moduleId;
ModuleType moduleType;
unsigned long timestamp;
ErrorCode errorCode;
float batteryVoltage;
struct {
float flowRate1, flowRate2, flowRate3;
float totalFlow1, totalFlow2, totalFlow3;
bool relay1State, relay2State, relay3State;
} irrigation;
};
struct CommandData {
uint8_t targetModuleId;
uint8_t command;
float param1;
float param2;
};
// ==================== GLOBALNE SPREMENLJIVKE ====================
ModuleData sendData;
CommandData receivedCommand;
unsigned long lastSendTime = 0;
unsigned long lastFlowUpdate = 0;
unsigned long oldTime = 0;
volatile unsigned long pulseCount1 = 0, pulseCount2 = 0, pulseCount3 = 0;
float flowRate1 = 0, flowRate2 = 0, flowRate3 = 0;
float totalFlow1 = 0, totalFlow2 = 0, totalFlow3 = 0;
bool relay1State = false, relay2State = false, relay3State = false;
// ==================== ESP-NOW CALLBACKI ====================
void onDataSent(const esp_now_send_info_t *send_info, esp_now_send_status_t status) {
digitalWrite(STATUS_LED, HIGH);
delay(5);
digitalWrite(STATUS_LED, LOW);
}
void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len) {
if (len == sizeof(CommandData)) {
memcpy(&receivedCommand, incomingData, sizeof(receivedCommand));
if (receivedCommand.targetModuleId == MODULE_ID) {
switch(receivedCommand.command) {
case 1: // SET_RELAY
setRelay((int)receivedCommand.param1, (bool)receivedCommand.param2);
break;
case 2: // RESET_FLOW_TOTALS
resetFlowTotals();
break;
}
}
}
}
// ==================== INTERRUPT HANDLERJI ====================
void IRAM_ATTR pulseCounter1() { pulseCount1++; }
void IRAM_ATTR pulseCounter2() { pulseCount2++; }
void IRAM_ATTR pulseCounter3() { pulseCount3++; }
// ==================== FUNKCIJE ZA ZMANJŠANO PORABO ====================
void goToSleep() {
sendData.timestamp = millis();
esp_now_send(masterMAC, (uint8_t *)&sendData, sizeof(sendData));
delay(50);
esp_sleep_enable_timer_wakeup(5 * 1000000);
esp_light_sleep_start();
}
// ==================== FUNKCIJE ZA FLOW SENZORJE ====================
void initializeFlowSensors() {
Serial.println("Inicializacija flow senzorjev...");
pinMode(FLOW_SENSOR_1_PIN, INPUT_PULLUP);
pinMode(FLOW_SENSOR_2_PIN, INPUT_PULLUP);
pinMode(FLOW_SENSOR_3_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_1_PIN), pulseCounter1, FALLING);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_2_PIN), pulseCounter2, FALLING);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_3_PIN), pulseCounter3, FALLING);
oldTime = millis();
lastFlowUpdate = millis();
}
void updateFlowSensors() {
unsigned long now = millis();
if (now - lastFlowUpdate < FLOW_UPDATE_INTERVAL) return;
float timeInSeconds = (now - oldTime) / 1000.0;
if (timeInSeconds > 0) {
flowRate1 = (pulseCount1 / (float)PULSES_PER_LITER) / (timeInSeconds / 60.0);
flowRate2 = (pulseCount2 / (float)PULSES_PER_LITER) / (timeInSeconds / 60.0);
flowRate3 = (pulseCount3 / (float)PULSES_PER_LITER) / (timeInSeconds / 60.0);
totalFlow1 += (pulseCount1 / (float)PULSES_PER_LITER);
totalFlow2 += (pulseCount2 / (float)PULSES_PER_LITER);
totalFlow3 += (pulseCount3 / (float)PULSES_PER_LITER);
pulseCount1 = pulseCount2 = pulseCount3 = 0;
oldTime = now;
}
lastFlowUpdate = now;
}
void resetFlowTotals() {
totalFlow1 = totalFlow2 = totalFlow3 = 0;
pulseCount1 = pulseCount2 = pulseCount3 = 0;
}
// ==================== FUNKCIJE ZA RELEJE ====================
void initializeRelays() {
Serial.println("Inicializacija relejev...");
pinMode(RELAY_1_PIN, OUTPUT);
pinMode(RELAY_2_PIN, OUTPUT);
pinMode(RELAY_3_PIN, OUTPUT);
digitalWrite(RELAY_1_PIN, LOW);
digitalWrite(RELAY_2_PIN, LOW);
digitalWrite(RELAY_3_PIN, LOW);
relay1State = relay2State = relay3State = false;
}
void setRelay(int relayNum, bool state) {
int pin;
bool* relayState;
switch(relayNum) {
case 1: pin = RELAY_1_PIN; relayState = &relay1State; break;
case 2: pin = RELAY_2_PIN; relayState = &relay2State; break;
case 3: pin = RELAY_3_PIN; relayState = &relay3State; break;
default: return;
}
digitalWrite(pin, state ? HIGH : LOW);
*relayState = state;
digitalWrite(STATUS_LED, HIGH);
delay(20);
digitalWrite(STATUS_LED, LOW);
}
// ==================== SETUP ====================
void setup() {
pinMode(STATUS_LED, OUTPUT);
digitalWrite(STATUS_LED, HIGH);
delay(100);
Serial.begin(115200);
delay(2000); // Daljši delay za Serial
Serial.println("\n\n=========================================");
Serial.println("MODUL 2 - NAMAKALNI SISTEM (KANAL 10)");
Serial.println("=========================================");
// ========== ZMANJŠAJ CPU FREKVENCO ==========
setCpuFrequencyMhz(80);
Serial.println("CPU frekvenca: 80MHz");
// ========== WIFI INICIALIZACIJA - KANAL 10 ==========
Serial.println("\n--- WiFi inicializacija ---");
// POMEMBNO: Najprej onemogoči WiFi
WiFi.mode(WIFI_OFF);
delay(500);
// Nato nastavi način STA
WiFi.mode(WIFI_STA);
delay(500);
// Počisti vse obstoječe nastavitve
WiFi.disconnect(true);
delay(500);
// Počakaj, da se WiFi modul stabilizira
for(int i = 0; i < 10; i++) {
delay(100);
Serial.print(".");
}
Serial.println();
// Preveri MAC naslov
String mac = WiFi.macAddress();
Serial.print("MAC naslov: ");
Serial.println(mac);
// Če je MAC neveljaven, poskusi ponovno inicializacijo
if (mac == "00:00:00:00:00:00" || mac == "FF:FF:FF:FF:FF:FF") {
Serial.println("⚠️ Neveljaven MAC naslov! Poskus ponovne inicializacije...");
// Popolna ponastavitev WiFi
WiFi.mode(WIFI_OFF);
delay(1000);
WiFi.mode(WIFI_STA);
delay(1000);
mac = WiFi.macAddress();
Serial.print("Nov MAC naslov: ");
Serial.println(mac);
}
// Nastavi fiksni kanal 10
WiFi.setChannel(10);
delay(200);
Serial.print("Nastavljen kanal: ");
Serial.println(WiFi.channel());
// ========== ESP-NOW INICIALIZACIJA ==========
Serial.println("\n--- ESP-NOW inicializacija ---");
// Počisti morebitne obstoječe peerje
esp_now_deinit();
delay(500);
if (esp_now_init() != ESP_OK) {
Serial.println("NAPAKA pri inicializaciji ESP-NOW!");
delay(1000);
if (esp_now_init() != ESP_OK) {
while(1) {
digitalWrite(STATUS_LED, HIGH);
delay(500);
digitalWrite(STATUS_LED, LOW);
delay(500);
}
}
}
Serial.println("ESP-NOW inicializiran");
esp_now_register_send_cb(esp_now_send_cb_t(onDataSent));
esp_now_register_recv_cb(esp_now_recv_cb_t(onDataRecv));
// Dodaj master kot peerja - KANAL 10
Serial.print("Dodajanje master peerja... ");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
// Če peer že obstaja, ga najprej odstrani
if (esp_now_is_peer_exist(masterMAC)) {
esp_now_del_peer(masterMAC);
delay(100);
}
esp_err_t addResult = esp_now_add_peer(&peerInfo);
if (addResult == ESP_OK) {
Serial.println("OK");
} else {
Serial.printf("NAPAKA (koda: %d)\n", addResult);
}
// ========== INICIALIZACIJA STROJNE OPREME ==========
initializeRelays();
initializeFlowSensors();
// ========== KONFIGURACIJA PODATKOV ==========
sendData.moduleId = MODULE_ID;
sendData.moduleType = MODULE_IRRIGATION;
sendData.errorCode = ERR_NONE;
sendData.batteryVoltage = 0;
Serial.println("\n=== MODUL 2 PRIPRAVLJEN ===");
Serial.print("MAC naslov: ");
Serial.println(WiFi.macAddress());
Serial.print("Kanal: ");
Serial.println(WiFi.channel());
Serial.println("=========================================\n");
// Utripni LED za uspešen zagon
digitalWrite(STATUS_LED, LOW);
delay(500);
for(int i = 0; i < 3; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(100);
digitalWrite(STATUS_LED, LOW);
delay(100);
}
}
void forceChannel10() {
int currentChannel = WiFi.channel();
if (currentChannel != 10) {
Serial.printf("⚠️ FORCE: Kanal je %d, silim na 10...\n", currentChannel);
// Onemogoči WiFi za trenutek
WiFi.mode(WIFI_OFF);
delay(100);
// Ponovno nastavi
WiFi.mode(WIFI_STA);
delay(200);
// Nastavi kanal
WiFi.setChannel(10);
delay(100);
Serial.printf(" Nov kanal: %d\n", WiFi.channel());
}
}
// ==================== LOOP ====================
void loop() {
// Takoj na začetku preveri kanal
forceChannel10();
unsigned long now = millis();
// ========== 1. PREVERI IN POPRAVI KANAL NA 10 (NE NA 1!) ==========
static unsigned long lastChannelCheck = 0;
if (now - lastChannelCheck > 10000) { // Vsakih 10 sekund
int currentChannel = WiFi.channel();
if (currentChannel != 10) {
Serial.printf("⚠ Kanal je %d, nastavljam na 10...\n", currentChannel);
WiFi.setChannel(10);
delay(100);
if (WiFi.channel() == 10) {
Serial.println("✓ Kanal uspešno nastavljen na 10");
} else {
Serial.printf("✗ Napaka pri nastavljanju kanala (trenutno: %d)\n", WiFi.channel());
}
}
lastChannelCheck = now;
}
// ========== 2. PREVERI ALI PEER OBSTAJA ==========
static unsigned long lastPeerCheck = 0;
if (now - lastPeerCheck > 30000) { // Vsakih 30 sekund
if (!esp_now_is_peer_exist(masterMAC)) {
Serial.println("⚠ Peer ne obstaja - ponovno dodajanje...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println("✓ Peer ponovno dodan");
}
}
lastPeerCheck = now;
}
// ========== 3. POSODABLJANJE FLOW SENZORJEV ==========
updateFlowSensors();
// ========== 4. POSODABLJANJE PODATKOV ZA POŠILJANJE ==========
sendData.irrigation.flowRate1 = flowRate1;
sendData.irrigation.flowRate2 = flowRate2;
sendData.irrigation.flowRate3 = flowRate3;
sendData.irrigation.totalFlow1 = totalFlow1;
sendData.irrigation.totalFlow2 = totalFlow2;
sendData.irrigation.totalFlow3 = totalFlow3;
sendData.irrigation.relay1State = relay1State;
sendData.irrigation.relay2State = relay2State;
sendData.irrigation.relay3State = relay3State;
// ========== 5. POŠILJANJE PODATKOV ==========
if (now - lastSendTime >= SEND_INTERVAL) {
sendData.timestamp = now;
Serial.println("\n--- Pošiljanje podatkov ---");
Serial.printf("Flow 1: %.2f L/min (skupaj: %.1f L)\n", flowRate1, totalFlow1);
Serial.printf("Flow 2: %.2f L/min (skupaj: %.1f L)\n", flowRate2, totalFlow2);
Serial.printf("Flow 3: %.2f L/min (skupaj: %.1f L)\n", flowRate3, totalFlow3);
Serial.printf("Releji: R1=%s, R2=%s, R3=%s\n",
relay1State ? "ON" : "OFF",
relay2State ? "ON" : "OFF",
relay3State ? "ON" : "OFF");
Serial.printf("Kanal: %d\n", WiFi.channel());
if (esp_now_is_peer_exist(masterMAC)) {
esp_err_t result = esp_now_send(masterMAC, (uint8_t *)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("✓ USPEŠNO poslano");
digitalWrite(STATUS_LED, HIGH);
delay(10);
digitalWrite(STATUS_LED, LOW);
} else {
Serial.printf("✗ NAPAKA pri pošiljanju (koda: %d)\n", result);
for(int i = 0; i < 3; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
delay(50);
}
}
} else {
Serial.println("✗ Peer ne obstaja!");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println(" Peer dodan, poskušam znova...");
esp_err_t retry = esp_now_send(masterMAC, (uint8_t *)&sendData, sizeof(sendData));
if (retry == ESP_OK) {
Serial.println(" ✓ Poslano po ponovnem dodajanju");
digitalWrite(STATUS_LED, HIGH);
delay(50);
digitalWrite(STATUS_LED, LOW);
}
}
}
lastSendTime = now;
}
delay(50);
}
//Modul 2
// Koda Modul 3:
// ==================== MODUL 3 - KRMILJENJE SENČENJA Z MOTORJEM ====================
// Avtor: SmartVitroGrov
// Opis: Krmiljenje NEMA17 motorja preko TB6600 driverja za upravljanje senčnika
// Funkcije: Dvojedrno izvajanje, linearni mehki zagon, absolutno štetje korakov
// ===================================================================================
#include <esp_now.h>
#include <esp_wifi.h>
#include <esp_mac.h>
#include <WiFi.h>
#include <Preferences.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// ==================== PIN DEFINICIJE ====================
#define STEP_PIN 18
#define DIR_PIN 19
#define ENABLE_PIN 21
#define LIMIT_SWITCH_OPEN 4
#define LIMIT_SWITCH_CLOSED 5
#define STATUS_LED 2
#define MANUAL_OPEN_BUTTON 14
#define MANUAL_CLOSE_BUTTON 15
// ==================== KONFIGURACIJA ====================
#define MODULE_ID 3
#define MODULE_TYPE 3
#define SEND_INTERVAL 5000
// ==================== NASTAVITVE HITROSTI ====================
#define SPEED_START 400 // Začetna hitrost (korakov/s)
#define SPEED_MAX 4000 // Maksimalna hitrost (korakov/s)
#define ACCEL_STEPS 600 // Število korakov za pospeševanje
#define DECEL_STEPS 600 // Število korakov za zaviranje
// ==================== NASTAVITVE ZA KALIBRACIJO ====================
#define CALIB_SPEED 400 // Hitrost med kalibracijo (korakov/s) - POČASNEJE ZA ZANESLJIVOST
#define CALIB_TIMEOUT 300000 // Časovna omejitev kalibracije (300 sekund = 5 minut)
#define CALIB_MAX_STEPS 150000 // Maksimalno število korakov
// ==================== MAC NASLOV GLAVNEGA SISTEMA ====================
uint8_t masterMAC[] = {0xB4, 0x3A, 0x45, 0xF3, 0xEB, 0xF0};
// ==================== ESP-NOW STRUKTURE ====================
enum ModuleType {
MODULE_WEATHER = 1,
MODULE_IRRIGATION = 2,
MODULE_SHADE_MOTOR = 3
};
enum ErrorCode {
ERR_NONE = 0,
ERR_MOTOR_STALL = 10,
ERR_LIMIT_SWITCH = 11,
ERR_CALIBRATION = 12,
ERR_OVER_CURRENT = 13
};
struct ModuleData {
uint8_t moduleId;
ModuleType moduleType;
unsigned long timestamp;
ErrorCode errorCode;
float batteryVoltage;
union {
struct {
float temperature;
float humidity;
float pressure;
float lux;
float bmpTemperature;
} weather;
struct {
float flowRate1, flowRate2, flowRate3;
float totalFlow1, totalFlow2, totalFlow3;
bool relay1State, relay2State, relay3State;
} irrigation;
struct {
int currentPosition;
int targetPosition;
bool isMoving;
bool isCalibrated;
bool limitSwitchOpen;
bool limitSwitchClosed;
float motorCurrent;
int motorSpeed;
} shade;
};
};
struct CommandData {
uint8_t targetModuleId;
uint8_t command;
float param1;
float param2;
};
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
};
// ==================== GLOBALNE SPREMENLJIVKE ====================
SemaphoreHandle_t motorMutex = NULL;
volatile bool calibrationRunning = false;
// Nastavitev smeri - če motor teče v napačno smer, spremenite to vrednost na true
bool reverseDirection = false; // Spremenite na true, če motor teče v napačno smer
struct MotorState {
int command;
int targetPosition;
int speedCmd;
bool commandPending;
bool isMoving;
int currentPos;
int targetPos;
int totalSteps;
int absoluteSteps;
bool calibrated;
bool calibrating;
};
MotorState motorState = {
.command = 0,
.targetPosition = 50,
.speedCmd = SPEED_MAX,
.commandPending = false,
.isMoving = false,
.currentPos = 50,
.targetPos = 50,
.totalSteps = 32027,
.absoluteSteps = 16013,
.calibrated = true,
.calibrating = false
};
ModuleData sendData;
int currentMotorSpeed = SPEED_MAX;
unsigned long lastRealtimeSend = 0;
int lastSentPercent = -1;
volatile bool limitOpenPressed = false;
volatile bool limitClosedPressed = false;
unsigned long lastLimitOpenTime = 0;
unsigned long lastLimitClosedTime = 0;
#define DEBOUNCE_TIME 50
Preferences preferences;
// ==================== PROTOTIPI ====================
void initMotor();
void enableMotor(bool enable);
void setDirection(bool closeDirection);
void doStep();
void motorTask(void *pvParameters);
void communicationTask(void *pvParameters);
bool isOpenSwitchPressed();
bool isClosedSwitchPressed();
void sendStatus();
void saveCalibration();
void loadCalibration();
bool autoCalibrate();
void onReceiveCommand(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len);
void testLimitSwitches();
void testMotorDirection();
// ==================== INTERRUPTI ====================
void IRAM_ATTR limitOpenISR() {
unsigned long now = millis();
if (now - lastLimitOpenTime > DEBOUNCE_TIME) {
lastLimitOpenTime = now;
limitOpenPressed = true;
}
}
void IRAM_ATTR limitClosedISR() {
unsigned long now = millis();
if (now - lastLimitClosedTime > DEBOUNCE_TIME) {
lastLimitClosedTime = now;
limitClosedPressed = true;
}
}
// ==================== FUNKCIJE ZA MOTOR ====================
void initMotor() {
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(ENABLE_PIN, OUTPUT);
digitalWrite(ENABLE_PIN, HIGH);
pinMode(LIMIT_SWITCH_OPEN, INPUT_PULLUP);
pinMode(LIMIT_SWITCH_CLOSED, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(LIMIT_SWITCH_OPEN), limitOpenISR, RISING);
attachInterrupt(digitalPinToInterrupt(LIMIT_SWITCH_CLOSED), limitClosedISR, RISING);
pinMode(MANUAL_OPEN_BUTTON, INPUT_PULLUP);
pinMode(MANUAL_CLOSE_BUTTON, INPUT_PULLUP);
motorMutex = xSemaphoreCreateMutex();
Serial.printf(" Začetna hitrost: %d korakov/s\n", SPEED_START);
Serial.printf(" Maksimalna hitrost: %d korakov/s\n", SPEED_MAX);
Serial.printf(" Pospeševanje: %d korakov\n", ACCEL_STEPS);
Serial.printf(" Zaviranje: %d korakov\n", DECEL_STEPS);
Serial.println(" Način: Absolutno štetje korakov");
}
void enableMotor(bool enable) {
digitalWrite(ENABLE_PIN, enable ? LOW : HIGH);
delayMicroseconds(100);
}
void setDirection(bool closeDirection) {
bool actualDirection = reverseDirection ? !closeDirection : closeDirection;
digitalWrite(DIR_PIN, actualDirection ? HIGH : LOW);
delayMicroseconds(100);
}
void doStep() {
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(5);
digitalWrite(STEP_PIN, LOW);
delayMicroseconds(5);
}
bool isOpenSwitchPressed() {
return digitalRead(LIMIT_SWITCH_OPEN) == HIGH;
}
bool isClosedSwitchPressed() {
return digitalRead(LIMIT_SWITCH_CLOSED) == HIGH;
}
// ==================== TEST SMERI MOTORJA ====================
void testMotorDirection() {
Serial.println("\n=== TEST SMERI MOTORJA ===");
Serial.println("Motor se bo kratek čas vrtel v obe smeri.");
Serial.println("Preverite, v katero smer se vrti gred motorja.");
enableMotor(true);
Serial.println("\n1. Test ODPIRANJE (proti 0%):");
setDirection(false);
for(int i = 0; i < 400; i++) {
doStep();
delayMicroseconds(2000);
}
delay(500);
Serial.println("\n2. Test ZAPIRANJE (proti 100%):");
setDirection(true);
for(int i = 0; i < 400; i++) {
doStep();
delayMicroseconds(2000);
}
delay(500);
enableMotor(false);
Serial.println("\n✅ Test končan!");
Serial.println(" Če se je motor vrtel v napačno smer glede na ukaz,");
Serial.println(" spremenite spremenljivko 'reverseDirection' na true");
Serial.println(" v kodi in ponovno naložite.\n");
}
// ==================== TEST LIMITNIH STIKAL ====================
void testLimitSwitches() {
Serial.println("\n=== TEST LIMITNIH STIKAL ===");
bool openPressed = isOpenSwitchPressed();
bool closedPressed = isClosedSwitchPressed();
Serial.printf("ODPRTO stikalo (pin 4): %s\n", openPressed ? "PRITISNJENO" : "NI PRITISNJENO");
Serial.printf("ZAPRTO stikalo (pin 5): %s\n", closedPressed ? "PRITISNJENO" : "NI PRITISNJENO");
if (!openPressed && !closedPressed) {
Serial.println("\n⚠️ Nobeno stikalo ni pritisnjeno!");
Serial.println(" Preverite fizične povezave:");
Serial.println(" - ODPRTO stikalo: pin 4 -> GND ob pritisku");
Serial.println(" - ZAPRTO stikalo: pin 5 -> GND ob pritisku");
Serial.println("\n Ročno pritisnite stikalo in opazujte spremembo.");
} else if (openPressed && closedPressed) {
Serial.println("\n⚠️ Obe stikali sta pritisnjeni hkrati!");
Serial.println(" To ni normalno stanje. Preverite povezave.");
} else if (openPressed) {
Serial.println("\n✅ ODPRTO stikalo deluje pravilno!");
} else if (closedPressed) {
Serial.println("\n✅ ZAPRTO stikalo deluje pravilno!");
}
Serial.println("===========================\n");
}
// ==================== KALIBRACIJA ====================
void saveCalibration() {
preferences.begin("motor-calib", false);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
preferences.putInt("totalSteps", motorState.totalSteps);
preferences.putBool("calibrated", motorState.calibrated);
preferences.putInt("position", motorState.currentPos);
preferences.putInt("absoluteSteps", motorState.absoluteSteps);
xSemaphoreGive(motorMutex);
}
preferences.end();
Serial.printf("💾 Kalibracija shranjena\n");
}
void loadCalibration() {
preferences.begin("motor-calib", true);
if (preferences.isKey("totalSteps")) {
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.totalSteps = preferences.getInt("totalSteps", 32027);
motorState.calibrated = preferences.getBool("calibrated", true);
motorState.currentPos = preferences.getInt("position", 50);
motorState.absoluteSteps = preferences.getInt("absoluteSteps", motorState.totalSteps / 2);
motorState.targetPos = motorState.currentPos;
xSemaphoreGive(motorMutex);
}
Serial.printf("📀 Kalibracija naložena: korakov=%d, pozicija=%d%%, absolutni koraki=%d\n",
motorState.totalSteps, motorState.currentPos, motorState.absoluteSteps);
} else {
Serial.println("⚠ Kalibracija ni najdena");
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.totalSteps = 32027;
motorState.calibrated = false;
motorState.currentPos = 50;
motorState.absoluteSteps = 16013;
xSemaphoreGive(motorMutex);
}
}
preferences.end();
}
// ==================== IZBOLJŠANA KALIBRACIJA ====================
bool autoCalibrate() {
Serial.println("\n╔══════════════════════════════════════════════════════════════╗");
Serial.println("║ AVTOMATSKA KALIBRACIJA SENČNIKA ║");
Serial.println("╚══════════════════════════════════════════════════════════════╝");
Serial.println("⚠️ POMEMBNO: Kalibracija lahko traja do 5 minut!");
Serial.printf(" Motor se bo premikal s hitrostjo %d korakov/s.\n", CALIB_SPEED);
Serial.printf(" Smer vrtenja: %s\n", reverseDirection ? "OBRNJENA" : "NORMALNA");
calibrationRunning = true;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.calibrating = true;
xSemaphoreGive(motorMutex);
}
// Preveri začetno stanje stikal
bool openPressed = isOpenSwitchPressed();
bool closedPressed = isClosedSwitchPressed();
Serial.printf("\n📌 Začetno stanje - ODPRTO: %s, ZAPRTO: %s\n",
openPressed ? "PRITISNJENO" : "NI PRITISNJENO",
closedPressed ? "PRITISNJENO" : "NI PRITISNJENO");
// Če je že na stikalu, ni potrebna kalibracija
if(openPressed) {
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.currentPos = 0;
motorState.absoluteSteps = 0;
motorState.calibrated = true;
motorState.calibrating = false;
xSemaphoreGive(motorMutex);
}
saveCalibration();
Serial.println("✅ Motor že na ODPRTO stikalu - kalibracija ni potrebna!");
calibrationRunning = false;
return true;
}
if(closedPressed) {
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.currentPos = 100;
motorState.absoluteSteps = motorState.totalSteps;
motorState.calibrated = true;
motorState.calibrating = false;
xSemaphoreGive(motorMutex);
}
saveCalibration();
Serial.println("✅ Motor že na ZAPRTO stikalu - kalibracija ni potrebna!");
calibrationRunning = false;
return true;
}
// ===== 1. Najprej se premakni proti ODPRTO stikalu =====
Serial.println("\n📍 KORAK 1/3: Iskanje ODPRTO stikala...");
enableMotor(true);
setDirection(false); // false = ODPIRANJE (proti 0%)
limitOpenPressed = false;
unsigned long startTime = millis();
int stepsToOpen = 0;
int calibSpeed = CALIB_SPEED;
unsigned long lastStepTime = micros();
int currentCalibDelay = 1000000 / calibSpeed;
int lastReportStep = 0;
Serial.print(" Premikanje proti ODPRTO (0%)");
while (!limitOpenPressed && stepsToOpen < CALIB_MAX_STEPS && (millis() - startTime < CALIB_TIMEOUT)) {
yield();
unsigned long now = micros();
if (now - lastStepTime >= currentCalibDelay) {
doStep();
lastStepTime = now;
stepsToOpen++;
if (stepsToOpen - lastReportStep >= 500) {
lastReportStep = stepsToOpen;
Serial.printf(".");
if (stepsToOpen % 5000 == 0) {
Serial.printf(" %d korakov", stepsToOpen);
}
}
if (isOpenSwitchPressed()) {
limitOpenPressed = true;
Serial.printf("\n✅ ODPRTO stikalo doseženo po %d korakih!\n", stepsToOpen);
break;
}
}
delayMicroseconds(50);
}
if (!limitOpenPressed) {
Serial.printf("\n⚠️ ODPRTO stikalo NI doseženo! Opravljenih korakov: %d\n", stepsToOpen);
Serial.println(" Motor se bo obrnil in poiskal ZAPRTO stikalo...");
// Če ni našel ODPRTO stikala, se obrni in pojdi proti ZAPRTO
setDirection(true);
lastReportStep = stepsToOpen;
while (!limitClosedPressed && stepsToOpen < CALIB_MAX_STEPS * 2 && (millis() - startTime < CALIB_TIMEOUT)) {
yield();
unsigned long now = micros();
if (now - lastStepTime >= currentCalibDelay) {
doStep();
lastStepTime = now;
stepsToOpen++;
if (stepsToOpen - lastReportStep >= 500) {
lastReportStep = stepsToOpen;
Serial.printf(".");
if (stepsToOpen % 5000 == 0) {
Serial.printf(" %d korakov", stepsToOpen);
}
}
if (isClosedSwitchPressed()) {
limitClosedPressed = true;
Serial.printf("\n✅ ZAPRTO stikalo doseženo po %d korakih!\n", stepsToOpen);
break;
}
}
delayMicroseconds(50);
}
if (!limitClosedPressed) {
Serial.println("\n❌ NAPAKA: Nobeno stikalo ni bilo doseženo!");
Serial.println(" Preverite fizične povezave in ali se motor vrti.");
enableMotor(false);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.calibrating = false;
xSemaphoreGive(motorMutex);
}
calibrationRunning = false;
return false;
}
// Če smo našli ZAPRTO stikalo, je to zdaj 100%
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.currentPos = 100;
motorState.absoluteSteps = stepsToOpen;
motorState.totalSteps = stepsToOpen;
xSemaphoreGive(motorMutex);
}
// Vrni se na 50%
Serial.println("\n📍 Vračanje na 50% pozicijo...");
setDirection(false);
int stepsToMiddle = stepsToOpen / 2;
int stepsDone = 0;
lastStepTime = micros();
Serial.printf(" Vračanje na sredino: %d korakov\n", stepsToMiddle);
Serial.print(" Premikanje");
for (int i = 0; i < stepsToMiddle; i++) {
if (i % 100 == 0) yield();
unsigned long now = micros();
if (now - lastStepTime >= currentCalibDelay) {
doStep();
lastStepTime = now;
stepsDone++;
if (stepsDone % 1000 == 0) {
Serial.printf(".");
if (stepsDone % 5000 == 0) {
Serial.printf(" %d/%d", stepsDone, stepsToMiddle);
}
}
}
delayMicroseconds(50);
}
Serial.printf("\n✅ Vračanje končano!\n");
} else {
// Našli smo ODPRTO stikalo - zabeleži pozicijo
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.currentPos = 0;
motorState.absoluteSteps = 0;
xSemaphoreGive(motorMutex);
}
delay(500);
yield();
// ===== 2. Poišči ZAPRTO stikalo =====
Serial.println("\n📍 KORAK 2/3: Iskanje ZAPRTO stikala...");
setDirection(true);
limitClosedPressed = false;
int stepsToClosed = 0;
startTime = millis();
lastStepTime = micros();
lastReportStep = 0;
Serial.print(" Premikanje proti ZAPRTO (100%)");
while (!limitClosedPressed && stepsToClosed < CALIB_MAX_STEPS && (millis() - startTime < CALIB_TIMEOUT)) {
yield();
unsigned long now = micros();
if (now - lastStepTime >= currentCalibDelay) {
doStep();
lastStepTime = now;
stepsToClosed++;
if (stepsToClosed - lastReportStep >= 500) {
lastReportStep = stepsToClosed;
Serial.printf(".");
if (stepsToClosed % 5000 == 0) {
Serial.printf(" %d korakov", stepsToClosed);
}
}
if (isClosedSwitchPressed()) {
limitClosedPressed = true;
Serial.printf("\n✅ ZAPRTO stikalo doseženo po %d korakih!\n", stepsToClosed);
break;
}
}
delayMicroseconds(50);
}
if (!limitClosedPressed) {
Serial.println("\n❌ NAPAKA: ZAPRTO stikalo ni najdeno!");
enableMotor(false);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.calibrating = false;
xSemaphoreGive(motorMutex);
}
calibrationRunning = false;
return false;
}
// Zabeleži skupno število korakov
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.totalSteps = stepsToClosed;
motorState.absoluteSteps = stepsToClosed;
xSemaphoreGive(motorMutex);
}
delay(500);
yield();
// ===== 3. Vrni se na 50% =====
Serial.println("\n📍 KORAK 3/3: Vračanje na 50% pozicijo...");
setDirection(false);
int stepsToMiddle = stepsToClosed / 2;
int stepsDone = 0;
lastStepTime = micros();
Serial.printf(" Vračanje na sredino: %d korakov\n", stepsToMiddle);
Serial.print(" Premikanje");
for (int i = 0; i < stepsToMiddle; i++) {
if (i % 100 == 0) yield();
unsigned long now = micros();
if (now - lastStepTime >= currentCalibDelay) {
doStep();
lastStepTime = now;
stepsDone++;
if (stepsDone % 1000 == 0) {
Serial.printf(".");
if (stepsDone % 5000 == 0) {
Serial.printf(" %d/%d", stepsDone, stepsToMiddle);
}
}
}
delayMicroseconds(50);
}
Serial.printf("\n✅ Vračanje končano! Opravljenih korakov: %d\n", stepsDone);
}
enableMotor(true);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.calibrated = true;
motorState.currentPos = 50;
motorState.targetPos = 50;
motorState.absoluteSteps = motorState.totalSteps / 2;
motorState.calibrating = false;
xSemaphoreGive(motorMutex);
}
saveCalibration();
Serial.println("\n╔══════════════════════════════════════════════════════════════╗");
Serial.println("║ KALIBRACIJA USPEŠNO ZAKLJUČENA! ║");
Serial.println("╚══════════════════════════════════════════════════════════════╝");
Serial.printf(" 📊 Skupaj korakov med stikaloma: %d\n", motorState.totalSteps);
Serial.printf(" 📍 Sredinska pozicija: %d korakov\n", motorState.totalSteps / 2);
Serial.printf(" 🎯 Trenutna pozicija: 50%%\n");
Serial.printf(" ⚡ Hitrost kalibracije: %d korakov/s\n", CALIB_SPEED);
calibrationRunning = false;
return true;
}
// ==================== POŠILJANJE STATUSA ====================
void sendStatus() {
if (!esp_now_is_peer_exist(masterMAC)) return;
int currentPos = 0, targetPos = 0;
bool isMoving = false, calibrated = false;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
currentPos = motorState.currentPos;
targetPos = motorState.targetPos;
isMoving = motorState.isMoving;
calibrated = motorState.calibrated;
xSemaphoreGive(motorMutex);
}
sendData.moduleId = MODULE_ID;
sendData.moduleType = MODULE_SHADE_MOTOR;
sendData.timestamp = millis();
sendData.errorCode = ERR_NONE;
sendData.batteryVoltage = 0;
sendData.shade.currentPosition = currentPos;
sendData.shade.targetPosition = targetPos;
sendData.shade.isMoving = isMoving;
sendData.shade.isCalibrated = calibrated;
sendData.shade.limitSwitchOpen = isOpenSwitchPressed();
sendData.shade.limitSwitchClosed = isClosedSwitchPressed();
sendData.shade.motorSpeed = currentMotorSpeed;
esp_err_t result = esp_now_send(masterMAC, (uint8_t *)&sendData, sizeof(sendData));
}
// ==================== FUNKCIJA ZA PREJEM UKAZOV ====================
void onReceiveCommand(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len) {
if (len != sizeof(CommandData)) return;
const uint8_t* mac_addr = recv_info->src_addr;
for(int i = 0; i < 6; i++) {
if(mac_addr[i] != masterMAC[i]) return;
}
CommandData cmd;
memcpy(&cmd, incomingData, sizeof(cmd));
if (cmd.targetModuleId != MODULE_ID) return;
Serial.printf("\n📡 PREJET UKAZ: cmd=%d, p1=%.1f, p2=%.1f\n", cmd.command, cmd.param1, cmd.param2);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
switch(cmd.command) {
case CMD_MOVE_TO_POSITION:
motorState.targetPosition = (int)cmd.param1;
motorState.command = 1;
motorState.commandPending = true;
Serial.printf(" ✅ Premik na %d%%\n", motorState.targetPosition);
break;
case CMD_MOVE_STEP:
{
int step = (int)cmd.param1;
int newTarget = constrain(motorState.currentPos + step, 0, 100);
motorState.targetPosition = newTarget;
motorState.command = 1;
motorState.commandPending = true;
Serial.printf(" ✅ Premik za %d%% na %d%%\n", step, newTarget);
}
break;
case CMD_STOP:
motorState.command = 2;
motorState.commandPending = true;
Serial.println(" ✅ Zaustavitev");
break;
case CMD_EMERGENCY_STOP:
motorState.command = 2;
motorState.commandPending = true;
enableMotor(false);
Serial.println(" 🚨 NUJNA ZAUSTAVITEV");
break;
case CMD_CALIBRATE:
if (!calibrationRunning) {
Serial.println(" 🔧 Začenjam kalibracijo...");
xSemaphoreGive(motorMutex);
autoCalibrate();
xSemaphoreTake(motorMutex, portMAX_DELAY);
} else {
Serial.println(" ⏳ Kalibracija že poteka!");
}
break;
case CMD_SET_SPEED:
motorState.speedCmd = (int)cmd.param1;
motorState.command = 4;
motorState.commandPending = true;
Serial.printf(" ⚡ Hitrost %d\n", motorState.speedCmd);
break;
default:
Serial.printf(" ❌ Neznan ukaz: %d\n", cmd.command);
break;
}
xSemaphoreGive(motorMutex);
sendStatus();
}
}
// ==================== TASK ZA MOTOR ====================
void motorTask(void *pvParameters) {
Serial.println("🔧 Naloga motorja zagnana na jedru 1");
Serial.println(" Način: Tekoče premikanje na ciljno pozicijo");
bool isMoving = false;
bool movingDirection = false;
int targetPos = 50;
int totalSteps = 32027;
int stepsToMove = 0;
int stepsDone = 0;
int absoluteSteps = 16013;
int currentSpeed = SPEED_START;
unsigned long stepDelay = 1000000 / currentSpeed;
unsigned long lastStepMicros = 0;
int lastPrintedPos = -1;
while(1) {
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
if (motorState.commandPending) {
int cmd = motorState.command;
int newTarget = motorState.targetPosition;
int newSpeed = motorState.speedCmd;
int currentTotalSteps = motorState.totalSteps;
int currentAbsoluteSteps = motorState.absoluteSteps;
motorState.commandPending = false;
xSemaphoreGive(motorMutex);
switch(cmd) {
case 1:
if (!motorState.calibrating && motorState.calibrated) {
totalSteps = currentTotalSteps;
absoluteSteps = currentAbsoluteSteps;
int currentPos = (absoluteSteps * 100) / totalSteps;
currentPos = constrain(currentPos, 0, 100);
newTarget = constrain(newTarget, 0, 100);
if (newTarget != currentPos) {
movingDirection = (newTarget > currentPos);
int targetSteps = (newTarget * totalSteps) / 100;
stepsToMove = abs(targetSteps - absoluteSteps);
stepsDone = 0;
isMoving = true;
targetPos = newTarget;
currentSpeed = SPEED_START;
stepDelay = 1000000 / currentSpeed;
lastPrintedPos = -1;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.isMoving = true;
motorState.targetPos = targetPos;
xSemaphoreGive(motorMutex);
}
enableMotor(true);
setDirection(movingDirection);
Serial.printf("🎯 Tekoči premik na %d%% (razdalja: %d korakov, smer: %s)\n",
targetPos, stepsToMove,
movingDirection ? "ZAPIRANJE" : "ODPIRANJE");
}
}
break;
case 2:
isMoving = false;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.isMoving = false;
xSemaphoreGive(motorMutex);
}
break;
case 4:
currentMotorSpeed = constrain(newSpeed, SPEED_START, SPEED_MAX);
break;
}
} else {
xSemaphoreGive(motorMutex);
}
}
if (isMoving && stepsDone < stepsToMove) {
unsigned long now = micros();
int stepsLeft = stepsToMove - stepsDone;
if (stepsDone < ACCEL_STEPS) {
float progress = (float)stepsDone / ACCEL_STEPS;
currentSpeed = SPEED_START + (currentMotorSpeed - SPEED_START) * progress;
stepDelay = 1000000 / currentSpeed;
} else if (stepsLeft < DECEL_STEPS) {
float progress = (float)stepsLeft / DECEL_STEPS;
currentSpeed = SPEED_START + (currentMotorSpeed - SPEED_START) * progress;
stepDelay = 1000000 / currentSpeed;
} else {
currentSpeed = currentMotorSpeed;
stepDelay = 1000000 / currentSpeed;
}
if (now - lastStepMicros >= stepDelay) {
doStep();
lastStepMicros = now;
stepsDone++;
if (movingDirection) absoluteSteps++;
else absoluteSteps--;
absoluteSteps = constrain(absoluteSteps, 0, totalSteps);
int currentPos = (absoluteSteps * 100) / totalSteps;
currentPos = constrain(currentPos, 0, 100);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.absoluteSteps = absoluteSteps;
motorState.currentPos = currentPos;
xSemaphoreGive(motorMutex);
}
if (currentPos != lastPrintedPos && (currentPos % 5 == 0 || stepsDone == stepsToMove)) {
lastPrintedPos = currentPos;
Serial.printf(" Pozicija: %d%% (hitrost: %d korakov/s)\n", currentPos, currentSpeed);
}
if (movingDirection && isClosedSwitchPressed()) {
isMoving = false;
absoluteSteps = totalSteps;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.isMoving = false;
motorState.absoluteSteps = totalSteps;
motorState.currentPos = 100;
xSemaphoreGive(motorMutex);
}
Serial.println("⚠ ZAPRTO stikalo doseženo - ustavljam");
break;
}
if (!movingDirection && isOpenSwitchPressed()) {
isMoving = false;
absoluteSteps = 0;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.isMoving = false;
motorState.absoluteSteps = 0;
motorState.currentPos = 0;
xSemaphoreGive(motorMutex);
}
Serial.println("⚠ ODPRTO stikalo doseženo - ustavljam");
break;
}
if (stepsDone >= stepsToMove) {
isMoving = false;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.isMoving = false;
motorState.currentPos = targetPos;
xSemaphoreGive(motorMutex);
}
Serial.printf("✅ Ciljna pozicija %d%% dosežena!\n", targetPos);
}
}
}
bool openPressed = (digitalRead(MANUAL_OPEN_BUTTON) == LOW);
bool closePressed = (digitalRead(MANUAL_CLOSE_BUTTON) == LOW);
if (openPressed && !isMoving && !motorState.calibrating && motorState.calibrated) {
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
totalSteps = motorState.totalSteps;
absoluteSteps = motorState.absoluteSteps;
xSemaphoreGive(motorMutex);
}
int currentPos = (absoluteSteps * 100) / totalSteps;
int newPos = max(0, currentPos - 10);
if (newPos != currentPos) {
int targetSteps = (newPos * totalSteps) / 100;
stepsToMove = abs(targetSteps - absoluteSteps);
stepsDone = 0;
isMoving = true;
targetPos = newPos;
movingDirection = false;
currentSpeed = SPEED_START;
stepDelay = 1000000 / currentSpeed;
lastPrintedPos = -1;
enableMotor(true);
setDirection(false);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.isMoving = true;
motorState.targetPos = targetPos;
xSemaphoreGive(motorMutex);
}
Serial.printf("🔘 Ročni ODPRI na %d%%\n", targetPos);
}
delay(300);
}
if (closePressed && !isMoving && !motorState.calibrating && motorState.calibrated) {
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
totalSteps = motorState.totalSteps;
absoluteSteps = motorState.absoluteSteps;
xSemaphoreGive(motorMutex);
}
int currentPos = (absoluteSteps * 100) / totalSteps;
int newPos = min(100, currentPos + 10);
if (newPos != currentPos) {
int targetSteps = (newPos * totalSteps) / 100;
stepsToMove = abs(targetSteps - absoluteSteps);
stepsDone = 0;
isMoving = true;
targetPos = newPos;
movingDirection = true;
currentSpeed = SPEED_START;
stepDelay = 1000000 / currentSpeed;
lastPrintedPos = -1;
enableMotor(true);
setDirection(true);
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.isMoving = true;
motorState.targetPos = targetPos;
xSemaphoreGive(motorMutex);
}
Serial.printf("🔘 Ročni ZAPRI na %d%%\n", targetPos);
}
delay(300);
}
delay(1);
}
}
// ==================== TASK ZA KOMUNIKACIJO ====================
void communicationTask(void *pvParameters) {
Serial.println("📡 Naloga komunikacije zagnana na jedru 0");
// ========== WIFI INICIALIZACIJA - KANAL 10 ==========
WiFi.mode(WIFI_STA);
WiFi.disconnect(true);
delay(100);
// POMEMBNO: Nastavi fiksni kanal 10
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
// Ne shranjuj WiFi nastavitev v NVS
esp_wifi_set_storage(WIFI_STORAGE_RAM);
delay(100);
Serial.println("📡 Kanal nastavljen na 10");
Serial.printf("📡 MODUL 3 - Kanal: %d\n", WiFi.channel());
Serial.print("📡 DEJANSKI MAC naslov: ");
Serial.println(WiFi.macAddress());
delay(100);
// ========== ESP-NOW INICIALIZACIJA ==========
// Počisti morebitne obstoječe peerje
esp_now_deinit();
delay(100);
esp_err_t initResult = esp_now_init();
if (initResult != ESP_OK) {
WiFi.mode(WIFI_OFF);
delay(500);
WiFi.mode(WIFI_STA);
delay(500);
initResult = esp_now_init();
if (initResult != ESP_OK) {
Serial.printf("❌ ESP-NOW napaka! Koda: %d\n", initResult);
while(1) delay(100);
}
}
Serial.println("✅ ESP-NOW inicializiran");
esp_now_register_recv_cb(onReceiveCommand);
Serial.println("✅ Callback registriran");
// ========== DODAJ MASTER PEER - KANAL 10 ==========
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10; // POMEMBNO: KANAL 10
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
// Če peer že obstaja, ga najprej odstrani
if (esp_now_is_peer_exist(masterMAC)) {
esp_now_del_peer(masterMAC);
delay(50);
}
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println("✅ Master peer dodan (kanal 10)");
} else {
Serial.println("❌ Napaka pri dodajanju master peer-ja!");
}
Serial.println("\n✅ Modul 3 pripravljen!\n");
Serial.println("📌 Za testiranje uporabite ročna gumba na modulu 3:");
Serial.println(" - Gumb ODPRI (pin 14) - premik proti 0%");
Serial.println(" - Gumb ZAPRI (pin 15) - premik proti 100%");
unsigned long lastStatusSend = 0;
unsigned long lastDebugPrint = 0;
unsigned long lastChannelCheck = 0;
while(1) {
unsigned long now = millis();
// ========== PREVERI IN POPRAVI KANAL ==========
if (now - lastChannelCheck > 10000) { // Vsakih 10 sekund
int currentChannel = WiFi.channel();
if (currentChannel != 10) {
Serial.printf("⚠ Kanal je %d, nastavljam na 10...\n", currentChannel);
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
delay(50);
}
lastChannelCheck = now;
}
// ========== PREVERI ALI PEER OBSTAJA ==========
if (now - lastDebugPrint > 30000) {
if (!esp_now_is_peer_exist(masterMAC)) {
Serial.println("⚠ Master peer ne obstaja - ponovno dodajanje...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
esp_now_add_peer(&peerInfo);
}
Serial.println("🔁 Čakam na ukaze...");
lastDebugPrint = now;
}
// ========== POŠILJANJE STATUSA ==========
if (now - lastStatusSend >= SEND_INTERVAL) {
sendStatus();
lastStatusSend = now;
}
delay(10);
}
}
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n\n╔════════════════════════════════════════════════════╗");
Serial.println("║ MODUL 3 - KRMILJENJE SENČENJA ║");
Serial.println("╚════════════════════════════════════════════════════╝");
pinMode(STATUS_LED, OUTPUT);
digitalWrite(STATUS_LED, LOW);
initMotor();
loadCalibration();
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println("\n========== MAC NASLOVI ==========");
Serial.print("📡 DEJANSKI MAC naslov MODULA 3: ");
Serial.println(WiFi.macAddress());
Serial.print("📡 MAC MASTER (v kodi): ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", masterMAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.println("================================\n");
testLimitSwitches();
Serial.println("\n📌 TEST SMERI MOTORJA:");
Serial.println(" Motor se bo kratek čas vrtel v obe smeri.");
Serial.println(" Preverite, v katero smer se vrti gred motorja.\n");
delay(2000);
testMotorDirection();
if (!motorState.calibrated) {
Serial.println("\n⚠ Motor ni kalibriran! Začenjam kalibracijo...");
Serial.println(" Prepričajte se, da se motor vrti v pravo smer!");
delay(3000);
autoCalibrate();
} else {
Serial.println("✅ Motor je kalibriran!");
enableMotor(true);
}
currentMotorSpeed = SPEED_MAX;
if (xSemaphoreTake(motorMutex, portMAX_DELAY)) {
motorState.speedCmd = SPEED_MAX;
xSemaphoreGive(motorMutex);
}
xTaskCreatePinnedToCore(motorTask, "MotorTask", 8192, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(communicationTask, "CommTask", 8192, NULL, 1, NULL, 0);
Serial.println("\n╔════════════════════════════════════════════════════╗");
Serial.println("║ MODUL 3 PRIPRAVLJEN ║");
Serial.println("╚════════════════════════════════════════════════════╝");
Serial.printf(" Kalibriran: %s\n", motorState.calibrated ? "DA" : "NE");
Serial.printf(" Pozicija: %d%%\n", motorState.currentPos);
Serial.printf(" Korakov: %d/%d\n", motorState.absoluteSteps, motorState.totalSteps);
Serial.printf(" DEJANSKI MAC: %s\n", WiFi.macAddress().c_str());
Serial.printf(" Hitrost premikanja: %d korakov/s\n", SPEED_MAX);
Serial.printf(" Hitrost kalibracije: %d korakov/s\n", CALIB_SPEED);
Serial.printf(" Smer vrtenja: %s\n", reverseDirection ? "OBRNJENA" : "NORMALNA");
Serial.println(" Jedro 0: Komunikacija (ESP-NOW)");
Serial.println(" Jedro 1: Krmiljenje motorja");
Serial.println(" Čakam na ukaze...\n");
for(int i = 0; i < 3; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(100);
digitalWrite(STATUS_LED, LOW);
delay(100);
}
}
// ==================== GLAVNA ZANKA ====================
void loop() {
delay(1000);
}
// Modul 3
//Koda Modul 4:
// ==================== MODUL 4 - SENZORJI VITRINE (BREZ YS01) ====================
// ESP32-S3 - Koda za MCN32R16V - YS01 IZKLJUČEN
#include <esp_now.h>
#include <esp_wifi.h>
#include <esp_mac.h>
#include <WiFi.h>
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Preferences.h>
// ==================== DEFINICIJE PINOV ====================
// DHT22 (temperatura in vlaga zraka)
#define DHTPIN 14
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// DS18B20 (temperatura zemlje)
#define ONE_WIRE_BUS 10
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature soilTempSensor(&oneWire);
// LDR (svetloba)
#define LDR_PIN 4
// Senzor vlage tal (kapacitivni)
#define SOIL_MOISTURE_PIN 5
// ==================== GLOBALNE SPREMENLJIVKE ====================
float airTemperature = 0;
float airHumidity = 0;
int lightPercent = 0;
float soilTemperature = 0;
int soilMoisturePercent = 0;
bool dhtInitialized = false;
bool ds18b20Initialized = false;
// Kalibracijske vrednosti
int ldrMinADC = 500;
int ldrMaxADC = 3000;
int SOIL_DRY_VALUE = 4095;
int SOIL_WET_VALUE = 1500;
unsigned long lastSensorRead = 0;
#define SENSOR_READ_INTERVAL 2000
// ESP-NOW
uint8_t masterMAC[] = {0xB4, 0x3A, 0x45, 0xF3, 0xEB, 0xF0};
unsigned long lastSendTime = 0;
#define SEND_INTERVAL 5000
Preferences preferences;
// ==================== FUNKCIJE ZA BRANJE SENZORJEV ====================
void readDHT22() {
if (!dhtInitialized) return;
float h = dht.readHumidity();
float t = dht.readTemperature();
if (isnan(t) || isnan(h)) {
delay(100);
h = dht.readHumidity();
t = dht.readTemperature();
}
if (!isnan(t) && !isnan(h)) {
airTemperature = t;
airHumidity = h;
Serial.printf(" DHT22: T=%.1f°C, H=%.1f%%\n", airTemperature, airHumidity);
} else {
Serial.println(" DHT22: Napaka pri branju!");
}
}
void readDS18B20() {
if (!ds18b20Initialized) return;
soilTempSensor.requestTemperatures();
float temp = soilTempSensor.getTempCByIndex(0);
if (temp != -127.00 && !isnan(temp)) {
soilTemperature = temp;
Serial.printf(" DS18B20: SoilT=%.1f°C\n", soilTemperature);
} else {
Serial.println(" DS18B20: Napaka pri branju!");
}
}
void readLDR() {
int rawValue = analogRead(LDR_PIN);
rawValue = constrain(rawValue, 0, 4095);
if (ldrMaxADC > ldrMinADC) {
lightPercent = map(rawValue, ldrMinADC, ldrMaxADC, 100, 0);
} else {
lightPercent = map(rawValue, 500, 3000, 100, 0);
}
lightPercent = constrain(lightPercent, 0, 100);
Serial.printf(" LDR: ADC=%d, Light=%d%%\n", rawValue, lightPercent);
}
void readSoilMoisture() {
int rawValue = analogRead(SOIL_MOISTURE_PIN);
rawValue = constrain(rawValue, 0, 4095);
if (SOIL_DRY_VALUE > SOIL_WET_VALUE) {
soilMoisturePercent = map(rawValue, SOIL_DRY_VALUE, SOIL_WET_VALUE, 0, 100);
} else {
soilMoisturePercent = map(rawValue, 4095, 1500, 0, 100);
}
soilMoisturePercent = constrain(soilMoisturePercent, 0, 100);
Serial.printf(" Soil Moisture: ADC=%d, Percent=%d%%\n", rawValue, soilMoisturePercent);
}
void readAllSensors() {
Serial.println("\n📊 Branje senzorjev:");
readDHT22();
readLDR();
readSoilMoisture();
readDS18B20();
}
// ==================== POŠILJANJE PODATKOV ====================
void sendDataToMaster() {
if (!dhtInitialized && airTemperature == 0 && airHumidity == 0) {
return;
}
// Format: temp,hum,0,light,soilTemp,soilMoisture,TVOC=0,CO2=0,CH2O=0
char buffer[256];
snprintf(buffer, sizeof(buffer),
"%.1f,%.1f,0.0,%d,%.1f,%d,TVOC=0,CO2=0,CH2O=0",
airTemperature, airHumidity, lightPercent,
soilTemperature, soilMoisturePercent);
Serial.printf("\n📡 Pošiljam podatke: %s\n", buffer);
esp_err_t result = esp_now_send(masterMAC, (uint8_t*)buffer, strlen(buffer));
if (result == ESP_OK) {
Serial.println(" ✅ Podatki poslani!");
} else {
Serial.printf(" ❌ Napaka pri pošiljanju: %d\n", result);
}
}
// ==================== INICIALIZACIJA SENZORJEV ====================
void initSensors() {
Serial.println("\n🔧 INICIALIZACIJA SENZORJEV");
// DHT22
Serial.println("\n Inicializiram DHT22...");
dht.begin();
delay(1000);
float testH = dht.readHumidity();
float testT = dht.readTemperature();
if (!isnan(testT) && !isnan(testH)) {
dhtInitialized = true;
airTemperature = testT;
airHumidity = testH;
Serial.printf(" ✅ DHT22 inicializiran! T=%.1f°C, H=%.1f%%\n", testT, testH);
} else {
Serial.println(" ❌ DHT22 ni zaznan!");
dhtInitialized = false;
}
// DS18B20
Serial.println("\n Inicializiram DS18B20...");
pinMode(ONE_WIRE_BUS, INPUT_PULLUP);
soilTempSensor.begin();
int deviceCount = soilTempSensor.getDeviceCount();
if (deviceCount > 0) {
ds18b20Initialized = true;
Serial.printf(" ✅ DS18B20 inicializiran! (%d naprav)\n", deviceCount);
} else {
Serial.println(" ⚠️ DS18B20 ni najden");
ds18b20Initialized = false;
}
// LDR
Serial.println("\n Inicializiram LDR...");
pinMode(LDR_PIN, INPUT);
Serial.println(" ✅ LDR inicializiran!");
// Senzor vlage tal
Serial.println("\n Inicializiram senzor vlage tal...");
pinMode(SOIL_MOISTURE_PIN, INPUT);
Serial.println(" ✅ Senzor vlage tal inicializiran!");
}
// ==================== NALAGANJE KALIBRACIJSKIH VREDNOSTI ====================
void loadCalibration() {
Serial.println("\n🔧 Nalagam kalibracijske vrednosti...");
preferences.begin("module4-calib", true);
ldrMinADC = preferences.getInt("ldrMin", 500);
ldrMaxADC = preferences.getInt("ldrMax", 3000);
SOIL_DRY_VALUE = preferences.getInt("soilDry", 4095);
SOIL_WET_VALUE = preferences.getInt("soilWet", 1500);
preferences.end();
Serial.printf(" LDR kalibracija: Svetlo ADC=%d, Temno ADC=%d\n", ldrMinADC, ldrMaxADC);
Serial.printf(" Vlaga tal kalibracija: Suho ADC=%d, Mokro ADC=%d\n", SOIL_DRY_VALUE, SOIL_WET_VALUE);
}
// ==================== INICIALIZACIJA ESP-NOW ====================
void initESPNow() {
Serial.println("\n🔧 INICIALIZACIJA ESP-NOW");
// ========== WIFI INICIALIZACIJA - KANAL 10 ==========
WiFi.mode(WIFI_STA);
WiFi.disconnect(true);
delay(100);
// POMEMBNO: Nastavi fiksni kanal 10
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
// Ne shranjuj WiFi nastavitev v NVS
esp_wifi_set_storage(WIFI_STORAGE_RAM);
delay(100);
Serial.printf("📡 Kanal nastavljen na: %d\n", WiFi.channel());
// ========== ESP-NOW INICIALIZACIJA ==========
// Počisti morebitne obstoječe peerje
esp_now_deinit();
delay(100);
if (esp_now_init() != ESP_OK) {
Serial.println(" ❌ Napaka pri inicializaciji ESP-NOW!");
return;
}
Serial.println(" ✅ ESP-NOW inicializiran!");
// ========== DODAJ MASTER PEER - KANAL 10 ==========
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10; // POMEMBNO: KANAL 10
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
// Če peer že obstaja, ga najprej odstrani
if (esp_now_is_peer_exist(masterMAC)) {
esp_now_del_peer(masterMAC);
delay(50);
Serial.println(" ⚠ Star peer odstranjen");
}
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println(" ✅ Peer (glavni sistem) dodan!");
Serial.print(" MAC naslov glavnega sistema: ");
for(int i = 0; i < 6; i++) {
Serial.printf("%02X", masterMAC[i]);
if(i < 5) Serial.print(":");
}
Serial.println();
Serial.printf(" Kanal: %d\n", peerInfo.channel);
} else {
Serial.println(" ❌ Napaka pri dodajanju peer-ja!");
}
}
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n\n╔════════════════════════════════════════════════════╗");
Serial.println("║ MODUL 4 - SENZORJI VITRINE (BREZ YS01) ║");
Serial.println("╚════════════════════════════════════════════════════╝\n");
WiFi.mode(WIFI_STA);
delay(100);
Serial.print("📡 MAC naslov Modula 4: ");
Serial.println(WiFi.macAddress());
initSensors();
loadCalibration();
initESPNow();
Serial.println("\n📊 Začetne meritve:");
delay(1000);
readAllSensors();
Serial.println("\n📊 STATUS INICIALIZACIJE:");
Serial.printf(" DHT22: %s\n", dhtInitialized ? "✅ OK" : "❌ NEDOSEGLJIV");
Serial.printf(" DS18B20: %s\n", ds18b20Initialized ? "✅ OK" : "⚠️ OPCIONALEN");
Serial.printf(" LDR: ✅ OK\n");
Serial.printf(" Soil Moisture: ✅ OK\n");
Serial.println("\n✅ Modul 4 pripravljen!");
Serial.println("📡 Pošiljanje podatkov vsakih 10 sekund...\n");
lastSendTime = millis();
lastSensorRead = millis();
}
// ==================== LOOP ====================
void loop() {
unsigned long now = millis();
// ========== 1. PREVERI IN POPRAVI KANAL NA 10 ==========
static unsigned long lastChannelCheck = 0;
if (now - lastChannelCheck > 10000) { // Vsakih 10 sekund
int currentChannel = WiFi.channel();
if (currentChannel != 10) {
Serial.printf("⚠ Kanal je %d, nastavljam na 10...\n", currentChannel);
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(10, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
delay(50);
if (WiFi.channel() == 10) {
Serial.println("✓ Kanal uspešno nastavljen na 10");
} else {
Serial.printf("✗ Napaka pri nastavljanju kanala (trenutno: %d)\n", WiFi.channel());
}
}
lastChannelCheck = now;
}
// ========== 2. PREVERI ALI PEER OBSTAJA ==========
static unsigned long lastPeerCheck = 0;
if (now - lastPeerCheck > 30000) { // Vsakih 30 sekund
if (!esp_now_is_peer_exist(masterMAC)) {
Serial.println("⚠ Peer ne obstaja - ponovno dodajanje...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println("✓ Peer ponovno dodan");
}
}
lastPeerCheck = now;
}
// ========== 3. BRANJE SENZORJEV ==========
if (now - lastSensorRead > SENSOR_READ_INTERVAL) {
readAllSensors();
lastSensorRead = now;
}
// ========== 4. POŠILJANJE PODATKOV ==========
if (now - lastSendTime > SEND_INTERVAL) {
// Preveri peer pred pošiljanjem
if (esp_now_is_peer_exist(masterMAC)) {
sendDataToMaster();
} else {
Serial.println("⚠ Peer ne obstaja, poskušam dodati...");
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo));
memcpy(peerInfo.peer_addr, masterMAC, 6);
peerInfo.channel = 10;
peerInfo.encrypt = false;
peerInfo.ifidx = WIFI_IF_STA;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.println("✓ Peer dodan, pošiljam...");
sendDataToMaster();
}
}
lastSendTime = now;
}
delay(100);
}
// Modul 4
// Moduli zgubljajo povezavo z glavnim ESP
Senzor Pin na modulu Povezava
AHT20 VIN 3.3V 3.3V
AHT20 GND GND GND
AHT20 SDA GPIO4 SDA
AHT20 SCL GPIO5 SCL
BMP280 VIN 3.3V 3.3V
BMP280 GND GND GND
BMP280 SDA GPIO4 SDA
BMP280 SCL GPIO5 SCL
LDR en konec 3.3V 3.3V
LDR drug konec GPIO6 A0
10k upor GPIO6 -> GND Pull-down