Getting a State machine to control a heating controller

Hi all, back for some further guidance. This is my previous thread Nearly SOLVED Using While function is only giving a partial answer - #118 by johnem51

I have an Arduino based three channel central heating programmer which has an Override facility to enable heating outside of the normal operating hours, when the system would normally be in the ‘Frost protection’ mode.

The time sequence and temperature control work fine; however, when I press the ‘Right’ button to initiate Override, which should time out after a given period of time, the system switches the Override OFF and signals normal control and then reverts back to Override mode.

Here is the serial monitor output.

"
20:41:42
20:41:43
20:41:43
20:41:43
20:41:43
20:41:43
20:41:44
Auto termination of override
LCD Backlight ON
20:41:44
FSM state is mode normal
Normal temperature control is available for all areas
20:41:44
FSM state is mode override
Heating Override is On
20:41:44
20:41:44
20:41:45
20:41:45
"
When the system is in Override Mode and I press the Enter button to manually terminate the Override, this is the serial monitor output.
"
20:43:50
20:43:50
20:43:50
Enter Button has been pressed
LCD Backlight ON
20:43:50
Manual termination of override
LCD Backlight ON
20:43:50
FSM state is mode normal
Normal temperature control is available for all areas
20:43:51
FSM state is mode override
Manual termination of override
LCD Backlight ON
20:43:51
FSM state is mode normal
20:43:51
FSM state is mode override

20:43:51
FSM state is mode override
Manual termination of override
"
The is the code of the sketch.

// MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START *
// a detailed explanation how these macros work is given in this tutorial
// https://forum.arduino.cc/t/comfortable-serial-debug-output-short-to-write-fixed-text-name-and-content-of-any-variable-code-example/888298

#define dbg(myFixedText, variableName) \
  Serial.print(F(#myFixedText " " #variableName "=")); \
  Serial.println(variableName);  // print it every time line of code is executed

#define dbgi(myFixedText, variableName, timeInterval) \
  { \
    static unsigned long intervalStartTime; \
    if (millis() - intervalStartTime >= timeInterval) { \
      intervalStartTime = millis(); \
      Serial.print(F(#myFixedText " " #variableName "=")); \
      Serial.println(variableName); \
    } \
  }  // print only after a specified interval of milliseconds has passed by

#define dbgc(myFixedText, variableName) \
  { \
    static long lastState; \
    if (lastState != variableName) { \
      Serial.print(F(#myFixedText " " #variableName " changed from ")); \
      Serial.print(lastState); \
      Serial.print(F(" to ")); \
      Serial.println(variableName); \
      lastState = variableName; \
    } \
  }  // print only if the value of the variable has changed

#define dbgcf(myFixedText, variableName) \
  { \
    static float lastState; \
    if (lastState != variableName) { \
      Serial.print(F(#myFixedText " " #variableName " changed from ")); \
      Serial.print(lastState); \
      Serial.print(F(" to ")); \
      Serial.println(variableName); \
      lastState = variableName; \
    } \
  }
// MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END *

unsigned long MyHeartbeatTimer = 0;  // Timer-variables MUST be of type unsigned long
const byte OnBoard_LED = 13;

/* Author : John Marchant G0RJM
 Created : 25/01/2024 - 14/11/2025 V3.006B
 Description : A long term ongoing project to display the current date, UK time and temperature on the 20x4 LCD screen and create a 
 programmable thermostatic heating control of three areas which are Range, Club and Airgun to include frost protection and override facilities.
 This also incorporates the facility to change the temperature settings and On-Off time settings from the pushbuttons.
 This has been very loosely based on many code parts borrowed from several other files/sketches and in collaboration with
 the input and genorous help from several authors from the Arduino Forum with particular thanks to @alto777, @blh64, @cattledog, 
 @DaveX, @PerryBebbington, @StefanL38 and @UKHeliBob.  */
#include "RTClib.h"
#include <OneWire.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DallasTemperature.h>
#include <EEPROM.h>
#include <DST_RTC.h>

#define ONE_WIRE_BUS_1 3
#define ONE_WIRE_BUS_2 4
#define ONE_WIRE_BUS_3 5
#define TEMPERATURE_PRECISION 11    // 0.125deg resolution
#define heart_beat_pin LED_BUILTIN  // digital pin for heart beat LED

OneWire oneWire_range(ONE_WIRE_BUS_1);
OneWire oneWire_club(ONE_WIRE_BUS_2);
OneWire oneWire_airgun(ONE_WIRE_BUS_3);

const byte range_relay = 11;  // Relay outputs
const byte club_relay = 12;
const byte buzzer_pin = A0;
const byte airgun_relay = A1;
const byte boiler_relay = A2;

const byte rangeIndex = 0;
const byte clubIndex = 1;
const byte airgunIndex = 2;

float temperature_setpoint[3];  // define an array that goes from index 0 to index 2 which means the array has three elements. => [3]

float range_temperature;
float club_temperature;
float airgun_temperature;

float temperature_setpoint_range;   // C
float temperature_setpoint_club;    // C
float temperature_setpoint_airgun;  // C

const int frost_prev_setpoint = 12.0;  // C
const float deadzone = 1.0;            // C

const uint8_t buttonPin[] = { 6, 7, 8, 9, 10 };  // Inputs for buttons.
const uint8_t buttonCount = 5;                   // Number of buttons.

int n = 0;

int EEPROM_address_1 = 0;    // Address of the location of Range setpoint temperature setting
int EEPROM_address_2 = 4;    // Address of the location of Club setpoint temperature setting
int EEPROM_address_3 = 8;    // Address of the location of Airgun setpoint temperature setting
int EEPROM_address_4 = 12;   // Address of the location of Range On time setting
int EEPROM_address_5 = 16;   // Address of the location of Range Off time setting
int EEPROM_address_6 = 20;   // Address of the location of Club On time setting
int EEPROM_address_7 = 24;   // Address of the location of Club Off time setting
int EEPROM_address_8 = 28;   // Address of the location of Airgun On time setting
int EEPROM_address_9 = 32;   // Address of the location of Airgun Off time setting
int EEPROM_address_10 = 36;  // Address of the location of Sunday On time setting
int EEPROM_address_11 = 40;  // Address of the location of Sunday Off time setting

unsigned long startMillis = 0;
unsigned long MillisMenu = 0;
unsigned long overrideTimer1 = 0;           // Heating override timer
unsigned long sensorDelayTimer = 0;         // Sensor reading delay
unsigned long heatingDelayTimer = 0;        // Heating delay
unsigned long EEPROMSaveDelay = 0;          // EEPROM save delay
unsigned long EEPROMSaveDelay1 = 0;         // EEPROM save delay
unsigned long backlightDisplayTimer = 0;    // Backlight display delay
unsigned long temperatureDisplayTimer = 0;  // Temperature display delay
unsigned long printPause = 0;               // Used for print delay.
unsigned long printPause1 = 0;              // Used for print delay
unsigned long printPause3 = 0;              // Used for print delay.
unsigned long printPause4 = 0;              // Used for print delay.
unsigned long generalPause = 0;             // Used for general delay.
unsigned long currentMillis = 0;
unsigned long last_heart_beat_time;    // time in milliseconds of last heart beat status change
unsigned long heart_beat_freq = 2000;  // time in milliseconds of heart beat frequency
unsigned long heart_beat_on_off_time;  // the time the LED is on and off - 1/2 frequency

bool isSetupOn = false;               // if = True allows the setup to be used
bool isUpButtonPressed = false;       // if = True up button action is being done
bool isDownButtonPressed = false;     // if = True down button action is being done
bool isLeftButtonPressed = false;     // if = True left button action is being done
bool isRightButtonPressed = false;    // if = True right button action is being done
bool isEnterButtonPressed = false;    // if = True enter button action is being done
bool isHeartBeatOn = false;           // if = True shows current status of heart beat
bool isTimeDisplayOn = false;         // if = True allows normal time control function to operate
bool isTimeSettingOn = false;         // if = True allows the time to be adjusted
bool isTimedSessionOn = false;        // if = True allows the timed period control function to operate
bool isClubHeatingOn = false;         // if = True allows the Club heating
bool isRangeHeatingOn = false;        // if = True allows the Range heating
bool isAirgunHeatingOn = false;       // if = True allows the Airgun heating
bool isHeatingOverrideOn = false;     // if = True allows the heating override facility
bool isTemperatureSettingOn = false;  // if = True allows the temperature to be adjusted
bool buttonPressedFlag[buttonCount];  // if = True flags to indicate that a button has been detected as pressed and debounced

bool TimePeriodIsOver(unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis = millis();
  if (currentMillis - startOfPeriod >= TimePeriod) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis;  // a new period starts right here so set new starttime
    return true;
  } else return false;  // actual TimePeriod is NOT yet over
}

typedef void (*functionPtrs)(void);
functionPtrs functions[5] = { &leftButton, &upButton, &downButton, &rightButton, &enterButton };

DallasTemperature range_sensor(&oneWire_range);
DallasTemperature club_sensor(&oneWire_club);
DallasTemperature airgun_sensor(&oneWire_airgun);

enum { SUNDAY,
       MONDAY,
       TUESDAY,
       WEDNESDAY,
       THURSDAY,
       FRIDAY,
       SATURDAY };

enum operationModes { NORMAL_MODE,
                      OVERRIDE_MODE };

enum setupModes { TEMPERATURE_SETUP_MODE,
                  TIME_SETUP_MODE };

int myMode = NORMAL_MODE;

int On_Normal_Range;    // 16:00 Normal ON time
int Off_Normal_Range;   // 21:30 Normal OFF time
int On_Normal_Club;     // 16:00 Normal ON time
int Off_Normal_Club;    // 21:30 Normal OFF time
int On_Normal_Airgun;   // 16:00 Normal ON time
int Off_Normal_Airgun;  // 21:30 Normal OFF time
int On_Sunday;          // 12:00 Sunday ON time
int Off_Sunday;         // 17:30 Sunday OFF time

int old_data;
int new_data;

LiquidCrystal_I2C lcd(0X27, 20, 4);
RTC_DS3231 rtc;
DST_RTC dst_rtc;  // DST object

// Define US or EU rules for DST comment out as required. More countries could be added with different rules in DST_RTC.cpp
// const char rulesDST[] = "US"; // US DST rules
const char rulesDST[] = "EU";  // EU DST rules

char daysOfTheWeek[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
byte char_temp[8] = { B00100, B01010, B01010, B01110, B01110, B11111, B11111, B01110 };  // for thermometer icon

char *modeTags[] = { "normal", "override", "setup", "temperature-setup", "time-setup" };
byte nModes = sizeof modeTags / sizeof *modeTags;

byte IndexNr;  // global declaration of variable "IndexNr"

void setup() {

  Serial.begin(115200);
  Serial.println(F("Setup-Start"));
  PrintFileNameDateTime();  // prints the real filename and compiletime
  Wire.begin();

  rtc.begin();
  checkRTC();

  startMillis = millis();
  heart_beat_on_off_time = heart_beat_freq / 2;  // LED is on and off at 1/2 frequency time

  setPinModes();
  setupOneWireSensors();

  initiateSetpoints();
  retrieveSetpoints();

  initLCD();

}  // End of setup

void loop() {

  Heartbeat();  // first thing to do at top of loop heartbeat blinking

  unsigned long currentMillis = millis();  // all time elements are data type unsigned long

  //---------------------------General control-------------------------//

  myStateMachine();

  backGroundItems();

  //--------------------Daylight Saving Setting---------------------//

  DateTime standardTime = rtc.now();

  DateTime theTime = dst_rtc.calculateTime(standardTime);  // takes into account DST

  printTheTime(theTime);

  int currentTime = theTime.hour() * 60UL + theTime.minute();

  //--------------------Timed heating setting-----------------------//

  if (isTimedSessionOn) {

    //  Serial.println(F("Something time controlled is happening here!"));
    if (theTime.dayOfTheWeek() != SUNDAY) {
      // Monday to Saturday so use normal times
      if (currentTime > On_Normal_Range && currentTime < Off_Normal_Range) {
        // this is within normal Range area heating time
        isRangeHeatingOn = true;
      } else {
        // this is outside normal Range area heating time
        isRangeHeatingOn = false;
      }
    }
    if (theTime.dayOfTheWeek() != SUNDAY) {
      // Monday to Saturday so use normal times
      if (currentTime > On_Normal_Club && currentTime < Off_Normal_Club) {
        // this is within normal Club area heating time
        isClubHeatingOn = true;
      } else {
        // this is outside normal Club area heating time
        isClubHeatingOn = false;
      }
    }
    if (theTime.dayOfTheWeek() != SUNDAY) {
      // Monday to Saturday so use normal times
      if (currentTime > On_Normal_Airgun && currentTime < Off_Normal_Airgun) {
        // this is within normal Airgun heating time
        isAirgunHeatingOn = true;
      } else {
        // this is outside normal Airgun heating time
        isAirgunHeatingOn = false;
      }
    } else {
      if (currentTime > On_Sunday && currentTime < Off_Sunday) {
        // this is within Sunday heating time
        isRangeHeatingOn = true;
        isClubHeatingOn = true;
        isAirgunHeatingOn = true;
      } else {
        // this is outside Sunday heating time
        isRangeHeatingOn = false;
        isClubHeatingOn = false;
        isAirgunHeatingOn = false;
      }
    }
  }

  //---------------------Date & Time display----------------------//

  if ((isTimeDisplayOn) && (currentTime != 'theTimeP')) {
    lcd.setCursor(0, 0);
    lcd.print(daysOfTheWeek[theTime.dayOfTheWeek()]);
    lcd.setCursor(3, 0);
    lcd.print(F(":"));
    lcd.setCursor(4, 0);
    if (theTime.day() < 10) lcd.print('0');
    lcd.print(theTime.day(), DEC);
    lcd.print(':');
    if (theTime.month() < 10) lcd.print('0');
    lcd.print(theTime.month(), DEC);
    lcd.print(':');
    lcd.print(theTime.year(), DEC);
    lcd.setCursor(14, 0);
    lcd.print(" ");
    lcd.setCursor(15, 0);
    if (theTime.hour() < 10) lcd.print('0');
    lcd.print(theTime.hour(), DEC);
    lcd.print(':');
    if (theTime.minute() < 10) lcd.print('0');
    lcd.print(theTime.minute(), DEC);
    currentTime = 'theTimeP';
  }

}  // End of loop

//----------------------------Check RTC------------------------------//
void checkRTC() {

  if (rtc.lostPower()) {
    Serial.println(F("RTC is NOT running!"));
    /* Uncomment line "375" to set the RTC to the current PC date & time or "376" to manually input the date & time.
      Renmove the battery and upload sketch. Then re-comment line "375" or "376" Upload sketch again. Refiut the battery and then remove external power. Then re-instate power. */
    // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    // rtc.adjust(DateTime(2025, 11, 3, 19, 46, 0));
    /* DST? If we're in it, let's subtract an hour from the RTC time to keep our DST calculation correct. This gives us
      Standard Time which our DST check will add an hour back to if we're in DST.
    */
    DateTime standardTime = rtc.now();
    if (dst_rtc.checkDST(standardTime) == true) {  // check whether we're in DST right now. If we are, subtract an hour.
      standardTime = standardTime.unixtime() - 3600;
    }
    rtc.adjust(standardTime);
  }
}

//----------------------------Pin Modes------------------------------//
void setPinModes() {

  for (uint8_t i = 0; i < buttonCount; ++i) {
    pinMode(buttonPin[i], INPUT_PULLUP);
  }

  pinMode(range_relay, OUTPUT);
  pinMode(club_relay, OUTPUT);
  pinMode(airgun_relay, OUTPUT);
  pinMode(boiler_relay, OUTPUT);
  pinMode(buzzer_pin, OUTPUT);

  pinMode(heart_beat_pin, OUTPUT);
}

//------------------------OneWire Sensors----------------------------//
void setupOneWireSensors() {

  range_sensor.begin();
  club_sensor.begin();
  airgun_sensor.begin();

  range_sensor.setResolution(TEMPERATURE_PRECISION);
  club_sensor.setResolution(TEMPERATURE_PRECISION);
  airgun_sensor.setResolution(TEMPERATURE_PRECISION);
}

//-----------------------Initiate Setpoints--------------------------//
void initiateSetpoints() {

  // EEPROM.put(EEPROM_address_1, temperature_setpoint_range);   // write the float value to EEPROM ONLY used to initially set up setpoint
  // EEPROM.put(EEPROM_address_2, temperature_setpoint_club);    // write the float value to EEPROM ONLY used to initially set up setpoint
  // EEPROM.put(EEPROM_address_3, temperature_setpoint_airgun);  // write the float value to EEPROM ONLY used to initially set up setpoint
  // EEPROM.put(EEPROM_address_4, 16 * 60 + 00);   // write the value to EEPROM ONLY used to initially set up Range Normal On Time
  // EEPROM.put(EEPROM_address_5, 21 * 60 + 30);   // write the value to EEPROM ONLY used to initially set up Range Normal Off Time
  // EEPROM.put(EEPROM_address_6, 16 * 60 + 00);   // write the value to EEPROM ONLY used to initially set up Club Normal On Time
  // EEPROM.put(EEPROM_address_7, 21 * 60 + 30);   // write the value to EEPROM ONLY used to initially set up Club Normal Off Time
  // EEPROM.put(EEPROM_address_8, 16 * 60 + 00);   // write the value to EEPROM ONLY used to initially set up Airgun Normal On Time
  // EEPROM.put(EEPROM_address_9, 21 * 60 + 30);   // write the value to EEPROM ONLY used to initially set up Airgun Normal Off Time
  // EEPROM.put(EEPROM_address_10, 12 * 60 + 00);  // write the value to EEPROM ONLY used to initially set up Sunday On Time
  // EEPROM.put(EEPROM_address_11, 17 * 60 + 30);  // write the value to EEPROM ONLY used to initially set up Sunday Off Time
}

//------------------------Retrieve setpoint--------------------------//
void retrieveSetpoints() {

  EEPROM.get(EEPROM_address_1, temperature_setpoint_range);   //retrieve the range setpoint from EEPROM
  EEPROM.get(EEPROM_address_2, temperature_setpoint_club);    //retrieve the club setpoint from EEPROM
  EEPROM.get(EEPROM_address_3, temperature_setpoint_airgun);  //retrieve the airgun setpoint from EEPROM
  EEPROM.get(EEPROM_address_4, On_Normal_Range);              //retrieve the Normal Range On Time from EEPROM
  EEPROM.get(EEPROM_address_5, Off_Normal_Range);             //retrieve the Normal Range Off Time from EEPROM
  EEPROM.get(EEPROM_address_6, On_Normal_Club);               //retrieve the Normal Club On Time from EEPROM
  EEPROM.get(EEPROM_address_7, Off_Normal_Club);              //retrieve the Normal Club Off Time from EEPROM
  EEPROM.get(EEPROM_address_8, On_Normal_Airgun);             //retrieve the Normal Airgun On Time from EEPROM
  EEPROM.get(EEPROM_address_9, Off_Normal_Airgun);            //retrieve the Normal Airgun Off Time from EEPROM
  EEPROM.get(EEPROM_address_10, On_Sunday);                   //retrieve the Sunday On Time from EEPROM
  EEPROM.get(EEPROM_address_11, Off_Sunday);                  //retrieve the Sunday Off Time from EEPROM

  Serial.print(F("Range Temperature Setpoint  - "));
  Serial.print(temperature_setpoint_range);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_1);
  Serial.print(F("Club Temperature Setpoint   - "));
  Serial.print(temperature_setpoint_club);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_2);
  Serial.print(F("Airgun Temperature Setpoint - "));
  Serial.print(temperature_setpoint_airgun);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_3);
  Serial.print(F("Normal Range On Time - "));
  Serial.print(On_Normal_Range);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_4);
  Serial.print(F("Normal Range Off Time - "));
  Serial.print(Off_Normal_Range);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_5);
  Serial.print(F("Normal Club On Time - "));
  Serial.print(On_Normal_Club);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_6);
  Serial.print(F("Normal Club Off Time - "));
  Serial.print(Off_Normal_Club);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_7);
  Serial.print(F("Normal Airgun On Time - "));
  Serial.print(On_Normal_Airgun);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_8);
  Serial.print(F("Normal Airgun Off Time - "));
  Serial.print(Off_Normal_Airgun);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_9);
  Serial.print(F("Sunday On Time - "));
  Serial.print(On_Sunday);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_10);
  Serial.print(F("Sunday Off Time - "));
  Serial.print(Off_Sunday);
  Serial.print(F(" Stored at addr "));
  Serial.println(EEPROM_address_11);
}

//---------------------------Save Setpoints---------------------------//
void saveSetpoints() {

  {
    for (int n = 0; n < 1; n++) {
      Serial.println(F("Changes saved"));
      EEPROM.put(EEPROM_address_1, temperature_setpoint_range);   // write the new range setpoint value to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_2, temperature_setpoint_club);    // write the new club setpoint value to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_3, temperature_setpoint_airgun);  // write the new airgun setpoint value to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_4, On_Normal_Range);              // write the new Range Normal On time to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_5, Off_Normal_Range);             // write the new Range Normal Off time to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_6, On_Normal_Club);               // write the new Club Normal On time to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_7, Off_Normal_Club);              // write the new Club Normal Off time to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_8, On_Normal_Airgun);             // write the new Airgun Normal On time to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_9, Off_Normal_Airgun);            // write the new Airgun Normal Off time to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_10, On_Sunday);                   // write the new Sunday On time to EEPROM, only if it has changed
      EEPROM.put(EEPROM_address_11, Off_Sunday);                  // write the new Sunday Off time to EEPROM, only if it has changed

      if (TimePeriodIsOver(EEPROMSaveDelay, 2500)) {
        retrieveSetpoints();
        lcd.setCursor(3, 3);
        lcd.print(F("SETTINGS SAVED"));
      }
      if (TimePeriodIsOver(EEPROMSaveDelay1, 2500)) {
        lcd.clear();
        isTimeSettingOn = false;
        isSetupOn = false;
      }
    }
  }
}

//--------------------------------LCD--------------------------------//
void initLCD() {

  lcd.init();
  lcd.backlight();
  lcd.createChar(0, char_temp);
  lcd.setCursor(2, 0);
  lcd.print(F("G0RJM Three Zone"));
  lcd.setCursor(1, 1);
  lcd.print(F("Temperature Control"));
  delay(1000);
  lcd.setCursor(2, 2);
  lcd.print(F("V3.006B 14/11/25"));
  delay(1000);
  lcd.setCursor(4, 3);
  lcd.print(F("Working on it"));
  Serial.println(F("G0RJM Three zone Temperature Control"));
  delay(3000);
  lcd.clear();
}

//---------------------------Heartbeat-------------------------------//
void Heartbeat() {

  if (millis() - last_heart_beat_time >= heart_beat_on_off_time) {  // time to swap status of the heart beat LED and update it
    last_heart_beat_time = millis();
    isHeartBeatOn = !isHeartBeatOn;               // invert current heart beat status value
    digitalWrite(heart_beat_pin, isHeartBeatOn);  // update LED with new status
  }
}

//--------------------------Background Items------------------------//
void backGroundItems() {

  boilerInterlock();
  serialPrintAreaTemperatures();
  backlightDisplay();
  buttonSettings();
  buttonControl();
}
// -------------------------Backlight display-----------------------//
void backlightDisplay() {

  if ((TimePeriodIsOver(backlightDisplayTimer, 60000)) && (isTimeDisplayOn)) {
    lcd.noBacklight();
    Serial.println(F("LCD Backlight OFF"));
  }
}

//---------------------------Button Settings------------------------//
void buttonSettings() {

#define buttonPressed LOW                    // When the button is pressed the input will be low.
  uint32_t currentMillis = millis();         // Millis times used to debounce the button
  static uint32_t lastMillis[buttonCount];   // Start of the debounce timeout for each button
  const uint32_t bounceTimeout = 50;         // Debounce time in milliseconds
  bool currentButtonState[buttonCount];      // Holds the current state of each button
  static bool lastButtonState[buttonCount];  // Holds the previous debounced state of the button
  uint8_t i;

  for (i = 0; i < buttonCount; ++i) {
    currentButtonState[i] = digitalRead(buttonPin[i]);       // Reads the current state of each button and saves the result
    if (lastButtonState[i] != currentButtonState[i]) {       // Checks to see if each button has been pressed or released, at this point each button has not been debounced
      if (currentMillis - lastMillis[i] >= bounceTimeout) {  // Checks to see if the state of each button has been stable for at least bounceTimeout duration
        lastButtonState[i] = currentButtonState[i];          // At this point the button has been debounced, so save the last state
        if (currentButtonState[i] == buttonPressed) {        // The button might have been pressed or released, this make sure only presses are acted on, not releases
          buttonPressedFlag[i] = true;                       // Button press has been detected and debounced, set a flag to indicate to the next function that some action can be taken
        }
      }
    } else {
      lastMillis[i] = currentMillis;  // Saves the current value of millis in last millis so the debounce timer for each button starts from current millis
    }
  }
}

//---------------------------Button Actions---------- --------------//
void buttonControl() {

  uint8_t i;
  for (i = 0; i < buttonCount; ++i) {
    if (buttonPressedFlag[i]) {
      buttonPressedFlag[i] = false;  // Clear the flag to ensure the action only happens once
      functions[i]();                // Calls one of the 5 functions depending on which button was pressed
    }
  }
}

// 5 functions, which are called depending on which button has been pressed

void leftButton() {

  Serial.println(F("Left Button has been pressed"));
  isLeftButtonPressed = true;
  Buzzer();
}

void upButton() {

  Serial.println(F("Up Button has been pressed"));
  isUpButtonPressed = true;
  Buzzer();
}

void downButton() {

  Serial.println(F("Down Button has been pressed"));
  isDownButtonPressed = true;
  Buzzer();
}

void rightButton() {

  Serial.println(F("Right Button has been pressed"));
  isRightButtonPressed = true;
  Buzzer();
}

void enterButton() {

  Serial.println(F("Enter Button has been pressed"));
  isEnterButtonPressed = true;
  Buzzer();
}

//---------------------------Buzzer----------------------------------//
void Buzzer() {

  lcd.backlight();
  tone(buzzer_pin, 3000, 100);
  Serial.println(F("LCD Backlight ON"));
}

//----------------------Print time to serial-------------------------//
void printTheTime(DateTime theTimeP) {

  static float currentTime;
  if (currentTime != 'theTimeP') {
    Serial.print(theTimeP.day(), DEC);
    Serial.print('/');
    Serial.print(theTimeP.month(), DEC);
    Serial.print('/');
    Serial.print(theTimeP.year(), DEC);
    Serial.print(' ');
    currentTime = 'theTimeP';
  }
  if (TimePeriodIsOver(printPause, 1)) {
    Serial.print(theTimeP.hour(), DEC);
    Serial.print(':');
    Serial.print(theTimeP.minute(), DEC);
    Serial.print(':');
    Serial.print(theTimeP.second(), DEC);
    Serial.println();
  }
}

//-----------------------Print The State----------------------------//

void printStateIf(int theState) {

  static int lastPrinted = -1;  // no, it does not!

  if (lastPrinted != theState) {
    if (theState >= 0 && theState < nModes) {
      Serial.print(F("FSM state is mode "));
      Serial.println(modeTags[theState]);
    } else {
      Serial.print(F("WHAT? state is "));
      Serial.println(theState);
    }

    lastPrinted = theState;
  }
}

//------------------------My State machine---------------------------//
void myStateMachine() {

  printStateIf(myMode);

  switch (myMode) {
    case NORMAL_MODE:
      normalModeSelect();
      if (isRightButtonPressed) {
        myMode = OVERRIDE_MODE;
      }
      break;  // skip cases below and jump down to END-OF-SWITCH

    case OVERRIDE_MODE:
      heatingOverride();
      if (!isHeatingOverrideOn) {
        myMode = NORMAL_MODE;
      }
      break;

    default:
      myMode = NORMAL_MODE;
      break;
  }
}  // END-OF-SWITCH

//-------------------------Normal Mode Select----------------------//
void normalModeSelect() {

  temperatureDisplay();
  heatingIsAvailable();
  isTimeDisplayOn = true;
  isTimedSessionOn = true;
  if (TimePeriodIsOver(printPause1, 15000)) {
    Serial.println(F("Normal temperature control is available for all areas"));
  }
}

//------------------------Heating Override Select---------------------//
void heatingOverride() {

  {
    lcd.setCursor(0, 0);
    lcd.print(F("   OVERRIDE MODE    "));
    temperatureDisplay();
    heatingIsAvailable();
    isRangeHeatingOn = true;
    isClubHeatingOn = true;
    isAirgunHeatingOn = true;
    isTimedSessionOn = false;
    isHeatingOverrideOn = true;
    if (TimePeriodIsOver(printPause3, 15000)) {
      Serial.println(F("           Heating Override is On"));
    }

    if (TimePeriodIsOver(overrideTimer1, 75000)) {  // Automatically cancels the override
      Serial.println(F("         Auto termination of override"));
      heatingOverrideIsCompleted();
    } else if (isEnterButtonPressed) {  // Manually cancels the override
      Serial.println(F("         Manual termination of override"));
      heatingOverrideIsCompleted();
    }
  }
}

//----------------------Heating Override Completed--------------------//
void heatingOverrideIsCompleted() {

  isHeatingOverrideOn = false;
  lcd.clear();
  lcd.print(F("   OVERRIDE IS OFF   "));
  Buzzer();
}

//---------------------Temperature display - ------------------------//
void temperatureDisplay() {

  if (TimePeriodIsOver(temperatureDisplayTimer, 15000)) {
    lcd.setCursor(0, 1);  // Range Temperature
    lcd.print(F("Range: "));
    lcd.print(range_sensor.getTempCByIndex(0), 1);
    lcd.write((char)223);
    lcd.print('C');
    lcd.setCursor(0, 2);  // Club Temperature
    lcd.print(F("Club:  "));
    lcd.print(club_sensor.getTempCByIndex(0), 1);
    lcd.write((char)223);
    lcd.print('C');
    lcd.setCursor(0, 3);  // Airgun Temperature
    lcd.print(F("Airgun:"));
    lcd.print(airgun_sensor.getTempCByIndex(0), 1);
    lcd.write((char)223);
    lcd.print('C');
  }
}

//----------------Serial Print Area Temperatures----------------------//
void serialPrintAreaTemperatures() {

  if (TimePeriodIsOver(sensorDelayTimer, 30000)) {
    PrintFileNameDateTime();
    Serial.print(F("Requesting temperatures (deg.C)..."));
    range_sensor.requestTemperatures();
    club_sensor.requestTemperatures();
    airgun_sensor.requestTemperatures();
    Serial.println(F(" done"));
    Serial.print(F("Range:  "));
    Serial.println(range_sensor.getTempCByIndex(0));
    Serial.print(F("Club:   "));
    Serial.println(club_sensor.getTempCByIndex(0));
    Serial.print(F("Airgun: "));
    Serial.println(airgun_sensor.getTempCByIndex(0));
  }
}

//-----------------------Heating Available---------------------------//
void heatingIsAvailable() {

  if (isRangeHeatingOn) {
    normalHeatingRangeArea();
  } else {
    frostPreventionRangeArea();
  }

  if (isClubHeatingOn) {
    normalHeatingClubArea();
  } else {
    frostPreventionClubArea();
  }

  if (isAirgunHeatingOn) {
    normalHeatingAirgunArea();
  } else {
    frostPreventionAirgunArea();
  }
}

//------------------------Boiler Interlock--------------------------//
void boilerInterlock() {

  if ((digitalRead(range_relay) == HIGH) || (digitalRead(club_relay) == HIGH) || (digitalRead(airgun_relay) == HIGH)) {
    digitalWrite(boiler_relay, HIGH);
    if (TimePeriodIsOver(heatingDelayTimer, 15000)) {
      Serial.println(F("Heating should be happening, as temperature is below the set point!"));
    }
  } else {
    digitalWrite(boiler_relay, LOW);
  }
}

//-------------------Frost Prevention Range--------------------------//
void frostPreventionRangeArea() {

  if ((range_sensor.getTempCByIndex(0) <= (frost_prev_setpoint - deadzone))) {
    digitalWrite(range_relay, HIGH);
    lcd.setCursor(13, 1);
    lcd.print(F("  ON  F"));
  } else {
    if ((range_sensor.getTempCByIndex(0) >= (frost_prev_setpoint + deadzone))) {
      digitalWrite(range_relay, LOW);
      lcd.setCursor(13, 1);
      lcd.print(F("  OFF F"));
    }
  }
}

//--------------------Frost Prevention Club-------------------------//
void frostPreventionClubArea() {

  if ((club_sensor.getTempCByIndex(0) <= (frost_prev_setpoint - deadzone))) {
    digitalWrite(club_relay, HIGH);
    lcd.setCursor(13, 2);
    lcd.print(F("  ON  F"));
  } else {
    if ((club_sensor.getTempCByIndex(0) >= (frost_prev_setpoint + deadzone))) {
      digitalWrite(club_relay, LOW);
      lcd.setCursor(13, 2);
      lcd.print(F("  OFF F"));
    }
  }
}

//---------------------Frost Prevention Airgun-----------------------//
void frostPreventionAirgunArea() {

  if ((airgun_sensor.getTempCByIndex(0) <= (frost_prev_setpoint - deadzone))) {
    digitalWrite(airgun_relay, HIGH);
    lcd.setCursor(13, 3);
    lcd.print(F("  ON  F"));
  } else {
    if ((airgun_sensor.getTempCByIndex(0) >= (frost_prev_setpoint + deadzone))) {
      digitalWrite(airgun_relay, LOW);
      lcd.setCursor(13, 3);
      lcd.print(F("  OFF F"));
    }
  }
}

//-------------------Normal Heating Range----------------------------//
void normalHeatingRangeArea() {

  if ((range_sensor.getTempCByIndex(0) <= (temperature_setpoint_range - deadzone))) {
    digitalWrite(range_relay, HIGH);
    lcd.setCursor(13, 1);
    lcd.print(F("  ON  T"));
  } else {
    if ((range_sensor.getTempCByIndex(0) >= (temperature_setpoint_range + deadzone))) {
      digitalWrite(range_relay, LOW);
      lcd.setCursor(13, 1);
      lcd.print(F("  OFF T"));
    }
  }
}

//--------------------Normal Heating Club ---------------------------//
void normalHeatingClubArea() {

  if ((club_sensor.getTempCByIndex(0) <= (temperature_setpoint_club - deadzone))) {
    digitalWrite(club_relay, HIGH);
    lcd.setCursor(13, 2);
    lcd.print(F("  ON  T"));
  } else {
    if ((club_sensor.getTempCByIndex(0) >= (temperature_setpoint_club + deadzone))) {
      digitalWrite(club_relay, LOW);
      lcd.setCursor(13, 2);
      lcd.print(F("  OFF T"));
    }
  }
}

//---------------------Normal Heating Airgun-------------------------//
void normalHeatingAirgunArea() {

  if ((airgun_sensor.getTempCByIndex(0) <= (temperature_setpoint_airgun - deadzone))) {
    digitalWrite(airgun_relay, HIGH);
    lcd.setCursor(13, 3);
    lcd.print(F("  ON  T"));
  } else {
    if ((airgun_sensor.getTempCByIndex(0) >= (temperature_setpoint_airgun + deadzone))) {
      digitalWrite(airgun_relay, LOW);
      lcd.setCursor(13, 3);
      lcd.print(F("  OFF T"));
    }
  }
}

// helper-functions
void PrintFileNameDateTime() {
  Serial.println(F("Code running comes from file "));
  Serial.println(F(__FILE__));
  Serial.print(F("  compiled "));
  Serial.print(F(__DATE__));
  Serial.print(F(" "));
  Serial.println(F(__TIME__));
}


// easy to use helper-function for non-blocking timing
// explanation see here
// https://forum.arduino.cc/t/example-code-for-timing-based-on-millis-easier-to-understand-through-the-use-of-example-numbers-avoiding-delay/974017
/*boolean TimePeriodIsOver(unsigned long &startOfPeriod, unsigned long TimePeriod) {
    unsigned long currentMillis = millis();
  if (currentMillis - startOfPeriod >= TimePeriod) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis;  // a new period starts right here so set new starttime
    return true;
  } else return false;  // actual TimePeriod is NOT yet over
}
*/

void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {
  static unsigned long MyBlinkTimer;
  pinMode(IO_Pin, OUTPUT);

  if (TimePeriodIsOver(MyBlinkTimer, BlinkPeriod)) {
    digitalWrite(IO_Pin, !digitalRead(IO_Pin));
  }
}

I am hopeful that someone will be able to identify what error I have managed to incorporate and suggest what I need to change/improve to correct it.

The Wokwi link to my file is here https://wokwi.com/projects/442190998089547777

I appreciate that I have been advised to start using arrays, etc, but until I can get the basics correct for the State machine, I do not want to complicate things further.

You seem to have forgotten how to use code tags when you post code

Please edit your post ad add them as appropriate

@UKHeliBob Evening, sorry, I thought I had inserted them. Hopefully, they should be there now!

@johnem51 Go to the IDE, press ctrl-T, then ctrl-shft-c, then come back, select that whole mess, and press ctrl-v. That should clean it up.

Thanks @camsysca

Not sure what happened - are those interesting \ characters in your code in the IDE? If not, please repeat one more time, BUT, select the standard markdown editor (top left corner of the post editing window) before performing the ctrl-v paste action.

I think that’s where they come from. My method description above comes from a time before we had the alternate edit modes, and I don’t particularly like the result.

Reading on the phone so not sure but may be your FSM flips back because myMode remains OVERRIDE_MODE when heatingOverride() runs, so even after terminating override, the loop sets it back.

Possibly try to force myMode to NORMAL_MODE when override ends.

void heatingOverrideIsCompleted() {
  isHeatingOverrideOn = false;
  myMode = NORMAL_MODE;  // >>>>>>>> force FSM to normal
  lcd.clear();
  lcd.print(F("   OVERRIDE IS OFF   "));
  Buzzer();
}

You might ask a moderator to re-open your original thread and move this thread over to it.

a7

You need to add a wire between resistor and LED for it to work. All the LEDs are redundant, but since you added them...

Additionally, If you want to make your diagram easier to read, you can hide the wires by deleting their color ("Black" -> "") in the diagram.json tab.

Your button acts as if it is forever stuck down because nothing ever sets isRightButtonPressed = false;

You can turn a variable into a one-shot by resetting the variable when you make use of it:

      if (isRightButtonPressed) {
        myMode = OVERRIDE_MODE;
        isRightButtonPressed = false;
      }

Oh, a comment in the code @-s me:

You already are using arrays. But, at least in some parts, only in a way that makes the code more complicated for no benefit at all. In fact, negative benefit, because bugs have crept in, hidden by the over-complication. I think much of your code could be simpler, shorter and more efficient.

I can't see a problem with your state machine. I think the problem is caused by your button handling code, as @DaveX suggested. Your button handling code is a great example of where your code is way more complex than it needs to be, resulting in bugs.

You could fix the problem in the way @DaveX suggested, but you would be missing the opportunity to learn how to spot over-complication in your own code and remove it. Let me know if you would like help with that.

Thanks @DaveX That suggestion has helped in that the Override Mode now cancels out after the time-out period, however, if manually terminated, the Override cancels and is then not available without pressing the Reset. I will check into this a bit further.

Hi @PaulRB I have been struggling a bit with this code for a long time, purely as I was not sure how to take the next step forward and hopefully tidy it up whilst learning. Any guidance that you may be able to offer would be most appreciated.

I didn’t look, but is the logic for the cancel button a similar one time latch that never gets reset?

There's a much easier way of dealing with saving multiple settings in EEPROM. Simply collect all your settings variables together into a struct. Then you can read and write the entire struct with a single EEPROM.get() and EEPROM.put(). No need to calculate individual addresses and risk getting them wrong. This won't increase wear on the EEPROM memory, because EEPROM.put() checks each byte of the struct to see if it was been changed.

2 things not quite right here.

  1. This doesn't need to be an array. It can be an ordinary non-array variable. If you can't see why, ask me.
  2. It should really be int not bool, because that's the type returned by digitalRead(), which returns the integer values HIGH or LOW. It doesn't return true or false. Ok, it does work because it just so happens that LOW == 0 == false and HIGH == 1 == true, but it's a bit of a "dirty habit" to assume that. There's no need to.

There's no need to have a separate debounce timer for each button. Maybe if 5 humans were pressing the 5 buttons, unaware of each other, it might be needed, but not in this case. You can simplify this by only reading all 5 buttons once every 50ms.

Here is where a lot of the over-complication of handling the buttons appears.

You have an array, buttonPressedFlag[], which indicates that a button has just been pressed. Then you use an array of functions to call individual functions based on which button was just pressed. All seems very clever, but...

When you look at what those individual functions actually do, they all do the same thing: they print a string to serial monitor, they make the buzzer sound and they set one of 5 non-array bool variables to true. So your array of bool variables get copied to one of 5 individual variables, all of which has no benefit, adds unnecessary complexity and could easily be avoided with a little thought.

Ask if you need more hints about this.

at just over 1000 line, the code seems excessive, difficult to maintain -- read, debug, enhance. It looks like it can be simplified and made easier to work on and debug

can you provide a description of features?

the purpose for some of these functions is not ovious

bool TimePeriodIsOver(unsigned long &startOfPeriod, unsigned long TimePeriod) {
typedef void (*functionPtrs)(void);
void setup() {
void loop() {
void checkRTC() {
void setPinModes() {
void setupOneWireSensors() {
void initiateSetpoints() {
void retrieveSetpoints() {
void saveSetpoints() {
void initLCD() {
void Heartbeat() {
void backGroundItems() {
void backlightDisplay() {
void buttonSettings() {
void buttonControl() {
void leftButton() {
void upButton() {
void downButton() {
void rightButton() {
void enterButton() {
void Buzzer() {
void printTheTime(DateTime theTimeP) {
void printStateIf(int theState) {
void myStateMachine() {
void normalModeSelect() {
void heatingOverride() {
void heatingOverrideIsCompleted() {
void temperatureDisplay() {
void serialPrintAreaTemperatures() {
void heatingIsAvailable() {
void boilerInterlock() {
void frostPreventionRangeArea() {
void frostPreventionClubArea() {
void frostPreventionAirgunArea() {
void normalHeatingRangeArea() {
void normalHeatingClubArea() {
void normalHeatingAirgunArea() {
void PrintFileNameDateTime() {
void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {

what is this? why is theTime in quotes? theTime is defined as DateTime

    DateTime theTime = dst_rtc.calculateTime (standardTime);