Tracking the cause of crashes

Hi everyone,

I've made a controller for my terrarium and it's been running for a while now. It's running on an Arduino Giga connected to the Arduino IoT cloud. So far it's been good. Still having minor trouble with the cloud messing up my RTC but syncing with an NTP every 10 minutes is a workaround for now. I've built in something that keeps count of every new boot. Sometimes this is 0 times per week and sometimes it's suddenly 140 ... I've not seen it happening but I have a hunch it's to do with the wifi/cloud connection.

In order to learn what is happening I want the Arduino to keep track of what it's doing and store that. After a crash and boot it'll do the same but saved under a new location/marker/key. There is a counter in the setup that goes up by one on every boot, and the counter is used for the save idientifier. For now I'd like to track the last 10 crashes, and then it overwrites itself. Each crash will have a time stamp and error code related to the step the program was at.

Storage on the Giga was hard to figure out but apparently kv_store works, so I'm trying that. I found a thread with an outline to use a struct to combine the time stamp and relevant attachement to read out later.

To figure out how to make a unique key for every new instance (up to 10) I asked chatGPT and it suggested I use snprintf() . It looks like it should work and it compiles but I get nothing on the Serial Monitor after uploading and/or hitting the reset button.

Once the setup runs I'd like to see the log of all previous instances roll out on the Serial Monitor, but might it be possible this causes a problem if I'm asking kv_get to fetch something non existant?

Here is what I've got

#include "KVStore.h"
#include "kvstore_global_api.h"

int errorTracker = 0;
int counter = 0;
const char* key0 = "counter";
char crashKey[20];

struct StructData {
  unsigned long timeStamp;
  int errorCode;
};

StructData dataSaved;
StructData dataRetrieved;

void setup() {
  Serial.begin(9600);

  // Print error log
  Serial.println("Error log:");

  for (int i = 0; i < 10; i++) {
    // Create key for each saved crash
    snprintf(crashKey, sizeof(crashKey), "Crash_%d", i);

    // Retrieve each crash entry
    size_t retrievedSize = sizeof(dataRetrieved);
    kv_get(crashKey, (uint8_t*)&dataRetrieved, retrievedSize, 0);

    // Print the retrieved crash data
    Serial.print(crashKey);
    Serial.print(" - Timestamp: ");
    Serial.print(dataRetrieved.timeStamp);
    Serial.print(", Error Code: ");
    Serial.print(dataRetrieved.errorCode);
    Serial.println();
  }

  // Initialize counter
  size_t size = sizeof(counter);
  kv_get(key0, &counter, size, 0);

  // Track 0 - 9
  if (counter < 9) {
    counter++;
  } else {
    counter = 0;
  }

  // Save new counter
  kv_set(key0, &counter, sizeof(counter), 0);

  // Prepare the crash key
  snprintf(crashKey, sizeof(crashKey), "Crash_%d", counter);

  // Initialize struct data
  dataSaved.timeStamp = getLocalTime();
  dataSaved.errorCode = errorTracker;

  // Save the data with a unique key
  kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
}

void loop() {

  dataSaved.timeStamp = getLocalTime();
    
   errorTracker++;

   delay(1000);
  

  dataSaved.errorCode = errorTracker;
  
  kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);

}

unsigned long getLocalTime() {
  // Placeholder for actual time function
  return millis(); // Just using millis as an example
}

Try adding this to hold the print out until the monitor is connected

Serial.begin(9600);
 while(!Serial);

This is what I saw after uploading your code and opening the monitor

Error log:
Crash_0 - Timestamp: 0, Error Code: 0
Crash_1 - Timestamp: 124303, Error Code: 125
Crash_2 - Timestamp: 158177, Error Code: 158
Crash_3 - Timestamp: 158177, Error Code: 158
Crash_4 - Timestamp: 158177, Error Code: 158
Crash_5 - Timestamp: 158177, Error Code: 158
Crash_6 - Timestamp: 158177, Error Code: 158
Crash_7 - Timestamp: 158177, Error Code: 158
Crash_8 - Timestamp: 158177, Error Code: 158
Crash_9 - Timestamp: 158177, Error Code: 158

I have done this testing on a Nano33BLE and not a Giga.

What is connected to the Arduino? Can you post an annotated schematic?

@cattledog that did the trick! I now also get the list. At first it's filled with some random things and a lot of copies but every boot after it fills the slots one by one.

This is everything. It's 75% connectors and sensors.

Now that it's working my followup question is if implementing this will slow the whole thing down to a noticable degree. Will it impact perfomance, is it worth it?

At the start of each loop the time stamp will get an actual date and time from the RTC and the error code will be changing everytime the code executes a new "block". Of course everytime the code changes I need to call kv_set so every change is stored.
I could limit how many times the code changes and get's saved but that reduces the chance of the reported error being accurate.

Is there such a thing as a standard for this?

if implementing this will slow the whole thing down to a noticable degree. Will it impact perfomance, is it worth it?

Can you please explain more about what you are trying to do?

What kind of errors are you seeing and trying to log? What is the root cause of the errors? Are you trying to look at the last time stamp before a program stops running? What do you do with the information you are collecting?

Writing to NVS is typically "slow" but what does that mean with a dual-core Giga with a Cortex-M7 at 480 MHz and a Cortex-M4 at 240 MHz.

Is there such a thing as a standard for this?

Typically you save things when values have changed

I am trying to understand if my code has "weak" spots.

If something goes wrong, is it the same thing over and over or is it something seemingly random.

I'm not seeing any errors yet, but I'm hoping I can look back and learn things by having a log.

If it's the same thing over and over it's clear there is work to be done.

Ironically when I was writing the kv_store stuff I had the serial monitor open and it was disconnected by the Arduino Cloud 6 times in a row, causing the RTC to lose it's timezone and DST. Yet it did not crash... so no log :zipper_mouth_face:

I've built in something that keeps count of every new boot. Sometimes this is 0 times per week and sometimes it's suddenly 140 ... I've not seen it happening but I have a hunch it's to do with the wifi/cloud connection.

I'm not clear that your log tells you where in the code the error occurs, just the time. You may want to use some sort of state variable which tracks you where you are. Crashes with restarts may be atypical as well.

You have not posted your complete code, so without understanding the design and the use of functions and if they are returning success or errors, it is hard to comment on how to best do this. Network and communications issues are typically difficult as well. Connect and disconnect are often easier to track.

One thing to consider is that the relay on/off often produces emi and power issues which can effect displays and possibly cause a reset if the power to the Arduino is lowered.

+1
Most common problem reported when a processor is driving relays.
That, and possible ground loops with the computer/laptop.
Show us your relay setup, including loads and snubber circuits.
Leo..

Looking at your schematic, assuming it is correct, you have Vin connected to the 5V, that will cause the Arduino processor to run at a lower voltage and any transients will be coupled directly into its logic. That pin goes through a Diode and a 5V regulator before being applied to the Arduino. It is operating at the edge and probably out of specification. The output of D1 (~4,6V) should go to the 5V pin.

Don't know the Giga (not a common board), but I'm sure it's a 3.3volt processor.
V-in needs of course more than 5volt for sensors/devices powered from the 5volt pin.
Not sure what the dropout voltage is of the built-in buck converter, but ~1volt is common.
Leo..

This could be a hardware or a software problem. For the software, it could be memory corruption, and it will start happening long before it crashes.

You could check everywhere you access an array and add some activable sanity check code. Every time you access an array with a calculated index, check that the index is between zero and max. The same for other variables and library calls, check that the values are not null when they shouldn't, etc. And freeze the program with a message when something unexpected happens.
Avoid or remove all malloc, new and free. Allocate the arrays for the worst case at compiler time, in the needed scope.

You could also try a stress test, if possible. Program it to simulate a very high permanent activity and let it running.

For power issues you could use something like the power profiler kit (Nordic Semiconductor). It is a wonderful tool and helps a lot to debug hardware issues. You can for example set triggers for when the current is out of the expected values.

your microcontroller is connected to your local WiFi - correct?

If yes you could send UDP-messages all ovr in your code and the receiving computer can write all the UDP-messages into a textfile.

As you are "just" controlling a terrarium I guess you have no time-critical detail in your code.
Time-critical in the sense of if whatever action happends 0.1 seconds later it doesn't matter at all.

Implementation into the full project didn't go flawlessly so I cannot give you any examples of saved errors yet as there aren't any, yet. I'll post the full code at the bottom, after I answer the other questions.

There is a 8 relay module attached, but they're all solid state. No coils. Even then there is only 1 in actual use right now, for the sprinklers. 4 times a day, noisy activation alsways draws attention and has never coincided with a crash. (Also I dont know what a snubber circuit is)

the giga is 3.3 so the 5 should be fine

I'm using an array or 2 but I think they are super basic, no idea if they use a calculated index. I'm not a programmer, I'm learning as I go. I do understand this comes with the danger of unknown side effects like memory corruption. There is no use of malloc anywhere, I'm happy to say :wink:

Here is the complete code, that currently does not get past pasetup and returns some interesting characters ... now that I think of it this might be related to running th test setup with int timestamp and using String timestamp when implementing in the project. Might be reading from previous stored values and encoutering int instead of String is an issue.:

Look for "kv_" for the relevant bits I suppose

/*
  Sketch generated by the Arduino IoT Cloud Thing "Giga"
  https://create.arduino.cc/cloud/things/1170c289-7b0d-4273-8557-9c30106bd3cc

  Arduino IoT Cloud Variables description

  The following variables are automatically generated and updated when changes are made to the Thing

  float airTempAverage;
  float humidityAverage;
  float pressureAverage;

  Variables which are marked as READ/WRITE in the Cloud Thing will also have functions
  which are called when their values are changed from the Dashboard.
  These functions are generated with the Thing and added at the end of this sketch.
*/
#include "arduino_secrets.h"
#include "thingProperties.h"
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <SPI.h>
#include <Wire.h>
#include <SHT31.h>
#include <DHT.h>
#include <Bme280.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <mbed_mktime.h>
#include <NTPClient.h>
#include <Timezone.h>
#include <I2C_eeprom.h>
#include <EMailSender.h>
#include <MultiMap.h>
#include "KVStore.h"
#include "kvstore_global_api.h"

//for updating desired settings during transition between values
typedef void (*TransitionCallback)(float);

#define DEBUG 1

#if DEBUG == 1
#define outputDebug(x); Serial.print(x);
#define outputDebugln(x); Serial.println(x);
#else
#define outputDebug(x); 
#define outputDebugln(x); 
#endif

//EEPROM
#define eepromLength 32768
I2C_eeprom EEPROM(0x50, eepromLength);
#define desiredTemp 70 //EEPROM address for settings configurable by user
#define desiredHumi 71
#define dayTemperature 64
#define nightTemperature 65

//TFT screen
#define TFT_CS        10
#define TFT_RST       9 // Or set to -1 and connect to Arduino RESET pin
#define TFT_DC        8
#define TFT_MOSI      7  // Data out --- SDA pin on TFT
#define TFT_SCLK      6  // Clock out --- SCL pin on TFT
Adafruit_ST7789 myScreen = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

//sensors
#define DHT22_PIN 11
#define DHT22_PIN_2 12
#define DHTTYPE DHT22
#define ThermistorPin A11
#define Thermistor2Pin A10
DHT dht1(DHT22_PIN, DHTTYPE);
DHT dht2(DHT22_PIN_2, DHTTYPE);
SHT31 sht1(0x44);
SHT31 sht2(0x45);
Bme280TwoWire bme1;
Bme280TwoWire bme2;
int Vo, Vo2;
float R1 = 10000;
float logR2, logR3, R2, R3;
float c1 = 1.009249522e-03, c2 = 2.378405444e-04, c3 = 2.019202697e-07;
float temp1, temp2, temp3, temp4, temp5, temp6, hum1, hum2, hum3, hum4, hum5, hum6, pres1, pres2, waterTemp1, waterTemp2, waterTemp_1, waterTemp_2;
//averages for main menu/dashboard
float waterTempAverage;
bool updateDisplay;

//Variables for auto scroll
unsigned long previousMillis;
unsigned long previousMillis2;
unsigned long readTime;

//menu variables
#define ROOT_MENU_CNT 4
#define SUB_MENU1_CNT 1
#define SUB_MENU2_CNT 8
#define SUB_MENU3_CNT 6
#define SUB_MENU4_CNT 5

//setup the emum with all the menu pages options
enum pageType {ROOT_MENU, SUB_MENU1, SUB_MENU2, SUB_MENU3, SUB_MENU4, SUB_MENU3_ITEM1, SUB_MENU3_ITEM2, SUB_MENU3_ITEM3, SUB_MENU3_ITEM4, SUB_MENU3_ITEM5, SUB_MENU4_ITEM1, SUB_MENU4_ITEM2, SUB_MENU4_ITEM3, SUB_MENU4_ITEM4, SUB_MENU4_ITEM5, SCREEN_SAVER};
enum pageType currPage = ROOT_MENU;
enum pageType prevPage = SUB_MENU4_ITEM1;
//screen names for sub pages
char *subPage3Items[] = {"LIGHTS", "RAIN", "BACKWALL DRIP", "FOGGER", "AIR PUMP"};
char *subPage4Items[] = {"SET TEMPERATURE", "SET HUMIDITY", "SET LAND TEMPERATURE", "SET RAINSTORM", "RESET CRASH COUNTER"};
//selected item pointer for the root menu
uint8_t root_Pos = 1;
uint8_t rel_Pos = 1;  // selected item pointers for the relay menu
//selected item pointers for the sub menu's
uint8_t sub_Pos_3 = 1; //for menu 3
uint8_t sub_Pos_3_1 = 1; //for menu 3 sub item light timer
uint8_t sub_Pos_3_2 = 1; //for menu 3 sub item rain timers
uint8_t sub_Pos_3_3 = 1; //for menu 3 sub item drip timer
uint8_t sub_Pos_3_4 = 1; //for menu 3 sub item fogger
uint8_t sub_Pos_3_5 = 1; //for menu 3 sub item air ump
uint8_t numberOfTimers = 5; //for menu 3
uint8_t sub_Pos_4 = 1; //for menu 4
uint8_t sub_Pos_4_1 = 1; //set temperature
uint8_t sub_Pos_4_2 = 1; //set humidity
uint8_t sub_Pos_4_3 = 1; //set land temperature
uint8_t sub_Pos_4_4 = 1; //set rainstorm 
uint8_t sub_Pos_4_5 = 1; //re-sync clock time
//maybe use something like sub_Pos_[x]_[y] ?

//joystick pins
#define xPin A0
#define yPin A1
#define kPin A2
//-----------
#define Neutral 0
#define Press 1
#define Up 2
#define Down 3
#define Right 4
#define Left 5
//-----------
#define highSwitchPoint 860 // mid position is around 700. top is 1023 and bottom is 0; //if its 1432 instead of 1023, like with the temperature calculation this changes.
#define lowSwitchPoint 350 // 860 about halfway between 700 and 1023 and 350 mid between 0 and 700

//relay block(s)
#define numberOfModules 1
//relay state array
#if numberOfModules == 2
#define numberOfRelays 16
unsigned int relay[numberOfRelays] = {23, 25, 27, 29, 31, 33, 35, 37, 46, 47, 48, 49, 50, 51, 52, 53};
#else 
#define numberOfRelays 8
unsigned int relay[numberOfRelays] = {23, 25, 27, 29, 31, 33, 35, 37};
#endif

//NTP and RTC 
WiFiUDP Udp; // A UDP instance to let us send and receive packets over UDP
NTPClient ntpClient(Udp, "europe.pool.ntp.org", 3600, 10000); //3600 for GMT +1 /call update only every 10 seconds
unsigned long lastUpdate = 60 * 60 * 24 * 1000UL; //86400000; //24h
unsigned long printNow{}; // = 0;
bool setRTC = false;
bool updatedYet = true;
TimeChangeRule mySTD = {"CET", Last, Sun, Nov, 2, 0};     // Standard time = GMT +1 hours already accounted for in ntpClient
TimeChangeRule myDST = {"CEST", Last, Sun, Mar, 2, 60};    // Daylight saving time = STD +1 hour
Timezone myTZ(myDST, mySTD);
TimeChangeRule *tcr;

uint8_t status = WL_IDLE_STATUS;

//climate control variables
#define pwmPin_1 2
#define pwmPin_2 3
#define pwmPin_3 4
#define pwmPin_4 5
unsigned int minAirTemp, maxAirTemp, minHumidity, maxHumidity, dayWaterTemp, nightWaterTemp;
float airTemp, humidity, mappedTemp, mappedHumidity;
unsigned int waterTemp, airHigh, airLow, humidityHigh, humidityLow;
float humidityVariation = 1, temperatureVariation = 1, daytimeTempVariation = 1, daytimeHumVariation = 1;
const unsigned int hysteresis = 50; //equals x/10 degrees C or x/10 % RH
unsigned long sequenceInterval[] {2000, 2000, 30000, 90000, 90000}; //credit noiasca  https://forum.arduino.cc/index.php?topic=666044
static byte startup;
bool hold = false;
unsigned long startMillis = 0;    
unsigned long intervalMark, startDelay;
unsigned long lightOn; 
const unsigned int pollInterval = 10000; //10 seconds check time for climateControl
float pwmVal1, pwmVal2, mappedVal, computedVal;
unsigned int pwm1Speed, pwm2Speed;
bool climateControlPause = false;
unsigned long pauseStart = 0;
unsigned long lastAirUpdateTime = 0, lastHumUpdateTime = 0;
uint8_t airStep = 0, humStep = 0;
float oldAirTemp = 230; //starting values for the transition function
float oldHumidity = 750;
float desiredAirTemp = 230; //let it be the same at the start so there is no transition trigger as soon as the board boots up
float desiredHumidity = 750;
float airStartVal, airEndVal, humStartVal, humEndVal;
bool airTransition = false, humTransition = false;
unsigned long fanPauseTime = 30000; //x minute(s) pause after sprinklers have been used
unsigned long resetDelay = 90000;
bool noRain = false;
bool rainyDay = false;
float humModifier, tempModifier, weatherVariation;
bool humidityMonitoring = true;
uint8_t rainCounter = 0;  
float rainCalculationFactor = 0.8; //used to be 1 for thunderstorms. want something more mundane
float currentReading = 0, previousReading = 0;
unsigned long previousPoint = 0;
enum ccState {CASE0, CASE1, CASE2, CASE3, CASE4, CASE5, CASE6, CASE7, CASE8, CASE9, CASE10, CASE11, CASE12};
enum ccState climateState = CASE0;
enum ccState previousClimateState = CASE5;

//set time function
enum ChangeTimeState {CHANGING_HOUR, CHANGING_MINUTE, SAVING_COMPLETE};

//time switching variables
unsigned int currentHour = 0;
unsigned int currentMinute = 0;
unsigned int currentTime = 0;
unsigned long wifiTime = 0; 
unsigned long checkSettings = 0; 
unsigned long checkNTP = 0;
unsigned long checkTime = 200; 
unsigned long adjustmentTime = 400; 
unsigned long screenTime = 600; 
unsigned int multipliedTime = 0;
bool dayTime = false;
#define numberOfTimers 16
unsigned long countdown[numberOfTimers];
bool timerActive[numberOfTimers] = {false};
bool hasNotRunYet[numberOfTimers] = {true};

//multiMap settings
float input[11] = {1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; //fanspeed
float output[11] = {1, 7, 15, 28, 43, 59, 77, 100, 140, 191, 255};

//updateStatistic variables
#define totalReadings 14
const unsigned int maxCount = 120;  
float airDeviation = 5.0; //degress C
float humidityDeviation = 20.0; //% RH
float reading[totalReadings];
float sensorValue[totalReadings];
unsigned int sensor[totalReadings];
bool sensorFault[totalReadings];
char msg[10];

//for alarm e-mails
EMailSender emailSend(SECRET_EMAIL, SECRET_GMAIL_PASS);
bool emailSent = false;

//Bluetooth = HC-05 ID and pass = APC / 3511
#define btState 13
bool BTconnected = false;
bool relayState[8];
const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars];  // temporary array for use when parsing
unsigned int eepromAddress = 0;
unsigned int newSetting = 0;
bool newData = false;
bool dataToParse = false;
bool timeToSet = false;
uint8_t relayNumber = 0;

//tracking errors
int counter = 0;
const char* key0 = "counter";
char crashKey[20];

struct StructData {
  String timeStamp;
  int errorCode;
};
StructData dataSaved;
StructData dataRetrieved;

//===========================================================================================================================
//                                                  SETUP 
//===========================================================================================================================
void setup() {
  Serial.begin(115200);
  Serial1.begin(38400);
  delay(1000);  

  //screen
  myScreen.init(240, 320);
  myScreen.fillScreen(ST77XX_BLACK);
  Serial.println("TFT Initialized");
  myScreen.setRotation(1);
  myScreen.setCursor(0, 0);
  myScreen.setTextSize(3);
  myScreen.setTextWrap(false);
  myScreen.setTextColor(ST77XX_GREEN);
  myScreen.println("Arne's");
  myScreen.println("Paludarium");
  myScreen.println("Controller");
  myScreen.println("0.9.7.6");    
  myScreen.println();
  myScreen.println("Connecting . . .");    
  myScreen.setTextSize(2);
  
  while (status != WL_CONNECTED) { // attempt to connect to WiFi network:
    Serial.print("Attempting to connect to: ");
    Serial.println(SSID);
    status = WiFi.begin(SSID, PASS);
    delay(5000);
  }
  
  myScreen.fillScreen(ST77XX_BLACK);

  // Print error log
  Serial.println("Error log:");
  for (int i = 0; i < 10; i++) {
    
    snprintf(crashKey, sizeof(crashKey), "Crash_%d", i); // Create key for each saved crash
    
    size_t retrievedSize = sizeof(dataRetrieved); // Retrieve each crash entry
    kv_get(crashKey, (uint8_t*)&dataRetrieved, retrievedSize, 0);
    
    Serial.print(crashKey); // Print the retrieved crash data
    Serial.print(" - Timestamp: ");
    Serial.print(dataRetrieved.timeStamp);
    Serial.print(", Error Code: ");
    Serial.print(dataRetrieved.errorCode);
    Serial.println();
  }

  // Initialize struct data
  dataSaved.timeStamp = getDateTime();
  dataSaved.errorCode = 0;  

  // Initialize error counter. Load previous and add 1
  size_t size = sizeof(counter);
  kv_get(key0, &counter, size, 0);
  
  if (counter < 9) { counter++; } // Track 0 - 9
  else { counter = 0; }
  
  kv_set(key0, &counter, sizeof(counter), 0); // Save new counter  
  snprintf(crashKey, sizeof(crashKey), "Crash_%d", counter); // Prepare the crash key with the counter
    
  initProperties(); // Defined in thingProperties.h  
  ArduinoCloud.begin(ArduinoIoTPreferredConnection); // Connect to Arduino IoT Cloud
  setDebugMessageLevel(2);
  ArduinoCloud.printDebugInfo();   

  //joystick
  pinMode(xPin, INPUT);
  pinMode(yPin, INPUT);
  pinMode(kPin, INPUT_PULLUP);

  //Bluetooth state
  pinMode(btState, INPUT);

  //assign relay pins as output
  for (int i = 0; i < numberOfRelays; i++) {
  pinMode(relay[i], OUTPUT); }

  // Initialize countdown array
  for (int i = 0; i < numberOfTimers; i++) {
    countdown[i] = 0;
  }

  //variable fans
  pinMode(pwmPin_1, OUTPUT);
  pinMode(pwmPin_2, OUTPUT);
  pinMode(pwmPin_3, OUTPUT);
  pinMode(pwmPin_4, OUTPUT);

  //LED indicators
  pinMode(LEDR, OUTPUT);
  pinMode(LEDB, OUTPUT);
  digitalWrite(LEDR, LOW);
  digitalWrite(LEDB, HIGH);
  
  //sensors
  Wire.begin();
  dht1.begin();
  dht2.begin();
  sht1.begin();
  sht2.begin();
  bme1.begin(Bme280TwoWireAddress::Primary);
  bme2.begin(Bme280TwoWireAddress::Secondary);
  
  ntpClient.begin();    

  //crash counter
  int crashCounter = EEPROM.readByte(100);
  EEPROM.writeByte(100, crashCounter + 1);

}

//===========================================================================================================================
//                                                  MAIN LOOP 
//===========================================================================================================================
void loop() {

  ArduinoCloud.update();

  dataSaved.timeStamp = getDateTime();
  dataSaved.errorCode = 1; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0); // Save the data with the new key

  if (millis() - wifiTime >= 1000) { //wifi and cloud status led and RTC

    if (status != WL_CONNECTED) {
      digitalWrite(LEDB, HIGH);
      digitalWrite(LEDR, LOW);
      dataSaved.errorCode = 2; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0); 
    }
    if (status = WL_CONNECTED) {

      if (!ArduinoCloud.connected()) {
        digitalWrite(LEDR, HIGH);
        digitalWrite(LEDB, HIGH);
        dataSaved.errorCode = 3; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
      }
      if (ArduinoCloud.connected()) {
        digitalWrite(LEDR, HIGH);
        digitalWrite(LEDB, LOW);
      }
      if (setRTC == false) {  // Set RTC once on startup      
        dataSaved.errorCode = 4; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
        updateTime();
      }
      if (millis() - lastUpdate >= (60 * 60 * 24 * 1000UL)) {  // reset e-mail notifications every 24hrs
        lastUpdate = millis();
        emailSent = false; //piggyback reset for e-mail notifications
      }
    }
    wifiTime = millis();
  }

  if (millis() - checkSettings >= 60000) {  //check settings every minute
    dataSaved.errorCode = 5; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
    climateSettings();
    checkSettings = millis();
  }
    
  if (millis() - adjustmentTime >= 1000) {  //climateChange() adjust fan speeds every second
    dataSaved.errorCode = 6; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
    climateChange();
    adjustmentTime = millis();
  }
    
  if (millis() - screenTime >= 100) { //don't run continuously. Fuzz in checkJoystick() readings will never be coninuous 0
    dataSaved.errorCode = 7; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
    autoGoBackToHomePage();
    screenSaver();
    activateScreen();
    screenTime = millis();
  }
    
  if (millis() - checkTime >= 900) {  //run automatedswitching every x seconds
    dataSaved.errorCode = 8; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
    automatedSwitching();
    checkTime = millis();
  }

  if (millis() - readTime >= 2000) {  //poll all sensors every 2 seconds
    dataSaved.errorCode = 9; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);
    updateStatistics();
    updateBluetooth();
    if (currPage == ROOT_MENU) {
      printAverages();
      printRelays();
    }
    if (currPage == SUB_MENU1) {
      printStatistics();
    }
    readTime = millis();
  }

  if (millis() - printNow >= 60000 && currPage != SCREEN_SAVER) {  //print time on screen every minute
    printTime();
    printNow = millis();
  }

  if (millis() - startMillis >= pollInterval && !climateControlPause) {  //run climateControll every x seconds, except in a pause 
    dataSaved.errorCode = 10; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);   
    climateControl();    
    startMillis = millis();
  }
   
  if (millis() - checkNTP >= 60 * 10 * 1000UL) {  //update RTC 10 minutes
    setRTC = false;
    checkNTP = millis();
  }

  if (digitalRead(relay[0])) {  //adjust climateControl for day and night
    dayTime = true;
  } else if (!digitalRead(relay[0])) {
    dayTime = false;
  }

  dataSaved.errorCode = 11; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);

  checkJoystick();
  navigationFunctioner();
  changePage();
  fanPause();
  makeItRain();  

  dataSaved.errorCode = 12; kv_set(crashKey, (uint8_t*)&dataSaved, sizeof(dataSaved), 0);

  bluetooth();
  if (newData == true) { //receiving bluetooth data
    strcpy(tempChars, receivedChars); // this temporary copy is necessary to protect the original data because strtok() used in parseData() replaces the commas with \0       
    if (dataToParse) {
      parseData();
    }     
    if (timeToSet) {
      setTime();
    } else {
      switchRelay();
    }
    newData = false;
  }

}

//===========================================================================================================================
//                                                  CHECK PAGE 
//===========================================================================================================================
void changePage() {
  if (currPage != prevPage){
    switch (currPage){
      case ROOT_MENU:     page_RootMenu(); break;
      case SUB_MENU1:     page_SubMenu1(); break;
      case SUB_MENU2:     page_SubMenu2(); break;
      case SUB_MENU3:     page_SubMenu3(); break;
      case SUB_MENU4:     page_SubMenu4(); break;
      //sub menu 3
      case SUB_MENU3_ITEM1:     page_SubMenu3_Item(0); break;
      case SUB_MENU3_ITEM2:     page_SubMenu3_Item(1); break;
      case SUB_MENU3_ITEM3:     page_SubMenu3_Item(2); break;
      case SUB_MENU3_ITEM4:     page_SubMenu3_Item(3); break;
      case SUB_MENU3_ITEM5:     page_SubMenu3_Item(4); break;
      //sub menu 4
      case SUB_MENU4_ITEM1:     page_SubMenu4_Item(0); break;
      case SUB_MENU4_ITEM2:     page_SubMenu4_Item(1); break;
      case SUB_MENU4_ITEM3:     page_SubMenu4_Item(2); break;
      case SUB_MENU4_ITEM4:     page_SubMenu4_Item(3); break;
      case SUB_MENU4_ITEM5:     page_SubMenu4_Item(4); break;
      case SCREEN_SAVER:        page_ScreenSaver();    break;
    }
    //update first time
    updateDisplay = true;
    prevPage = currPage;
  }
}

//===========================================================================================================================
//                                                  CHECK JOYSTICK POSITION 
//===========================================================================================================================
int checkJoystick() {
  int joystickXPin = analogRead(xPin);
  int joystickYPin = analogRead(yPin);
  int joystickSW = analogRead(kPin);

  /* inverted:  
  if (joystickXPin < lowSwitchPoint)  return Right;
  if (joystickXPin > highSwitchPoint) return Left;
  if (joystickYPin < lowSwitchPoint)  return Down;
  if (joystickYPin > highSwitchPoint) return Up;
  */
  if (joystickXPin > highSwitchPoint) return Right;
  if (joystickXPin < lowSwitchPoint)  return Left;
  if (joystickYPin > highSwitchPoint) return Down;
  if (joystickYPin < lowSwitchPoint)  return Up;
  
  if (!joystickSW) return Press;
  return Neutral;
}

//===========================================================================================================================
//                                                  CHECK SYSTEM STATS
//===========================================================================================================================
void updateStatistics() {
  
  sht1.read();
  sht2.read();

  reading[0] = dht1.readTemperature();
  reading[1] = dht2.readTemperature();
  reading[2] = sht1.getTemperature();
  reading[3] = sht2.getTemperature();
  reading[4] = bme1.getTemperature();
  reading[5] = bme2.getTemperature();

  reading[6] = dht1.readHumidity();
  reading[7] = dht2.readHumidity();
  reading[8] = sht1.getHumidity();
  reading[9] = sht2.getHumidity();
  reading[10] = bme1.getHumidity();
  reading[11] = bme2.getHumidity();

  reading[12] = bme1.getPressure() / 100;
  reading[13] = bme2.getPressure() / 100;

  // Check sensor readings and update sensor status
  for (int i = 0; i < totalReadings; i++) {
    if (!isnan(reading[i])) {
      sensorValue[i] = reading[i];
      sensor[i] = 0; //reset NAN counter
    } else {
      if (sensor [i] < maxCount) { //if NAN counter +
        sensor[i]++;        
      }
    }

    if (sensor[i] == maxCount) { //if x consecutive readings fail, trigger the alarm
      alarm(i, " DEFECT.");
      emailSent = false;
    }
  }
  
  temp1 = sensorValue[0];
  temp2 = sensorValue[1];
  temp3 = sensorValue[2];
  temp4 = sensorValue[3];
  temp5 = sensorValue[4];
  temp6 = sensorValue[5];
  hum1 = sensorValue[6];
  hum2 = sensorValue[7];
  hum3 = sensorValue[8];
  hum4 = sensorValue[9];
  hum5 = sensorValue[10];
  hum6 = sensorValue[11];
  pres1 = sensorValue[12];
  pres2 = sensorValue[13];

  airTempAverage = (temp1 + temp2 + temp3 + temp4 + temp5 + temp6) / 6;
  humidityAverage = (hum1 + hum2 + hum3 + hum4 + hum5 + hum6) / 6;  
  pressureAverage = (pres1 + pres2) / 2;

  // Check for significant deviations in sensor readings
  for (int i = 0; i < 5; i++) { //1-6 is temperature
    sensorFault[i] = false;
    if ((sensorValue[i] > airTempAverage + airDeviation) || (sensorValue[i] < airTempAverage - airDeviation)) {
      alarm(i, " Deviation.");
      sensorFault[i] = true;
    }
  }
  
  for (int i = 6; i < 12; i++) {  //7-12 is humidity
    sensorFault[i] = false;
    if ((sensorValue[i] > humidityAverage + humidityDeviation) || (sensorValue[i] < humidityAverage - humidityDeviation)) {
      alarm(i, " Deviation.");
      sensorFault[i] = true;
    }
  }

  //read water temperature1
  Vo = analogRead(ThermistorPin);
  R2 = R1 * (1558 / (float)Vo - 1.0); //4.96 (actual voltage on pin) / 3.26 (internal 3.3 ref voltage) * 1024 = 1561.1 I don't really understand why I need this conversion but it works.
  logR2 = log(R2);
  waterTemp_1 = (1.0 / (c1 + c2 * logR2 + c3 * logR2 * logR2 * logR2));
  waterTemp1 = waterTemp_1 - 273.15; // to Celsius
  //read water temperature2
  Vo2 = analogRead(Thermistor2Pin);
  R3 = R1 * (1558 / (float)Vo2 - 1.0);
  logR3 = log(R3);
  waterTemp_2 = (1.0 / (c1 + c2 * logR3 + c3 * logR3 * logR3 * logR3));
  waterTemp2 = waterTemp_2 - 273.15;
  //WaterTemp Average
  waterTempAverage = (waterTemp1 + waterTemp2) / 2;

}

void alarm(int x, char *msg) {

  outputDebug("Sensor reading ");
  outputDebug(x);
  outputDebugln(msg);

  String sensorName;

  if (x = 0 || 6) { sensorName = "DHT22-1. "; }
  if (x = 1 || 7) { sensorName = "DHT22-2. "; }
  if (x = 2 || 8) { sensorName = "SHT31-1. "; }
  if (x = 3 || 9) { sensorName = "SHT31-2. "; }
  if (x = 4 || 10 || 12) { sensorName = "BME280-1. "; }
  if (x = 5 || 11 || 13) { sensorName = "BME280-2. "; }

  String emailMessage = "Malfunction in sensor ";
  emailMessage += sensorName; 
  emailMessage += String(msg);
    
  if (!emailSent) {
    EMailSender::EMailMessage message;
    message.subject = "APC alarm";
    message.message = emailMessage;

    EMailSender::Response resp = emailSend.send("arnekolman@gmail.com", message);
    
    outputDebugln("Sending status: ");
    outputDebugln(resp.status);
    outputDebugln(resp.code);
    outputDebugln(resp.desc);

    emailSent = true;
  }
  
}

//===========================================================================================================================
//                                                     BLUETOOTH
//===========================================================================================================================
void bluetooth() {

  static boolean recvInProgress = false;
  static byte ndx = 0;
  char startMarker = '<';
  char endMarker = '>';
  char rc;

  if (Serial1.available() > 0 && newData == false) {
    rc = Serial1.read();

    if (recvInProgress == true) {
      if (rc != endMarker) {
        receivedChars[ndx] = rc;
        ndx++;
        if (ndx >= 4) {
          dataToParse = true;
        }
        if (ndx >= 9) {
          dataToParse = false; //bypass the parse function
          timeToSet = true;    //go to setTime function instead
        }
        if (ndx >= numChars) {
          ndx = numChars - 1;
        }
      } else {
        receivedChars[ndx] = '\0';  // terminate the string
        recvInProgress = false;
        ndx = 0;
        newData = true;
      }
    }

    else if (rc == startMarker) {
      recvInProgress = true;
    }
  }
}

void parseData() {  // split the data into its parts

  char* strtokIndx;  // this is used by strtok() as an index

  strtokIndx = strtok(tempChars, ",");  // get the first part
  eepromAddress = atoi(strtokIndx); // convert this part to an integer
  
  strtokIndx = strtok(NULL, ",");  // this continues where the previous call left off
  newSetting = atoi(strtokIndx);

  EEPROM.writeByte(eepromAddress, newSetting);
  dataToParse = false;

  outputDebug("EEPROM address ");
  outputDebugln(eepromAddress);
  outputDebug("New value ");
  outputDebugln(newSetting);
}

void setTime() {  // split the data into its parts

  char* strtokIndx;  // this is used by strtok() as an index  
  unsigned int Hour, Minute, Day, Mon, Year;

  strtokIndx = strtok(tempChars, ",");  // get the first part
  Hour = atoi(strtokIndx);   
  strtokIndx = strtok(NULL, ",");  // this continues where the previous call left off
  Minute = atoi(strtokIndx);
  strtokIndx = strtok(NULL, ",");  
  Day = atoi(strtokIndx);
  strtokIndx = strtok(NULL, ",");  
  Mon = atoi(strtokIndx);
  strtokIndx = strtok(NULL, ",");  
  Year = atoi(strtokIndx); //sends 4 digit year

  tm t;
  t.tm_sec = (0);       
  t.tm_min = (Minute);        
  t.tm_hour = (Hour);         
  t.tm_mday = (Day);   
  t.tm_mon = (Mon-1);      //jan = 0. receiving jan = 1 count  
  t.tm_year = (Year-1900); // year since 1900. receiving 202x
  set_time(mktime(&t));    // set RTC clock 
  timeToSet = false;

  outputDebug("Time manually set to: ");
  outputDebug(Hour);
  outputDebug(":");
  outputDebug(Minute);
  outputDebug("  date: ");
  outputDebug(Day);
  outputDebug("-");
  outputDebug(Mon);
  outputDebug("-");
  outputDebugln(Year);
}

void switchRelay() {

  relayNumber = atoi(receivedChars);
  digitalWrite(relay[relayNumber], !digitalRead(relay[relayNumber]));
  
  outputDebug("Switching relay ");
  outputDebugln(relayNumber);

}

void updateBluetooth() {

  if (digitalRead(btState) == HIGH) {
    BTconnected = true;
  } else {
    BTconnected = false; 
  }

  for (int i = 0; i < numberOfRelays; i++) {
  relayState[i] = digitalRead(relay[i]); }
  
  if (BTconnected) {
    //outputDebugln("Bluetooth Connected");
    Serial1.print("t1:"); Serial1.println(temp1, 1); 
    Serial1.print("t2:"); Serial1.println(temp2, 1); 
    Serial1.print("t3:"); Serial1.println(temp3, 1);
    Serial1.print("t4:"); Serial1.println(temp4, 1); 
    Serial1.print("t5:"); Serial1.println(temp5, 1); 
    Serial1.print("t6:"); Serial1.println(temp6, 1); 
    
    Serial1.print("h1:"); Serial1.println(hum1, 1); 
    Serial1.print("h2:"); Serial1.println(hum2, 1); 
    Serial1.print("h3:"); Serial1.println(hum3, 1); 
    Serial1.print("h4:"); Serial1.println(hum4, 1); 
    Serial1.print("h5:"); Serial1.println(hum5, 1); 
    Serial1.print("h6:"); Serial1.println(hum6, 1); 
    
    Serial1.print("ata:"); Serial1.println(airTempAverage, 1); 
    Serial1.print("ha:");  Serial1.println(humidityAverage, 1); 
    Serial1.print("pa:");  Serial1.println(pressureAverage, 1); 
    Serial1.print("wta:"); Serial1.println(waterTempAverage, 1); 
    
    Serial1.print("r0:"); Serial1.println(relayState[0]); 
    Serial1.print("r1:"); Serial1.println(relayState[1]); 
    Serial1.print("r2:"); Serial1.println(relayState[2]); 
    Serial1.print("r3:"); Serial1.println(relayState[3]); 
    Serial1.print("r4:"); Serial1.println(relayState[4]); 
    Serial1.print("r5:"); Serial1.println(relayState[5]); 
    Serial1.print("r6:"); Serial1.println(relayState[6]); 
    Serial1.print("r7:"); Serial1.println(relayState[7]); 

    Serial1.print("desT:"); Serial1.println(EEPROM.readByte(desiredTemp)); //value stored in EEPROM address
    Serial1.print("desH:"); Serial1.println(EEPROM.readByte(desiredHumi)); 
    Serial1.print("dT:");   Serial1.println(EEPROM.readByte(dayTemperature)); 
    Serial1.print("nT:");   Serial1.println(EEPROM.readByte(nightTemperature)); 
    Serial1.print("EEdesT:"); Serial1.println(desiredTemp); // EEPROM address to read or write
    Serial1.print("EEdesH:"); Serial1.println(desiredHumi); 
    Serial1.print("EEdT:");   Serial1.println(dayTemperature); 
    Serial1.print("EEnT:");   Serial1.println(nightTemperature); 
    
    Serial1.print("clock:");  Serial1.println(99); 
    Serial1.print("LTON:");   Serial1.println(0); 
    Serial1.print("LTOF:");   Serial1.println(2); 
  }

}

//===========================================================================================================================
//                                                  PAGE - ROOT MENU
//===========================================================================================================================
void page_RootMenu() {

  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  //print title
  myScreen.setCursor(0, 0);
  myScreen.println(F("MAIN MENU"));
  myScreen.drawLine(0, 15, 320, 15, ST77XX_GREEN);
  myScreen.drawLine(0, 16, 320, 16, ST77XX_GREEN);
  myScreen.drawLine(0, 17, 320, 17, ST77XX_GREEN);
  //print framework
  myScreen.setTextSize(1);
  myScreen.setCursor(10, 30);
  myScreen.print(F("Average"));
  myScreen.setCursor(10, 40);
  myScreen.print(F("Temperature"));
  myScreen.setCursor(100, 30);
  myScreen.print(F("Average"));
  myScreen.setCursor(100, 40);
  myScreen.print(F("Humidity"));
  myScreen.setCursor(180, 30);
  myScreen.print(F("Average"));
  myScreen.setCursor(180, 40);
  myScreen.print(F("Pressure"));
  myScreen.setTextSize(2);

  myScreen.setCursor(200, 80);
  myScreen.print(F("PWM1"));
  myScreen.setCursor(185, 100);
  myScreen.print(F("STATE"));
  myScreen.setCursor(220, 120);
  myScreen.print(F("RC"));

  
  myScreen.setTextSize(1);
  myScreen.setCursor(10, 200); myScreen.print(F("lights"));
  myScreen.setCursor(50, 220); myScreen.print(F("ventilation"));
  myScreen.setCursor(90, 200); myScreen.print(F("sprinklers"));
  myScreen.setCursor(130, 220); myScreen.print(F("irrigation"));
  myScreen.setCursor(170, 200); myScreen.print(F("fogger"));
  myScreen.setCursor(210, 220); myScreen.print(F("land heat"));
  myScreen.setCursor(250, 200); myScreen.print(F("window heat"));
  myScreen.setCursor(290, 220); myScreen.print(F("airpump"));
  myScreen.setTextSize(2);

  printTime();

  //rest in handled in navigationFunctioner();

}

void page_ScreenSaver() {

  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  updateDisplay = false;
}
//=======================================================================================
//                                              PRINT AVERAGE STATS
//=======================================================================================
void printAverages() {

  myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);

  //framework is created in ROOT_MENU
  //update readings on timer from updateStatistics()
  myScreen.setCursor(10, 50);  
  printOutOfBounds(airTempAverage, true);

  myScreen.setCursor(100, 50);  
  printOutOfBounds(humidityAverage, false);  

  myScreen.setCursor(180, 50);  
  myScreen.print(pressureAverage, 1);
  myScreen.print(F(" hPa")); 

  //print target values
  if (!airTransition) { //if there is a transition use the callback to print those values
    myScreen.setTextSize(1);
    myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK);
    myScreen.setCursor(10, 70); 
    myScreen.print((desiredAirTemp / 10), 1);
    myScreen.print("\367 ");
    myScreen.setTextSize(2);
    myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  }    
  if (!humTransition) {
    myScreen.setTextSize(1);
    myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK);
    myScreen.setCursor(100, 70); 
    myScreen.print((desiredHumidity / 10), 1);
    myScreen.print("% ");
    myScreen.setTextSize(2);
    myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  }

  //print PWM speed     
  if (pwm1Speed <= 100) { //the mapped value can spike to negative read over 458628%
    myScreen.setCursor(255, 80);
    myScreen.print(pwm1Speed);
    myScreen.print(F("%  ")); 
  }
  
  //print climateState
  myScreen.setCursor(255, 100);  
  myScreen.print(climateState);
  myScreen.print(" ");

  //print rainCounter
  myScreen.setCursor(255, 120); 
  myScreen.print(rainCounter);
  myScreen.print(" ");  

}

//=======================================================================================

//                                              PRINT RELAY STATES
//=======================================================================================
void printRelays() {   

  if (digitalRead(relay[0]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
  else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); }     
    myScreen.setCursor(10, 180); myScreen.print(F("R0"));
  
  if (digitalRead(relay[1]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
  else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); }  
    myScreen.setCursor(50, 180); myScreen.print(F("R1"));    
  
  if (digitalRead(relay[2]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
  else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); } 
    myScreen.setCursor(90, 180); myScreen.print(F("R2")); 
      
  if (digitalRead(relay[3]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
  else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); }
    myScreen.setCursor(130, 180); myScreen.print(F("R3")); 
      
  if (digitalRead(relay[4]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
    else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); } 
    myScreen.setCursor(170, 180); myScreen.print(F("R4")); 
    
  if (digitalRead(relay[5]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
  else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); } 
    myScreen.setCursor(210, 180); myScreen.print(F("R5"));
      
  if (digitalRead(relay[6]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
  else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); } 
    myScreen.setCursor(250, 180); myScreen.print(F("R6")); 
      
  if (digitalRead(relay[7]) == LOW) 
  { myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK); } 
  else { myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK); } 
    myScreen.setCursor(290, 180); myScreen.print(F("R7")); 
        
  myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
}

//=======================================================================================
//                                              PRINT TIME 
//=======================================================================================
void printTime() { 
  myScreen.setCursor(210, 0);;
  myScreen.setTextSize(2);  
  myScreen.print(getLocalTime()); 
}

void printDigits(int digits) {  
  //this void function is really useful; it adds a "0" to the beginning of the number, so that 5 minutes is displayed as "00:05:00", rather than "00:5 :00"
  if (digits < 10) {
    myScreen.print("0");
    myScreen.print(digits);
  } else {
    myScreen.print(digits);
  }
}

void updateTime() {

  ntpClient.forceUpdate();   

  if (ntpClient.isTimeSet()) {
    unsigned long ntpTime = ntpClient.getEpochTime(); // ntpTime = epoch
    time_t localTime = myTZ.toLocal(ntpTime, &tcr);   
    set_time(localTime);
    Serial.println("RTC is set");
    setRTC = true;
  }
}

String getLocalTime() {
  char buffer[16];
  tm t;
  _rtc_localtime(time(NULL), &t, RTC_4_YEAR_LEAP_YEAR_SUPPORT);
  strftime(buffer, 16, "%a %H:%M", &t);
  return String(buffer);
}

String getDateTime() {
  char buffer[16];
  tm t;
  _rtc_localtime(time(NULL), &t, RTC_4_YEAR_LEAP_YEAR_SUPPORT);
  strftime(buffer, 16, "%d/%m %H:%M:%S", &t);
  return String(buffer);
}

int getCurrentTime() { //formatted to hhmm for comparisons
  int multipliedTime;
  tm t;
  _rtc_localtime(time(NULL), &t, RTC_4_YEAR_LEAP_YEAR_SUPPORT);
  multipliedTime = (t.tm_hour * 100) + t.tm_min;
  return multipliedTime;
}
int getLocalHour() {
  int hour;
  tm t;
  _rtc_localtime(time(NULL), &t, RTC_FULL_LEAP_YEAR_SUPPORT);
  hour = t.tm_hour;
  return hour;
}
int getLocalMinutes() {
  int minutes;
  tm t;
  _rtc_localtime(time(NULL), &t, RTC_FULL_LEAP_YEAR_SUPPORT);
  minutes = t.tm_min;
  return minutes;
}
int getDay() {
  int currentDay;
  tm t;
  _rtc_localtime(time(NULL), &t, RTC_4_YEAR_LEAP_YEAR_SUPPORT);
  currentDay = t.tm_mday;
  return currentDay;
}
int getMonth() {
  int currentMonth;
  tm t;
  _rtc_localtime(time(NULL), &t, RTC_4_YEAR_LEAP_YEAR_SUPPORT);
  currentMonth = t.tm_mon;
  return currentMonth;
}

//===========================================================================================================================
//                                              PAGE - SUB MENU 1 STATISTICS                        
//===========================================================================================================================
void page_SubMenu1() {
  
  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  //print title      
  myScreen.setCursor(0, 0);
  myScreen.println(F("STATISTICS"));
  myScreen.drawLine(0, 15, 320, 15, ST77XX_GREEN);
  myScreen.drawLine(0, 16, 320, 16, ST77XX_GREEN);
  myScreen.drawLine(0, 17, 320, 17, ST77XX_GREEN);  

  //draw framework once
  //horizontal framework 
  myScreen.setTextSize(1);
  myScreen.setCursor(70, 40);
  myScreen.print(F("Temperature"));    
  myScreen.setCursor(150, 40);
  myScreen.print(F("Humidity")); 
  myScreen.setCursor(220, 40);
  myScreen.print(F("Pressure"));   
  //vertical framework 
  myScreen.setCursor(10, 60);
  myScreen.print(F("NTC-1"));    
  myScreen.setCursor(10, 80);
  myScreen.print(F("DHT22-1")); 
  myScreen.setCursor(10, 100); 
  myScreen.print(F("SHT31-1"));   
  myScreen.setCursor(10, 120);
  myScreen.print(F("BME280-1")); 
  myScreen.setCursor(10, 140);  
  myScreen.print(F("NTC-2"));   
  myScreen.setCursor(10, 160); 
  myScreen.print(F("DHT22-2")); 
  myScreen.setCursor(10, 180);
  myScreen.print(F("SHT31-2"));   
  myScreen.setCursor(10, 200);
  myScreen.print(F("BME280-2"));

}

//=======================================================================================
//                                              PRINT SYSTEM STATS
//=======================================================================================
void printStatistics() {

  myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  myScreen.setTextSize(1);
  //framework is created in SUB_MENU1
  //update readings on timer, called from main loop in updateStatistics

  //sensor set 1
  myScreen.setCursor(80, 60); //NTC 1
  myScreen.print(waterTemp1, 1); 
  myScreen.print(F("\367"));

  myScreen.setCursor(80, 80); //DHT22 1
  printRedOrWhite(temp1, sensorFault[0], 1);
  myScreen.setCursor(150, 80); 
  printRedOrWhite(hum1, sensorFault[6], 0); 
  myScreen.setCursor(80, 100); //SHT31 1  
  printRedOrWhite(temp3, sensorFault[2], 1);
  myScreen.setCursor(150, 100);  
  printRedOrWhite(hum3, sensorFault[8], 0);
  myScreen.setCursor(80, 120); //BME280 1  
  printRedOrWhite(temp5, sensorFault[4], 1);
  myScreen.setCursor(150, 120); 
  printRedOrWhite(hum5, sensorFault[10], 0);
  myScreen.setCursor(220, 120); 
  myScreen.print(pres1, 1);
  myScreen.print(F(" hPa"));
  
  //sensor set 2
  myScreen.setCursor(80, 140); //NTC 2
  myScreen.print(waterTemp2, 1); 
  myScreen.print(F("\367"));
  myScreen.setCursor(80, 160); // DHT22 2
  printRedOrWhite(temp2, sensorFault[1], 1);
  myScreen.setCursor(150, 160); 
  printRedOrWhite(hum2, sensorFault[7], 0);
  myScreen.setCursor(80, 180); //SHT31 2
  printRedOrWhite(temp4, sensorFault[3], 1);
  myScreen.setCursor(150, 180); 
  printRedOrWhite(hum4, sensorFault[9], 0);
  myScreen.setCursor(80, 200); //BME280 2
  printRedOrWhite(temp6, sensorFault[5], 1);
  myScreen.setCursor(150, 200); 
  printRedOrWhite(hum6, sensorFault[11], 0);
  myScreen.setCursor(220, 200); 
  myScreen.print(pres2, 1);
  myScreen.print(F(" hPa"));  

  myScreen.setTextSize(2);

}

void printRedOrWhite(float sensorData, bool fault, bool temperature) {  
  //if the sensor returns NAN the variable will not be updated and will be displayed in red
  if (fault) {
    myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK);
    myScreen.print(sensorData, 1);
    if (temperature) {
      myScreen.print("\367");
    } else {      
      myScreen.print("%");
    }
    myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  } else {
     myScreen.print(sensorData, 1);
    if (temperature) {
      myScreen.print("\367");
    } else {      
      myScreen.print("%");
    }
  }
  
}

void printOutOfBounds(float sensorData, bool temperature) {  
  //if the sensor data requires adjusting it will be displayed in red
  int val = sensorData * 10; //sensorData = float, airHigh = int from float * 10. So sensorData must also convert with * 10
  if (temperature) {
    if (val >= airHigh || val <= airLow) { 
      myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK);
      myScreen.print(sensorData, 1);
      myScreen.print("\367");
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);   
    } else {
      myScreen.print(sensorData, 1);
      myScreen.print("\367");
    }   
  } 
  if (!temperature) {
    if (val >= humidityHigh || val <= humidityLow) {
      myScreen.setTextColor(ST77XX_RED, ST77XX_BLACK);
      myScreen.print(sensorData, 1);
      myScreen.print("%");
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
    } else {      
      myScreen.print(sensorData, 1);
      myScreen.print("%");
    }
  }
  
}

//===========================================================================================================================
//                                              PAGE - SUB MENU2 RELAYS                            
//===========================================================================================================================
void page_SubMenu2() {
  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  //print title      
  myScreen.setCursor(0, 0);
  myScreen.println(F("RELAYS"));
  myScreen.drawLine(0, 15, 320, 15, ST77XX_GREEN);
  myScreen.drawLine(0, 16, 320, 16, ST77XX_GREEN);
  myScreen.drawLine(0, 17, 320, 17, ST77XX_GREEN);
  myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  //print R-button positions
  myScreen.setCursor(0, 100); myScreen.print(F(" R0"));
  myScreen.setCursor(40, 100); myScreen.print(F(" R1")); 
  myScreen.setCursor(80, 100); myScreen.print(F(" R2")); 
  myScreen.setCursor(120, 100); myScreen.print(F(" R3")); 
  myScreen.setCursor(160, 100); myScreen.print(F(" R4")); 
  myScreen.setCursor(200, 100); myScreen.print(F(" R5")); 
  myScreen.setCursor(240, 100); myScreen.print(F(" R6")); 
  myScreen.setCursor(280, 100); myScreen.print(F(" R7"));       
}

void drawRelaysState(int number, bool state, int xPos, int yPos) {
   
  xPos = number*xPos;

  if (state == HIGH) {     
    myScreen.setCursor(xPos+10, yPos -20); //print relay state behind the selector icon(xPos+10), above or under the numbered relay    
    myScreen.setTextColor(ST77XX_BLACK, ST77XX_GREEN);
    myScreen.print(F("ON"));      
    myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);  
    myScreen.setCursor(xPos, yPos +20);       
    myScreen.print(F("   ")); 
    }
  else if (state == LOW) { 
    myScreen.setCursor(xPos, yPos +20);   // not shifting xPos to prevent the row from wrapping around to the front -> -1 even
    myScreen.setTextColor(ST77XX_BLACK, ST77XX_RED);
    myScreen.print(F("OFF"));      
    myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);  
    myScreen.setCursor(xPos+10, yPos -20);       
    myScreen.print(F("   ")); 
  }   
}

// ===========================================================================================================================
//                                              PAGE - SUB MENU 3 TIMERS                           
// ===========================================================================================================================
void page_SubMenu3() {

  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  //print title      
  myScreen.setCursor(0, 0);
  myScreen.println(F("TIMERS"));
  myScreen.drawLine(0, 15, 320, 15, ST77XX_GREEN);
  myScreen.drawLine(0, 16, 320, 16, ST77XX_GREEN);
  myScreen.drawLine(0, 17, 320, 17, ST77XX_GREEN);

}
  
// =======================================================================================
//                                        GENERIC SUB MENU 3 ITEM PAGE                            
// =======================================================================================
void page_SubMenu3_Item(uint8_t item_number) {

  uint8_t item_Pos = 1; //for menu 3

  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  //print title      
  myScreen.setCursor(0, 0);
  myScreen.println(subPage3Items[item_number]);
  myScreen.drawLine(0, 15, 320, 15, ST77XX_GREEN);
  myScreen.drawLine(0, 16, 320, 16, ST77XX_GREEN);
  myScreen.drawLine(0, 17, 320, 17, ST77XX_GREEN);  
  
}

// ===========================================================================================================================
//                                        PAGE - SUB MENU 4 SETTINGS                           
// ===========================================================================================================================
void page_SubMenu4() {
  
  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  //print title      
  myScreen.setCursor(0, 0);
  myScreen.println(F("SETTINGS"));
  myScreen.drawLine(0, 15, 320, 15, ST77XX_GREEN);
  myScreen.drawLine(0, 16, 320, 16, ST77XX_GREEN);
  myScreen.drawLine(0, 17, 320, 17, ST77XX_GREEN);

}

// =======================================================================================
//                                      GENERIC SUB MENU 4 ITEM PAGE                            
// =======================================================================================
void page_SubMenu4_Item(uint8_t item_number) { 
  
  //clear screen
  myScreen.fillScreen(ST77XX_BLACK);
  //print title      
  myScreen.setCursor(0, 0);
  myScreen.println(subPage4Items[item_number]);
  myScreen.drawLine(0, 15, 320, 15, ST77XX_GREEN);
  myScreen.drawLine(0, 16, 320, 16, ST77XX_GREEN);
  myScreen.drawLine(0, 17, 320, 17, ST77XX_GREEN);
  
}

//===========================================================================================================================
//                                       MOVING THROUGH ALL PAGES CONTENT
//===========================================================================================================================
void navigationFunctioner() {  

  bool down_was_down = false;
  bool up_was_down = false;
  bool right_was_down = false;
  bool left_was_down = false;

  if (checkJoystick() == Down) { down_was_down = true; delay(100); }
  if (checkJoystick() == Up) { up_was_down = true; delay(100); }
  if (checkJoystick() == Right) { right_was_down = true; delay(100); }
  if (checkJoystick() == Left) { left_was_down = true; delay(100); }

  if (currPage == ROOT_MENU) { //=====================================================================ROOT=================================================
    if (updateDisplay) {
      
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, 80);    
      printSelected(1, root_Pos);    myScreen.println(F("STATISTICS"));
      printSelected(2, root_Pos);    myScreen.println(F("RELAYS"));
      printSelected(3, root_Pos);    myScreen.println(F("TIMERS"));
      printSelected(4, root_Pos);    myScreen.println(F("SETTINGS"));

      updateDisplay = false;
    }

    //move the pointer down
    if (checkJoystick() == Down && down_was_down == true) {
      if (root_Pos == ROOT_MENU_CNT) {      root_Pos = 1;    } 
      else {      root_Pos++;    }
      updateDisplay = true;
      down_was_down = false;
    }

    //move the pointer Up
    if (checkJoystick() == Up && up_was_down == true) {
      if (root_Pos == 1) {      root_Pos = ROOT_MENU_CNT;    } 
      else {      root_Pos--;    }
      updateDisplay = true;
      up_was_down = false;
    }

    //move to the selected page
    if (checkJoystick() == Right && right_was_down == true) {
      switch (root_Pos) {
        case 1: currPage = SUB_MENU1; return;
        case 2: currPage = SUB_MENU2; return;
        case 3: currPage = SUB_MENU3; return;
        case 4: currPage = SUB_MENU4; return;
      }
    }
  }

  if (currPage == SUB_MENU1) { //=====================================================================SUB1 STATS===========================================
    //exit - move back to the root menu
    if (checkJoystick() == Left && left_was_down == true) { 
      currPage = ROOT_MENU; return;     
    }
  }

  if (currPage == SUB_MENU2) { //=====================================================================SUB2 RELAYS===========================================    
    if (updateDisplay){  
      //draw selector
      myScreen.setCursor(0, 100); printSelectedSmall(1, rel_Pos);
      myScreen.setCursor(40, 100); printSelectedSmall(2, rel_Pos);
      myScreen.setCursor(80, 100); printSelectedSmall(3, rel_Pos);
      myScreen.setCursor(120, 100); printSelectedSmall(4, rel_Pos);
      myScreen.setCursor(160, 100); printSelectedSmall(5, rel_Pos);
      myScreen.setCursor(200, 100); printSelectedSmall(6, rel_Pos);
      myScreen.setCursor(240, 100); printSelectedSmall(7, rel_Pos);
      myScreen.setCursor(280, 100); printSelectedSmall(8, rel_Pos);

      //draw drawRelaysState(int number, bool state, int xPos, int yPos) 10 pixels to the right of the first rel_Pos
      for (int i = 0; i < numberOfRelays; i++) drawRelaysState(i, digitalRead(relay[i]), 40, 100);  
        
      if (rel_Pos == 1) {myScreen.setCursor(10, 150); myScreen.print(F("LIGHTS        "));}
      if (rel_Pos == 2) {myScreen.setCursor(10, 150); myScreen.print(F("FANS          "));}
      if (rel_Pos == 3) {myScreen.setCursor(10, 150); myScreen.print(F("RAIN          "));}
      if (rel_Pos == 4) {myScreen.setCursor(10, 150); myScreen.print(F("BACKWALL DRIP "));}
      if (rel_Pos == 5) {myScreen.setCursor(10, 150); myScreen.print(F("FOGGER        "));}
      if (rel_Pos == 6) {myScreen.setCursor(10, 150); myScreen.print(F("LAND HEATING  "));}
      if (rel_Pos == 7) {myScreen.setCursor(10, 150); myScreen.print(F("WINDOW HEATING"));}
      if (rel_Pos == 8) {myScreen.setCursor(10, 150); myScreen.print(F("AIRPUMP       "));}            
      // clear the update flag    
      updateDisplay = false;
    }
    //move the pointer right
    if (checkJoystick() == Right && right_was_down == true) {
      if (rel_Pos == SUB_MENU2_CNT) {rel_Pos = 1;} 
      else {rel_Pos++;}
      updateDisplay = true;
      right_was_down = false;
    }

    //move the pointer left / back to main menu
    if (checkJoystick() == Left && left_was_down == true) {
      if (rel_Pos == 1) { currPage = ROOT_MENU; return; } 
      else {rel_Pos--;}
      updateDisplay = true;
      left_was_down = false;      
    }
    
    //switch selected relay on
    if (checkJoystick() == Up && up_was_down == true) {
      if (digitalRead(relay[rel_Pos-1]) == LOW) { //first position in array in 0, so rel_Pos -1      
        digitalWrite(relay[rel_Pos-1], HIGH); }
      updateDisplay = true;
      up_was_down = false;
    }

    //switch selected relay off
    if (checkJoystick() == Down && down_was_down == true) {
      if (digitalRead(relay[rel_Pos-1]) == HIGH) {
        digitalWrite(relay[rel_Pos-1], LOW); }
      updateDisplay = true;
      down_was_down = false;
    }
  }

  if (currPage == SUB_MENU3) { //=====================================================================SUB3 TIMERS===========================================
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, 30);
      printSelected(1, sub_Pos_3); myScreen.println(F("LIGHTS"));
      printSelected(2, sub_Pos_3); myScreen.println(F("RAIN"));
      printSelected(3, sub_Pos_3); myScreen.println(F("BACKWALL DRIP"));
      printSelected(4, sub_Pos_3); myScreen.println(F("FOGGER"));
      printSelected(5, sub_Pos_3); myScreen.println(F("AIR PUMP"));
      printSelected(6, sub_Pos_3); myScreen.println(F("SYNC TIME"));
      // clear the update flag
      updateDisplay = false;
    }

    //move the pointer down
    if (checkJoystick() == Down && down_was_down == true) {
      if (sub_Pos_3 == SUB_MENU3_CNT) {sub_Pos_3 = 1;} else {sub_Pos_3++;}
      updateDisplay = true;
      down_was_down = false;
    }

    //move the pointer Up
    if (checkJoystick() == Up && up_was_down == true) {
      if (sub_Pos_3 == 1) {sub_Pos_3 = SUB_MENU3_CNT;} else {sub_Pos_3--;}
      updateDisplay = true;
      up_was_down = false;
    }

    //move to the selected page
    if (checkJoystick() == Right && right_was_down == true) {
      switch (sub_Pos_3) {
        case 1: currPage = SUB_MENU3_ITEM1; return;
        case 2: currPage = SUB_MENU3_ITEM2; return;
        case 3: currPage = SUB_MENU3_ITEM3; return;
        case 4: currPage = SUB_MENU3_ITEM4; return;
        case 5: currPage = SUB_MENU3_ITEM5; return;
        case 6: setRTC = false; return; //manually tell the RTC to re-sync
      }
    }
    //move back to the root menu
    if (checkJoystick() == Left && left_was_down == true) {
       currPage = ROOT_MENU; return;     
    }
  }
  
  if (currPage == SUB_MENU3_ITEM1) { //================================SUB3 ITEM 1 MAIN LIGHTS===============================================
    
    int xPos1 = 30, yPos1 = 40, yPos2 = 80, offset1 = 80;
    
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_3_1); printTimer(1, xPos1, yPos1, 0, false);  myScreen.print(" START");  //printTimer(int number, int xPos, int yPos, int eepromNumber, bool printDuration)
      myScreen.setCursor(0, yPos2);
      printSelected(2, sub_Pos_3_1); printTimer(2, xPos1, yPos2, 2, false);  myScreen.print(" STOP");
      // clear the update flag
      updateDisplay = false;
    }  
        
    if (checkJoystick() == Down && down_was_down == true) { //move the pointer down
      if (sub_Pos_3_1 == 1) {sub_Pos_3_1 = 2;}
      updateDisplay = true;
      down_was_down = false;
    }
   
    if (checkJoystick() == Up && up_was_down == true) {  //move the pointer Up
      if (sub_Pos_3_1 == 2) {sub_Pos_3_1 = 1;} 
      updateDisplay = true;
      up_was_down = false;
    }

    if (sub_Pos_3_1 == 1 && checkJoystick() == Right && right_was_down == true) { // change ON timer
      //outputDebugln("startChangeTime ON timer");
      startChangeTime(0, 1, xPos1 + offset1, yPos1); // startChangeTime(int eepromHour, int eepromMinutes, int xPos, int yPos)
      right_was_down = false;
    }
    
    if (sub_Pos_3_1 == 2 && checkJoystick() == Right && right_was_down == true) { // change OFF timer
      //outputDebugln("startChangeTime OFF timer");
      startChangeTime(2, 3, xPos1 + offset1, yPos2); 
      right_was_down = false;
    }
    
    //move back to the root menu
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU3;
      return;
    }

  }

  if (currPage == SUB_MENU3_ITEM2) { //=================================SUB3 ITEM 2 SPRINKLERS================================================
    
    int xPos1 = 30, yPos1 = 40, yPos2 = 70, yPos3 = 100, yPos4 = 130, yPos5 = 160, offset1 = 80, offset2 = 260; 

    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_3_2); printTimer(1, xPos1, yPos1, 4, true); //printTimers(int number, int xPos, int yPos, int eepromNumber, bool printDuration)
      myScreen.setCursor(0, yPos2);
      printSelected(2, sub_Pos_3_2); printTimer(2, xPos1, yPos2, 8, true);  
      myScreen.setCursor(0, yPos3);
      printSelected(3, sub_Pos_3_2); printTimer(3, xPos1, yPos3, 12, true); 
      myScreen.setCursor(0, yPos4);
      printSelected(4, sub_Pos_3_2); printTimer(4, xPos1, yPos4, 16, true); 
      myScreen.setCursor(0, yPos5);
      printSelected(5, sub_Pos_3_2); printTimer(5, xPos1, yPos5, 20, true); 
      // clear the update flag
      updateDisplay = false;
    }  
            
    if (checkJoystick() == Down && down_was_down == true) { //move the pointer down
      if (sub_Pos_3_2 == numberOfTimers) {sub_Pos_3_2 = 1;} else {sub_Pos_3_2++;}
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { //move the pointer Up
      if (sub_Pos_3_2 == 1) {sub_Pos_3_2 = numberOfTimers;} else {sub_Pos_3_2--;}
      updateDisplay = true;
      up_was_down = false;
    }

    if (sub_Pos_3_2 == 1 && checkJoystick() == Right && right_was_down == true) { // change timer 1 start and duration
      startChangeTime(4, 5, xPos1 + offset1, yPos1); // startChangeTime(int eepromHour, int eepromMinutes, int xPos, int yPos)
      startChangeSeconds(6, xPos1 + offset2, yPos1); // startChangeSeconds(int eepromSeconds, int xPos, int yPos)
      right_was_down = false;
    }    
    if (sub_Pos_3_2 == 2 && checkJoystick() == Right && right_was_down == true) {
      startChangeTime(8, 9, xPos1 + offset1, yPos2);
      startChangeSeconds(10, xPos1 + offset2, yPos2); 
      right_was_down = false;
    }
    if (sub_Pos_3_2 == 3 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(12, 13, xPos1 + offset1, yPos3);
      startChangeSeconds(14, xPos1 + offset2, yPos3); 
      right_was_down = false;
    }
    if (sub_Pos_3_2 == 4 && checkJoystick() == Right && right_was_down == true) {
      startChangeTime(16, 17, xPos1 + offset1, yPos4);
      startChangeSeconds(18, xPos1 + offset2, yPos4); 
      right_was_down = false;
    }
    if (sub_Pos_3_2 == 5 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(20, 21, xPos1 + offset1, yPos5);
      startChangeSeconds(22, xPos1 + offset2, yPos5); 
      right_was_down = false;
    }
    
    //move back to the root menu
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU3;
      return;
    }
  }

  if (currPage == SUB_MENU3_ITEM3) { //=================================SUB3 ITEM 3 BACKWALL DRIP=============================================

    int xPos1 = 30, yPos1 = 40, yPos2 = 80, offset1 = 80, offset2 = 260;

    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_3_3); printTimer(1, xPos1, yPos1, 24, true); 
      myScreen.setCursor(0, yPos2);
      printSelected(2, sub_Pos_3_3); printTimer(2, xPos1, yPos2, 28, true); 
      // clear the update flag
      updateDisplay = false;
    }  
            
    if (checkJoystick() == Down && down_was_down == true) { 
      if (sub_Pos_3_3 == 2) {sub_Pos_3_3 = 1;} else {sub_Pos_3_3++;}
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { 
      if (sub_Pos_3_3 == 1) {sub_Pos_3_3 = 2;} else {sub_Pos_3_3--;}
      updateDisplay = true;
      up_was_down = false;
    }

    if (sub_Pos_3_3 == 1 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(24, 25, xPos1 + offset1 , yPos1); 
      startChangeSeconds(26, xPos1 + offset2, yPos1); 
      right_was_down = false;
    }    
    if (sub_Pos_3_3 == 2 && checkJoystick() == Right && right_was_down == true) {
      startChangeTime(28, 29, xPos1 + offset1 , yPos2);
      startChangeSeconds(30, xPos1 + offset2, yPos2); 
      right_was_down = false;
    }
    
    //move back to the root menu
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU3;
      return;
    }
  }

  if (currPage == SUB_MENU3_ITEM4) { //=================================SUB3 ITEM 4 FOGGER====================================================
    
    int xPos1 = 30, yPos1 = 40, yPos2 = 80, offset1 = 80;
    
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_3_4); printTimer(1, xPos1, yPos1, 32, false);  myScreen.print(" START");
      myScreen.setCursor(0, yPos2);
      printSelected(2, sub_Pos_3_4); printTimer(2, xPos1, yPos2, 34, false);  myScreen.print(" STOP");
      // clear the update flag
      updateDisplay = false;
    }  
        
    if (checkJoystick() == Down && down_was_down == true) {
      if (sub_Pos_3_4 == 1) {sub_Pos_3_4 = 2;}
      updateDisplay = true;
      down_was_down = false;
    }
   
    if (checkJoystick() == Up && up_was_down == true) {
      if (sub_Pos_3_4 == 2) {sub_Pos_3_4 = 1;} 
      updateDisplay = true;
      up_was_down = false;
    }

    if (sub_Pos_3_4 == 1 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(32, 33, xPos1 + offset1, yPos1); 
      right_was_down = false;
    }
    
    if (sub_Pos_3_4 == 2 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(34, 35, xPos1 + offset1, yPos2); 
      right_was_down = false;
    }
    
    //move back to the root menu
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU3;
      return;
    }
  }

  if (currPage == SUB_MENU3_ITEM5) { //=================================SUB3 ITEM 5 AIR PUMP==================================================
    
    int xPos1 = 30, yPos1 = 40, yPos2 = 70, yPos3 = 100, yPos4 = 130, yPos5 = 160, offset1 = 80, offset2 = 260; 

    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_3_5); printTimer(1, xPos1, yPos1, 38, true); 
      myScreen.setCursor(0, yPos2);
      printSelected(2, sub_Pos_3_5); printTimer(2, xPos1, yPos2, 42, true);  
      myScreen.setCursor(0, yPos3);
      printSelected(3, sub_Pos_3_5); printTimer(3, xPos1, yPos3, 46, true); 
      myScreen.setCursor(0, yPos4);
      printSelected(4, sub_Pos_3_5); printTimer(4, xPos1, yPos4, 50, true); 
      myScreen.setCursor(0, yPos5);
      printSelected(5, sub_Pos_3_5); printTimer(5, xPos1, yPos5, 54, true); 
      // clear the update flag
      updateDisplay = false;
    }  
            
    if (checkJoystick() == Down && down_was_down == true) { 
      if (sub_Pos_3_5 == numberOfTimers) {sub_Pos_3_5 = 1;} else {sub_Pos_3_5++;}
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { 
      if (sub_Pos_3_5 == 1) {sub_Pos_3_5 = numberOfTimers;} else {sub_Pos_3_5--;}
      updateDisplay = true;
      up_was_down = false;
    }

    if (sub_Pos_3_5 == 1 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(38, 39, xPos1 + offset1 , yPos1);
      startChangeSeconds(40, xPos1 + offset2, yPos1); //duration of seconds can be turned into minutes in on/off function *60
      right_was_down = false;
    }    
    if (sub_Pos_3_5 == 2 && checkJoystick() == Right && right_was_down == true) {
      startChangeTime(42, 43, xPos1 + offset1 , yPos2);
      startChangeSeconds(44, xPos1 + offset2, yPos2); 
      right_was_down = false;
    }
    if (sub_Pos_3_5 == 3 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(46, 47, xPos1 + offset1 , yPos3);
      startChangeSeconds(48, xPos1 + offset2, yPos3); 
      right_was_down = false;
    }
    if (sub_Pos_3_5 == 4 && checkJoystick() == Right && right_was_down == true) {
      startChangeTime(50, 51, xPos1 + offset1 , yPos4);
      startChangeSeconds(52, xPos1 + offset2, yPos4); 
      right_was_down = false;
    }
    if (sub_Pos_3_5 == 5 && checkJoystick() == Right && right_was_down == true) { 
      startChangeTime(54, 55, xPos1 + offset1 , yPos5);
      startChangeSeconds(56, xPos1 + offset2, yPos5); 
      right_was_down = false;
    }
    
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU3;
      return;
    }
  }

  if (currPage == SUB_MENU4) { //=====================================================================SUB4=================================================
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, 30);
      printSelected(1, sub_Pos_4); myScreen.println(F("SET TEMPERATURE"));
      printSelected(2, sub_Pos_4); myScreen.println(F("SET HUMIDITY"));
      printSelected(3, sub_Pos_4); myScreen.println(F("SET LAND TEMPERATURE"));
      printSelected(4, sub_Pos_4); myScreen.println(F("SET RAINSTORM"));
      myScreen.println();
      printSelected(5, sub_Pos_4); myScreen.println(F("RESET CRASH COUNTER"));
      // clear the update flag
      updateDisplay = false;
    }

    //move the pointer down
    if (checkJoystick() == Down && down_was_down == true) {
      if (sub_Pos_4 == SUB_MENU4_CNT) {sub_Pos_4 = 1;} else {sub_Pos_4++;}
      updateDisplay = true;
      down_was_down = false;
    }

    //move the pointer Up
    if (checkJoystick() == Up && up_was_down == true) {
      if (sub_Pos_4 == 1) {sub_Pos_4 = SUB_MENU4_CNT;} else {sub_Pos_4--;}
      updateDisplay = true;
      up_was_down = false;
    }

    //move to the selected page
    if (checkJoystick() == Right && right_was_down == true) {
      switch (sub_Pos_4) {
        case 1: currPage = SUB_MENU4_ITEM1; return;
        case 2: currPage = SUB_MENU4_ITEM2; return;
        case 3: currPage = SUB_MENU4_ITEM3; return;
        case 4: currPage = SUB_MENU4_ITEM4; return;
        case 5: currPage = SUB_MENU4_ITEM5; return;
      }
    }
    //move back to the root menu
    if (checkJoystick() == Left && left_was_down == true) {
       currPage = ROOT_MENU; return;     
    }
  }
  
  if (currPage == SUB_MENU4_ITEM1) { //===================SUB4 ITEM 1 SET TEMPERATURE====================================
    
    int xPos1 = 30, xPos2 = 160, yPos1 = 40, yPos2 = 80, yPos3 = 120;
    
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_4_1); printMinMax(xPos1, yPos1, 60, "MIN TEMPERATURE ", "\367");  //printMinMax(int xPos, int yPos, int eepromNumber, const char* txt, const char* txt2)
      myScreen.setCursor(0, yPos2); 
      printSelected(2, sub_Pos_4_1); printMinMax(xPos1, yPos2, 61, "MAX TEMPERATURE ", "\367");
      myScreen.setCursor(0, yPos3); 
      printSelected(3, sub_Pos_4_1); printMinMax(xPos1, yPos3, 70, "IDEAL ", "\367"); 
      myScreen.setCursor(xPos2, yPos3); myScreen.print("Mod: "); myScreen.print(tempModifier, 2);
      // clear the update flag
      updateDisplay = false;
    }  
        
    if (checkJoystick() == Down && down_was_down == true) { 
      if (sub_Pos_4_1 == 3) {sub_Pos_4_1 = 1;} else {sub_Pos_4_1++;}
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { 
      if (sub_Pos_4_1 == 1) {sub_Pos_4_1 = 3;} else {sub_Pos_4_1--;}
      updateDisplay = true;
      up_was_down = false;
    }

    if (sub_Pos_4_1 == 1 && checkJoystick() == Right && right_was_down == true) { //min temp
      setValue(xPos1, yPos1, 10, 25, 60, 190); //setValue(int xPos, int yPos, int minValue, int maxValue, int eepromNumber, int shiftToRight)
      right_was_down = false;
    }
    
    if (sub_Pos_4_1 == 2 && checkJoystick() == Right && right_was_down == true) { //max temp
      setValue(xPos1, yPos2, 20, 35, 61, 190); 
      right_was_down = false;
    }

    if (sub_Pos_4_1 == 3 && checkJoystick() == Right && right_was_down == true) { //ideal temp
      setValue(xPos1, yPos3, 10, 35, 70, 70); 
      right_was_down = false;
    }
    
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU4;
      return;
    }
  }

  if (currPage == SUB_MENU4_ITEM2) { //===================SUB4 ITEM 2 SET HUMIDITY=======================================
    
    int xPos1 = 30, xPos2 = 160, yPos1 = 40, yPos2 = 80, yPos3 = 120;
    
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_4_2); printMinMax(xPos1, yPos1, 62, "MIN HUMIDITY ", "%");  //printMinMax(int xPos, int yPos, int eepromNumber, const char* txt, const char* txt2)
      myScreen.setCursor(0, yPos2); 
      printSelected(2, sub_Pos_4_2); printMinMax(xPos1, yPos2, 63, "MAX HUMIDITY ", "%");
      myScreen.setCursor(0, yPos3); 
      printSelected(3, sub_Pos_4_2); printMinMax(xPos1, yPos3, 71, "IDEAL ", "%");
      myScreen.setCursor(xPos2, yPos3); myScreen.print("Mod: "); myScreen.print(humModifier, 2);
      // clear the update flag
      updateDisplay = false;
    }  
        
    if (checkJoystick() == Down && down_was_down == true) { 
      if (sub_Pos_4_2 == 3) {sub_Pos_4_2 = 1;} else {sub_Pos_4_2++;}
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { 
      if (sub_Pos_4_2 == 1) {sub_Pos_4_2 = 3;} else {sub_Pos_4_2--;}
      updateDisplay = true;
      up_was_down = false;
    }

    if (sub_Pos_4_2 == 1 && checkJoystick() == Right && right_was_down == true) { //min humidity
      setValue(xPos1, yPos1, 40, 70, 62, 155); //setValue(int xPos, int yPos, int minValue, int maxValue, int eepromNumber, int shiftToRight)
      right_was_down = false;
    }
    
    if (sub_Pos_4_2 == 2 && checkJoystick() == Right && right_was_down == true) { //max humidity
      setValue(xPos1, yPos2, 50, 95, 63, 155); 
      right_was_down = false;
    }

    if (sub_Pos_4_2 == 3 && checkJoystick() == Right && right_was_down == true) { //ideal humidity
      setValue(xPos1, yPos3, 50, 90, 71, 70); 
      right_was_down = false;
    }
    
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU4;
      return;
    }
  }

  if (currPage == SUB_MENU4_ITEM3) { //===================SUB4 ITEM 3 SET LAND TEMPERATURE===============================
    
    int xPos1 = 30, yPos1 = 40, yPos2 = 80;
    
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_4_3); printMinMax(xPos1, yPos1, 64, "DAY TEMPERATURE ", "\367");  //printMinMax(int xPos, int yPos, int eepromNumber, const char* txt, const char* txt2)
      myScreen.setCursor(0, yPos2); 
      printSelected(2, sub_Pos_4_3); printMinMax(xPos1, yPos2, 65, "NIGHT TEMPERATURE ", "\367");
      // clear the update flag
      updateDisplay = false;
    }  

    if (checkJoystick() == Down && down_was_down == true) { 
      if (sub_Pos_4_3 == 1) {sub_Pos_4_3 = 2;} 
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { 
      if (sub_Pos_4_3 == 2) {sub_Pos_4_3 = 1;} 
      updateDisplay = true;
      up_was_down = false;
    }       

    if (sub_Pos_4_3 == 1 && checkJoystick() == Right && right_was_down == true) { //day temp
      setValue(xPos1, yPos1, 10, 25, 64, 190); //setValue(int xPos, int yPos, int minValue, int maxValue, int eepromNumber, int shiftToRight)
      right_was_down = false;
    }

    if (sub_Pos_4_3 == 2 && checkJoystick() == Right && right_was_down == true) { //night temp
      setValue(xPos1, yPos2, 10, 20, 65, 220); 
      right_was_down = false;
    }    
    
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU4;
      return;
    }
  }

  if (currPage == SUB_MENU4_ITEM4) { //===================SUB4 ITEM 4 SET RAINSTORM======================================
    
    int xPos1 = 30, yPos1 = 40, yPos2 = 80, yPos3 = 120, yPos4 = 160;
    
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_4_4); printMinMax(xPos1, yPos1, 66, "MAX NUMBER ", "");  //printMinMax(int xPos, int yPos, int eepromNumber, const char* txt, const char* txt2)
      myScreen.setCursor(0, yPos2); 
      printSelected(2, sub_Pos_4_4); printMinMax(xPos1, yPos2, 67, "DURATION ", "");
      myScreen.setCursor(0, yPos3); 
      printSelected(3, sub_Pos_4_4); printMinMax(xPos1, yPos3, 68, "VENT PAUSE ", "");
      myScreen.setCursor(0, yPos4); 
      printSelected(4, sub_Pos_4_4); myScreen.print("RAINY DAY: "); if (rainyDay) { myScreen.print("TRUE "); } else { myScreen.print("FALSE"); }
      // clear the update flag
      updateDisplay = false;
    }  

    if (checkJoystick() == Down && down_was_down == true) { 
      if (sub_Pos_4_4 == 4) {sub_Pos_4_4 = 1;} else {sub_Pos_4_4++;}
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { 
      if (sub_Pos_4_4 == 1) {sub_Pos_4_4 = 4;} else {sub_Pos_4_4--;}
      updateDisplay = true;
      up_was_down = false;
    }     

    if (sub_Pos_4_4 == 1 && checkJoystick() == Right && right_was_down == true) { 
      setValue(xPos1, yPos1, 1, 5, 66, 130); //setValue(int xPos, int yPos, int minValue, int maxValue, int eepromNumber, int shiftToRight)
      right_was_down = false;
    }
    
    if (sub_Pos_4_4 == 2 && checkJoystick() == Right && right_was_down == true) { 
      setValue(xPos1, yPos2, 10, 60, 67, 110); 
      right_was_down = false;
    }    

    if (sub_Pos_4_4 == 3 && checkJoystick() == Right && right_was_down == true) { 
      setValue(xPos1, yPos3, 10, 120, 68, 130); 
      right_was_down = false;
    }    

    if (sub_Pos_4_4 == 4 && checkJoystick() == Right && right_was_down == true) { 
      rainyDay = !rainyDay;      
      updateDisplay = true;
      right_was_down = false;
    }
    
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU4;
      return;
    }
  }  

  if (currPage == SUB_MENU4_ITEM5) { //===================SUB4 ITEM 5 RESET CRASHCOUNTER=================================
    
    int xPos1 = 30, yPos1 = 40, yPos2 = 80;
    
    if (updateDisplay){      
      // print the items
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(0, yPos1);
      printSelected(1, sub_Pos_4_5); myScreen.print("CRASH COUNTER = "); myScreen.print(EEPROM.readByte(100)); myScreen.print("  ");
      myScreen.setCursor(0, yPos2); 
      myScreen.setTextColor(ST77XX_RED);
      printSelected(2, sub_Pos_4_5); myScreen.print("CONFIRM RESET");
      myScreen.setTextColor(ST77XX_WHITE);
      // clear the update flag
      updateDisplay = false;
    }  

    if (checkJoystick() == Down && down_was_down == true) { 
      if (sub_Pos_4_5 == 1) {sub_Pos_4_5 = 2;} 
      updateDisplay = true;
      down_was_down = false;
    }
    
    if (checkJoystick() == Up && up_was_down == true) { 
      if (sub_Pos_4_5 == 2) {sub_Pos_4_5 = 1;} 
      updateDisplay = true;
      up_was_down = false;
    }     
    
    if (sub_Pos_4_5 == 2 && checkJoystick() == Right && right_was_down == true) { 
      EEPROM.writeByte(100, 0);
      updateDisplay = true;
      right_was_down = false;
    }    
    
    if (checkJoystick() == Left && left_was_down == true) {
      currPage = SUB_MENU4;
      return;
    }
  }    

}

void printSelected(uint8_t p1, uint8_t p2) {
  if (p1 == p2) { myScreen.print(F(" > ")); } 
  else { myScreen.print(F("   ")); }
}
void printSelectedSmall(uint8_t p1, uint8_t p2) {
  if (p1 == p2) { myScreen.print(F(">")); } 
  else { myScreen.print(F(" ")); }
} 

//===========================================================================================================================
//                                             AUTO GO BACK TO HOMEPAGE
//===========================================================================================================================
void autoGoBackToHomePage() {
  unsigned long currentMillis = millis();  //call current millis

  if (checkJoystick() != 0) {  // Reset millis counter if joystick is moved
    previousMillis = currentMillis;
  }
  if (currPage != SCREEN_SAVER) {
    if (currPage != ROOT_MENU && currentMillis - previousMillis > 60 * 1000) {  //1 minute
      previousMillis = currentMillis;  //replace previous millis by current millis as new start point
      currPage = ROOT_MENU;
    }
  }
}

void screenSaver() {
  unsigned long currentMillis2 = millis();  

  if (checkJoystick() != 0) {  // Reset millis counter if joystick is moved
    previousMillis2 = currentMillis2;
  }
  if (currPage == ROOT_MENU && currentMillis2 - previousMillis2 > 300 * 1000) {  //after 5 minutes of no movement
    previousMillis2 = currentMillis2; 
    currPage = SCREEN_SAVER;  
    outputDebugln("Screensaver activated");
  }
}

void activateScreen() {

  if (currPage == SCREEN_SAVER && checkJoystick() != 0) {  //turn back on when joystick is moved
    currPage = ROOT_MENU;
    outputDebugln("Screen reactivated");
  }
}

//===========================================================================================================================
//                                                     CLIMATE CONTROL
//===========================================================================================================================
void climateSettings() {
  
  //adjust settings for seasons with multiMap arrays. 
  //date[] is the 'x-axis' and the xModifier is the 'y-axis' for a graph to calculate all points based on weather data from Costa Rica      
  float date[13] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; //months. 13 is added so month 12 can have 'days'
  float tempertureModifier[13] = {0.96, 0.92, 0.92, 0.92, 0.92, 0.96, 1.12, 1.04, 1, 0.96, 0.92, 0.96, 0.96}; //based on 25 degrees celsius
  float humidityModifier[13] = {1.2, 1.15, 1.09, 1.04, 1.01, 1, 0.97, 1, 1.13, 1.2, 1.07, 1.13, 1.2}; //based on 75% RH --- depending on sensor positions you might need to change the base
  float today = getDay() * (1/32) + getMonth(); //today is number of the month plus a fraction for each day, filling out the date[] graph axis

  humidityVariation = multiMap(today, date, humidityModifier, 13);
  temperatureVariation = multiMap(today, date, tempertureModifier, 13);
  
  //adjust settings for daytime  
  if (getLocalHour() == 13 || getLocalHour() == 14) { //midday
    daytimeHumVariation = 0.9; 
    daytimeTempVariation = 1.1;
  } else if (getLocalHour() >= 23 || getLocalHour() <= 7) { //night
    daytimeHumVariation = 1.25; 
    daytimeTempVariation = 0.8;
  } else {
    daytimeHumVariation = 1; 
    daytimeTempVariation = 1;
  }

  //adjust settings for weather outside
  if (rainyDay) {
    weatherVariation = 1.1; //10% more humidity
  } else {    
    weatherVariation = 1;
  }

  humModifier = humidityVariation * daytimeHumVariation * weatherVariation;
  tempModifier = temperatureVariation * daytimeTempVariation;

  //everything times 10 for better resolution
  minAirTemp = EEPROM.readByte(60) * 10;
  maxAirTemp = EEPROM.readByte(61) * 10;
  minHumidity = EEPROM.readByte(62) * 10;
  maxHumidity = EEPROM.readByte(63) * 10;
  dayWaterTemp = EEPROM.readByte(dayTemperature) * 10;
  nightWaterTemp = EEPROM.readByte(nightTemperature) * 10;
  if (!airTransition) { //if the value changed due to a modifier or user change it will start to transition. While transitioning don't reset the value
    desiredAirTemp = (EEPROM.readByte(desiredTemp) * 10) * tempModifier;
  }
  if (!humTransition) {
    desiredHumidity = (EEPROM.readByte(desiredHumi) * 10) * humModifier;  
  }
  if (desiredAirTemp < (waterTemp - hysteresis / 4)) { //cannot cool lower that outside temperature. For now waterTemp = external airTemp
    desiredAirTemp = waterTemp - hysteresis / 4; //compensate for hysteresis upper limit 
  }
  airHigh = desiredAirTemp + (hysteresis / 2);  
  airLow = desiredAirTemp - (hysteresis / 2); 
  humidityHigh = desiredHumidity + hysteresis;
  humidityLow = desiredHumidity - hysteresis;    
  /*
  outputDebug("Temperature modifier: ");
  outputDebugln(tempModifier);
  outputDebug("Humidity modifier: ");
  outputDebugln(humModifier);
  */
}

void climateControl() {  

  airTemp = airTempAverage * 10; //all average readings have 1 decimal number. Scale up by 10 to work with ints
  humidity = humidityAverage * 10;
  waterTemp = waterTempAverage * 10;

  if (dayTime) { //day schedule

    hold = false;

    if (humidity >= maxHumidity) { //disable sprinklers
      noRain = true;
    } else { noRain = false; }

    if (waterTemp - 10 < dayWaterTemp) {
      digitalWrite(relay[5], HIGH);  //land heat on
    }
    if (waterTemp >= dayWaterTemp) {
      digitalWrite(relay[5], LOW);   //land heat off
    }
    
    //if all conditions are within half the hysteresis margins, do nothing
    if (airTemp <= (desiredAirTemp + hysteresis / 4) && airTemp >= (desiredAirTemp - hysteresis / 4) && humidity <= (desiredHumidity + hysteresis / 2) && humidity >= (desiredHumidity - hysteresis / 2)) {
      climateState = CASE0;
    }
    
    if (humidityMonitoring) {
      if (airTemp > airHigh && humidity > humidityHigh) {
        climateState = CASE1;
      }

      else if (airTemp < airLow && humidity < humidityLow) {
        hold = true;
        climateState = CASE2;
      }

      else if (airTemp > airHigh && humidity < humidityLow) {
        hold = true;  // prevent other singular trigger
        climateState = CASE3;
      }

      else if (airTemp < airLow && humidity > humidityHigh) {
        hold = true;
        climateState = CASE4;
      }
    }

    if (!hold) {
      if (humidity > humidityHigh && humidityMonitoring) {
        climateState = CASE5;
      }    

      else if (airTemp > airHigh) {
        climateState = CASE6;
      }

      else if (humidity < humidityLow) {
        climateState = CASE7;
      }

      else if (airTemp < airLow) {
        climateState = CASE8;
      }
    }
  }

  if (!dayTime) { //night schedule

    if (waterTemp - 10 < nightWaterTemp) {
      digitalWrite(relay[5], HIGH);  //land heat on
    }
    if (waterTemp >= nightWaterTemp) {
      digitalWrite(relay[5], LOW);   //land heat off
    }
    
    if (humidity >= humidityLow && humidity <= humidityHigh) { 
      climateState = CASE0; 
    }
    
    else if (airTemp > airHigh) { //cannot run until AirCo
      //climateState = CASE9; //pushes humidity too low in summer
    }
    else if (humidity < humidityLow) {
      climateState = CASE10;
    }

    else if (airTemp < minAirTemp) {
      climateState = CASE11;
    }

    else if (humidity > (970)) { //don't let it get 100%
      climateState = CASE12;
    }
  }
}

void climateChange() {  //it's real

  airTemp = airTempAverage * 10; //all average readings have 1 decimal number
  humidity = humidityAverage * 10;
  waterTemp = waterTempAverage * 10;  
  
  //If desiredAirTemp has a new value, start a transition from the old to the new
  if (desiredAirTemp != oldAirTemp && !airTransition) { //If the program sets a new target temperature, start to transition gradually.      
    airStartVal = oldAirTemp;
    airEndVal = desiredAirTemp;
    airTransition = true;    
  } 
  if (airTransition) {
    desiredAirTemp = smoothTransition(airStartVal, airEndVal, 40, 15000, 1, updateTemp); //In this case 40 intervals of 15 seconds. 1 for temp 0 for humidity
    if (desiredAirTemp == airEndVal) {
      oldAirTemp = desiredAirTemp;   
      airStep = 0;  //reset step for next transition  
      airTransition = false; //exit transition phase
    }
  }

  if (desiredHumidity != oldHumidity && !humTransition) {       
    humStartVal = oldHumidity;
    humEndVal = desiredHumidity;
    humTransition = true;    
  } 
  if (humTransition) {
    desiredHumidity = smoothTransition(humStartVal, humEndVal, 40, 15000, 0, updateHum); 
    if (desiredHumidity == humEndVal) {
      oldHumidity = desiredHumidity;
      humStep = 0;
      humTransition = false;    
    }
  }
  
  if (climateState != previousClimateState) {
    switch (climateState) {

      case CASE0:                      //default do nothing
        digitalWrite(relay[1], LOW);  //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1); 
        digitalWrite(relay[6], LOW);  //window heating off --- ??? maybe not?
        if (humidityMonitoring) {
          digitalWrite(relay[4], LOW);  //fogger off
        }
        outputDebugln("CASE 0");
        break;

      case CASE1:                      //decrease temperature and decrease humidity
        digitalWrite(relay[1], HIGH);  //fans on
        digitalWrite(relay[6], LOW);   //window heating off
        digitalWrite(relay[4], LOW);   //fogger off
        outputDebugln("CASE 1 airTemp high & humidity high --- fans on max");
        startup = 0;
        startDelay = millis();
        break;

      case CASE2:                      //increase temperature and increase humidity
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[4], HIGH);  //fogger on
        digitalWrite(relay[6], HIGH);  //window heating on
        outputDebugln("CASE 2 airTemp low & humidity low --- heating on & fogger on");
        break;

      case CASE3:                      //decrease temperature and increase humidity
        digitalWrite(relay[6], LOW);  //window heating off
        //running sequence below
        outputDebugln("CASE 3 airTemp high & humidity low --- fans off & sprinklers on");
        break;

      case CASE4:                      //increase temperature and decrease humidity
        digitalWrite(relay[6], HIGH);  //heating on
        digitalWrite(relay[1], HIGH);  //fans low        
        pwmVal1 = 60;                  //low state --- needs testing --- 23% is lowest setting for current fans to start moving
        analogWrite(pwmPin_1, pwmVal1);  
        outputDebugln("CASE 4 airTemp low && humidity high --- heating on & fans low");         
        break;

      //single states
      case CASE5:                      //decrease humidity
        if (humidityMonitoring) {
          digitalWrite(relay[4], LOW); //fogger off
          digitalWrite(relay[1], HIGH);//fans on
          startup = 0;
          startDelay = millis();
        }
        outputDebugln("CASE 5 humidity high --- fans on");
        break;

      case CASE6:                      //decrease temperature
        digitalWrite(relay[6], LOW);   //window heating off
        digitalWrite(relay[1], HIGH);  //fans on
        outputDebugln("CASE 6 airTemp high --- fans on");
        startup = 0;
        startDelay = millis();
        break;

      case CASE7:                      //increase humidity
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[4], HIGH);  //fogger on
        outputDebugln("CASE 7 humidity low --- fogger on");
        break;

      case CASE8:                      //increase temperature
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[6], HIGH);  //window heat on
        outputDebugln("CASE 8 airTemp low --- heat on");
        break;

      //night
      case CASE9:                      //decrease temperature
        digitalWrite(relay[6], LOW);   //window heating off
        digitalWrite(relay[1], HIGH);  //fans on
        outputDebugln("CASE 9 airTemp high --- fans on");
        startup = 0;
        startDelay = millis();
        break;

      case CASE10:                     //increase humidity
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[4], HIGH);  //fogger on
        outputDebugln("CASE 10 humidity low --- fogger on");
        break;

      case CASE11:                     //increase temperature
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[6], HIGH);  //window heat on
        outputDebugln("CASE 11 airTemp low --- heat on");
        break;

      case CASE12:                     //decrease humidity
        digitalWrite(relay[4], LOW);   //fogger off
        digitalWrite(relay[1], HIGH);  //fans on
        outputDebugln("CASE 12 humidity max --- fans on");
        startup = 0;
        startDelay = millis();
        break;
    }
    previousClimateState = climateState;
  }

  if (climateState == CASE1) {      //decrease temperature and decrease humidity
    fanSpeedController(2);    
  } 

  else if (climateState == CASE3) {  //decrease temperature and increase humidity
    //turn on sprinklers and wait a bit for endothermic evaporation, then turn on fans again. This event is unlikely to happen.
    static byte state = 0;  //at start go to state 0 with ++
    if (millis() - intervalMark >= sequenceInterval[state]) {
      // go to the next state
      state++;
      state = state % 5;
      outputDebug(F("state = "));
      outputDebugln(state);

      // act according to state
      switch (state) {
        case 0:  //fans off and wait 2 seconds
          digitalWrite(relay[1], LOW);
          pwmVal1 = 0;
          analogWrite(pwmPin_1, pwmVal1);
        break;
        case 1:  //sprinklers for 3 seconds
          digitalWrite(relay[2], HIGH);
        break;
        case 2:  //sprinklers off and wait another 30 seconds
          digitalWrite(relay[2], LOW);
        break;
        case 3:  //fans back on for 5 minutes
          digitalWrite(relay[1], HIGH);
          if (airTemp > maxAirTemp) { airTemp = maxAirTemp; }
          mappedTemp = map(airTemp, desiredAirTemp, (desiredAirTemp + 50), 0, 100);
          pwmVal1 = multiMap<float>(mappedTemp, input, output, 11);
          analogWrite(pwmPin_1, pwmVal1);          
        break;
        case 4:  //same as 3 because of max lenght difficulty
          if (airTemp > maxAirTemp) { airTemp = maxAirTemp; }
          mappedTemp = map(airTemp, desiredAirTemp, (desiredAirTemp + 50), 0, 100);
          pwmVal1 = multiMap<float>(mappedTemp, input, output, 11);
          analogWrite(pwmPin_1, pwmVal1); 
        break;
      }
      intervalMark = millis();
    }
  }

  else if (climateState == CASE5) {  //decrease humidity
    fanSpeedController(1);
  }

  else if (climateState == CASE6) {  //decrease temperature
    fanSpeedController(2);
  }

  else if (climateState == CASE8) {  //increase temperature
    //digitalWrite(relay[1], LOW); //fans_2 to be made icm with front window ventilation
    //mappedTemp = map(airTemp, desiredAirTemp, maxAirTemp, 0, 100);
    //pwmVal2 = multiMap<float>(mappedTemp, input, output, 11);
  }

  else if (climateState == CASE9) {  //decrease temperature
    fanSpeedController(2);    
  }

  else if (climateState == CASE12) {  //decrease humidity
    fanSpeedController(1);    
  }  

  pwm1Speed = map(pwmVal1, 0, 255, 0, 100);

}

void makeItRain() { //needs work

  if (millis() - previousPoint >= 3600000UL) { // rainstorm is after 1 hPa drop for 3 consecutive hours
    
    currentReading = pressureAverage;

    if(previousReading - currentReading >= rainCalculationFactor) {
      rainCounter ++;
      outputDebug("rainCounter ");
      outputDebugln(rainCounter);
    } else if (rainCounter != 0) {
      rainCounter --;
      outputDebug("rainCounter ");
      outputDebugln(rainCounter);
    } 

    previousReading = currentReading;
    previousPoint = millis ();
  }

  if (rainCounter >= 3) {
    rainyDay = true;
    rainCounter = 1;
    outputDebugln("it's a rainy day!");
  }

}

void fanSpeedController(int type) {
  
  float data, targetValue, maxValue, rangeValue;
  
  if (climateControlPause) { //stop fans during pause    
    pwmVal1 = 0;
    analogWrite(pwmPin_1, pwmVal1);
  }

  if (!climateControlPause) {
    if (type == 1) {
      if (dayTime) {
        data = humidity;
        maxValue = maxHumidity;
        targetValue = desiredHumidity;
        rangeValue = targetValue + (2 * hysteresis);
      } else if (!dayTime) { //at night allow high humidity but not 100%
        data = humidity;
        maxValue = 1000;
        targetValue = 980;
        rangeValue = 1000;
      }
    }
    if (type == 2) {
      data = airTemp;
      maxValue = maxAirTemp;
      targetValue = desiredAirTemp;
      rangeValue = targetValue + hysteresis;
    }

    if (data > maxValue) { data = maxValue; }
    
    switch (startup) { //set PWM to minimum value to start the fan moving
      case 0:
        pwmVal1 = 60;
        analogWrite(pwmPin_1, pwmVal1);
        if (millis() - startDelay >= 1000) { startup++; }
      break;
      case 1: //switch to automatic variable control
        mappedTemp = map(data, targetValue, rangeValue, 0, 100);
        pwmVal1 = multiMap<float>(mappedTemp, input, output, 11);
        analogWrite(pwmPin_1, pwmVal1);
      break;
    }  
  }
}

float smoothTransition(float start, float end, uint8_t intervals, unsigned long intervalTime, bool temp, TransitionCallback callback) {

  float delta = (float)(end - start) / intervals;  

  if (temp) { 
    if ((millis() - lastAirUpdateTime >= intervalTime) && (airStep <= intervals)) { 
      start += delta * airStep;
      airStep++;
      lastAirUpdateTime = millis();
      callback(start); //callback function for real-time updated values during transition      
      //outputDebug("transitioning to new temperature value ");
      //outputDebugln(start);
    } 
  } else { 
    if ((millis() - lastHumUpdateTime >= intervalTime) && (humStep <= intervals)) { 
      start += delta * humStep;
      humStep++;
      lastHumUpdateTime = millis();
      callback(start); 
      //outputDebug("transitioning to new humidity value ");
      //outputDebugln(start);
    } 
  } 
  return start;

}

void updateTemp(float value) {
  if (currPage == ROOT_MENU) {
    myScreen.setTextSize(1);
    myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK);
    myScreen.setCursor(10, 70); 
    myScreen.print((value / 10), 1);
    myScreen.print("\367 ");
    myScreen.setTextSize(2);
    myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  }
}
void updateHum(float value) {
  if (currPage == ROOT_MENU) {
    myScreen.setTextSize(1);
    myScreen.setTextColor(ST77XX_GREEN, ST77XX_BLACK);
    myScreen.setCursor(100, 70); 
    myScreen.print((value / 10), 1);
    myScreen.print("% ");
    myScreen.setTextSize(2);
    myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  }
}

//===========================================================================================================================
//                                               ENTER AND SAVE VALUES TO EEPROM
//=========================================================================================================================== 
void startChangeTime(int eepromHour, int eepromMinutes, int xPos, int yPos) { // xPos is starting location for Hour digits. xPos for minutes is shifted right

  int hourTime = EEPROM.readByte(eepromHour);
  int minuteTime = EEPROM.readByte(eepromMinutes);
  bool right_was_down = false;
  bool hourHasBeenSaved = false;  
  bool minuteHasBeenSaved = false;  
  bool everythingHasBeenSaved = false;  
  ChangeTimeState state = CHANGING_HOUR;

  //if (!everythingHasBeenSaved) {
  while (!everythingHasBeenSaved) {
    switch (state) {
      case CHANGING_HOUR: 
        if (checkJoystick() == Right) { right_was_down = true; delay(200); }

        hourTime = changeTime(hourTime, 0, 23, xPos, yPos);

        if (checkJoystick() == Right && right_was_down == true) {
          EEPROM.writeByte(eepromHour, hourTime);
          myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK); //revert color to normal
          myScreen.setCursor(xPos, yPos);          
          printDigits(hourTime);
          right_was_down = false;
          hourHasBeenSaved = true;  
        }   
        if (hourHasBeenSaved && !minuteHasBeenSaved) {
          state = CHANGING_MINUTE;
        }   
      break;

      case CHANGING_MINUTE: 
        if (checkJoystick() == Right) { right_was_down = true; delay(200); }

        minuteTime = changeTime(minuteTime, 0, 59, xPos +35, yPos); //+35 to shift from hour digits to minute digits   
        
        if (checkJoystick() == Right && right_was_down == true) {
          EEPROM.writeByte(eepromMinutes, minuteTime);
          myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK); //revert color to normal
          myScreen.setCursor(xPos +35, yPos);          
          printDigits(minuteTime);
          right_was_down = false;
          minuteHasBeenSaved = true; 
        }
        if (minuteHasBeenSaved && hourHasBeenSaved) {
          state = SAVING_COMPLETE;
          delay(400);
        }   
      break;      
      
      case SAVING_COMPLETE:      
        everythingHasBeenSaved = true;
        //outputDebugln("saving completed");
      break;

    }
  }
}

int changeTime(int &timeValue, int minValue, int maxValue, int xPos, int yPos) { 
  
  bool down_was_down = false;
  bool up_was_down = false;

  if (checkJoystick() == Down) { down_was_down = true; delay(100); }
  if (checkJoystick() == Up) { up_was_down = true; delay(100); } 
  
  myScreen.setTextColor(ST77XX_BLACK, ST77XX_WHITE); //invert the selected block  
  myScreen.setCursor(xPos, yPos);
  printDigits(timeValue);
  
  if (checkJoystick() == Up && up_was_down == true) {
    timeValue++; 
    myScreen.setCursor(xPos, yPos);
    printDigits(timeValue);
    if (timeValue > maxValue) {
      timeValue = minValue;
    }
    up_was_down = false;
  }
  if (checkJoystick() == Down && down_was_down == true) {
    timeValue--; 
    myScreen.setCursor(xPos, yPos);
    printDigits(timeValue);
    if (timeValue < minValue) {
      timeValue = maxValue;
    }
    down_was_down = false;
  }  
  return timeValue;  
}

void startChangeSeconds(int eepromSeconds, int xPos, int yPos) { 

  int secondsTime = EEPROM.readByte(eepromSeconds);
  bool right_was_down = false;
  bool timeHasBeenSaved = false;
  bool valueStored = false; //extra delay flag to prevent instant exit and restart

  while(!timeHasBeenSaved) {

    if (checkJoystick() == Right) { right_was_down = true; delay(100); }
    secondsTime = changeTime(secondsTime, 0, 59, xPos, yPos);

    if (checkJoystick() == Right && right_was_down == true) {
      EEPROM.writeByte(eepromSeconds, secondsTime);
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK); //revert color to normal
      myScreen.setCursor(xPos, yPos);          
      printDigits(secondsTime);
      valueStored = true;
      //outputDebugln("duration stored");
      right_was_down = false;
    }
    if (valueStored){
      delay(200);
      timeHasBeenSaved = true;
    }
  }
}

void printTimer(int number, int xPos, int yPos, int eepromNumber, bool printDuration) {

  myScreen.setCursor(xPos, yPos);
  myScreen.print("TIMER");
  myScreen.print(number);
  myScreen.setCursor(xPos + 80, yPos); // 80 only works for the time + duration notation
  printDigits(EEPROM.readByte(eepromNumber));
  myScreen.print(":");
  printDigits(EEPROM.readByte(eepromNumber + 1)); 
  if (printDuration){
    myScreen.print(" DURATION");
    myScreen.setCursor(xPos + 260, yPos);
    printDigits(EEPROM.readByte(eepromNumber + 2));
  }
}

void printMinMax(int xPos, int yPos, int eepromNumber, const char* txt, const char* txt2) {

  myScreen.setCursor(xPos, yPos);
  myScreen.print(txt);
  printDigits(EEPROM.readByte(eepromNumber));  
  myScreen.print(txt2);
}

void setValue(int xPos, int yPos, int minValue, int maxValue, int eepromNumber, int shiftToRight) { 

  int xPos2 = xPos + shiftToRight; //adjusting location after text
  int valueToSet = EEPROM.readByte(eepromNumber);
  bool right_was_down = false;  
  bool valueHasBeenStored = false;
  bool setValueStored = false; //extra delay flag to prevent instant exit and restart

  while (!valueHasBeenStored) { 

    if (checkJoystick() == Right) { right_was_down = true; delay(100); }
    valueToSet = changeTime(valueToSet, minValue, maxValue, xPos2, yPos);

    if (checkJoystick() == Right && right_was_down == true) {
      EEPROM.writeByte(eepromNumber, valueToSet);
      myScreen.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      myScreen.setCursor(xPos2, yPos);          
      printDigits(valueToSet);
      setValueStored = true;  
      right_was_down = false;
    }   
    if (setValueStored) {
      delay(200);
      valueHasBeenStored = true;
    }   
  }
}

//===========================================================================================================================
//                                                     AUTOMATED TIMER FUNCTIONS
//===========================================================================================================================
void automatedSwitching() {
  
  currentTime = getCurrentTime();

//====================================LIGHTS=====================================

  int lightOnHour = EEPROM.readByte(0), lightOnMinute = EEPROM.readByte(1), lightOffHour = EEPROM.readByte(2), lightOffMinute = EEPROM.readByte(3);

  int onTime = (lightOnHour * 100) + lightOnMinute;
  int offTime = (lightOffHour * 100) + lightOffMinute;

  switchTimer(0, onTime, offTime); //switchTimer(relay, ON, OFF)    

//====================================VENTILATION================================
  
  int delayedOn = onTime + 30; //30 minutes after light on
  int delayedOff = offTime - 30; //30 minutes before light off
  //switchTimer (1, delayedOn, delayedOff); // fans should not be on by default

//====================================SPRINKLERS=================================

  int rain1Hour = EEPROM.readByte(4),   rain1Minute = EEPROM.readByte(5),  rain1Duration = EEPROM.readByte(6);
  int rain2Hour = EEPROM.readByte(8),   rain2Minute = EEPROM.readByte(9),  rain2Duration = EEPROM.readByte(10);
  int rain3Hour = EEPROM.readByte(12),  rain3Minute = EEPROM.readByte(13), rain3Duration = EEPROM.readByte(14);
  int rain4Hour = EEPROM.readByte(16),  rain4Minute = EEPROM.readByte(17), rain4Duration = EEPROM.readByte(18);
  int rain5Hour = EEPROM.readByte(20),  rain5Minute = EEPROM.readByte(21), rain5Duration = EEPROM.readByte(22);

  if (!noRain) {
    switchCountdown(2, rain1Hour, rain1Minute, rain1Duration, 1); //switchCountdown(relay, start hour, start minute, duration, timerNumber)
    switchCountdown(2, rain2Hour, rain2Minute, rain2Duration, 2); 
    switchCountdown(2, rain3Hour, rain3Minute, rain3Duration, 3); 
    switchCountdown(2, rain4Hour, rain4Minute, rain4Duration, 4); 
    switchCountdown(2, rain5Hour, rain5Minute, rain5Duration, 5); 
  }
  if (rainyDay) { 
    switchCountdown(2, 16, 5, 10, 13); //extra 'rain' at 16:05 for x secs   
    switchCountdown(2, 20, 5, 10, 14);
    if (!hasNotRunYet[13] || !hasNotRunYet[14]) { //if one of the above 2 timers goes off, then reset rainyDay
      rainyDay = false;
      outputDebugln("extra rain");
    }
  }
//====================================BACKWALL DRIP==============================

  int drip1Hour = EEPROM.readByte(24),   drip1Minute = EEPROM.readByte(25),  drip1Duration = EEPROM.readByte(26);
  int drip2Hour = EEPROM.readByte(28),   drip2Minute = EEPROM.readByte(29),  drip2Duration = EEPROM.readByte(30);

  switchCountdown(3, drip1Hour, drip1Minute, drip1Duration, 6); 
  switchCountdown(3, drip2Hour, drip2Minute, drip2Duration, 7); 

//====================================FOGGER=====================================

  int fogOnHour = EEPROM.readByte(32), fogOnMinute = EEPROM.readByte(33), fogOffHour = EEPROM.readByte(34), fogOffMinute = EEPROM.readByte(35);

  int fogOn = (fogOnHour * 100) + fogOnMinute;
  int fogOff = (fogOffHour * 100) + fogOffMinute;
  
  //switchTimer(4, fogOn, fogOff); //is preventing fogger to be turned on any other time than it's scheduled on time. could be a problem
  switchCountdown(4, fogOnHour, fogOnMinute, 3600000, 15); //on for 1 hr as an alternative?
  
  if (currentTime >= onTime && currentTime <= fogOn) {
    humidityMonitoring = true;
  } else {
    humidityMonitoring = false;
  }

//====================================WINDOW HEAT================================

  //switchTimer(6, onTime, offTime); //for now linked to temperature controll. Will eventuelly be on permanent and fans over the heat to controll the temp

//====================================AIRPUMP====================================
  
  int airpump1Hour = EEPROM.readByte(38),  airpump1Minute = EEPROM.readByte(39), airpump1Duration = EEPROM.readByte(40);
  int airpump2Hour = EEPROM.readByte(42),  airpump2Minute = EEPROM.readByte(43), airpump2Duration = EEPROM.readByte(44);
  int airpump3Hour = EEPROM.readByte(46),  airpump3Minute = EEPROM.readByte(47), airpump3Duration = EEPROM.readByte(48);
  int airpump4Hour = EEPROM.readByte(50),  airpump4Minute = EEPROM.readByte(51), airpump4Duration = EEPROM.readByte(52);
  int airpump5Hour = EEPROM.readByte(54),  airpump5Minute = EEPROM.readByte(55), airpump5Duration = EEPROM.readByte(56);

  switchCountdown(7, airpump1Hour, airpump1Minute, airpump1Duration, 8); 
  switchCountdown(7, airpump2Hour, airpump2Minute, airpump2Duration, 9); 
  switchCountdown(7, airpump3Hour, airpump3Minute, airpump3Duration, 10); 
  switchCountdown(7, airpump4Hour, airpump4Minute, airpump4Duration, 11); 
  switchCountdown(7, airpump5Hour, airpump5Minute, airpump5Duration, 12); 

}

void switchTimer(int relayNumber, int startTime, int stopTime) { 

  if (currentTime >= startTime && currentTime < stopTime) {
    if (digitalRead(relay[relayNumber]) == LOW) {   
      digitalWrite(relay[relayNumber], HIGH);
      outputDebug("Timer ");
      outputDebug(relayNumber); 
      outputDebugln(" on");
    } 
  }
  if (currentTime < startTime || currentTime >= stopTime) {
    if (digitalRead(relay[relayNumber]) == HIGH) {   
      digitalWrite(relay[relayNumber], LOW); 
      outputDebug("Timer ");
      outputDebug(relayNumber); 
      outputDebugln(" off");
    } 
  }   

}

void switchCountdown(int relayNumber, int startHour, int startMinute, long duration, int timerNumber) { 
  
  currentHour = getLocalHour();
  currentMinute = getLocalMinutes();

  if (currentHour == startHour && currentMinute == startMinute && hasNotRunYet[timerNumber]) {
    
    digitalWrite(relay[relayNumber], HIGH);
    countdown[timerNumber] = millis(); 
    hasNotRunYet[timerNumber] = false; //flag prevents being activated more than once
    timerActive[timerNumber] = true;

    outputDebug("relay ");
    outputDebug(relayNumber); 
    outputDebug(" on with ");
    outputDebug(duration); 
    outputDebugln(" seconds countdown");      
  }    

  if (timerActive[timerNumber] && digitalRead(relay[relayNumber]) == HIGH) {       
    if (millis() - countdown[timerNumber] > (duration * 1000)) { //seconds to milliseconds 

      digitalWrite(relay[relayNumber], LOW);
      timerActive[timerNumber] = false;  

      outputDebug("relay ");
      outputDebug(relayNumber); 
      outputDebugln(" off");
    }
  }   

  if (!hasNotRunYet[timerNumber]) { 
    if (millis() - countdown[timerNumber] > resetDelay) { 

      hasNotRunYet[timerNumber] = true;  
    
      outputDebug("hasNotYetRun "); 
      outputDebug(timerNumber); 
      outputDebugln(" reset");    
    }
  }  

}

void fanPause() {

  if (digitalRead(relay[2]) == HIGH && !climateControlPause) { 
    pauseStart = millis();
    analogWrite(pwmPin_1, 0); //fans off
    outputDebugln("ClimateControl paused");    
    climateControlPause = true;
  }
 
  if (climateControlPause) { 
    if (millis() - pauseStart > fanPauseTime) {  
      climateControlPause = false;  
      outputDebugln("Pause ended");  
    }
  }
}

right now it seems I've done something wrong with kv_store

If I try to upload a sketch that has any kv_ call the board won't boot. The giga blinks 4 times red fast, then 4 times fast. This indicates the OS has crashed. I can upload an empty sketch with the libraries but as soon as I try to add kv_reset("/kv/"); it crashes the OS. The other functions don't work any more either.

Did I somehow corrupt the memory then? :grimacing:

Have you read this about how to clear and reset from the OS crash
https://docs.arduino.cc/tutorials/giga-r1-wifi/cheat-sheet/

In case you encounter the red LED, you can either:

  • Press the reset button once (this resets the sketch).
  • Double-tap the reset button to enter bootloader mode (allowing you to re-program the board).

If you can get the board to where it can be programmed, the code below is the mbed example for kv store adapted for the Arduino format of setup/loop.

It should give an error code for kv_reset("/kv/"); is there is still an issue.

#include "KVStore.h"
#include "kvstore_global_api.h"
#include "mbed.h"
REDIRECT_STDOUT_TO(Serial)

#define EXAMPLE_KV_VALUE_LENGTH 64
#define EXAMPLE_KV_KEY_LENGTH 32
#define err_code(res) MBED_GET_ERROR_CODE(res)

void setup() {
    Serial.begin(115200);
    while (!Serial)
        ;
    // put your setup code here, to run once:
    char kv_value_in[EXAMPLE_KV_VALUE_LENGTH] = {"kvstore_dummy_value_hello_world"};
    char kv_key_in[EXAMPLE_KV_KEY_LENGTH] = {"/kv/dummy_key1"};
    char kv_key_out[EXAMPLE_KV_KEY_LENGTH] = {0};
    size_t actual_size = 0;

    /* key information container */
    kv_info_t info;

    /* kv store iterator */
    kv_iterator_t kvstore_it;

    int i_ind = 0;

    printf("--- Mbed OS KVStore static API example ---\n");

    int res = MBED_ERROR_NOT_READY;

    /* Start By Resetting the KV Storage */
    printf("kv_reset\n");
    res = kv_reset("/kv/");
    printf("kv_reset -> %d\n", err_code(res));

    /* Set First 'Dummy' Key/Value pair with unprotected clear value data */
    printf("kv_set first dummy key\n");
    res = kv_set(kv_key_in, kv_value_in, strlen(kv_value_in), 0);
    printf("kv_set -> %d\n", err_code(res));

    /* Read the KV Pair you've just set */
    /* Start by getting key's information */
    printf("kv_get_info of first key\n");
    res = kv_get_info(kv_key_in, &info);
    printf("kv_get_info -> %d\n", err_code(res));
    printf("kv_get_info key: %s\n", kv_key_in);
    printf("kv_get_info info - size: %u, flags: %lu\n", info.size, info.flags);

    /* Now that you know the data value size of this key,
     * allocate a buffer with matching size and get the value data */
    printf("kv_get first key\n");
    char *kv_first_value_out = new char[info.size + 1];
    memset(kv_first_value_out, 0, info.size + 1);
    res = kv_get(kv_key_in, kv_first_value_out, info.size, &actual_size);
    printf("kv_get -> %d\n", err_code(res));
    printf("kv_get key: %s\n", kv_key_in);
    printf("kv_get value: %s\n", kv_first_value_out);
    delete[] kv_first_value_out;

    /* Lets set some more 'Dummy' and 'Real' KV pairs */
    /* Set 'Dummy' Key2 */
    printf("kv_set second dummy key \n");
    res = kv_set("/kv/dummy_key2", "Hello its dummy_value2", strlen("Hello its dummy_value2"), 0);
    printf("kv_set -> %d\n", err_code(res));

    //**
    printf("kv_get_info of second key\n");
    res = kv_get_info("/kv/dummy_key2", &info);
    printf("kv_get_info -> %d\n", err_code(res));
    printf("kv_get_info key: %s\n", "/kv/dummy_key2");
    printf("kv_get_info info - size: %u, flags: %lu\n", info.size, info.flags);

    printf("kv_get second key\n");
    char *kv_second_value_out = new char[info.size + 1];
    memset(kv_second_value_out, 0, info.size + 1);
    res = kv_get("/kv/dummy_key2", kv_second_value_out, info.size, &actual_size);
    printf("kv_get -> %d\n", err_code(res));
    printf("kv_get key: %s\n", "/kv/dummy_key2");
    printf("kv_get value: %s\n", kv_second_value_out);
    delete[] kv_second_value_out;

    //*
    

    /* Set an authenticated-encrypted 'Dummy' key with Replay protection */
    printf("kv_set third key with Confidentiality and Replay Protection flags\n");
    res = kv_set("/kv/dummy_auth_enc_key", "auth_enc_value", strlen("auth_enc_value"),
                 KV_REQUIRE_CONFIDENTIALITY_FLAG | KV_REQUIRE_REPLAY_PROTECTION_FLAG);
    printf("kv_set -> %d\n", err_code(res));

    /* Set 2 non-dummy 'Real' KV pairs */
    /* Set 'Real' Key 1 */
    printf("kv_set Set 'Real' Key 1\n");
    res = kv_set("/kv/real_key1", "real_value1", strlen("real_value1"), 0);
    printf("kv_set -> %d\n", err_code(res));
    /* Set 'Real' Write-Once Key2 for a key that you do not want to be removed */
    printf("kv_set Set 'Real' Key 2 with flag write-once\n");
    res = kv_set("/kv/real_wo_key", "real_wo_value", strlen("real_wo_value"), KV_WRITE_ONCE_FLAG);
    printf("kv_set -> %d\n", err_code(res));

    /* Now lets remove all of the 'Dummy' Keys and remain with the 'Real' ones */
    printf("Removing 'Dummy' Keys\n");
    /* Iterate and remove Keys that start with prefix 'dummy' */
    res = kv_iterator_open(&kvstore_it, "dummy");
    memset(kv_key_out, 0, EXAMPLE_KV_KEY_LENGTH);
    while (kv_iterator_next(kvstore_it, kv_key_out, EXAMPLE_KV_KEY_LENGTH) != MBED_ERROR_ITEM_NOT_FOUND)
    {
        i_ind++;
        printf("%d) Removing %s\n", i_ind, kv_key_out);
        kv_remove(kv_key_out);
        memset(kv_key_out, 0, EXAMPLE_KV_KEY_LENGTH);
    }
    res = kv_iterator_close(kvstore_it);
    printf("Remaining with 'Real' Keys:\n");
    /* Iterate on all remaining Keys */
    res = kv_iterator_open(&kvstore_it, NULL);
    memset(kv_key_out, 0, EXAMPLE_KV_KEY_LENGTH);
    i_ind = 0;
    while (kv_iterator_next(kvstore_it, kv_key_out, EXAMPLE_KV_KEY_LENGTH) != MBED_ERROR_ITEM_NOT_FOUND)
    {
        i_ind++;
        printf("%d) %s\n", i_ind, kv_key_out);
        memset(kv_key_out, 0, EXAMPLE_KV_KEY_LENGTH);
    }
    res = kv_iterator_close(kvstore_it);

    /* Try to remove write-once Key - should fail */
    printf("kv_remove write-once file - should fail!\n");
    res = kv_remove("/kv/real_wo_key");
    printf("kv_remove -> %d\n", err_code(res));

    /* Finally, reset will format kvstore and remove All Keys (including write-once keys) */
    printf("kv_reset format kvstore (including write-once)\n");
    res = kv_reset("/kv/");
    printf("kv_reset -> %d\n", err_code(res));
}

void loop() {
    // put your main code here, to run repeatedly:
}

Oh yes I know, this was not a problem. It's happened a few times before :sweat_smile:

I'll try running the code you posted

The sketch compiles and uploads but before it can execute the OS crashes again. Nothing is printed on the Serial Monitor (on the correct baud rate).

edit: any other sketch without kv_ lines works fine

That is correct. I suppose sending UDP messages is not difficult but what should I install on my PC to receive and write all these messages?