/*
 * Copyright (c) 2022, Vincent Bloemen (VinzzB)
 * All rights reserved.
 *   
 * This source code is licensed under the Apache 2.0 license found in the
 * LICENSE file in the root directory of this source tree. 
 * 
 * 
 * Philips Airfryer. Model: HD9240
 * Creator: Vincent Bloemen (VinzzB / https://vinzz.be)
 * Github: https://github.com/VinzzB/Arduino-Airfryer
 * Simulator: https://wokwi.com/projects/335149333902000724
 *
 * Airfryer components :
 * 1x 230Vc motor
 * 1x 230Vc Spiral heating coil
 * 1x Temperature sensor
 * 
 * 
 * Circuit:
 * - A1: Temperature Sensor
 * - A4: LiquidCrystal Display - Data (SDA)
 * - A5: LiquidCrystal Display - Clock (SCL)
 * - D2: Rotary Encoder - Switch (SW)
 * - D3: Rotary Encoder - Data (DT)
 * - D4: Rotary Encoder - Clock (CLK)
 * - D7: Fan Relay  / Blue led
 * - D8: Heater Relay / Red led
 * - D9: PWM Piezo Speaker (SPK) 
 * 
 * LEDs:
 * - D8 -> 220 Ohm -> Red Led -> Ground
 * - D7 -> 220 Ohm -> Blue Led -> Ground
 * 
 * REQUIRED LIBRARIES
 * - LiquidCrystal I2C (optionally from https://github.com/fmalpartida/New-LiquidCrystal if needed)
 * - Encoder
 *
 */

/* INCLUDES */
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#include "FryEngine.h"
#include "LCD1602.h"
#include "MultiButton.h"
#include <Encoder.h>
#include "Eeprom_cookbook.h"

/* PROPERTIES */
const byte heaterPin      = 8;     //D8 = pin 14
const byte fanPin         = 7;     //D7 = pin 13
const byte buttonPin      = 2;     //D2 = pin 04 - Interrupt 0 (!)
const byte tempSensorPin  = A1;    //A1 = pin 24
const byte speakerPin     = 9;     //D9 = pin 15
const byte rotaryDatPin   = 3;     //D3 = pin 05 - Interrupt 1 (!)
const byte rotaryClkPin   = 4;     //D4 = pin 06

const bool invertRotary   = false; //swap rotary direction.
const int buzzFrequency   = 3520;  //buzzer frequency (3520 = NOTE_A7)
const int exitEditDelay   = 10;    //in seconds! 
const int powerOffTimeout = 60;    //in seconds! PowerOff does not work in WokWi simulation
const int preHeatTimeout  = 300;   //in seconds!
const int tempSteps[]     = {1,  5, 10,  20,  30,  40,  50,  60}; //rotary intervals. (slow > fast rotations)
const int timeSteps[]     = {1, 15, 60, 120, 180, 240, 300, 360}; //rotary intervals. (slow > fast rotations)

LiquidCrystal_I2C lcd(0x27, 16, 2); //find I2C address with I2C_scanner script
//LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); //0x20 & 0x27

/* FOODLIST */
//max steps per product. See MAX_STEPS in Product.h
Product           product = { "Custom 0", 0, MAX_STEPS };
EEPROM_Cookbook   cookbook(1024);

/* GLOBAL VARS */
LCD1602           screen(lcd);
FryEngine         engine(heaterPin, fanPin, tempSensorPin, preHeatTimeout, &stepCompletedCallBack);
Encoder           rotary(rotaryDatPin, rotaryClkPin);
MultiButton       button;           //rotary button.
byte              menuProductIdx    = 0;
byte              menuStepIdx       = 0;
byte              ADCSRA_State;     //used for hibernate state.
long              oldPosition       = -999; //rotary last position
byte              switchPreHeatText = 0; //counter for switching preheat text on screen
unsigned long     editingSince      = 0;
unsigned long     lastActionOn      = millis();
bool              isDirty           = false;
short             dialogResult      = 0;

void setup() {  
    
  //SERIAL 
  delay(100);
  Serial.begin(2000000); 

  //SPEAKER
  pinMode(speakerPin,OUTPUT); //piezo
  tone(speakerPin, buzzFrequency, 500);

  //ROTARY ENCODER AND BUTTON
  pinMode(rotaryDatPin, INPUT_PULLUP);
  pinMode(rotaryClkPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(buttonPin), wakeUpInterrupt, FALLING); //wake-up interrupt.
  button.setup(buttonPin);
  
  //TEMPERATURE SENSOR
  engine.resetTemperature();

  //LCD SCREEN
  screen.init();

  //EEPROM  
  if(!cookbook.containsData()) {
    lcd.setCursor(0,0);
    lcd.print(F("Preparing EEPROM"));
    lcd.setCursor(0,1);
    lcd.print(F("structure..."));
    cookbook.prepareEEPROM();
  }
  cookbook.readProduct(menuProductIdx, &product);
  screen.printMenu(product.name); //show menu on startup
}

void loop() {

  //go to sleep?
  checkSleepMode();  
  
  //exit edit mode when idle for [exitEditDelay] seconds.
  bool exitEdit = (millis() - editingSince) / 1000 >= exitEditDelay;
  if(screen.current > SCREEN_RUNNING && exitEdit) {
    if(screen.current == SCREEN_EDIT_NAME){
      screen.current = SCREEN_MENU;
    } else if(engine.isRunning()) {
      menuStepIdx = engine.getCurrentStepIdx(); //go back to the step being processed by the engine.
      screen.current = SCREEN_RUNNING;     
    } else {
      screen.current = SCREEN_PRODUCT;
    }    
    lcd.noBlink();
  }

  //Process engine ticks and update screen.
  if(engine.timer()) {
    //Update running screen
    screen.menuBlinkItem = !screen.menuBlinkItem;
    if(screen.current != SCREEN_MENU 
    && screen.current != SCREEN_MENU_SAVE
    && screen.current != SCREEN_EDIT_NAME)
      printRunDisplay();
  }
  
  //reset hibernate timeout when engine is running
  if(engine.isRunning())
    lastActionOn = millis();
  
  //execute user interactions
  userInteraction();
}

void checkSleepMode() {
  //Did device State changed
  bool powerOff = (millis() - lastActionOn) / 1000 >= powerOffTimeout;
  if(powerOff) {
      //Serial.println("Hibernate");      
      screen.lcdPowerMode(false);
      goToSleep(); 
      //wakes-up here...        
      lastActionOn = millis();  // set last action time. 
      engine.resetTemperature(); //reset temp buffer.
      screen.lcdPowerMode(true);         
      rotary.write(0); //reset rotary movements
      //Serial.println("Woke up from hibernate");        
      //wait for button release (= LOW while pressed)
      while(!digitalRead(buttonPin)) { delay(10); };            
  }
}

void goToSleep() {
  //https://www.youtube.com/watch?v=urLSDi7SD8M
  //init sleep mode
  __asm__("sei");                         //make sure interrupts are on  (awaker)
  ADCSRA_State = ADCSRA;                  //store current register config
  ADCSRA &= ~(1<<7);                      // disable ADC // ~ = inverted
  SMCR |= (1<<2);                         //power down mode ('1' shift 2 places as OR (only changes second bit))
  SMCR |= 1;                              //enable sleep; (first bit)
  MCUCR |= (3 << 5);                      //set BODS and BODSE (shift 11 with 5 places)
  MCUCR = (MCUCR & ~(1 << 5)) | (1 << 6); //set BODS bit and clear BODSE bit.   
  __asm__("sleep");                       //Goodnight! (inline assembler: executes sleep command)  
  //SLEEPING....
  //awakes here when interrupted!
  SMCR |= 0;                              //disable sleep; (first bit)  
  ADCSRA = ADCSRA_State;                  //restore ADC.   
}

void wakeUpInterrupt() {/* Just an empty wake-up interrupt! */}

short readRotaryPosition() {
  
  // Read rotary state
  long newPosition = round(rotary.read()/(double)8);
  short rotaryPosition = 0;
  
  //change direction (if configured) and store postion
  if(newPosition != oldPosition)
    oldPosition = rotaryPosition = invertRotary ? -newPosition : newPosition;        
    
  // Reset rotary state
  if(rotaryPosition != 0)
    rotary.write(0);
    
  return rotaryPosition;
}

void userInteraction() {

  // Read button & rotary state    
  int buttonState = button.check();
  short rotaryPosition = readRotaryPosition();    
  int direction = rotaryPosition < 0 ? -1 : 1; //Using an INT type for time calculations! 
  
  switch (screen.current) {

    case SCREEN_MENU: 
    /* =================================================================
     * Rotate rotary = scroll through products.
     * Pressed once  = Load selected item without starting engine
     * Pressed twice = Load step and start engine. 
     * Long press = edit product name.
     * =================================================================  */
      
      //Rotary actions in menu-
      if(rotaryPosition != 0) {
        if(isDirty) {
          screen.current = SCREEN_MENU_SAVE;
          screen.printSaveDialog(dialogResult = 0);          
        } else {
          menuProductIdx = constrain(menuProductIdx + direction, 0, cookbook.count()-1);
          cookbook.readProduct(menuProductIdx, &product);
          screen.printMenu(product.name);        
        }
      }
      
      //button actions in menu.
      switch (buttonState)  {  
        
        case BTN_SINGLE_CLICK: /* SELECT */
        case BTN_DOUBLE_CLICK: /* SELECT & RUN */ {
          bool startEngine = buttonState == BTN_DOUBLE_CLICK;
          screen.current = startEngine ? SCREEN_RUNNING : SCREEN_PRODUCT;
          if(startEngine)
            engine.start(&product);             
          menuStepIdx = 0; //reset to first step
          lcd.clear();
          printRunDisplay();
          break;
        }
        case BTN_LONG_PRESS: /* ENTER NAME EDITOR */ 
          lcd.setCursor(2,1);
          lcd.blink();
          screen.textEditIdx = 0;
          screen.current = SCREEN_EDIT_NAME;
          break;          
      }
        
      break;
    
    case SCREEN_MENU_SAVE: {
    /* =================================================================
     * Rotate rotary = change dialog option
     * Pressed once  = confirm option     
     * ================================================================= */

      if(rotaryPosition != 0) {
        short prevDialogResult = dialogResult;
        dialogResult = constrain(dialogResult-direction, -1, 1);
        if(prevDialogResult != dialogResult)
          screen.printSaveDialog(dialogResult);
      }
      
      if(buttonState == BTN_SINGLE_CLICK) {
        switch(dialogResult) {
          case DIALOG_RESULT_NO:  cookbook.readProduct(menuProductIdx, &product); break;
          case DIALOG_RESULT_YES: cookbook.writeProduct(menuProductIdx, product); break;
        }
        isDirty = dialogResult == DIALOG_RESULT_ABORT;
        screen.current = SCREEN_MENU; //todo: should go into LCD1602 class. PrintMenu() = Screen Menu...
        screen.printMenu(product.name);
      }

      break;
    }
    case SCREEN_PRODUCT:
    /* =================================================================
     * Rotate rotary = Enter edit mode and change time
     * Pressed once  = Start engine
     * Pressed twice = Open menu 
     * Long press    = Enter edit mode
     * ================================================================= */

      if(rotaryPosition != 0)
        screen.current = SCREEN_EDIT_TIME;
        
      switch (buttonState) {
        
        case BTN_SINGLE_CLICK: /* START ENGINE */
          screen.current = SCREEN_RUNNING;
          menuStepIdx = 0;          
          engine.start(&product);                   
          break;
        
        case BTN_DOUBLE_CLICK: /* ENTER MENU */
          screen.current = SCREEN_MENU; 
          screen.printMenu(product.name);
          break;
          
        case BTN_LONG_PRESS: /* ENTER EDIT MODE */
          screen.current = SCREEN_EDIT_STEP;
          break;
      }
      break;
    
    case SCREEN_RUNNING:
    /* =================================================================
     * Rotate rotary = Enter edit mode and change time
     * Pressed once  = Skip preheat stage / Stop engine       
     * Pressed twice = Enter edit mode         
     * Long press    = Enter edit mode
     * ================================================================= */
      
      if(rotaryPosition != 0)
        screen.current = SCREEN_EDIT_TIME;
        
      switch(buttonState) {
        
        case BTN_SINGLE_CLICK: /* EXIT PREHEAT STAGE OR STOP ENGINE */
          if(engine.getPreHeat()){
            engine.setPreHeat(false);
          } else {
            engine.stop();
          }
          break;
          
        case BTN_DOUBLE_CLICK:
        case BTN_LONG_PRESS: /* ENTER EDIT MODE */
          screen.current = SCREEN_EDIT_STEP;
          break;
      }        
      break;

    case SCREEN_EDIT_NAME:
    /* =================================================================
     * Rotate rotary = roll current char
     * Pressed once  = select next char
     * Pressed twice = select next char (+2)
     * Long press    = Exit edit mode
     * Timeout timer = Exit edit mode. (see exitEdit in loop function)
     * ================================================================= */
      
      //change char at current string position
      if(rotaryPosition != 0) {
        char currChar = rollNameChar(screen.textEditIdx, rotaryPosition);
        screen.changeChar(currChar, screen.textEditIdx + 2 ,1);
        isDirty = true;
      }

      switch (buttonState) {
        
        case BTN_SINGLE_CLICK:              
        case BTN_DOUBLE_CLICK: /* CHANGE CHAR POSITION */
          screen.textEditIdx += buttonState;
          if(screen.textEditIdx > PRODUCTNAME_MAX_LEN - 2)
            screen.textEditIdx = 0;
          lcd.setCursor(screen.textEditIdx + 2, 1);
          break;
          
        case BTN_LONG_PRESS:  /* EXIT NAME EDITOR */
          screen.current = SCREEN_MENU;
          lcd.noBlink();
          break;
      }
      break;
         
    case SCREEN_EDIT_STEP ... SCREEN_EDIT_PREHEAT: {
    /* =================================================================
     * Rotate rotary = Change value for current edit field
     * Pressed once  = Select next edit field. (Step > Melody > Time > Temperature > PreHeat)
     * Pressed twice = select edit field +2
     * Long press    = Exit edit mode
     * Timeout timer = Exit edit mode. (see exitEdit in loop function)
     * ================================================================= */

      if(rotaryPosition != 0) {      
        
        //Product* product = &products[menuProductIdx];
        CookStep* currStep = engine.isRunning() ? engine.getStep(menuStepIdx) : &product.steps[menuStepIdx];
        
        switch(screen.current) {
          case SCREEN_EDIT_STEP: menuStepIdx = constrain(menuStepIdx + direction, 0, product.stepsCount - 1); break;
          case SCREEN_EDIT_BEEP: currStep->beep = !currStep->beep ; break;
          case SCREEN_EDIT_TIME: { //brackets needed for scoped var!
            int newTime = currStep->timeInSec + (direction * timeSteps[abs(rotaryPosition)-1]);
            currStep->timeInSec = newTime < 0 ? 0 : newTime;
            break;
          }
          case SCREEN_EDIT_TEMP: currStep->temp = currStep->temp + (direction * tempSteps[abs(rotaryPosition)-1]); break;
          case SCREEN_EDIT_PREHEAT: product.preHeat = !product.preHeat; break;
        }
        
        isDirty |= !engine.isRunning() && screen.current > SCREEN_EDIT_STEP;        
        screen.menuBlinkItem = false; //delay blink when editing (true = hidden)    
        printRunDisplay();
      } 

      switch(buttonState){
        
        case BTN_SINGLE_CLICK: /* CHANGE EDIT FIELD */
        case BTN_DOUBLE_CLICK: {
          byte newScreenIdx = screen.current + buttonState;
          if(newScreenIdx > SCREEN_EDIT_PREHEAT || ( engine.isRunning() && newScreenIdx == SCREEN_EDIT_PREHEAT ))
            newScreenIdx = SCREEN_EDIT_STEP;
          screen.current = newScreenIdx;
          break;
        }
        
        case BTN_LONG_PRESS: { /* EXIT EDIT MODE */
          screen.current = engine.isRunning() ? SCREEN_RUNNING : SCREEN_PRODUCT;
          if(engine.isRunning())
            menuStepIdx = engine.getCurrentStepIdx();
          break;
        }
      }
    }
  }

  //reset timers
  if(buttonState > 0 || rotaryPosition != 0){
    // Reset powerOff timer
    lastActionOn = millis();      
    // Reset edit timer.
    if(screen.current > SCREEN_RUNNING) {
      editingSince = millis();
      screen.menuBlinkItem = false;
    }
  }
  
} /* END userInteraction() */

char rollNameChar(byte pos, short roll) {      
  char c = product.name[pos] + roll;
  if(c < 32) c = 126;
  if(c > 126) c = 32;
  return product.name[pos] = c;
}

void printRunDisplay() {
  //First line
  if(engine.isRunning()) {
    byte tempSign = engine.isOnTemperature() ? '<' : '>';
    if(engine.getPreHeat()) {  
      char* actionText = engine.getPreHeatReached() ? "START NOW?" : "!PREHEAT!";
      screen.printProductLine(++switchPreHeatText % 10 > 4 ? product.name : actionText, engine.getTemperature(), tempSign);
    } else {
      screen.printRunLine(engine.getElapsedSeconds(), engine.getTemperature(), tempSign);
    }
  } else {
      screen.printProductLine(product.name, engine.getTemperature(), product.preHeat ? HEAT_CHAR : ' ');
  }
  
  //second line
  CookStep* currStep = engine.isRunning() ? engine.getStep(menuStepIdx) : &product.steps[menuStepIdx];   
  screen.printStepLine(menuStepIdx,
     engine.isRunning() && menuStepIdx == engine.getCurrentStepIdx() ? engine.getRemainingSeconds() : currStep->timeInSec, 
     currStep->temp,
     currStep->beep); 
}

void stepCompletedCallBack(int stepIdx) {

  //Actions when engine stops.
  if(stepIdx == ENGINE_STOPPED_STEP) {
    screen.current = SCREEN_PRODUCT;
    menuStepIdx = 0;
  } 
  
  //show next step on screen
  if (stepIdx == menuStepIdx){
    menuStepIdx = engine.getCurrentStepIdx();
  }
  
  //Buzz? (preHeat & steps with buzz option)
  if (stepIdx == PREHEAT_COMPLETE_STEP 
  || (stepIdx >= 0 && engine.getStep(stepIdx)->beep)) {    
    tone(speakerPin, buzzFrequency, 2000);
  }
}
NOCOMNCVCCGNDINLED1PWRRelay Module
Airfryer Heater
NOCOMNCVCCGNDINLED1PWRRelay Module
Airfryer Fan
Temperature (simulation with potentiometer)