//*******************************************************************************************************************
//###################################################################################################################
/*
Display OLED Sistema de menus - versão i2c SSD1306 - 25/04/2023
Para controlar LED-RGB com Encoder rotativo
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
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 // i2c pins
#define encoder0PinA 19 // CLK Encoder
#define encoder0PinB 18 // DT Encoder
#define encoder0Press 5 // SW Encoder
#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 <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
//LED-RGB
#define L_RED 4
#define L_BLUE 2
#define L_GREEN 15
// 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
#define TMenu "Menu LED-RGB"
// main menu
void Main_Menu_1() {
menuTitle = TMenu; // definir o título do menu
setMenu(0, ""); // limpar todos os itens do menu
setMenu(0, "Valor Fixo"); // escolha de uma lista
setMenu(1, "Valor RED"); // insira um valor
setMenu(2, "Valor BLUE"); // exibir uma mensagem
setMenu(3, "Valor GREEN"); // 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 == TMenu && 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 == TMenu && menuItemClicked == 1) {
menuItemClicked = 100;
int V_RED = enterValue("Valor Cor RED", 50, 1, 0, 100); // insira um valor (título, valor inicial, tamanho do passo, limite baixo, limite alto)
Serial.println("Menu: Valor definido = " + String(V_RED));
analogWrite(L_RED,V_RED);
}
// _____________________________________ Opção 3 _____________________________________
if (menuTitle == TMenu && menuItemClicked == 2) {
menuItemClicked = 100;
int V_BLUE = enterValue("Valor Cor BLUE", 50, 1, 0, 100); // insira um valor (título, valor inicial, tamanho do passo, limite baixo, limite alto)
Serial.println("Menu: Valor definido = " + String(V_BLUE));
analogWrite(L_BLUE,V_BLUE);
}
// _____________________________________ Opção 4 _____________________________________
if (menuTitle == TMenu && menuItemClicked == 3) {
menuItemClicked = 100;
int V_GREEN = enterValue("Valor Cor Green", 50, 1, 0, 100); // insira um valor (título, valor inicial, tamanho do passo, limite baixo, limite alto)
Serial.println("Menu: Valor definido = " + String(V_GREEN));
analogWrite(L_GREEN,V_GREEN);
Serial.println("Verde");
}
}
//###################################################################################################################
// 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);
pinMode(L_RED, OUTPUT);
pinMode(L_BLUE, OUTPUT);
pinMode(L_GREEN, OUTPUT);
// 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"));
}
// 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();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(EspacoLinha1, 0);
display.print(title);
display.drawLine(0, EspacoLinha1, display.width(), EspacoLinha1, WHITE);
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 - ---------------------------------------------