// 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);
}