// LIBRARIES INCLUDES ---------------------------------------------------------------------------------------------
#include <Arduino.h>
 // DEFINES -------------------------------------------------------------------------------------------------------

// DEFINE DEBUG MODE ACTIVE/DEACTIVE
#define DEBUG 1
#if DEBUG == 1
#define debug(x) Serial.print(x)
#define debugln(x) Serial.println(x)
#else
#define debug(x)
#define debugln(x)
#endif
// DEFINE WHETHER THE LOOPS SHOULD BE SHOWN WHEN DEBUG MODE IS ON
#define DEBUG_LOOPS 0

// DEFINES GENERAL
#define HOSTNAME "FirstFloorSwitchBoard"
#define ONBOARD_LED 2
#define DIGITAL_OUTPUTS 2
#define DIGITAL_INPUTS 5
#define TIMERS_ON_OFF 0

// DEFINES RELAY OUTPUTS PINS
#define RELAY0 12 
#define RELAY1 14 

// DEFINES INPUTS PINS
#define INPUT0 21 //Feedback
#define INPUT1 22 //Feedback
#define INPUT2 25 //Switch Taster
#define INPUT3 26 //Switch Relay 0
#define INPUT4 27 //Switch Relay 1


// FEEDBACK SIGNAL FOR RELAYS WITH ASSIGNED FEEDBACK INPUT (only when relay is set to type 1 (pulse relay))
const byte feedbackInputFailCountsMax = 5;
const unsigned long feedbackSignalMonitoringTimeMs = 1500;

// VARIOUS --------------------------------------------------------------------------------------------------------
long lastMillis = 0;
long loops = 0;
const int READ_INPUTS_CYCLIC = 300; // Read inputs cyclic all xxxMilliseconds
const int CHECK_AUTO_OFF_CYCLIC = 10000; // Check if any relay must be switched off automatically by time xxxMilliseconds
const int CHECK_FEEDBACK_SIGNAL = 100; // When a relay has an assinged feedback input check every set xxxMilliseconds wheterh a signal check is required (must be less than the set 'expectedFeedbackTimeMs'!)
const int SET_RELAYS_IMPULSE_CYCLIC = 50; // Check if Relay need to be set by impulse mode all xxxMilliseconds
int offsetTimeSyncCyclic = 0; // Offset to sync call to clock
bool firstCycleDone = false;
unsigned long setRelaysImpulse;
unsigned long checkAutoOffRelays;
unsigned long checkFeedbackSignal;
unsigned long readInputsCalled; // Time set when function to read inputs has been called
unsigned long currentMillis;

// RELAY OUPUTS DEFINITON AND MQTT TOPICS STRUCTS  ----------------------------------------------------------------
    //Info for feedbackInput:
    //Number of input which is used as feedback (used when a relay works as a pulse relay), 128 = no feedback signal defined
    //for example relay 0 expects feedback from input 1, we assign input 1 to relay 0, relay 1 has no feedback signal, we assing 128
struct outputDefinition {
  int outputPins[DIGITAL_OUTPUTS] = {
    RELAY0,
    RELAY1
  };
  int feedbackInput[DIGITAL_OUTPUTS] = {
    0,
    1
  };   
  const char * outputBaseTopic[DIGITAL_OUTPUTS] = {
    "basement/switchBoard/relay0",
    "basement/switchBoard/relay1"
  };
  const char * outputStateTopic[DIGITAL_OUTPUTS] = {
    "basement/switchBoard/relay0/state",
    "basement/switchBoard/relay1/state"
  };
  const char * outputTimerTopic[DIGITAL_OUTPUTS] = {
    "basement/switchBoard/relay0/timer",
    "basement/switchBoard/relay1/timer"
  };   
};
struct outputDefinition outputDefine;

// FEEDBACK SIGNAL STATE STRUCT  ---------------------------------------------------------------------------------
  //Info for setTime:
  //When the relay is set to type 1 (pulse relay) and a state change is requested by autoOff or clock then set the current time into this var  to compare later on after a defined time whether the state is correct
  //Info for expectedFeedbackTimeMs:
  //expected time till the correct feedback state should be given
  //Info for compareType:
  //when is set to 1 it checks desired state to current state for equlity else for unequality
  //Info for desiredState:
  //is the expected state for the feedbacksignal
  //Info for failureCount:
  //When feedback state is not correct add 1 to the counter
  //Info for failureCountMax:
  //When defined max counter, when it is reached se
struct feedbackSignal {
  unsigned long setTime[DIGITAL_OUTPUTS] = {
    0,
    0
  };
  unsigned long expectedFeedbackTimeMs[DIGITAL_OUTPUTS] = {
    500,
    500
  };
  bool compareType[DIGITAL_OUTPUTS] = {
    0,
    0
  }; 
  bool desiredState[DIGITAL_OUTPUTS] = {
    0,
    0
  };  
  int failureCount[DIGITAL_OUTPUTS] = {
    0,
    0
  };
  int failureCountMax[DIGITAL_OUTPUTS] = {
    3,
    3
  };       
};
struct feedbackSignal feedbackSignalCheck;

// TIMER STRUCT  --------------------------------------------------------------------------------------------------
struct relayTimerStruct {
  byte relay;
  byte relayType; // 0 = standard relay / 1 = impulse relay (stairway automatic, needs feedback signal of an input whether lamp is on or off)
  byte setMode;   // 0 = manual / 1 = clock timer / 2 = impulse / 3 = autoOff
  unsigned long autoOffMilliSec;
  unsigned long impulseMilliSec = 0;
  unsigned long startImpulseMilliSec;
  byte day;
  int switchOn[TIMERS_ON_OFF];
  int switchOff[TIMERS_ON_OFF];
  bool setByRemoteControl = 0;
};
struct relayTimerStruct relayTimers[DIGITAL_OUTPUTS];

// INPUT STATE STRUCT  --------------------------------------------------------------------------------------------
struct inputDefinition {
  int inputPin[DIGITAL_INPUTS] = {
    INPUT0,
    INPUT1,
    INPUT2,
    INPUT3,
    INPUT4
  };
  const char * inputBaseTopic[DIGITAL_INPUTS] = {
    "basement/switchBoard/input0",
    "basement/switchBoard/input1",
    "basement/switchBoard/input2",
    "basement/switchBoard/input3",
    "basement/switchBoard/input4"
  };
  const char * inputStateTopic[DIGITAL_INPUTS] = {
    "basement/switchBoard/input0/state",
    "basement/switchBoard/input1/state",
    "basement/switchBoard/input2/state",
    "basement/switchBoard/input3/state",
    "basement/switchBoard/input4/state"
  }; 
};
struct inputDefinition inputDefine;

// INPUT STATE STRUCT  --------------------------------------------------------------------------------------------
struct inputStates {
  bool current = 0;
  bool old = 0;
  unsigned long switchOnTimeMs;
};
struct inputStates inputState[DIGITAL_INPUTS];

// COMPARE FEEDBACK SIGNAL (only for relays with defined feedback input) --------------------------------------------

void compareFeedbackSignal(int inputNo, bool desiredState, int relayNo, bool equal){
  //compare states of desired state and feedback input if set to equal both must have the same state
  debug("Compare mode is (1=equal / 0=unequal): ");
  debugln(equal);   
  debug("Feedback input no: ");
  debug(inputNo);  
  if(inputState[inputNo].current == desiredState && equal){
    //reset counter
    feedbackSignalCheck.failureCount[relayNo] = 0;  
    debug(" is in correct state, equal to desired state. ");
  }else if(inputState[inputNo].current != desiredState && !equal){
    //reset counter
    feedbackSignalCheck.failureCount[relayNo] = 0;
    debug(" is in correct state, not equal to desired state. ");
  }else{
    feedbackSignalCheck.failureCount[relayNo] += 1;
    if(feedbackSignalCheck.failureCount[relayNo] >= feedbackSignalCheck.failureCountMax[relayNo]){
      //client.publish(outputDefine.outputStateTopic[relayNo], "ERROR");
      debug("Failure Count ERROR");
    }else{
      //client.publish(outputDefine.outputStateTopic[relayNo], "WARNING");
      debug("Failure Count WARNING");
    }
    debug(" is not in correct state to desired state. ");  
  }
  debug("Input state is: ");
  debug(inputState[inputNo].current);
  debug(" / Desired state is: ");
  debugln(desiredState);  
  debug("Fail counter is now: ");
  debugln(feedbackSignalCheck.failureCount[relayNo]); 
}


// IOs ------------------------------------------------------------------------------------------------------------
void setup_ios() {
  // Outputs
  if(DIGITAL_OUTPUTS != 0){
    pinMode(RELAY0, OUTPUT);
    pinMode(RELAY1, OUTPUT);
     // set outputs by default off
    digitalWrite(RELAY0, LOW);
    digitalWrite(RELAY1, LOW);     
  }
 
  // Inputs
  if(DIGITAL_INPUTS != 0){  
    //INPUT_PULLUP  
    pinMode(INPUT0, INPUT_PULLUP);
    pinMode(INPUT1, INPUT_PULLUP);
    pinMode(INPUT2, INPUT_PULLUP);
    pinMode(INPUT3, INPUT_PULLUP);
    pinMode(INPUT4, INPUT_PULLUP);      
  }
}

// SET DIGITAL OUTPUTS (RELAYS) -----------------------------------------------------------------------------------
void setOutputs(bool OnOff, int relayNumber) {
    
  if (!OnOff) { // Switch off
    debug("RELAY: ");
    debug(relayNumber);
    debugln(" OFF");
    digitalWrite(outputDefine.outputPins[relayNumber], HIGH);
    //reset current switch on time
    relayTimers[relayNumber].startImpulseMilliSec = 0;
    //client.publish(outputDefine.outputStateTopic[relayNumber], "OFF");
  } else if (OnOff) { // Switch on
    debug("RELAY: ");
    debug(relayNumber);
    debugln(" ON");
    digitalWrite(outputDefine.outputPins[relayNumber], LOW);
    //for relays defined as type 1 (impulse relay) set desired state for input to check whether the feedback signal is in correct position
    if(relayTimers[relayNumber].relayType == 1 && outputDefine.feedbackInput[relayNumber] != 128){
      feedbackSignalCheck.setTime[relayNumber] = currentMillis;
      int inputNo = outputDefine.feedbackInput[relayNumber];     
      if(inputState[inputNo].current == LOW){
        debugln("Impulse relay feedback check low");
        feedbackSignalCheck.desiredState[relayNumber] = 0;
      }else{
        debugln("Impulse relay feedback check high");
        feedbackSignalCheck.desiredState[relayNumber] = 1; 
      }
      debug("Feedback-Input state: ");
      debugln(inputState[inputNo].current);  
      debug("Desired feedback state: ");
      debugln(feedbackSignalCheck.desiredState[relayNumber]);  
    }    
    //set current switch on time
    relayTimers[relayNumber].startImpulseMilliSec = currentMillis;
    //client.publish(outputDefine.outputStateTopic[relayNumber], "ON");
  } else {
    debug("RELAY: ");
    debug(relayNumber);
    debugln("OUTPUT STATE UNDEFINDED!");
  }
}

// READ DIGITAL OUTPUT STATES (RELAYS) ----------------------------------------------------------------------------
void readoutputStates() {

  if(DIGITAL_OUTPUTS != 0){
    for (int i = 0; i < DIGITAL_OUTPUTS; i++) {
      if (digitalRead(outputDefine.outputPins[i]) == 0) {
        //client.publish(outputDefine.outputStateTopic[i], "ON");
      } else {
        //client.publish(outputDefine.outputStateTopic[i], "OFF");
      }
    }
  }
}

// READ INPUTS AND SEND MQTT IF THE STATE HAS CHANGED -------------------------------------------------------------
void readInputs(bool force) {
//parameter force is used when websocket connects and request a HeartBeat from this ESP32 device

  if(DIGITAL_INPUTS != 0){
    /*
      inputDefine[0].current =    digitalRead(PinNumber1);
      inputDefine[1].current =    pcfIn.digitalRead(PinNumber2);
    */
  
    // check if input state has been changed to the previos cycyle state
    for (int i = 0; i < DIGITAL_INPUTS; i++) {
      // get current state
        inputState[i].current = digitalRead(inputDefine.inputPin[i]);
      
      // compare old and current state
      if (inputState[i].old != inputState[i].current || force) {
        inputState[i].old = inputState[i].current;
  
        // send mqtt signal on/off for input state
        if (inputState[i].current == LOW) {
          debug("Key: ");
          debug(i);
          debugln(" pressed");
          //client.publish(inputDefine.inputStateTopic[i], "ON");
          //set switch on time to current time
          inputState[i].switchOnTimeMs = millis();
          if(i==3){
            if(digitalRead(outputDefine.outputPins[i]) == 0){
              setOutputs(1, 0);
              debugln("set ON relay 0");
            }else{
              setOutputs(0, 0);
              debugln("set OFF relay 0");
            }
          }
          else if(i==4){
            if(digitalRead(outputDefine.outputPins[i]) == 0){
              setOutputs(1, 1);
              debugln("set ON relay 1");
            }else{
              setOutputs(0, 1);
              debugln("set OFF relay 1");
            }
          }          
        } else {
          debug("Key: ");
          debug(i);
          debugln(" not pressed");
          //client.publish(inputDefine.inputStateTopic[i], "OFF");
          //set swtich on time to 0
          inputState[i].switchOnTimeMs = 0;
        }
      }
    }
  }
}

// ARDUINO SETUP --------------------------------------------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  if (DEBUG) {
    Serial.println("DEBUG MODE IS ON");
  } else {
    Serial.println("DEBUG MODE IS OFF");
  }
  Serial.println("setup in progress...");
  
  // if onboard LED is activated
  if (ONBOARD_LED != 0){
    pinMode(ONBOARD_LED,OUTPUT);
  }
  // init inputs + outputs
  setup_ios();
  Serial.println("setup is done!");
}
// ----------------------------------------------------------------------------------------------------------------
// PROGRAM LOOP ---------------------------------------------------------------------------------------------------
void loop() {

  // LOOP COUNTER
  currentMillis = millis();
  loops++;

  if (currentMillis - lastMillis > 1000 && DEBUG_LOOPS) {
    debug("Loops last second:");
    debugln(loops);
    lastMillis = currentMillis;
    loops = 0;
  }

  // READ INUPTS CYCLIC
  if (currentMillis - readInputsCalled >= READ_INPUTS_CYCLIC && DIGITAL_INPUTS != 0) {
    readInputs(0);
    readInputsCalled = currentMillis;
  } 

  // CHECK FEEDBACK SIGNAL (only when relay type 1 (impulse relay) )
  if (currentMillis - checkFeedbackSignal >= CHECK_FEEDBACK_SIGNAL && DIGITAL_INPUTS != 0 && DIGITAL_OUTPUTS != 0) {
    for (int i = 0; i < DIGITAL_OUTPUTS; i++) {
      if(relayTimers[i].relayType == 1){          
        if( feedbackSignalCheck.setTime[i] != 0 && (currentMillis - feedbackSignalCheck.setTime[i] >= feedbackSignalCheck.expectedFeedbackTimeMs[i]) ){
          debug("CurrentMillis: ");
          debugln(currentMillis);
          debug("Set Time: ");
          debugln(feedbackSignalCheck.setTime[i]);
          debug("Expected Feedback Time: ");
          debugln(feedbackSignalCheck.expectedFeedbackTimeMs[i]);            
          //call function to compare state, parameter 1=inputNo, 2=desiredState 3=relayNo 4=equal
          compareFeedbackSignal(outputDefine.feedbackInput[i], feedbackSignalCheck.desiredState[i], i, feedbackSignalCheck.compareType[i]);
          feedbackSignalCheck.setTime[i] = 0;              
        }
      }
    }
    checkFeedbackSignal = currentMillis;
  }       

  // RELAY IMPULSE MODE CYCLIC
  if (currentMillis - setRelaysImpulse >= SET_RELAYS_IMPULSE_CYCLIC && DIGITAL_OUTPUTS != 0) {
    for (int i = 0; i < DIGITAL_OUTPUTS; i++) {
      //check if mode is impulse or relay type is pulse relay
      if (relayTimers[i].setMode == 2 || relayTimers[i].relayType == 1) {
        // If time matches then switch relay off  
        if ((currentMillis - relayTimers[i].startImpulseMilliSec >= relayTimers[i].impulseMilliSec) && relayTimers[i].startImpulseMilliSec > 0) {     
          debug("Result: ");
          debugln(currentMillis - relayTimers[i].startImpulseMilliSec);
          debug("currentMillisec: ");
          debugln(currentMillis);
          debug("impulseMilliSec: ");
          debugln(relayTimers[i].impulseMilliSec);
          debug("startImpulseMilliSec: ");
          debugln(relayTimers[i].startImpulseMilliSec);
          relayTimers[i].startImpulseMilliSec = 0;
          // Switch off relay
          setOutputs(0, i);
        }
      }
    }
    setRelaysImpulse = currentMillis;
  }

  // CHECK FOR AUTO OFF CYCLIC
  if (currentMillis - checkAutoOffRelays >= CHECK_AUTO_OFF_CYCLIC && DIGITAL_INPUTS != 0 && DIGITAL_OUTPUTS != 0) {
    for (int i = 0; i < DIGITAL_OUTPUTS; i++) {
      //get index for the relay which is assigned to the input
      int assignedInput = outputDefine.feedbackInput[i];
      
      debug("Index: ");
      debugln(assignedInput);      
      debug("Input ");
      debug(i);
      debug(" switch on Time: ");
      debugln(inputState[assignedInput].switchOnTimeMs);
      debug("Relay Type: ");
      debugln(relayTimers[i].relayType);
      debug("Relay Mode: ");
      debugln(relayTimers[i].setMode);  
      debug("Auto Off Time Milliseconds: ");
      debugln(relayTimers[i].autoOffMilliSec);
      
      //check if a relay is assinged to an input, if 128 then it is not and if the max failure counter has not reached yet
      if(assignedInput != 128 && feedbackSignalCheck.failureCount[i] < feedbackSignalCheck.failureCountMax[i]){
        //check if this relay is set to 1 (impulse relay type), whether mode 3 (autoOff) is set and if the autoOff time bigger 0
        //in case the feedback signal check reaches the max count, then stop trying to switch off, this in order to avoid endless switch off tries when the feedback signal does not work
        if(relayTimers[i].relayType == 1 && relayTimers[i].setMode == 3 && relayTimers[i].autoOffMilliSec > 0 ){        
          //check if the duration since the input was set expires or equal the set autoOff time
          if ( ( (relayTimers[i].autoOffMilliSec + inputState[assignedInput].switchOnTimeMs) <= currentMillis ) && inputState[assignedInput].switchOnTimeMs != 0) {
            // Since the relay works as an impulse relay it must switch on first and off after the defined time it will switch off by the above condition "RELAY IMPULSE MODE CYCLIC"
            debug("Relay ");
            debug(i);
            debug(" Switched Off by AutoOff after ");
            debug(relayTimers[i].autoOffMilliSec);
            debugln(" Milliseconds");
            setOutputs(1, i);
          }
        }
      }
    }
    checkAutoOffRelays = currentMillis;
  }  


  // SET FIRST CYCLE DONE TO TRUE AND SEND FIRST HEARTBEAT
  if (firstCycleDone == false) {
    firstCycleDone = true;
    //client.publish(mqttTopicHeartBeat, "online");    
    // read outputStateTopics
    readoutputStates();
  }
}
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module