// smartOven for esp32 with gc9a01 display
#include <TFT_eSPI.h>           // Hardware-specific library
#include <FS.h>
#include <SPI.h>
#include <WiFi.h>
#include "time.h"
#include <SD.h>
#include <JPEGDecoder.h>        // JPEG decoder library
#include <Free_Fonts.h>
#include <freeserifbold_48pt.h>     // only numerics and : 

//#define GFXFF 1
#define FSB48 &freeserifbold_48pt

// Port definitions 
#define sd_cs  32               // SD card chip select
#define TFT_CS 22               // Display chip select
#define TFT_DC 21               
                                // Encoder
#define ENCODER_CLK 15          
#define ENCODER_DT  2
#define ENCODER_SW  4
                                // Relays
#define RLY_UPPER 13              // Upper heater
#define RLY_LOWER 12              // Lower heater
#define RLY_GRILL 14              // Grill heater
#define RLY_FAN 27                // Fan
#define RLY_LIGHT 26              // Light

TFT_eSPI tft = TFT_eSPI();

// Global variables
char currentTime[6] = "18:56";
char previousTime[6] = "";

const int menuCount = 18;
const int lineLen = 30;

char captions[menuCount][10];     // Captions for menu items (max 9chr long captions)
int shelves[menuCount];            // You can specify on which shelf the food should be placed
int times_[menuCount];            // How many minutes to cook
int temps[menuCount];             // Target temperature
byte funcs[menuCount];            // Which relays are activated during operation (it's a hexa code)

int currentMenu = 0;
int previousMenu = 0;

int level = 0;                    //0-main menu, 1-set time, 2-set temp, 3-operate

int Timer = 0;
int Temperature = 0;
unsigned long timeToShowTimer = 0;
unsigned long timeToUpdateClock = 0;
unsigned long timeToShowTemp = 0;
int lastMinute = 60;

int lastState = HIGH;             // the previous state from the switch pin
int currentState;                 // the current reading from the switch pin
unsigned long pressedTime  = 0;
unsigned long releasedTime = 0;
const int LONG_PRESS_TIME  = 500; // 500 milliseconds

unsigned int colorBackground = TFT_BLACK;
unsigned int colorText = TFT_WHITE;

int8_t encoder = 0;
int8_t encoderMax = 0;
byte lastValue = 0;
byte lastFont = 0;

byte currentTemp = 0;
int8_t pauseSelected = -1;

// #########################################################################
//  setup
// #########################################################################
void setup(void) {

  pinMode(33, INPUT);           //Temperature measurement (it will works with MAX6675 module in real)

  // setup ports
  pinMode(ENCODER_CLK, INPUT);
  pinMode(ENCODER_DT, INPUT);
  pinMode(ENCODER_SW, INPUT_PULLUP);
  pinMode(RLY_UPPER, OUTPUT);
  pinMode(RLY_LOWER, OUTPUT);
  pinMode(RLY_GRILL, OUTPUT);
  pinMode(RLY_FAN, OUTPUT);
  pinMode(RLY_LIGHT, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), readEncoder, FALLING);

  Serial.begin(9600);                       // serial for debugging
  WiFi.begin("Wokwi-GUEST", "", 6);         // WiFi settings
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
  configTime(3600, 3600, "pool.ntp.org");   // NTP for clock
  getTime();

  if (!SD.begin(sd_cs)) {                   // setup SD card
    Serial.println(F("SD init failed!"));
    while (1);                              // SD initialisation failed so wait here
  }

  tft.begin();                              // Display setup
  tft.setRotation(1);
  tft.fillScreen(colorBackground);
  tft.setTextColor(colorText,colorBackground);  
//  tft.setFreeFont(FSB12);
  csv2array("/data.csv");                   // convert data to arrays
}

#define needShowCaption(item) ((funcs[item] & 0b10000000) > 0)
#define needSetTime(item) ((funcs[item] & 0b01000000) > 0)
#define needSetTemp(item) ((funcs[item] & 0b00100000) > 0)
// #########################################################################
// loop
// #########################################################################
void loop() {
  int idx = currentMenu-1;
  buttonState();
  switch( level) {
    case 0:                               //Menu select
      previousMenu = currentMenu;
      encoderMax = menuCount - 1;
      currentMenu = encoder;
      showMenu();
      break;
    case 1:                               //Set timer
      encoderMax = -60;                      //max 5 hours
      if (shelves[idx] > 0) {
        drawShelf(shelves[idx] - 1);
      }
      if (Timer == 0) Timer = times_[idx];
      if (needSetTime(idx) || pauseSelected == 1) {
        timeRing(Timer  + encoder * 5);
      } else {
        level++;
      }
      break;
    case 2:                               //Set temperature
      encoderMax = -25;
      if (Temperature == 0) Temperature = temps[idx];
      if (needSetTemp(idx) || pauseSelected == 2) {
        tempRing(Temperature + encoder * 10);
      } else {
        level++;
      }
      break;
    case 3:                               //operate
      if (timeToShowTimer == 0) {
        TurnOn(funcs[idx]);
      }
      ShowTimer();
      CheckTemperature(Temperature);
      pauseSelected = -1;
      break;
    case 4:                               //pause
      pauseMenu();
      TurnOn(funcs[idx] & 0b11100011 );   //Turn off heaters
      break;
    case 5:                               //pause menu was selected
      switch (pauseSelected) {
        case 0:                             //continue
          level = 3;                        
          break;
        case 1:                             //set timer
          level = 1;
          break;
        case 2:                             //set temp
          level = 2;
          break;
        case 3:                             //Turn OFF
          level = 6;
          break;
      }
      tft.fillScreen(colorBackground);
      timeToShowTimer = 0;
    case 6:                               //Turn off and init everything
      TurnOn(0);                        
      currentMenu = 0;
      previousMenu = -1;
      Timer = -1;
      Temperature = -1;
      pauseSelected = -1;
  }
  getEncoder();
}
// ----------------------------- pauseMenu --------------------------------
void pauseMenu() {

  if (pauseSelected == -1) {
    encoderMax = 3;
    tft.fillScreen(colorBackground);
  }
  if (pauseSelected != encoder) {
    pauseSelected = encoder;
    char *menuTitle[9] = {"Continue","Set time","Set temp","Turn off"};
    for ( int8_t i = 0; i < 4; i++) {
      if ( i == pauseSelected) {
        tft.setTextColor(colorBackground, colorText);
      } else {
        tft.setTextColor(colorText, colorBackground);
      }
      drawCenter(menuTitle[i], 120, 70 + i * 30, 1);
    }
  }
}
// ------------------------------- button ---------------------------------
void buttonState() {
  currentState = digitalRead(ENCODER_SW);
  if(lastState == HIGH && currentState == LOW) {      // button is pressed
    pressedTime = millis();
  } else { 
    if(lastState == LOW && currentState == HIGH) {    // button is released
      releasedTime = millis();
      long pressDuration = releasedTime - pressedTime;
      if ( pressDuration > 20 ) {
        switch(level) {
          case 0:
            tft.fillScreen(colorBackground);
            drawCenter(captions[currentMenu - 1], 120, 40, 1);
            break;
          case 1:
            Timer += encoder * 5;
            break;
          case 2:
            Temperature += encoder * 10;
            break;
        }
        if( pressDuration < LONG_PRESS_TIME ) {
          level++;
          lastValue = 0;
        } else {
          level--;
        }
        encoder = 0;
        clearRing();
      }
    }
  }
  lastState = currentState;
}

// ------------------------------- menu -----------------------------------
void showMenu() {

  if (currentMenu == 0) {
    if (previousMenu != 0) {
      strcpy(previousTime,"");                // We want to show clock
      tft.fillScreen(colorBackground);
    }
    showClock();
  } else {
    if (currentMenu != previousMenu) {
//      tft.fillRect(48,40,144,8,colorBackground);     // Clear previous caption
//      tft.fillScreen(colorBackground);
      char file[8];
      sprintf(file,"/%d.jpg",currentMenu);
      drawSdJpeg(file,48,48);
      if (needShowCaption(currentMenu - 1)) {   // show caption
        drawCenter(captions[currentMenu - 1], 120, 40, 1);
      }
      menuRing(currentMenu);
    }
  }
}
//---------------------------- drawCenter ----------------------------------
void drawCenter(const char *buf, int x, int y, int font)
{
  if ( lastFont != font ) {
    lastFont = font;
    switch( font ) {
      case 1:
        tft.setFreeFont(FSB12);
        break;
      case 2:
        tft.setFreeFont(FSB24);
        break;
      case 3:
        tft.setFreeFont(FSB48);
        break;
    }
  }
  tft.setTextDatum(TC_DATUM);
  int width = tft.textWidth(buf);
  int height = tft.fontHeight();
  tft.fillRect(x + width / 2, y, 2, height / 2 + 6, colorBackground);  // clear the dust
  //tft.fillRect(x - width / 2, y, 2, height / 2 + 6, TFT_RED);
  tft.drawString(buf, x, y, GFXFF);
}

// ------------------------------- clock -----------------------------------
void showClock() {
  if ( millis() > timeToUpdateClock ) {
    getTime();
    timeToUpdateClock = millis() + 60000;             //one minute
  }
  if (strcmp(currentTime, previousTime) != 0 ) {
      strcpy(previousTime, currentTime);
      if (currentMenu != previousMenu) {
        tft.fillScreen(colorBackground);
      }
      //tft.drawFastHLine(0,120,240,TFT_RED);   //red lines helps for alignment
      //tft.drawFastVLine(120,0,240,TFT_RED);
      //tft.drawCircle(120,120,120,TFT_RED);
      //tft.drawRect(0,80,240,72,TFT_RED);
      drawCenter(currentTime, 120, 84, 3);
  }
}
// ------------------------------ getTime ----------------------------------
void getTime()
{
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    return;
  }
  strftime(currentTime, 6, "%H:%M", &timeinfo);
  return;
}
// ------------------------------ encoder ----------------------------------
void readEncoder() {
  int dtValue = digitalRead(ENCODER_DT);
  if (dtValue == HIGH) {
    encoder++; // Clockwise
  }
  if (dtValue == LOW) {
    encoder--; // Counterclockwise
  }
}

void getEncoder() {
  int result;
  noInterrupts();               // Get the counter value, disabling interrupts.
  if ( encoderMax > 0) {
    if ( encoder < 0 ) {
      encoder = encoderMax;
    }
    if ( encoder > encoderMax ) {
      encoder = 0;
    }
  } else {                          //negative encoderMax means it's not circular
    encoderMax *= -1;
    if ( encoder < 0 ) {
      encoder = 0;
    }
    if ( encoder > encoderMax ) {
      encoder = encoderMax;
    }
  }
  interrupts();
}
// ------------------------------ ringMeter ----------------------------------
void menuRing(int value) {
  ringMeter(value,0,18,0);
}
void timeRing(int value) {
  ringMeter(value,0,60,1);
}
void tempRing(int value) {
  ringMeter(value,50,250,2);
}
void clearRing() {
  tft.drawArc(120,120,122,108,0,360,colorBackground,colorBackground,false);
}
void ringMeter(int value, int vmin, int vmax, byte type)
{
  int x = 0;          // X coordinate
  int y = 0;          // Y coordinate
  int r = 120;        // radius
  int w = 10;         // width;
  int scheme;         // color
  byte seg = 5;       // Segments are 5 degrees wide = 60 segments for 300 degrees
  byte inc = 5;       // Draw segments every 5 degrees, increase to 10 for segmented ring
  int angle = 180;    // full circle
  bool top = true;    // start from top
  bool slice = false; // only slice has been drawed
  int font = 2;       // font size
  int v;
  int initFor;
  int endFor;
  char buffer[5] = "    ";
  int textX = 120;
  int textY = 0;
  //int over = 0;
  int blank;
  int h = 0;
  int m = 0;

  x += r; y += r;   // Calculate coords of centre of ring

  switch (type) {
      case 0:                 //menuRing
        scheme = 1;
        slice = true;
        lastValue = value;    //Don't want to show value
        break;
      case 1:                 //timeRing
        scheme = 2;
        seg = 5;
        inc = 6;
        h = int(value / 60);
        m = value - h * 60;
        sprintf(buffer, "%d:%02d", h, m);
        textY = 84;
        font = 3;
        if (value < 0) value = 0;
        value -= vmax * h;
        break;
      case 2:                 //tempRing
        angle = 150;
        scheme = 4; 
        top = false;
        dtostrf(value, 3, 0, buffer);
        textY = 180;
        if (value > vmax) value = vmax;
        if (value < vmin) value = vmin;
        break;
  }

  if (value != lastValue) {
    lastValue = value;
    drawCenter(buffer, textX, textY, font);
  }

  if (top) {
    v = map(value, vmin, vmax, 0, angle*2);
    initFor = 0;
    endFor = angle * 2;
  } else {
    v = map(value, vmin, vmax, -angle, angle); // Map the value to an angle v
    initFor = -angle;
    endFor = angle;
  }

  
  for (int i = initFor; i < endFor; i += inc) {   // Draw colour blocks every inc degrees
    int colour = 0;
    switch (scheme) {
      case 0: colour = TFT_RED; break; // Fixed colour
      case 1: colour = TFT_GREEN; break; // Fixed colour
      case 2: colour = TFT_BLUE; break; // Fixed colour
      case 3: colour = rainbow(map(i, -angle, angle, 0, 127)); break; // Full spectrum blue to red
      case 4: colour = rainbow(map(i, -angle, angle, 85, 127)); break; // Green to red (high temperature etc)
      case 5: colour = rainbow(map(i, -angle, angle, 127, 63)); break; // Red to green (low battery etc)
      default: colour = colorBackground; break; // This will clear the display
    }

    int shadesOfBlue[] = {0x0006, 0x001a, 0x421a, 0x841a, 0xc61f, 0xffff};
    if (type == 1) {                    // in time settings we have different
                                        // colours for segments depending hours
      blank = shadesOfBlue[h];
      colour = shadesOfBlue[h+1];
    } else {
      blank = colour & 0b0001100001100011;
    }
    // Calculate pair of coordinates for segment start
    float sx = cos((i - 90) * 0.0174532925);
    float sy = sin((i - 90) * 0.0174532925);
    uint16_t x0 = sx * (r - w) + x;
    uint16_t y0 = sy * (r - w) + y;
    uint16_t x1 = sx * r + x;
    uint16_t y1 = sy * r + y;

    // Calculate pair of coordinates for segment end
    float sx2 = cos((i + seg - 90) * 0.0174532925);
    float sy2 = sin((i + seg - 90) * 0.0174532925);
    int x2 = sx2 * (r - w) + x;
    int y2 = sy2 * (r - w) + y;
    int x3 = sx2 * r + x;
    int y3 = sy2 * r + y;

    if ( slice && i == v || !slice && i < v ) { // Fill in coloured segments with 2 triangles
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, colour);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, colour);
    } else  {   // Fill in blank segments
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, blank);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, blank);
    }
  }
}

// ------------------------------ rainbow ----------------------------------
unsigned int rainbow(byte value) {

  byte red = 0; // Red is the top 5 bits of a 16 bit colour value
  byte green = 0;// Green is the middle 6 bits
  byte blue = 0; // Blue is the bottom 5 bits
  byte quadrant = value / 32;

  if (quadrant == 0) {
    blue = 31;
    green = 2 * (value % 32);
    red = 0;
  }
  if (quadrant == 1) {
    blue = 31 - (value % 32);
    green = 63;
    red = 0;
  }
  if (quadrant == 2) {
    blue = 0;
    green = 63;
    red = value % 32;
  }
  if (quadrant == 3) {
    blue = 0;
    green = 63 - 2 * (value % 32);
    red = 31;
  }
  return (red << 11) + (green << 5) + blue;
}
//------------------------------ TurnOn ------------------------------------
void TurnOn( int func ) {
  digitalWrite(RLY_UPPER, ( func & 0b00010000 ) > 0 );
  digitalWrite(RLY_LOWER, ( func & 0b00001000 ) > 0 );
  digitalWrite(RLY_GRILL, ( func & 0b00000100 ) > 0 );
  digitalWrite(RLY_FAN,   ( func & 0b00000010 ) > 0 );
  digitalWrite(RLY_LIGHT, ( func & 0b00000001 ) > 0 );
}
//----------------------------- ShowTimer ----------------------------------
void ShowTimer() {
  char buffer[5] = "";
  int m = 0;
  int s = 0;

  if ( Timer > 1) {                            
    if ( millis() > timeToShowTimer ) {           //1 minute passed
      timeToShowTimer = millis() + 60000;
      timeRing( Timer );
      Timer--;
      if ( Timer == 1) {
        timeRing( Timer );                     //last 1 minute shows immediately
        timeToShowTimer = millis() + 1000;
      }
    }
  } else {
    if ( lastMinute == 0 ) {
      TurnOn(0);                              //Turn OFF
    } else {
      if ( millis() > timeToShowTimer) {         //1 second passed
        timeToShowTimer = millis() + 1000;
        m = int(lastMinute / 60);
        s = lastMinute - m * 60;
        sprintf(buffer, "%d:%02d", m, s);
        drawCenter(buffer, 120, 84, 3);
        lastMinute--;
      }
    }
  }
}
//--------------------------- CheckTemperature --------------------------------
void CheckTemperature( int targetTemp ) {
  const float BETA = 3950; // should match the Beta Coefficient of the thermistor
  int analogValue = analogRead(33);
  int temp = 1 / (log(1 / (4095. / analogValue - 1)) / BETA + 1.0 / 298.15) - 273.15;
  temp *= 3.125;
  if (temp > targetTemp) {
    TurnOn( funcs[currentMenu - 1] & 0b11100011 );   //Turn off heaters
  } else {
    TurnOn( funcs[currentMenu - 1] );                //Turn on heaters
  }
  if ( millis() > timeToShowTemp ) {
    timeToShowTemp = millis() + 10000;      //every 10 seconds
    temp = int(temp/5) * 5;
    char buffer[4];
    //itoa(temp, buffer, 10);
    sprintf(buffer, "%03d", temp);
    drawCenter(buffer, 120, 180, 2);
  }

}
//----------------------------- drawShelf ----------------------------------
void drawShelf(int shelf) {
  const int color = 0x2104;  //DARKGREY
  char buffer[4];
/*
  for( int i = 0; i < 4; i++ ) {
    tft.drawFastHLine(40,60+i*40,3,colour);
    tft.drawFastHLine(40,63+i*40,3,colour);
    tft.drawFastHLine(198,60+i*40,3,colour);
    tft.drawFastHLine(198,63+i*40,3,colour);
  }
  tft.fillTriangle(44,61+shelf*40,48,58+shelf*40,48,64+shelf*40,colour);
  tft.fillTriangle(196,61+shelf*40,192,58+shelf*40,192,64+shelf*40,colour);
*/
  sprintf(buffer, "[%d]", shelf);
  tft.setTextColor(color,colorBackground);  
  drawCenter(buffer,120,60,1);
  tft.setTextColor(colorText,colorBackground);  
}
//----------------------------- csv2array ----------------------------------
void csv2array( char * csv ) {
  int lineIndex = 0;            
  int fieldIndex = 0;           // 0-captions, 1-selves, 2-times, 3-temps, 4-functions
  char field[10];               //longest word in csv is 9 chars
  byte rb;                      //one byte of file
  int i = 0;                    //chr index

  File csvFile = SD.open( csv, FILE_READ);  // or, file handle reference for SD library

  if(!csvFile){
      Serial.println("Failed to open file for reading");
      return;
  }
  while (csvFile.available()) {
    rb = csvFile.read();
    if (rb == ',' || rb == '\n') {    //end of field, so we save data to array
      field[i] = '\0';
      switch (fieldIndex) {
        case 0:
          strcpy(captions[lineIndex], field);
          break;
        case 1:
          shelves[lineIndex] = atoi(field);
          break;
        case 2:
          times_[lineIndex] = atoi(field);
          break;
        case 3:
          temps[lineIndex] = atoi(field);
          break;
        case 4:
          funcs[lineIndex] = (byte) strtol(field, NULL, 16);
          break;
      }
      fieldIndex++;
      if (rb == '\n') {              // end of line
        fieldIndex = 0;              
        lineIndex++;
      }
      i = 0;
    } else {                          // adds up data byte by byte
      field[i] = rb;                  // rb is the readedbyte from file
      i++;                            // increase byte index
    }
  }
  csvFile.close();                    // close the file
}

#define minimum(a,b)     (((a) < (b)) ? (a) : (b))
//-------------- Draw a JPEG on the TFT pulled from SD Card ---------------
void drawSdJpeg(const char *filename, int xpos, int ypos) {

  File jpegFile = SD.open( filename, FILE_READ);  // or, file handle reference for SD library
  if ( !jpegFile ) {
    return;
  }

  bool decoded = JpegDec.decodeSdFile(jpegFile);  // Pass the SD file handle to the decoder,
  if (decoded) {
    jpegRender(xpos, ypos);
  }
}

// Draw a JPEG on the TFT, images will be cropped on the right/bottom sides if they do not fit
void jpegRender(int xpos, int ypos) {

  uint16_t *pImg;
  uint16_t mcu_w = JpegDec.MCUWidth;
  uint16_t mcu_h = JpegDec.MCUHeight;
  uint32_t max_x = JpegDec.width;
  uint32_t max_y = JpegDec.height;

  bool swapBytes = tft.getSwapBytes();
  tft.setSwapBytes(true);
  
  uint32_t min_w = jpg_min(mcu_w, max_x % mcu_w);
  uint32_t min_h = jpg_min(mcu_h, max_y % mcu_h);

  uint32_t win_w = mcu_w;            // save the current image block size
  uint32_t win_h = mcu_h;

  max_x += xpos;
  max_y += ypos;

  while (JpegDec.read()) {          // While there is more data in the file
    pImg = JpegDec.pImage ;         // Decode a MCU (Minimum Coding Unit, typically a 8x8 or 16x16 pixel block)

    int mcu_x = JpegDec.MCUx * mcu_w + xpos;
    int mcu_y = JpegDec.MCUy * mcu_h + ypos;

    if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
    else win_w = min_w;

    if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
    else win_h = min_h;

    if (win_w != mcu_w)
    {
      uint16_t *cImg;
      int p = 0;
      cImg = pImg + win_w;
      for (int h = 1; h < win_h; h++)
      {
        p += mcu_w;
        for (int w = 0; w < win_w; w++)
        {
          *cImg = *(pImg + w + p);
          cImg++;
        }
      }
    }
    uint32_t mcu_pixels = win_w * win_h;

    if (( mcu_x + win_w ) <= tft.width() && ( mcu_y + win_h ) <= tft.height())
      tft.pushImage(mcu_x, mcu_y, win_w, win_h, pImg);
    else if ( (mcu_y + win_h) >= tft.height())
      JpegDec.abort(); // Image has run off bottom of screen so abort decoding
  }
  tft.setSwapBytes(swapBytes);
}
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module