#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#define EEPROM_SIZE 25
LiquidCrystal_I2C lcd(0x27, 20, 4); // Change the I2C address if needed
#define ENCODER_CLK 2
#define ENCODER_DT 15
#define ENCODER_BTN 4
//EEPROM Address first prefix indicating data type
#define b_TANK_B 0
#define f_TANK_F 1
#define s_TANK_S 5
#define b_TANK_B2 7
#define MENULEV_MAX 6
//Function Declare section
void subMenu____FX(int p);
void actionFun1_FX(int p);
void actionFun2_FX(int p);
void actionFun3_FX(int p);
void actionFun4_FX(int p);
void blankBool__FX(int p) {}
void blankByte__FX(int p) {}
void blankShort_FX(int p) {}
void blankFloat_FX(int p) {}
bool mnIsBackPressed();
void prn_gNum(String str);
enum EEprom_Type {pBLK, pBOL, pBYT, pSHO, pINT, pFLO};
//enum Encoder_Mode {eLIGHT, eMENU, eNUMBER};
enum Encoder_Mode {eLIGHT, eSCREEN, eOPTION, eNUMBER};
const char* enMode[] = {"eLIGHT", "eSCREEN", "eOPTION", "eNUMBER" };
Encoder_Mode EncMode = eSCREEN;
enum screenID {sMENU, sMAIN, sSENSOR, sPROC0, sPROC1, sPROC2, sPROC3};
const char* scLabel[] = {"MENU", "MAIN SCREEN", "SENSOR READING", "PROCESS 0", "PROCESS 1", "PROCESS 2", "PROCESS 3"};
screenID scID = sMENU;
bool scNavigat = false; //prevent cursor run around before encode turned
int mnCurLcdRow();
int CurLev =0;
//int mnPrvLev =99;
int MenuID[MENULEV_MAX]={0,-1,-1,-1,-1};
int MenuLN[MENULEV_MAX]={0,-1,-1,-1,-1};
int mnPrvLine =99; //for checking if any line change then update LCD
int mnCurPage =99;
bool mnRefresh = true;
struct MenuItem {
const byte menuID;
const char* label;
void (*const action)(int) ;
const byte para;
const int EPtyp;
};
enum menuName {mMAIN, mAUTO, mSETT, mCALB, mTEST, mPROC};
const char* mnLabel[] = {"mMAIN MENU", "mAUTO MENU", "mSETTING MENU", "mCALBRATION MENU", "mTEST MENU", "mPROCESS MENU"};
MenuItem Menu[] = {
//Main Menu, Max label lng is 13 writeEeprom FX
//{xXXXX, "0123456789012", xxxxxxxxxxxFX, Interger"},
{mMAIN, "M0 Option 0", actionFun1_FX, 5},
{mMAIN, "M0 Option 1", actionFun2_FX, 7},
{mMAIN, "M0 Sub Menu 2", subMenu____FX, mAUTO},
{mMAIN, "M0 Short 3", blankShort_FX, s_TANK_S, pSHO}, //6
{mMAIN, "M0 Byte 4", blankByte__FX, b_TANK_B2, pSHO}, //0
{mMAIN, "M0 Float 5", blankFloat_FX, f_TANK_F, pFLO}, //1
{mAUTO, "M1 Bool 6 ", blankBool__FX, b_TANK_B},
{mAUTO, "M1 Option 7 ", actionFun4_FX, 3},
{mAUTO, "M1 BOOL 8 ", blankBool__FX, b_TANK_B2},
{mAUTO, "M1 Option 9 ", subMenu____FX, mSETT},
{mAUTO, "M1 Float 10 ", blankFloat_FX, f_TANK_F},
{mSETT, "M2 Option 11 ", subMenu____FX, mCALB},
{mSETT, "M2 Option 12 ", actionFun4_FX, 3},
{mCALB, "M3 Option 13 ", subMenu____FX, mTEST},
{mTEST, "M4 Option 14 ", subMenu____FX, mPROC},
{mPROC, "M5 Option 15 ", actionFun3_FX, 3},
};
int c; //counting in main loop
struct field{
int row;
int col;
String label;
String valS;
int chn; // number of char
int Min;
int Max;
int EPads;
int EPtyp;
};
field gNum;
bool takeNumOn =false;
bool digitEn =false;
// Define the variables for the rotary encoder
volatile float EncCounter = 0; // The counter value
volatile bool EncTurned = false;
volatile int EncStep = 1; // Step of counter
volatile long int Enc_stime =millis();
volatile bool EncClockwise = true; // The direction of rotation
volatile bool EncPressed = false; // The button state
// Utilities Functions
String i2c(int val){
char buf[5];
sprintf(buf, "%d", val);
return buf;
}
String f2c(float val){
char buf[5];
sprintf(buf, "%.1f", val);
return buf;
}
void writeEEPROM_FX(int ads, int type, float val){
switch (type) {
case pBOL: EEPROM.writeByte(ads, val); break;
case pBYT: EEPROM.writeByte(ads, val); break;
case pSHO: EEPROM.writeShort(ads, val); break;
case pINT: EEPROM.writeInt(ads, val); break;
case pFLO: EEPROM.writeFloat(ads, val); break;
}
EEPROM.commit();
}
float readEEPROM_FX(int ads, int type){
float val;
switch (type) {
case pBOL: val= EEPROM.readByte(ads); break;
case pBYT: val= EEPROM.readByte(ads); break;
case pSHO: val= EEPROM.readShort(ads); break;
case pINT: val= EEPROM.readInt(ads); break;
case pFLO: val= EEPROM.readFloat(ads); break;
}
return val;
}
//
// Encoder Functions Group
void IRAM_ATTR isr_clk() {
int dt = digitalRead(ENCODER_DT);
EncTurned = true;
// If the CLK pin is low and the DT pin is high, it means clockwise rotation
if (dt == HIGH) EncClockwise = true;
else EncClockwise = false;
}
void IRAM_ATTR isr_btn() { // Read the value of the SW pin
int sw = digitalRead(ENCODER_BTN);
if (sw == LOW) { // If the SW pin is low, it means the button is pressed
EncPressed = true;
}
}
void prn_ENCODER_variable(String str){
if (str=="Before ") Serial.printf("\n___________________________________________________________________________________");
Serial.printf("\n%s>> EncMode[%.5s] Pressed[%d] Turned[%d] Clockwise:%d Step:%d counter:%4.1f takeNumOn:%d",
str, enMode[EncMode], EncPressed, EncTurned, EncClockwise, EncStep, EncCounter, takeNumOn);
//Serial.printf("\n >> scID:%d scLabel:%s mnRefresh:%d ", " ",
//scID, scLabel[scID], mnRefresh);
}
void CurSorPRN(){
switch (scID){
case sMENU:
if (digitEn==false) {
lcd.setCursor(0, mnCurLcdRow());
lcd.print(">");
lcd.noBlink();
lcd.noCursor();
}
break;
}
}
void isr_btn_action_loop(){
if (EncPressed==false) return;
prn_ENCODER_variable("Before ");
EncPressed=false;
switch (EncMode) {
case eLIGHT: EncMode=eSCREEN; lcd.backlight(); break;
case eSCREEN: // -> eOPTION
EncMode=eOPTION;
CurSorPRN();
break;
case eOPTION: // -> eNUMBER
switch (scID){
case sMENU:
//Serial.printf("\nEncPressed= %d MenuLN[CurLev]:%d",EncPressed, MenuLN[CurLev]);
lcd.setCursor(0, mnCurLcdRow()); lcd.print("*");
if (mnIsBackPressed()) {
Serial.print(" << Back Pressed >>");
if (CurLev!=0) CurLev--;
else EncMode=eSCREEN;
mnRefresh=true;
}
else { // Call for functions
mnPrvLine = -1; // To force LCD refresh screen
Serial.print(" << Fx Pressed >>");
byte para = Menu[MenuLN[CurLev]].para;
void (*action)(int);
action = Menu[MenuLN[CurLev]].action;
//bbb Call others function case
if (action!=&blankBool__FX && action!=&blankByte__FX && action!=&blankShort_FX && action!=&blankFloat_FX) {
if (action != nullptr) {
Serial.print(" << Run menu's assigned function >>");
action(para);
mnRefresh=true;
EncMode=eOPTION;
}
}
else {//mmm Write EEPROM case for sMENU
EncMode=eNUMBER;
gNum.row = mnCurLcdRow();
gNum.col = 16;
//gNum.label
//gNum.valS
gNum.chn =4;
gNum.Min =0;
gNum.Max =100;
gNum.EPads = Menu[MenuLN[CurLev]].para;
gNum.EPtyp = Menu[MenuLN[CurLev]].EPtyp;
EncCounter = readEEPROM_FX(gNum.EPads, gNum.EPtyp);
for (int i=0; i<10; i++){
Serial.println(readEEPROM_FX(gNum.EPads, gNum.EPtyp));
}
Serial.printf(" << Put gNum var >>");
takeNumOn = true; // -> continue handle by SCREEN_loop()
prn_gNum("btn_act ");
}
}
break;
}
break;
case eNUMBER: // -> eOPTION
if (digitEn==false) {
if (scID!=sMENU) { lcd.blink(); lcd.noCursor(); }
//else { scNavigat=true;} //vvv
prn_gNum("BF_write");
writeEEPROM_FX(gNum.EPads, gNum.EPtyp, EncCounter);
prn_gNum("AT_write");
if (gNum.EPtyp==pFLO) {
digitEn=true; EncMode=eNUMBER; EncCounter=0;
if (scID==sMENU) { lcd.setCursor(0, gNum.row); lcd.print("*");}
lcd.setCursor(gNum.col, gNum.row); for (int i=0; i<gNum.chn; i++) lcd.print(" ");
lcd.setCursor(gNum.col, gNum.row); lcd.print(".");
}
else EncMode=eOPTION;
//lcd.noCursor();
}
else {
Serial.println(" digitEn==true");
float f1, f2, f3;
f1 = (int)readEEPROM_FX(gNum.EPads, gNum.EPtyp);
f2 = EncCounter/10.0;
f3 = f1+ f2;
//Serial.printf("\n >>>> f1:%4.1f f2:%4.1f f3:%4.1f", f1, f2, f3);
writeEEPROM_FX(gNum.EPads, gNum.EPtyp, f3);
f3 = readEEPROM_FX(gNum.EPads, gNum.EPtyp); //vvv
lcd.setCursor(gNum.col, gNum.row); lcd.print(f2c(f3));
lcd.setCursor(gNum.col, gNum.row);
digitEn=false;
EncMode=eOPTION;
}
CurSorPRN();
break;
}
prn_ENCODER_variable("After ");
}
void isr_clk_action_loop(){
if (EncTurned==false) return;
prn_ENCODER_variable("Before ");
EncTurned=false;
switch (EncMode) {
case eLIGHT: return;
case eSCREEN:
break;
case eOPTION:
EncStep=1;
lcd.setCursor(0, mnCurLcdRow()); lcd.print(" ");
mnPrvLine = MenuLN[CurLev];
if (EncClockwise == true) MenuLN[CurLev]++;
if (EncClockwise == false) MenuLN[CurLev]--;
CurSorPRN();
EncCounter=0;
break;
case eNUMBER:
unsigned long period = millis()-Enc_stime;
Serial.printf("\ >>> period:%d", period);
if (period<300) EncStep=50;
else if (period<600) EncStep=10;
else EncStep=1;
Enc_stime=millis();
takeNumOn = true;
if(EncClockwise==true) EncCounter=EncCounter +EncStep;
if(EncClockwise==false) EncCounter=EncCounter -EncStep;
break;
}
prn_ENCODER_variable("After ");
}
//
void prn_gNum(String str){ //str fix 8 char to keep massage align
Serial.printf("\n%s>> gNum col:%d row:%d label:%s valS:%s chn:%d Min:%d Max:%d EPads:%d EPtyp:%d EPval:%.1f",
str, gNum.col, gNum.row, gNum.label, gNum.valS, gNum.chn, gNum.Min, gNum.Max, gNum.EPads, gNum.EPtyp, readEEPROM_FX(gNum.EPads, gNum.EPtyp));
}
// Menu Functions Group
void prn_MENU_variable(String str){
//print menu variable
char lev[MENULEV_MAX];
Serial.printf("\n ___________________________________________________________________________________");
for (int i=0; i<MENULEV_MAX; i++) lev[i]=' ';
lev[CurLev]='#';
Serial.printf("\n%.8s>> CurLev:", str); for (int i=0; i<MENULEV_MAX; i++) Serial.printf(" %2c", lev[i]);
Serial.printf(" mnCurLcdRow:%d", mnCurLcdRow());
Serial.printf(" mnPrvLine:%d", mnPrvLine);
Serial.printf("\n >> MenuID:"); for (int i=0; i<MENULEV_MAX; i++) Serial.printf(" %2d", MenuID[i]);
Serial.printf(" mnFirstLine:%d", mnFirstLine());
Serial.printf("\n >> MenuLN:"); for (int i=0; i<MENULEV_MAX; i++) Serial.printf(" %2d", MenuLN[i]);
Serial.printf(" mnNumLine:%d", mnNumLine());
}
bool mnIsBackPressed(){
bool back=false;
int lastLine=-1;
for (int i=0; i<mnQuantity(); i++){
int ID =Menu[i].menuID;
if (ID==MenuID[CurLev]) {lastLine=i;}
if (lastLine!=-1 && ID!=MenuID[CurLev]) break;
}
if (lastLine==MenuLN[CurLev]-1) back=true;
//Serial.printf("\nlastLine:%d back:%d",lastLine, back);
return back;
}
int mnFirstLine(){
int mnStart=-1;
for (int i=0; i<mnQuantity(); i++){
int ID =Menu[i].menuID;
if (ID==MenuID[CurLev]) {mnStart=i; break;}
}
return mnStart;
}
int mnNumLine(){
int mnLines =0;
for (int i=0; i<mnQuantity(); i++){
int ID =Menu[i].menuID;
if (ID==MenuID[CurLev]) mnLines++;
if (ID!=MenuID[CurLev] && mnLines >0) break;
}
return mnLines;
}
int mnCurLcdRow(){
int fstLine =mnFirstLine()-1; // minus 1 to compensate header line
int numLine =mnNumLine();
int row=0;
for (int i=fstLine; i<fstLine+numLine+1; i++){
if (i==MenuLN[CurLev]) break;
if(row==3) row=0; else row++;
}
return row;
}
byte mnQuantity(){
byte c=0;
for(auto i: Menu) {c++;}
//c=sizeof(Menu)/sizeof(Menu[0]);
return c;
}
//
void setup() {
pinMode(ENCODER_CLK, INPUT);
pinMode(ENCODER_DT, INPUT);
pinMode(ENCODER_BTN, INPUT_PULLUP);
EEPROM.begin(EEPROM_SIZE);
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Hello, ESP32!");
lcd.init();
lcd.backlight();
lcd.clear();
// Attach interrupt functions to the CLK and SW pins
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), isr_clk, FALLING);
attachInterrupt(digitalPinToInterrupt(ENCODER_BTN), isr_btn, FALLING);
Serial.printf("All menus lines:%d", mnQuantity());
EEPROM.writeFloat(f_TANK_F, 23.45);
}
void loop() {
static unsigned long c_stime = millis();
if (millis()-c_stime>1000) c++;
menu_loop();
SCREEN_loop();
isr_btn_action_loop();
isr_clk_action_loop();
delay(10); // this speeds up the simulation
}
void SCREEN_loop(){ //alway run not up to Encoder
// print number while Encoder turning
if (takeNumOn==true) {
takeNumOn=false;
bool exit=false;
int col;
if (digitEn==true) {
col=gNum.col+1;
if (EncCounter>9) EncCounter=0;
if (EncCounter<0) EncCounter=9;
}
else {
col=gNum.col;
if (EncCounter>gNum.Max) EncCounter=gNum.Max;
if (EncCounter<gNum.Min) EncCounter=gNum.Min;
}
lcd.setCursor(col, gNum.row); for (int i=0; i<gNum.chn; i++) lcd.print(" ");
lcd.setCursor(col, gNum.row); lcd.print((int)EncCounter);
lcd.setCursor(col, gNum.row); lcd.cursor();
prn_gNum("SCR_loop");
}
//print Cursor & Blink mmm
switch (EncMode){
case eSCREEN:
break;
case eOPTION:
if (scID==sMENU) {
}
break;
case eNUMBER:
switch (scID) {
case sMENU:
break;
}
break;
}
}
void menu_loop()
{
if (scID != sMENU) return;
bool prn =false;
if (MenuLN[CurLev]!=mnPrvLine) {
int Rows = 4; //4 rows
int mnStart, mnEnd, mnHead, mnBack;
bool mnStart_D =false;
int mnLines=0;
mnPrvLine=MenuLN[CurLev];
//MenuLN[CurLev-1]=MenuLN[CurLev];
// Find start/end/lines of Menu
for (int i=0; i<mnQuantity(); i++){
int ID =Menu[i].menuID;
if (mnStart_D==false && ID==MenuID[CurLev]) {mnStart=i; mnStart_D=true;}
if (ID==MenuID[CurLev]) mnLines++;
}
mnStart = mnStart;
mnEnd = mnStart+mnLines-1;
mnHead = mnStart-1; // add header line
mnBack = mnEnd+1;
if (MenuLN[CurLev]>mnBack) MenuLN[CurLev]=mnStart;
if (MenuLN[CurLev]<mnStart) MenuLN[CurLev]=mnBack;
if(prn) Serial.printf("\n1) mnLines:%d mnStart:%d mnEnd:%d mnHead:%d mnBack:%d",mnLines, mnStart, mnEnd, mnHead, mnBack);
// Cal number of pages
float p = ((float)mnLines+2.0)/(float)Rows; //plus back line & header line
int pages;
if ((int)p!=p) pages = (int)p+1; else pages = (int)p;
if(prn) Serial.printf("\n2) p:%.1f (int)p:%d pages:%d", p, (int)p, pages);
// Find mnCurPage related to cuurent menu line number
for (int i=0; i<pages; i++){
int stRowPage =mnHead+i*Rows;
int enRowPage =mnHead+(i+1)*Rows;
if(prn) Serial.printf("\n3.%d) MenuLN[CurLev]:%d stRowPage:%d enRowPage:%d ",i, MenuLN[CurLev], stRowPage, enRowPage);
if (MenuLN[CurLev] >= stRowPage && MenuLN[CurLev] <enRowPage) {
if (mnCurPage!=i) {mnRefresh=true; Serial.println(" << mnRefresh=true >> ");}
mnCurPage = i;
// Serial.print(" << flound menu line >>");
break;
}
}
if(prn) Serial.printf("\n4) mnCurPage:%d, MenuLN[CurLev]:%d ", mnCurPage, MenuLN[CurLev]);
// Print Menu Page
int pg_StartRow = mnStart+mnCurPage*Rows-1; //minus 1 for header
int pg_EndRow =pg_StartRow+Rows-1;
if (pg_EndRow >mnBack) pg_EndRow=mnBack;
if(prn) Serial.printf("\n5) pg_StartRow:%d pg_EndRow:%d mnBack:%d",pg_StartRow, pg_EndRow, mnBack);
if(prn) Serial.println();
if (mnRefresh==true){
mnRefresh =false;
lcd.clear();
for(int row=pg_StartRow; row<=pg_EndRow; row++){
if (mnCurPage==0 && row==pg_StartRow) {
lcd.setCursor(0, row-pg_StartRow);
lcd.print(mnLabel[MenuID[CurLev]]);
}
else if (mnCurPage==pages-1 && row==pg_EndRow) {
lcd.setCursor(1, row-pg_StartRow);
if (CurLev!=0) lcd.print("(Back)");
else lcd.print("(Exit)");
}
else {
lcd.setCursor(1, row-pg_StartRow);
lcd.print(Menu[row].label);
lcd.setCursor(16, row-pg_StartRow);
if (Menu[row].action == &subMenu____FX) lcd.print(" >");
if (Menu[row].action == &blankBool__FX) lcd.print(EEPROM.readByte(Menu[row].para));
if (Menu[row].action == &blankByte__FX) lcd.print(EEPROM.readByte(Menu[row].para));
if (Menu[row].action == &blankShort_FX) lcd.print(EEPROM.readShort(Menu[row].para));
if (Menu[row].action == &blankFloat_FX) lcd.print(f2c(EEPROM.readFloat(Menu[row].para)));
}
}
if (EncMode==eOPTION) CurSorPRN();
}
prn_MENU_variable("MENU_loop");
}
} //end menu_loop
// MENU Action Functions Group
void subMenu____FX(int p) {
Serial.printf("\nRun > subMenu____FX para:%d", p);
if (CurLev==MENULEV_MAX-1) {
lcd.setCursor(1, mnCurLcdRow());
lcd.print(" Over Max limit!"); delay(1000);
Serial.println("Menu over maximum limit!");
return;
}
CurLev++;
MenuID[CurLev] =p;
MenuLN[CurLev] =mnFirstLine();
}
void actionFun1_FX(int p) {
bool exit=false;
EncCounter=0;
Serial.printf("\nRun > actionFun1_FX para:%d", p);
lcd.clear();
lcd.setCursor(2, 1);
lcd.print("actionFun1_FX");
do {
if(EncTurned==true) {
EncTurned=false;
lcd.setCursor(2, 2);
EncCounter++;
lcd.print(EncCounter);
}
if(EncPressed==true) {
EncPressed=false;
lcd.setCursor(2, 3);
lcd.print("EXIT");
exit=true;
}
} while (!exit);
EncCounter=0;
}
void actionFun2_FX(int p) { // loop function
bool exit=false;
EncCounter=0;
lcd.clear();
lcd.setCursor(0,0);
lcd.print("actionFun2_FX");
do {
if(EncTurned==true) {
EncTurned=false;
lcd.setCursor(2, 2);
EncCounter++;
lcd.print(EncCounter);
}
if(EncPressed==true) {
EncPressed=false;
lcd.setCursor(2, 3);
lcd.print("EXIT");
exit=true;
}
} while (!exit);
EncCounter=0;
}
void actionFun3_FX(int p) {
Serial.printf("\nRun > subactionFun1_FX para:%d", p);
}
void actionFun4_FX(int p) {
Serial.printf("\nRun > actionFun4_FX para:%d", p);
}
//