//*******************************************************************************************************************
//###################################################################################################################
/*
 Display OLED Sistema de menus - versão i2c SSD1306 - 25/04/2023


Material de apoio:
https://github.com/alanesq/BasicOLEDMenu

Para mais informações antigas, consulte:
https://randomnerdtutorials.com/guide-for-oled-display-with-arduino/
https://lastminuteengineers.com/oled-display-esp32-tutorial/
*/
//###################################################################################################################
//*******************************************************************************************************************

// Configuração

const String stitle = "Menu Simples";        // título do roteiro
const String sversion = "25/04/2023";        // versão do roteiro
const String seditor = "Mario.Parente";      // Ultimo editor do roteiro

bool serialDebug = 1;                        // ativar informações de depuração na porta serial
int OLEDDisplayTimeout = 10;                 // tempo limite de exibição do menu oled (segundos)
int itemTrigger = 1;                         // codificador rotativo - conta por clique
#define OLED_ADDR 0x3C                       // Endereço OLED i2c

#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                    // botão - Nota: no esp8266 você pode mudar isso de d7 para d3 deixando d7 e d8 livres para usar 

#elif defined(ESP32)
// esp32
const String boardType = "ESP32";
#define I2C_SDA 21                          // i2c pins
#define I2C_SCL 22
#define encoder0PinA  25
#define encoder0PinB  26
#define encoder0Press 27                 // botão 

#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                    // botão  
#else
#error Unsupported board - must be esp32, esp8266 or Arduino Uno
#endif


//*******************************************************************************************************************
//###################################################################################################################

//#include <MemoryFree.h>                 // usado para exibir memória livre no Arduino (útil, pois pode ser muito limitado)
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Codificador rotativo
volatile int16_t encoder0Pos = 0;         // valor atual selecionado com codificador rotativo (atualizado na rotina de interrupção)
volatile bool encoderPrevA = 0;           // usado para codificador rotativo debounced
volatile bool encoderPrevB = 0;           // usado para codificador rotativo debounced
bool reButtonState = 0;                   // estado de debounce atual do botão
uint32_t reButtonTimer = millis();        // estado do botão de tempo alterado pela última vez
int reButtonMinTime = 500;                // milissegundos mínimos entre as alterações de status do botão permitidos

// oled menu
const byte menuMax = 5;                   // número máximo de itens de menu
const byte EspacoLinha1 = 9;              // espaçamento entre linhas (6 linhas)
const byte EspacoLinha2 = 16;             // espaçamento entre linhas (4 linhas)
String menuOption[menuMax];               // opções exibidas no menu
byte menuCount = 0;                       // qual item de menu está realçado no momento
byte menuMaxCount = 0;                    // qual item de menu está realçado no momento
String menuTitle = "";                    // número de ID do menu atual (em branco = nenhum)
byte menuItemClicked = 100;               // item de menu foi clicado sinalizador (100=nenhum)
uint32_t lastREActivity = 0;              // time last activity was seen on rotary encoder

// monitor SSD1306 oled conectado a I2C (pinos SDA, SCL)
#define SCREEN_WIDTH 128                  // Largura da tela OLED, em pixels
#define SCREEN_HEIGHT 64                  // Altura da tela OLED, em pixels
#define OLED_RESET -1                     // Pino de reset # (ou -1 se compartilhar o pino de reset do Arduino)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

//*******************************************************************************************************************
//###################################################################################################################


//###################################################################################################################
//                                        Personalize os menus abaixo
//###################################################################################################################

// Useful commands:
//      void reWaitKeypress(20000);         = aguarde o botão ser pressionado no codificador rotativo (timeout em 20 segundos, caso contrário)
//      chooseFromList(8, "TestList", q);   = escolha na lista de 8 itens em uma matriz de string 'q'
//      enterValue("Testval", 15, 0, 30);   = insira um valor entre 0 e 30 (com um valor inicial de 15)


// Available Menus

// main menu
void Main_Menu_1() {
  menuTitle = "Menu principal";                                 // definir o título do menu
  setMenu(0, "");                                               // limpar todos os itens do menu
  setMenu(0, "Lista");                                          // escolha de uma lista
  setMenu(1, "Insira um valor");                                // insira um valor
  setMenu(2, "Mensagem");                                       // exibir uma mensagem
  setMenu(3, "Menu Secundario");                                // mudar menu
 // setMenu(4, "Menu Secundario 2");  
}

// menu 2
void Main_Menu_2() {
  menuTitle = "Menu Secundario";                                // definir o título do menu
  setMenu(0, "");                                               // limpar todos os itens do menu
  setMenu(0, "menu off");                                       // Desliga o visor
  setMenu(1, "Menu principal");                                 // mudar menu
}
//*******************************************************************************************************************




//###################################################################################################################
// procedimentos de ação do menu
//###################################################################################################################

// verifica se o item do menu foi selecionado com: (menuTitle == "<menu name>" && menuItemClicked==<item number 1-4>)

void menuItemActions() {

  if (menuItemClicked == 100) return;                            // se nenhum item de menu foi clicado sai da função



// ------------------------------ Menu Principal Ação ------------------------------

// _____________________________________ Opção 1 _____________________________________
  if (menuTitle == "Menu principal" && menuItemClicked == 0) {
    menuItemClicked = 100;                                          // flag that the button press has been actioned (the menu stops and waits until this)
    String q[] = {"item 0", "item 1", "item 2", "item 3", "item 4", "item 5", "item 6", "item 7"};
    int tres = chooseFromList(8, "TestList", q);
    Serial.println("Menu: item " + String(tres) + " escolhido da lista");
  }
// _____________________________________ Opção 2 _____________________________________
  if (menuTitle == "Menu principal" && menuItemClicked == 1) {
    menuItemClicked = 100;
    int tres = enterValue("Valor", 50, 1, 0, 100);                // insira um valor (título, valor inicial, tamanho do passo, limite baixo, limite alto)
    Serial.println("Menu: Valor definido = " + String(tres));
  }
// _____________________________________ Opção 3 _____________________________________
  if (menuTitle == "Menu principal" && menuItemClicked == 2) {
    menuItemClicked = 100;
    Serial.println("Menu: display message selected");
    // display a message
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(WHITE);
    display.setCursor(20, 20);
    display.print("Mensagem");
    display.display();
    reWaitKeypress(2000);                                          // wait for key press on rotary encoder
  }
// _____________________________________ Opção 4 _____________________________________
  if (menuTitle == "Menu principal" && menuItemClicked == 3) {
    menuItemClicked = 100;
    Serial.println("Menu: Menu 2 selected");
    Main_Menu_2();                                                        // show a different menu
  }


// ------------------------------ Menu Secundario Ação ------------------------------

// _____________________________________ Opção 1 _____________________________________
  if (menuTitle == "Menu Secundario" && menuItemClicked == 0) {
    menuItemClicked = 100;
    Serial.println("Menu: Menu off");
    menuTitle = "";                                                 // turn menu off
    display.clearDisplay();
    display.display();
  }
// _____________________________________ Opção 2 _____________________________________
  if (menuTitle == "Menu Secundario" && menuItemClicked == 1) {
    menuItemClicked = 100;
    Serial.println("Menu principal selecionado");
    Main_Menu_1();                                                    // show main menu
  }

}

//###################################################################################################################
//                                        Personalize os menus acima
//###################################################################################################################


//Configuração
void setup() {

  Serial.begin(115200);
  delay(200);
  Serial.println("\nMenu simples em tela Oled");

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

  // initialise the oled display
  Wire.begin(I2C_SDA, I2C_SCL);   // se der algum erro pode ser que a placa que você está usando não permite definir os pinos nesse caso tente: Wire.begin();
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println(("\nErro ao inicializar o visor oled"));
  }

 // Exibe a tela inicial no OLED
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0, EspacoLinha1 * 1);
  display.print(stitle);
  display.setCursor(0, EspacoLinha1 * 2);
  display.print(sversion);
  display.setCursor(0, EspacoLinha1 * 3);
  display.print(seditor);
  display.setCursor(0, EspacoLinha1 * 4);
  display.print(boardType);
  display.setCursor(0, EspacoLinha1 * 5);
  //display.print(freeMemory()); // usado para exibir memória livre no Arduino (útil, pois pode ser muito limitado)
  display.display();
  delay(2500);

// Interrupção para leitura da posição do encoder rotativo
  attachInterrupt(digitalPinToInterrupt(encoder0PinA), doEncoder, CHANGE);

  Main_Menu_1();    // iniciar a exibição do menu - consulte menuItemActions() para alterar os menus
}

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


void loop() {

  // if a oled menu is active service it
  if (menuTitle != "") {                                  // se um menu estiver ativo
    menuCheck();                                          // verifique se o botão de seleção do encoder está pressionado
    menuItemSelection();                                  // verifique se há alteração no item de menu realçado
    staticMenu();                                         // exibir o menu
    menuItemActions();                                    // agir se um item de menu foi clicado
  }



  //     <<< seu código aqui >>>>




  yield();
}


//  -------------------------------------------------------------------------------------------
//  ---------------------------------- procedimentos de menu ----------------------------------
//  -------------------------------------------------------------------------------------------


// define item de menu
// pass: novos itens de menu número, nome (em branco inname limpa todas as entradas)

void setMenu(byte inum, String iname) {
  if (inum >= menuMax) return;    // número inválido
  if (iname == "") {              // limpar todos os itens do menu
    for (int i = 0; i < menuMax; i++)  menuOption[i] = "";
    menuCount = 0;                // mover destaque para o item de menu superior
  } else {
    menuOption[inum] = iname;
    menuItemClicked = 100;        // definir sinalizador de item selecionado como nenhum
  }
}

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

// confirma que uma ação solicitada não é um erro
// retorna 1 se confirmado

bool confirmActionRequired() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE, BLACK);
  display.setCursor(0, EspacoLinha2 * 0);
  display.print("SEGURAR");
  display.setCursor(0, EspacoLinha2 * 1);
  display.print("BOTAO PARA");
  display.setCursor(0, EspacoLinha2 * 2);
  display.print("CONFIRME!");
  display.display();          // exibição de atualização

  delay(2000);
  display.clearDisplay();
  display.display();          // exibição de atualização

  exitMenu();       // fecha o menu

  if (digitalRead(encoder0Press) == LOW) return 1;       // se o botão ainda estiver pressionado
  return 0;
}

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

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

  // menu options
  int i = 0;
  while (i < menuMax && menuOption[i] != "") {                           // se o item do menu não estiver em branco, mostre-o
    if (i == menuItemClicked) display.setTextColor(BLACK, WHITE);        // se este item foi clicado
    else display.setTextColor(WHITE, BLACK);
    display.setCursor(10, 18 + (i * EspacoLinha1));
    display.print(menuOption[i]);
    i++;
  }
  menuMaxCount = i;
  // item destacado se nenhum ainda tiver sido clicado
  if (menuItemClicked == 100) {
    display.setCursor(2, 18 + (menuCount * EspacoLinha1));
    display.print(">");
  }

  display.display();          // exibição de atualização
}


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


// botão de codificador rotativo
//    retorna 1 se o status do botão mudou desde a última vez

bool menuCheck() {

  if (digitalRead(encoder0Press) == reButtonState) return 0;    // sem alteração
  delay(40);
  if (digitalRead(encoder0Press) == reButtonState) return 0;    // debounce
  if (millis() - reButtonTimer  < reButtonMinTime) return 0;    // se muito cedo desde a última alteração

  // button status has changed
  reButtonState = !reButtonState;
  reButtonTimer = millis();                                     // atualiza o cronômetro
  if (serialDebug) Serial.println("O estado do botão mudou");

  // ação do menu oled ao pressionar o botão
  if (reButtonState == LOW) {                                   // se o botão agora for pressionado
    lastREActivity = millis();                                  // registre o tempo da última atividade vista (não conte a liberação do botão como atividade)
    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) + " selecionado");
    menuItemClicked = menuCount;                                // definir sinalizador de item selecionado
  }

  return 1;
}



// espera o pressionamento da tecla ou liga o codificador rotativo
//    pass timeout in ms
void reWaitKeypress(int timeout) {
  uint32_t tTimer = millis();   // tempo de registro
  // aguarde o botão ser liberado
  while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < timeout) ) {        // espera a liberação do botão
    yield();                  // atende a qualquer solicitação de página da web
    delay(20);
  }
  // limpa o contador de posição do codificador rotativo
  noInterrupts();
  encoder0Pos = 0;
  interrupts();
  // espera o botão ser pressionado ou o encoder ser girado
  while ( (digitalRead(encoder0Press) == HIGH) && (encoder0Pos == 0) && (millis() - tTimer < timeout) ) {
    yield();                  // atende a qualquer solicitação de página da web
    delay(20);
  }
  exitMenu();                  // fecha o menu
}


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


// manipula a seleção de itens de menu

void menuItemSelection() {
  if (encoder0Pos >= itemTrigger) {
    noInterrupts();
    encoder0Pos = 0;
    interrupts();
    lastREActivity = millis();                            // registrar o tempo da última atividade vista
    if (menuCount + 1 < menuMax) menuCount++;             // se não passar do máximo de itens de menu, mova
    if (menuCount + 1 == menuMax) {
      menuCount = 0;
    }
    if (menuOption[menuCount] == "") menuCount--;         // se o item do menu estiver em branco, volte
  }


  if (encoder0Pos <= -itemTrigger) {
    noInterrupts();
    encoder0Pos = 0;
    interrupts();
    lastREActivity = millis();                            // registrar o tempo da última atividade vista
    if (menuCount == 0 ){
      menuCount = menuMaxCount;
    }
    if (menuCount > 0) menuCount--;
  }

}


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


// insira um valor usando o codificador rotativo
// passa o título do valor, valor inicial, tamanho do passo, limite baixo, limite alto
// retorna o valor escolhido


int enterValue(String title, int start, int stepSize, int low, int high) {
  uint32_t tTimer = millis();                          // registra a hora de início da função
 
 
  // título de exibição
  display.clearDisplay();
  if (title.length() > 8) display.setTextSize(1);  // se o título tiver mais de 8 caracteres, reduz o texto
  else display.setTextSize(2);
  display.setTextColor(WHITE);
  display.setCursor(0, 0);
  display.print(title);
  display.display();                                      // exibição de atualização
  int tvalue = start;
  while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) delay(5);    // espera a liberação do botão
  tTimer = millis();
  while ( (digitalRead(encoder0Press) == HIGH) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) {   // enquanto o botão não é pressionado e ainda dentro do limite de tempo
    if (encoder0Pos >= itemTrigger) {                    // encoder0Pos é atualizado através do procedimento de interrupção
      tvalue -= stepSize;
      noInterrupts();                                   // para de interromper a alteração do valor enquanto ele é alterado aqui
      encoder0Pos -= itemTrigger;
      interrupts();
      tTimer = millis();
    }
    else if (encoder0Pos <= -itemTrigger) {
      tvalue += stepSize;
      noInterrupts();
      encoder0Pos += itemTrigger;
      interrupts();
      tTimer = millis();
    }
    // limites de valor
    if (tvalue > high) tvalue = high;
    if (tvalue < low) tvalue = low;
    display.setTextSize(3);
    const int textPos = 27;                            // altura do número exibido
    display.fillRect(0, textPos, SCREEN_WIDTH, SCREEN_HEIGHT - textPos, BLACK);   // limpa a metade inferior da tela (128x64)
    display.setCursor(0, textPos);
    display.print(tvalue);
    // gráfico de barras na parte inferior da tela
    int tmag = map(tvalue, low, high, 0, SCREEN_WIDTH);
    display.fillRect(0, SCREEN_HEIGHT - 10, tmag, 10, WHITE);
    display.display();                                   // exibição de atualização
    yield();                                           // atende a qualquer solicitação de página da web
  }
  exitMenu();                                           // fecha o menu
  return tvalue;
}


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


// escolha da lista usando o codificador rotativo
// passa o número de itens na lista (max 8), título da lista, lista de opções em um array de strings

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

  const byte noList = 10;                                // número máximo de itens para listar
  uint32_t tTimer = millis();                            // registrar a hora do início da função
  int highlightedItem = 0;                               // qual item da lista está destacado
  int xpos, ypos;

  // display title
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE, BLACK);
  display.setCursor(10, 0);
  display.print(listTitle);
  display.drawLine(0, EspacoLinha1, display.width(), EspacoLinha1, WHITE);

  // scroll through list
  while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) delay(5);    // espera a liberação do botão
  tTimer = millis();
  while ( (digitalRead(encoder0Press) == HIGH) && (millis() - tTimer < (OLEDDisplayTimeout * 1000)) ) {   // enquanto o botão não é pressionado e ainda dentro do limite de tempo
    if (encoder0Pos >= itemTrigger) {                    // encoder0Pos é atualizado através do procedimento de interrupção
      noInterrupts();
      encoder0Pos = 0;
      interrupts();
      highlightedItem++;
      tTimer = millis();
    }
    if (encoder0Pos <= -itemTrigger) {
      noInterrupts();
      encoder0Pos = 0;
      interrupts();
      highlightedItem--;
      tTimer = millis();
    }
   // limites de valor
    if (highlightedItem > noOfElements - 1) highlightedItem = noOfElements - 1;
    if (highlightedItem < 0) highlightedItem = 0;
    // exibir a lista
    for (int i = 0; i < noOfElements; i++) {
      if (i < (noList / 2)) {
        xpos = 0;
        ypos = EspacoLinha1 * (i + 1) + 7;
      } else {
        xpos = display.width() / 2;
        ypos = EspacoLinha1 * (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();                                    // exibição de atualização
    yield();                                              // atender a qualquer solicitação de página da web
  }

  // se expirou, defina a seleção para cancelar (i.e. item 0)
  if (millis() - tTimer >= (OLEDDisplayTimeout * 1000)) highlightedItem = 0;

  //  // espera o botão ser solto (até 1 segundo)
  //    tTimer = millis();                         // log time
  //    while ( (digitalRead(encoder0Press) == LOW) && (millis() - tTimer < 1000) ) {
  //      yield();        // service any web page requests
  //      delay(20);
  //    }

  exitMenu();        // fecha o menu

  return highlightedItem;
}


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


// close the menus and return to sleep mode

void exitMenu() {
  reButtonState = digitalRead(encoder0Press);                           // atualizar o status atual do botão
  lastREActivity = 0;                                                   // limpar o tempo do último uso do codificador rotativo
  noInterrupts();
  encoder0Pos = 0;                                                    // limpar o contador de mudança de posição do codificador rotativo
  interrupts();
}


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

// rotina de interrupção do codificador rotativo para atualizar o contador de posição quando girado
//      informações de interrupção: 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;  // nenhuma alteração desde a última vez (ou seja, rejeição de rejeição)

  // mesma direção (alternando entre 0,1 e 1,0 em uma direção ou 1,1 e 0,0 na outra direção)
  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;

  // mudança de direção
  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("Erro: estado inválido do pino do codificador rotativo - anterior=" + String(encoderPrevA) + ","
                                         + String(encoderPrevB) + " new=" + String(pinA) + "," + String(pinB));

  // atualizar leituras anteriores
  encoderPrevA = pinA;
  encoderPrevB = pinB;

}


// ---------------------------------------------- fim - ---------------------------------------------