/**************************************************************************************************
 *  
 *      OLED display Menu System - i2c version SSD1306 - 31Aug21
 * 
 *             https://github.com/alanesq/BasicOLEDMenu                      
 *             
 *             
 **************************************************************************************************
      
 oled pins: esp8266: sda=d2, scl=d1    
            esp32: sda=21, scl=22
            Uno: sda=A4 , scl=A5
 oled address = 3C 
 rotary encoder pins: 
            esp8266: d5, d6, d7 (button)
            esp32: 13, 14, 15
            Uno: 2, 3, 4
 Note: on esp8266 you can change the button from d7 to d3 instead leaving d7 and d8 free to use 

 
 The sketch displays a menu on the oled and when an item is selected it sets a 
 flag and waits until the event is acted upon.  Max menu items on a 128x64 oled 
 is four.
 
See the section "customise the menus below" for how to create custom menus inclusing selecting a value, 
choose from a list or display a message.
 

 for more oled info see: https://randomnerdtutorials.com/guide-for-oled-display-with-arduino/
                     or: https://lastminuteengineers.com/oled-display-esp32-tutorial/

 
 **************************************************************************************************/

  // settings

  #include <ESP32Servo.h>
  #include <Stepper.h>

  int servoPin = 19;
  Servo servo;
  int stepsPerRevolution = 5;
  Stepper myStepper(stepsPerRevolution, 18, 5, 4, 2);

  int pos;
  int speed = 10;
  TaskHandle_t Task1;

  const String stitle = "simple_menu";         // script title
  const String sversion = "25Dec2022";           // script version

  bool serialDebug = 1;                        // enable debug info on serial port

  int OLEDDisplayTimeout = 10;                 // oled menu display timeout (seconds)

  int itemTrigger = 1;                         // rotary encoder - counts per click

  #define OLED_ADDR 0x3C                       // OLED i2c address

  #if defined(ESP8266)
    // esp8266
    const String boardType="ESP8266";
    #define I2C_SDA D2                         // i2c pins
    #define I2C_SCL D1
    #define encoder0PinA  D5
    #define encoder0PinB  D6
    #define encoder0Press D7                   // button - Note: on esp8266 you can change this from d7 to d3 leaving d7 and d8 free to use 
    
  #elif defined(ESP32)
    // esp32
    const String boardType="ESP32";
    #define I2C_SDA 21                         // i2c pins
    #define I2C_SCL 22
    #define encoder0PinA  25
    #define encoder0PinB  33
    #define encoder0Press 32                   // button 
    
  #elif defined (__AVR_ATmega328P__)
    // Arduino Uno
    const String boardType="Uno";
    #define I2C_SDA A4                         // i2c pins
    #define I2C_SCL A5
    #define encoder0PinA  2
    #define encoder0PinB  3
    #define encoder0Press 4                    // button  
  #else
    #error Unsupported board - must be esp32, esp8266 or Arduino Uno
  #endif

  // Define pin connections & motor's steps per revolution

  // -------------------------------------------------------------------------------------------------

  //#include <MemoryFree.h>                    // used to display free memory on Arduino (useful as it can be very limited)
  #include <Adafruit_GFX.h>
  #include <Adafruit_SSD1306.h>
  
  // rotary encoder
    volatile int16_t encoder0Pos = 0;         // current value selected with rotary encoder (updated in interrupt routine)
    volatile bool encoderPrevA = 0;           // used to debounced rotary encoder 
    volatile bool encoderPrevB = 0;           // used to debounced rotary encoder 
    bool reButtonState = 0;                   // current debounced state of the button
    uint32_t reButtonTimer = millis();        // time button state last changed
    int reButtonMinTime = 500;                // minimum milliseconds between allowed button status changes
  
  // oled menu
    const byte menuMax = 5;                   // max number of menu items
    const byte lineSpace1 = 9;                // line spacing (6 lines)
    const byte lineSpace2 = 16;               // line spacing (4 lines)
    String menuOption[menuMax];               // options displayed in menu
    byte menuCount = 0;                       // which menu item is curently highlighted 
    String menuTitle = "";                    // current menu ID number (blank = none)
    byte menuItemClicked = 100;               // menu item has been clicked flag (100=none)
    uint32_t lastREActivity = 0;              // time last activity was seen on rotary encoder
  
  // oled SSD1306 display connected to I2C (SDA, SCL pins)
    #define SCREEN_WIDTH 128                  // OLED display width, in pixels
    #define SCREEN_HEIGHT 64                  // OLED display height, in pixels
    #define OLED_RESET -1                     // Reset pin # (or -1 if sharing Arduino reset pin)
    Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);


// -------------------------------------------------------------------------------------------------
//                                        customise the menus below
// -------------------------------------------------------------------------------------------------

// Useful commands:
//      void reWaitKeypress(20000);         = wait for the button to be pressed on the rotary encoder (timeout in 20 seconds if not)
//      chooseFromList(8, "TestList", q);   = choose from the list of 8 items in a string array 'q'
//      enterValue("Testval", 15, 0, 30);   = enter a value between 0 and 30 (with a starting value of 15)

  
// Available Menus

  // main menu
  void Main_Menu() {
      menuTitle = "Main Menu";                                        // set the menu title
      setMenu(0,"");                                               // clear all menu items
      setMenu(0,"Control Stepper");                                           // choose from a list
      setMenu(1,"Control Servo");                                  // enter a value
      setMenu(2,"Daffa Fairuzaufa Athallah Raharjo");                                        // display a message
      setMenu(3,"2006517915");                                         // change menu
  }

  
   
// -------------------------------------------------------------------------------------------------


// menu action procedures
//   check if menu item has been selected with:  if (menuTitle == "<menu name>" && menuItemClicked==<item number 1-4>)

void menuItemActions() {
    
  if (menuItemClicked == 100) return;                                 // if no menu item has been clicked exit function


    if (menuTitle == "Main Menu" && menuItemClicked==0) {
      menuItemClicked=100;                                            // flag that the button press has been actioned (the menu stops and waits until this)             
      speed = enterValue("speed",speed,5,30,1000);
      Serial.println("Menu: Value set = " + String(speed));

    }
    

    if (menuTitle == "Main Menu" && menuItemClicked==1) {
      menuItemClicked=100;                                           
      pos=enterValue("Pos", pos, 1, 0, 180);                  // enter a value (title, start value, step size, low limit, high limit)
      Serial.println("Menu: Value set = " + String(pos));
      servo.write(pos);
    }

}

// -------------------------------------------------------------------------------------------------
//                                        customise the menus above
// -------------------------------------------------------------------------------------------------


void setup() {

    Serial.begin(115200);
    delay(200);
    Serial.println("\n\n\nOled display sketch");

    // configure gpio pins
    pinMode(encoder0Press, INPUT_PULLUP);
    pinMode(encoder0PinA, INPUT);
    pinMode(encoder0PinB, INPUT);
    

  // initialise the oled display
    Wire.begin(I2C_SDA,I2C_SCL);    // if you get an error it may be the board you are using does not allow defining the pins in which case try:  Wire.begin();
    if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
      Serial.println(("\nError initialising the oled display"));
    }

  // Display splash screen on OLED
    display.clearDisplay();
    display.setTextColor(WHITE);
    display.setCursor(0, 0);
    display.setTextSize(1);
    display.print(stitle);
    display.setCursor(0, lineSpace1 * 2);
    display.print(sversion);
    display.setCursor(0, lineSpace1 * 4);
    display.print(boardType);
    display.setCursor(0, lineSpace1 * 5);
    //display.print(freeMemory());
    display.display();
    delay(2500);

  // Interrupt for reading the rotary encoder position
    attachInterrupt(digitalPinToInterrupt(encoder0PinA), doEncoder, CHANGE); 

  Main_Menu();    // start the menu displaying - see menuItemActions() to alter the menus

  servo.attach(servoPin, 500, 2400);
  servo.write(pos);

  xTaskCreatePinnedToCore(
             stepper, /* Task function. */
             "Task1",   /* name of task. */
             10000,     /* Stack size of task */
             NULL,      /* parameter of the task */
             1,         /* priority of the task */
             &Task1,    /* Task handle to keep track of created task */
             0);        /* pin task to core 0 */
}

//  -------------------------------------------------------------------------------------------

void loop() {

    // if a oled menu is active service it 
    if (menuTitle != "") {                                  // if a menu is active
      menuCheck();                                          // check if encoder selection button is pressed
      menuItemSelection();                                  // check for change in menu item highlighted
      staticMenu();                                         // display the menu 
      menuItemActions();                                    // act if a menu item has been clicked
    } 
    
    
    yield();
}

    
//  -------------------------------------------------------------------------------------------
//  ------------------------------------- menu procedures -------------------------------------
//  -------------------------------------------------------------------------------------------

// set menu item
// pass: new menu items number, name         (blank iname clears all entries)

void setMenu(byte inum, String iname) {
  if (inum >= menuMax) return;    // invalid number
  if (iname == "") {              // clear all menu items
    for (int i=0; i < menuMax; i++)  menuOption[i] = "";
    menuCount = 0;                // move highlight to top menu item
  } else {
    menuOption[inum] = iname;
    menuItemClicked = 100;        // set item selected flag as none
  }
}

//  --------------------------------------

// confirm that a requested action is not a mistake
//   returns 1 if confirmed

bool confirmActionRequired() {
  display.clearDisplay();  
  display.setTextSize(2);
  display.setTextColor(WHITE,BLACK);
  display.setCursor(0, lineSpace2 * 0);
  display.print("HOLD");
  display.setCursor(0, lineSpace2 * 1);
  display.print("BUTTON TO");  
  display.setCursor(0, lineSpace2 * 2);
  display.print("CONFIRM!");
  display.display();
  
  delay(2000);
  display.clearDisplay();  
  display.display();

  exitMenu();        // close menu

  if (digitalRead(encoder0Press) == LOW) return 1;        // if button still pressed
  return 0;
}



// display menu on oled
void staticMenu() {
  display.clearDisplay(); 
  // title
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(lineSpace1, 0);
    display.print(menuTitle);
    display.drawLine(0, lineSpace1, display.width(), lineSpace1, WHITE);

  // menu options
    int i=0;
    while (i < menuMax && menuOption[i] != "") {                           // if menu item is not blank display it
      if (i == menuItemClicked) display.setTextColor(BLACK,WHITE);         // if this item has been clicked
      else display.setTextColor(WHITE,BLACK);
      display.setCursor(10, 18 + (i*lineSpace1));
      display.print(menuOption[i]);
      i++;
    }

  // highlighted item if none yet clicked
    if (menuItemClicked == 100) {
      display.setCursor(2, 18 + (menuCount * lineSpace1));
      display.print(">");
    }
 
  display.display();          // update display
}

//  --------------------------------------

// rotary encoder button
//    returns 1 if the button status has changed since last time

bool menuCheck() {

  if (digitalRead(encoder0Press) == reButtonState) return 0;    // no change
  delay(40);
  if (digitalRead(encoder0Press) == reButtonState) return 0;    // debounce
  if (millis() - reButtonTimer  < reButtonMinTime) return 0;    // if too soon since last change

  // button status has changed
  reButtonState = !reButtonState;   
  reButtonTimer = millis();                                     // update timer
  if (serialDebug) Serial.println("button state has changed");

  // oled menu action on button press 
  if (reButtonState==LOW) {                                     // if button is now pressed      
    lastREActivity = millis();                                  // log time last activity seen (don't count button release as activity)
    if (menuItemClicked != 100 || menuTitle == "") return 1;    // menu item already selected or there is no live menu
    if (serialDebug) Serial.println("menu '" + menuTitle + "' item " + String(menuCount) + " selected");
    menuItemClicked = menuCount;                                // set item selected flag
  }

  return 1;
}


// wait for key press or turn on rotary encoder
//    pass timeout in ms
void reWaitKeypress(int timeout) {          
        uint32_t tTimer = millis();   // log time
        // wait for button to be released
          while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < timeout) ) {        // wait for button release
            yield();                  // service any web page requests
            delay(20);
          }
        // clear rotary encoder position counter
          noInterrupts();
            encoder0Pos = 0;       
          interrupts();
        // wait for button to be pressed or encoder to be turned
          while ( (digitalRead(encoder0Press) == HIGH) && (encoder0Pos == 0) && (millis() - tTimer < timeout) ) {
            yield();                  // service any web page requests
            delay(20);
          }
        exitMenu();                   // close menu
}

//  --------------------------------------

// handle menu item selection

void menuItemSelection() {
    if (encoder0Pos >= itemTrigger) {
      noInterrupts();
        encoder0Pos = 0;
      interrupts();
      lastREActivity = millis();                            // log time last activity seen
      if (menuCount+1 < menuMax) menuCount++;               // if not past max menu items move
      if (menuOption[menuCount] == "") menuCount--;         // if menu item is blank move back
    }
    if (encoder0Pos <= -itemTrigger) {
      noInterrupts();
        encoder0Pos = 0;
      interrupts();
      lastREActivity = millis();                            // log time last activity seen
      if (menuCount > 0) menuCount--;
    }
}   

//  ---------------------------------------

// enter a value using the rotary encoder
//   pass Value title, starting value, step size, low limit , high limit
//   returns the chosen value
int enterValue(String title, int start, int stepSize, int low, int high) {
  uint32_t tTimer = millis();                          // log time of start of function
  // display title
    display.clearDisplay();  
    if (title.length() > 8) display.setTextSize(1);  // if title is longer than 8 chars make text smaller
    else display.setTextSize(2);
    display.setTextColor(WHITE);
    display.setCursor(0, 0);
    display.print(title);
    display.display();                                  // update display
  int tvalue = start;
  while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) delay(5);    // wait for button release
  tTimer = millis();
  while ( (digitalRead(encoder0Press) == HIGH) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) {   // while button is not pressed and still within time limit
    if (encoder0Pos >= itemTrigger) {                    // encoder0Pos is updated via the interrupt procedure
      tvalue -= stepSize;
      noInterrupts();                                   // stop interrupt changing the value whilst it is changed here
        encoder0Pos -= itemTrigger;
      interrupts();
      tTimer = millis(); 
    } 
    else if (encoder0Pos <= -itemTrigger) {
      tvalue += stepSize;
      noInterrupts();
        encoder0Pos += itemTrigger;
      interrupts();
      tTimer = millis();
    }  
    // value limits
      if (tvalue > high) tvalue=high;              
      if (tvalue < low) tvalue=low;
    display.setTextSize(3);
    const int textPos = 27;                             // height of number on display
    display.fillRect(0, textPos, SCREEN_WIDTH, SCREEN_HEIGHT - textPos, BLACK);   // clear bottom half of display (128x64)
    display.setCursor(0, textPos);
    display.print(tvalue);
    // bar graph at bottom of display
      int tmag=map(tvalue, low, high, 0 ,SCREEN_WIDTH);
      display.fillRect(0, SCREEN_HEIGHT - 10, tmag, 10, WHITE);  
    display.display();                                  // update display
    yield();                                            // service any web page requests
  }
  exitMenu();                                           // close menu
  return tvalue;
}

//  --------------------------------------

        
// choose from list using rotary encoder
//  pass the number of items in list (max 8), list title, list of options in a string array

int chooseFromList(byte noOfElements, String listTitle, String list[]) {

  const byte noList = 10;                                // max number of items to list
  uint32_t tTimer = millis();                            // log time of start of function
  int highlightedItem = 0;                               // which item in list is highlighted
  int xpos, ypos;
  
  // display title
    display.clearDisplay();  
    display.setTextSize(1);
    display.setTextColor(WHITE,BLACK);
    display.setCursor(10, 0);
    display.print(listTitle);
    display.drawLine(0, lineSpace1, display.width(), lineSpace1, WHITE);

  // scroll through list
    while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) delay(5);    // wait for button release
    tTimer = millis();
    while ( (digitalRead(encoder0Press) == HIGH) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) {   // while button is not pressed and still within time limit
      if (encoder0Pos >= itemTrigger) {                    // encoder0Pos is updated via the interrupt procedure
        noInterrupts();
          encoder0Pos = 0;
        interrupts();
        highlightedItem++;
        tTimer = millis(); 
      }
      if (encoder0Pos <= -itemTrigger) {
        noInterrupts();
          encoder0Pos = 0;
        interrupts();
        highlightedItem--;
        tTimer = millis();
      }  
      // value limits
        if (highlightedItem > noOfElements - 1) highlightedItem = noOfElements - 1;        
        if (highlightedItem < 0) highlightedItem = 0;
      // display the list
        for (int i=0; i < noOfElements; i++) {
            if (i < (noList/2)) {
              xpos = 0; 
              ypos = lineSpace1 * (i+1) + 7;
            } else {
              xpos = display.width() / 2;
              ypos = lineSpace1 * (i-((noList/2)-1)) + 7;
            }
            display.setCursor(xpos, ypos);
            if (i == highlightedItem) display.setTextColor(BLACK,WHITE);
            else display.setTextColor(WHITE,BLACK);
            display.print(list[i]);
        }
      display.display();                                    // update display
      yield();                                              // service any web page requests
    }
  
  // if it timed out set selection to cancel (i.e. item 0)
    if (millis() - tTimer >= (OLEDDisplayTimeout * 1000)) highlightedItem=0;      

//  // wait for button to be released (up to 1 second)
//    tTimer = millis();                         // log time
//    while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < 1000) ) {
//      yield();        // service any web page requests
//      delay(20);
//    }

    exitMenu();        // close menu

    return highlightedItem;
}

//  --------------------------------------

// close the menus and return to sleep mode

void exitMenu() {    
    reButtonState = digitalRead(encoder0Press);                           // update current button status
    lastREActivity = 0;                                                   // clear time of last rotary encoder usage
    noInterrupts();
      encoder0Pos = 0;                                                    // clear rotary encoder position change counter
    interrupts();
}

//  --------------------------------------

// rotary encoder interrupt routine to update position counter when turned
//      interrupt info: https://www.gammon.com.au/forum/bbshowpost.php?id=11488

#if defined (__AVR_ATmega328P__)
  void doEncoder() {
#elif defined ESP32
  IRAM_ATTR void doEncoder() {
#else   // esp8266
  ICACHE_RAM_ATTR void doEncoder() {
#endif
      
  bool pinA = digitalRead(encoder0PinA);
  bool pinB = digitalRead(encoder0PinB);

  if ( (encoderPrevA == pinA && encoderPrevB == pinB) ) return;  // no change since last time (i.e. reject bounce)

  // same direction (alternating between 0,1 and 1,0 in one direction or 1,1 and 0,0 in the other direction)
         if (encoderPrevA == 1 && encoderPrevB == 0 && pinA == 0 && pinB == 1) encoder0Pos -= 1;
    else if (encoderPrevA == 0 && encoderPrevB == 1 && pinA == 1 && pinB == 0) encoder0Pos -= 1;
    else if (encoderPrevA == 0 && encoderPrevB == 0 && pinA == 1 && pinB == 1) encoder0Pos += 1;
    else if (encoderPrevA == 1 && encoderPrevB == 1 && pinA == 0 && pinB == 0) encoder0Pos += 1;
    
  // change of direction
    else if (encoderPrevA == 1 && encoderPrevB == 0 && pinA == 0 && pinB == 0) encoder0Pos += 1;   
    else if (encoderPrevA == 0 && encoderPrevB == 1 && pinA == 1 && pinB == 1) encoder0Pos += 1;
    else if (encoderPrevA == 0 && encoderPrevB == 0 && pinA == 1 && pinB == 0) encoder0Pos -= 1;
    else if (encoderPrevA == 1 && encoderPrevB == 1 && pinA == 0 && pinB == 1) encoder0Pos -= 1;

    else if (serialDebug) Serial.println("Error: invalid rotary encoder pin state - prev=" + String(encoderPrevA) + "," 
                                          + String(encoderPrevB) + " new=" + String(pinA) + "," + String(pinB));
    
  // update previous readings
    encoderPrevA = pinA;
    encoderPrevB = pinB;

}

void stepper(void * pvParameters){
  for(;;){
    myStepper.setSpeed(speed);
    myStepper.step(stepsPerRevolution);
  }
}

// ---------------------------------------------- end ----------------------------------------------