// pulse an output if switch changes to low
// using a finite state machine
// https://forum.arduino.cc/t/switching-circuit-using-relays-and-ups-with-arduino/1019626/

#define DEBUG 1    // 1 debug messages, 0 off
#if DEBUG
#define DEBUG_PRINT(...) Serial.print(__VA_ARGS__)
#else
#define DEBUG_PRINT(...)
#endif

constexpr uint8_t MAINSsupply {7};         // AC power detection using a 5v charger
constexpr uint8_t UPSrelay {9};            // UPS relay
//constexpr uint8_t upsSwitch {11};          // connected to UPS on/off switch
constexpr uint8_t MAINSrelay {12};         // MAINS relay

constexpr uint16_t intervalOn {1000};      // pulse time for UPS ON signal
constexpr uint16_t intervalOff {3000};     // pulse time for UPS OFF signal
constexpr uint16_t intervalWarning {3000}; // time before UPS will get switched on
constexpr uint16_t intervalRecover {5000}; // time before UPS will be switched off
constexpr uint16_t intervalCheckMAINS {10000}; // MAINS checking Time

enum class State {INITIALIZE,              // Check if Mains is available
                  IDLE,                    // AC is on,  waiting fo AC black out
                  WARNING,                 // AC is off, waiting for UPS switch on
                  MAINSRELAY_OFF,
                  UPSRELAY_ON,
                  FAIL,                    // AC is off, waiting for AC coming back
                  RECOVER,                 // AC is on,  recovering period, AC is back but we keep UPS running
                  UPSRELAY_OFF,            // UPS RELAY Off
                  BACK                     // preparing things back for idle
                 };                       
// a class to pulse a pin - nonblocking
class PulsePin
{
    const uint8_t pin;
    uint32_t previousMillis = 0;
    bool state = LOW;              // state of the pin
    uint16_t interval = intervalOn;
  public:
    PulsePin (const uint8_t pin ) : pin (pin) {}
    void begin()
    {
      pinMode(pin, OUTPUT);
    }
    void pulse(uint16_t interval)
    {
      this->interval = interval;
      digitalWrite(pin, HIGH);
      previousMillis = millis();
      state = HIGH;
    }
    void update()
    {
      if (state && millis() - previousMillis > interval )
      {
        digitalWrite(pin, LOW);
        state = LOW;
      }
    }
};
PulsePin upsSwitch(11);

void checkAC()
{
  static uint32_t previousMillis = 0;          // timestamp for state machine
  static State state {State::INITIALIZE};            // current state
  static uint8_t previousInputState = HIGH;    // previous state of input circuit / AC
  uint8_t inputState = digitalRead(MAINSsupply);  // current state of input circuit / AC
  switch (state)
  {   case State::INITIALIZE :
      DEBUG_PRINT('.');
      if (inputState == HIGH )  // check for AC presence
      if (millis() - previousMillis > intervalWarning)
      {
        Serial.println(F("\nAC ON - Enter IDLE"));
        digitalWrite(UPSrelay, LOW);  //  UPS relay turned OFF 
        digitalWrite(MAINSrelay, LOW);  //  MAINS relay turned OFF(MAINS ON)
        previousMillis = millis(); 
        state = State::IDLE;
      }
      if (inputState == LOW )  // check for AC fail
      if (millis() - previousMillis > intervalWarning)
      {
        Serial.println(F("\nAC off - Enter WARNING"));
        previousMillis = millis();
        state = State::WARNING;
      }
      break;
      case State::IDLE :
      DEBUG_PRINT('I');
      if (inputState == LOW && previousInputState == HIGH)  // check for AC fail
      {
        Serial.println(F("\nAC off - Enter WARNING"));
        previousMillis = millis();
        state = State::WARNING;
      }
      break;
     case State::WARNING :
      DEBUG_PRINT('W');
      if (inputState == HIGH && previousInputState == LOW)  // check for AC back
      {
        Serial.println(F("\nAC on again - go back to IDLE"));
        state = State::IDLE;
      }
      if (millis() - previousMillis > intervalWarning)
      {
        Serial.println(F("\nAC off --> MAINSRELAY_OFF"));
        digitalWrite(MAINSrelay, HIGH);  //  MAINS relay turned ON (MAINS OFF)
        previousMillis = millis();
        state = State::MAINSRELAY_OFF;
      }
      break;
     case State::MAINSRELAY_OFF :
      DEBUG_PRINT('m'); // lower m for OFF
      if (inputState == HIGH && previousInputState == LOW)  // check for AC back again
      {
        Serial.println(F("\nAC on again Turning MAINSrealy ON and go back to IDLE"));
        previousMillis = millis();
        digitalWrite(MAINSrelay, LOW);  //  MAINS relay turned OFF again (MAINS ON)
        state = State::IDLE;
      }
      if (millis() - previousMillis > intervalWarning)
      {
        Serial.println(F("\nAC off --> UPSRELAY_ON"));
        digitalWrite(UPSrelay, HIGH);  //  UPS relay turned ON
        previousMillis = millis();
        state = State::UPSRELAY_ON;
      }    
     break;
     case State::UPSRELAY_ON :
      DEBUG_PRINT('U'); // upper U for ON
      if (inputState == HIGH && previousInputState == LOW)  // check for AC back again
      {
        Serial.println(F("\nAC on again Turning MAINSrealy ON and go back to IDLE"));
        previousMillis = millis();
        digitalWrite(UPSrelay, LOW);  //  UPS relay turned OFF again
        digitalWrite(MAINSrelay, LOW);  //  MAINS relay turned OFF again (MAINS ON)
        state = State::IDLE;  
      }
      if (millis() - previousMillis > intervalWarning)
      {
        Serial.println(F("\nAC off --> FAIL"));
        upsSwitch.pulse(intervalOn); // switching UPS ON
        state = State::FAIL;
      }    
     break;
     case State::FAIL :
      DEBUG_PRINT('X');
      if (inputState == HIGH && previousInputState == LOW)
      {
        Serial.println(F("\nAC on --> Checking If Mains is Stable"));
        previousMillis = millis();
        state = State::RECOVER;
      }
      break;
      case State::RECOVER :
      DEBUG_PRINT('R');
      if (inputState == LOW && previousInputState == HIGH)
      {
        Serial.println(F("\nAC off - Not Stable --> go back to FAIL"));
        state = State::FAIL;
      }
      if (millis() - previousMillis > intervalCheckMAINS)
      {
        Serial.println(F("\nAC on - MAINS Stable --> UPSRELAY_OFF"));
        digitalWrite(UPSrelay, LOW);  //  UPS relay turned OFF
        previousMillis = millis();
        state = State::UPSRELAY_OFF;
      }
      break;
      case State::UPSRELAY_OFF :
      DEBUG_PRINT('u'); // lower u for OFF
      if (inputState == LOW && previousInputState == HIGH)  // check for AC fail again in between
      {
        Serial.println(F("\nAC OFF in Between, Turning UPSRELAY ON again"));
        digitalWrite(UPSrelay, HIGH);  //  UPS relay turned ON again
        previousMillis = millis();
        state = State::FAIL;
      }
      if (millis() - previousMillis > intervalRecover)
      {
        Serial.println(F("\nAC on --> Turn OFF UPS"));
        upsSwitch.pulse(intervalOff);  // switching UPS OFF
        previousMillis = millis();  
        state = State::BACK;
      }
      break;
      case State::BACK :
      DEBUG_PRINT('B');
      if (inputState == LOW && previousInputState == HIGH)  // check for AC fail again in between
      {
        Serial.println(F("\nAC OFF in Between, Turning UPSRELAY ON again"));
        previousMillis = millis();
        state = State::INITIALIZE;
      } 
      if (millis() - previousMillis > intervalRecover)
      {
        Serial.println(F("\nAC on - longer than timelimit --> IDLE again"));
        digitalWrite(MAINSrelay, LOW);  //  MAINS relay turned OFF (MAINS ON)
        previousMillis = millis();
        state = State::IDLE;
      }
      break;
  }
  previousInputState = inputState;
  delay(20); // this dirty delay is just to debounce switches
}

// this function gets called during delays. 
// so we use it also to check if we have to do something with the pulsePin
void yield()
{
  upsSwitch.update(); 
}


void setup() {
  Serial.begin(115200);
  Serial.println(F("Initializing Please Wait..."));
  upsSwitch.begin();
  pinMode(UPSrelay, OUTPUT);
  pinMode(MAINSrelay, OUTPUT);
  delay(50);
}

void loop() {
  checkAC();
  upsSwitch.update(); // must be called in loop - no delays allowed ;-)
#if DEBUG
  delay(500); // this is only to see which state is active, remove that in production
#endif
}