#include <Arduino.h> // required by platformIO
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "EncButton.h"
#define DEBUG true
// --- encoder ------
#define ENCODER_CLK 4 // Pin connected to CLK of KY-040 (A)
#define ENCODER_DT 2 // Pin connected to DT of KY-040 (B)
#define ENCODER_SW 15 // Pin connected to SW of KY-040 (button)
#define BUTTONPRESSEDSTATE 0 // rotary encoder gpio pin logic level when the button is pressed (usually 0)
#define DEBOUNCEDELAY 100 // debounce delay for button inputs
struct rotaryEncoders {
volatile int encoder0Pos = 0; // current value selected with rotary encoder (updated by interrupt routine)
volatile bool encoderPrevA; // used to debounced rotary encoder
volatile bool encoderPrevB; // used to debounced rotary encoder
uint32_t reLastButtonChange = 0; // last time state of button changed (for debouncing)
bool encoderPrevButton = 0; // used to debounce button
int reButtonDebounced = 0; // debounced current button state (1 when pressed)
const bool reButtonPressedState = BUTTONPRESSEDSTATE; // the logic level when the button is pressed
const uint32_t reDebounceDelay = DEBOUNCEDELAY; // button debounce delay setting
bool reButtonPressed = 0; // flag set when the button is pressed (it has to be manually reset)
};
rotaryEncoders rotaryEncoder;
// EncButton<EB_TICK, ENCODER_CLK, ENCODER_DT, ENCODER_SW> enc; // энкодер с кнопкой <A, B, KEY>
// EncButton<EB_TICK, ENCODER_CLK, ENCODER_DT> enc; // просто энкодер <A, B>
// EncButton<EB_TICK, ENCODER_SW> btn; // просто кнопка <KEY>
// ----- OLED ----------
#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)
#define OLED_SCL 22
#define OLED_SDA 21
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
// oled SSD1306 display connected to I2C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ----------- menu --------------
const int menuTimeout = 10; // menu inactivity timeout (seconds)
const bool menuLargeText = 0; // show larger text when possible (if struggling to read the small text)
const int maxmenuItems = 12; // max number of items used in any of the menus (keep as low as possible to save memory)
const int MaxmenuTitleLength = 10; // max characters per line when using text size 2 (usually 10)
const int displayMaxLines = 5; // max lines that can be displayed in lower section of display in textsize1 (5 on larger oLeds)
const int topLine = 18; // y position of lower area of the display (18 with two colour displays)
const byte lineSpace1 = 9; // line spacing for textsize 1 (small text)
const byte lineSpace2 = 17; // line spacing for textsize 2 (large text)
// modes that the menu system can be in
enum menuModes {
off, // display is off
menu, // a menu is active
value, // 'enter a value' none blocking is active
message, // displaying a message
blocking // a blocking procedure is in progress (see enter value)
};
menuModes menuMode = off; // default mode at startup is off
struct oledMenus {
// menu
String menuTitle = ""; // the title of active mode
int noOfmenuItems = 0; // number if menu items in the active menu
int selectedMenuItem = 0; // when a menu item is selected it is flagged here until actioned and cleared
int highlightedMenuItem = 0; // which item is curently highlighted in the menu
String menuItems[maxmenuItems+1]; // store for the menu item titles
uint32_t lastMenuActivity = 0; // time the menu last saw any activity (used for timeout)
// 'enter a value'
int mValueEntered = 0; // store for number entered by value entry menu
int mValueLow = 0; // lowest allowed value
int mValueHigh = 0; // highest allowed value
int mValueStep = 0; // step size when encoder is turned
};
oledMenus oledMenu;
// -------------------------------------------------------------------------------------------------
// menus below here
// -------------------------------------------------------------------------------------------------
// Start the default menu
void defaultMenu() {
demoMenu();
}
// demonstration menu
void demoMenu() {
resetMenu(); // clear any previous menu
menuMode = menu; // enable menu mode
oledMenu.noOfmenuItems = 8; // set the number of items in this menu
oledMenu.menuTitle = "demo_menu"; // menus title (used to identify it)
oledMenu.menuItems[1] = "item1"; // set the menu items
oledMenu.menuItems[2] = "item2";
oledMenu.menuItems[3] = "Quick menu";
oledMenu.menuItems[4] = "On or Off";
oledMenu.menuItems[5] = "Enter value";
oledMenu.menuItems[6] = "Enter value-blocking";
oledMenu.menuItems[7] = "Message";
oledMenu.menuItems[8] = "Exit";
}
// ----------------------------------------------------------------
// -service value entry
// ----------------------------------------------------------------
// if _blocking set to 1 then all other tasks are stopped until a value is entered
int serviceValue(bool _blocking) {
const int _valueSpacingX = 30; // spacing for the displayed value y position
const int _valueSpacingY = 5; // spacing for the displayed value y position
if (_blocking) {
menuMode = blocking;
oledMenu.lastMenuActivity = millis(); // log time of last activity (for timeout)
}
uint32_t tTime;
do {
// rotary encoder
if (rotaryEncoder.encoder0Pos >= 1) {
rotaryEncoder.encoder0Pos -= 1;
oledMenu.mValueEntered-= oledMenu.mValueStep;
oledMenu.lastMenuActivity = millis(); // log time
}
if (rotaryEncoder.encoder0Pos <= -1) {
rotaryEncoder.encoder0Pos += 1;
oledMenu.mValueEntered+= oledMenu.mValueStep;
oledMenu.lastMenuActivity = millis(); // log time
}
if (oledMenu.mValueEntered < oledMenu.mValueLow) {
oledMenu.mValueEntered = oledMenu.mValueLow;
oledMenu.lastMenuActivity = millis(); // log time
}
if (oledMenu.mValueEntered > oledMenu.mValueHigh) {
oledMenu.mValueEntered = oledMenu.mValueHigh;
oledMenu.lastMenuActivity = millis(); // log time
}
display.clearDisplay();
display.setTextColor(WHITE);
// title
display.setCursor(0, 0);
if (oledMenu.menuTitle.length() > MaxmenuTitleLength) display.setTextSize(1);
else display.setTextSize(2);
display.println(oledMenu.menuTitle);
display.drawLine(0, topLine-1, display.width(), topLine-1, WHITE); // draw horizontal line under title
// value selected
display.setCursor(_valueSpacingX, topLine + _valueSpacingY);
display.setTextSize(3);
display.println(oledMenu.mValueEntered);
// range
display.setCursor(0, display.height() - lineSpace1 - 1 ); // bottom of display
display.setTextSize(1);
display.println(String(oledMenu.mValueLow) + " to " + String(oledMenu.mValueHigh));
// bar
int Tlinelength = map(oledMenu.mValueEntered, oledMenu.mValueLow, oledMenu.mValueHigh, 0 , display.width());
display.drawLine(0, display.height()-1, Tlinelength, display.height()-1, WHITE);
display.display();
reUpdateButton(); // check status of button
tTime = (unsigned long)(millis() - oledMenu.lastMenuActivity); // time since last activity
} while (_blocking && rotaryEncoder.reButtonPressed == 0 && tTime < (menuTimeout * 1000)); // if in blocking mode repeat until button is pressed or timeout
if (_blocking) menuMode = off;
return oledMenu.mValueEntered; // used when in blocking mode
}
// actions for menu selections are put in here
void menuActions() {
// actions when an item is selected in "demo_menu"
if (oledMenu.menuTitle == "demo_menu") {
// demonstrate quickly create a menu from a list
if (oledMenu.selectedMenuItem == 3) {
String tList[]={"main menu", "2", "3", "4", "5", "6"};
createList("demo_list", 6, &tList[0]);
}
if (oledMenu.selectedMenuItem == 4) {
resetMenu();
menuMode = value;
// set parameters
oledMenu.menuTitle = "on or off";
oledMenu.mValueLow = 0;
oledMenu.mValueHigh = 1;
oledMenu.mValueStep = 1;
oledMenu.mValueEntered = 0;
}
// demonstrate usage of 'enter a value' (none blocking)
if (oledMenu.selectedMenuItem == 5) {
resetMenu();
menuMode = value; // enable value entry
oledMenu.menuTitle = "demo_value"; // title (used to identify which number was entered)
oledMenu.mValueLow = 0; // minimum value allowed
oledMenu.mValueHigh = 100; // maximum value allowed
oledMenu.mValueStep = 1; // step size
oledMenu.mValueEntered = 50; // starting value
}
// demonstrate usage of 'enter a value' (blocking) which is quick and easy but stops all other tasks until the value is entered
if (oledMenu.selectedMenuItem == 6) {
// set perameters
resetMenu();
menuMode = value;
oledMenu.menuTitle = "blocking";
oledMenu.mValueLow = 0;
oledMenu.mValueHigh = 50;
oledMenu.mValueStep = 1;
oledMenu.mValueEntered = 5;
int tEntered = serviceValue(1); // request value
Serial.println("The value entered was " + String(tEntered));
defaultMenu();
}
// demonstrate usage of message
if (oledMenu.selectedMenuItem == 7) {
displayMessage("Message", "Hello\nThis is a demo\nmessage."); // 21 chars per line, "\n" = next line
}
if (oledMenu.selectedMenuItem == 8) {
resetMenu(); // turn menus off
}
oledMenu.selectedMenuItem = 0; // clear menu item selected flag
}
// actions when an item is selected in the demo_list menu
if (oledMenu.menuTitle == "demo_list") {
// back to main menu
if (oledMenu.selectedMenuItem == 1) {
defaultMenu();
}
oledMenu.selectedMenuItem = 0; // clear menu item selected flag
}
}
// actions for value entered put in here
void menuValues() {
// action for "demo_value"
if (oledMenu.menuTitle == "demo_value") {
String tString = String(oledMenu.mValueEntered);
displayMessage("ENTERED", "\nYou entered\nthe value\n " + tString);
// alternatively use 'resetMenu()' here to turn menus off after value entered - or use 'defaultMenu()' to re-start the default menu
}
// action for "on or off"
if (oledMenu.menuTitle == "on or off") {
defaultMenu();
}
}
// ----------------------------------------------------------------
// -message display
// ----------------------------------------------------------------
// 21 characters per line, use "\n" for next line
// assistant: < line 1 >< line 2 >< line 3 >< line 4 >
void displayMessage(String _title, String _message) {
resetMenu();
menuMode = message;
display.clearDisplay();
display.setTextColor(WHITE);
// title
display.setCursor(0, 0);
if (menuLargeText) {
display.setTextSize(2);
display.println(_title.substring(0, MaxmenuTitleLength));
} else {
if (_title.length() > MaxmenuTitleLength) display.setTextSize(1);
else display.setTextSize(2);
display.println(_title);
}
// message
display.setCursor(0, topLine);
display.setTextSize(1);
display.println(_message);
display.display();
}
// ----------------------------------------------------------------
// -list create
// ----------------------------------------------------------------
// create a menu from a list
// e.g. String tList[]={"main menu", "2", "3", "4", "5", "6"};
// createList("demo_list", 6, &tList[0]);
void createList(String _title, int _noOfElements, String *_list) {
resetMenu(); // clear any previous menu
menuMode = menu; // enable menu mode
oledMenu.noOfmenuItems = _noOfElements; // set the number of items in this menu
oledMenu.menuTitle = _title; // menus title (used to identify it)
for (int i=1; i <= _noOfElements; i++) {
oledMenu.menuItems[i] = _list[i-1]; // set the menu items
}
}
// ----------------------------------------------------------------
// -reset menu system
// ----------------------------------------------------------------
void resetMenu() {
// reset all menu variables / flags
menuMode = off;
oledMenu.selectedMenuItem = 0;
oledMenu.noOfmenuItems = 0;
oledMenu.menuTitle = "";
oledMenu.highlightedMenuItem = 0;
oledMenu.mValueEntered = 0;
oledMenu.lastMenuActivity = millis(); // log time
rotaryEncoder.encoder0Pos = 0;
rotaryEncoder.reButtonPressed = 0;
// clear oled display
display.clearDisplay();
display.display();
}
// ----------------------------------------------------------------
// -service active menu
// ----------------------------------------------------------------
void serviceMenu() {
if (rotaryEncoder.encoder0Pos >= 1) {
rotaryEncoder.encoder0Pos -= 1;
oledMenu.highlightedMenuItem++;
oledMenu.lastMenuActivity = millis(); // log time
}
if (rotaryEncoder.encoder0Pos <= -1) {
rotaryEncoder.encoder0Pos += 1;
oledMenu.highlightedMenuItem--;
oledMenu.lastMenuActivity = millis(); // log time
}
if (rotaryEncoder.reButtonPressed == 1) {
oledMenu.selectedMenuItem = oledMenu.highlightedMenuItem; // flag that the item has been selected
oledMenu.lastMenuActivity = millis(); // log time
#ifdef DEBUG
Serial.println("menu '" + oledMenu.menuTitle + "' item '" + oledMenu.menuItems[oledMenu.highlightedMenuItem] + "' selected");
#endif
}
const int _centreLine = displayMaxLines / 2 + 1; // mid list point
display.clearDisplay();
display.setTextColor(WHITE);
// verify valid highlighted item
if (oledMenu.highlightedMenuItem > oledMenu.noOfmenuItems){
oledMenu.highlightedMenuItem = oledMenu.noOfmenuItems;
}
if (oledMenu.highlightedMenuItem < 1){
oledMenu.highlightedMenuItem = 1;
}
// draw title
display.setCursor(0, 0);
if (menuLargeText) {
display.setTextSize(2);
display.println(oledMenu.menuItems[oledMenu.highlightedMenuItem].substring(0, MaxmenuTitleLength));
}else{
if (oledMenu.menuTitle.length() > MaxmenuTitleLength){
display.setTextSize(1);
}else{
display.setTextSize(2);
}
display.println(oledMenu.menuTitle);
}
// draw line
display.drawLine(0, topLine-1, display.width(), topLine-1, WHITE); // draw horizontal line under title
// draw list menu
display.setTextSize(1);
display.setCursor(0, topLine);
for (int i=1; i <= displayMaxLines; i++) {
int item = oledMenu.highlightedMenuItem - _centreLine + i;
if (item == oledMenu.highlightedMenuItem){
display.setTextColor(BLACK, WHITE);
}else{
display.setTextColor(WHITE);
}
if (item > 0 && item <= oledMenu.noOfmenuItems){
display.println(oledMenu.menuItems[item]);
}else{
display.println(" ");
}
}
// how to display some updating info. on the menu screen
// display.setCursor(80, 25);
// display.println(millis());
display.display();
}
// ----------------------------------------------------------------
// -update the active menu
// ----------------------------------------------------------------
// update or action the oled menu
void menuUpdate(){
if (menuMode == off) return; // if menu system is turned off do nothing more
// Serial.println("menuMode - " + menuMode);
// if no recent activity then turn oled off
if ( (unsigned long)(millis() - oledMenu.lastMenuActivity) > (menuTimeout * 1000) ) {
Serial.println("(menuTimeout * 1000)");
resetMenu();
return;
}
switch (menuMode) {
// if there is an active menu
case menu:
serviceMenu();
menuActions();
break;
// if there is an active none blocking 'enter value'
case value:
Serial.println("menuMode - value");
serviceValue(0);
if (rotaryEncoder.reButtonPressed) { // if the button has been pressed
menuValues(); // a value has been entered so action it
break;
}
// if a message is being displayed
case message:
Serial.println("menuMode - message");
if (rotaryEncoder.reButtonPressed == 1){
defaultMenu(); // if button has been pressed return to default menu
}
break;
}
}
// ----------------------------------------------------------------
// -button debounce (rotary encoder)
// update rotary encoder current button status
// update rotary encoder button status (if pressed activate default menu)
// ----------------------------------------------------------------
void reUpdateButton(){
bool tReading = digitalRead(ENCODER_SW); // read current button state
if (tReading != rotaryEncoder.encoderPrevButton) rotaryEncoder.reLastButtonChange = millis(); // if it has changed reset timer
if ( (unsigned long)(millis() - rotaryEncoder.reLastButtonChange) > rotaryEncoder.reDebounceDelay ) { // if button state is stable
if (rotaryEncoder.encoderPrevButton == rotaryEncoder.reButtonPressedState) {
if (rotaryEncoder.reButtonDebounced == 0) { // if the button has been pressed
rotaryEncoder.reButtonPressed = 1; // flag set when the button has been pressed
if (menuMode == off) defaultMenu(); // if the display is off start the default menu
}
rotaryEncoder.reButtonDebounced = 1; // debounced button status (1 when pressed)
} else {
rotaryEncoder.reButtonDebounced = 0;
}
}
rotaryEncoder.encoderPrevButton = tReading; // update last state read
}
void dashboard(){
if (menuMode != off) return;
static uint32_t reloadTimer = millis();
if(millis() - reloadTimer <= 2000){
return;
}
reloadTimer = millis();
display.clearDisplay(); // очистка дисплея
display.setTextSize(1); // установка размера шрифта
display.setTextColor(WHITE); // установка цвета текста
display.setCursor(0, 0); // установка курсора в позицию X = 0; Y = 0
display.println ("dashboard"); // записываем в буфер дисплея нашу фразу
display.setCursor(10, 15);
display.setTextSize(2);
display.println(String(millis()));
display.display(); // и её выводим на экран
return;
display.clearDisplay(); // очистка дисплея
// выводим на экран горизонтальные линии
for (int16_t i = 0; i <= display.height(); i += 2) {
display.drawLine(0, i, display.width(), i, WHITE);
display.display();
delay(20);
}
delay (1500);
// поочерёдно убираем выведенные линии
for (int16_t i = 0; i <= display.height(); i += 2) {
display.drawLine(0, i, display.width(), i, BLACK);
display.display();
delay(20);
}
delay (1500);
display.clearDisplay(); // очистка дисплея
// выводим на экран вертикальные линии
for (int16_t i = 0; i <= display.width(); i += 8) {
display.drawLine(i, 0, i, display.height(), WHITE);
display.display();
delay(20);
}
delay (1500);
// поочерёдно убираем выведенные линии
for (int16_t i = 0; i <= display.width(); i += 8) {
display.drawLine(i, 0, i, display.height(), BLACK);
display.display();
delay(20);
}
delay (1500);
display.clearDisplay(); // очистка дисплея
// выводим на экран горизонтальные линии
for (int16_t i = 0; i <= display.height(); i += 4) {
display.drawLine(0, i, display.width(), i, WHITE);
display.display();
delay(20);
}
delay (1500);
// выводим на экран вертикальные линии
for (int16_t i = 0; i <= display.width(); i += 8) {
display.drawLine(i, 0, i, display.height(), WHITE);
display.display();
delay(20);
}
delay (1500);
display.clearDisplay(); // очистка дисплея
// поочерёдно рисуем вложенные прямоугольники
for (int16_t i = 0; i < display.height() / 2; i += 2) {
display.drawRect(i, i, display.width() - 2 * i, display.height() - 2 * i, WHITE);
display.display();
delay(100);
}
delay (2000);
// поочерёдно убираем нарисованные прямоугольники
for (int16_t i = 0; i < display.height() / 2; i += 2) {
display.drawRect(i, i, display.width() - 2 * i, display.height() - 2 * i, BLACK);
display.display();
delay(100);
}
delay (2000);
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Hello, ESP32!");
Serial.println("\n\n\nStarting menu demo\n");
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// configure gpio pins for rotary encoder
pinMode(ENCODER_SW, INPUT_PULLUP);
pinMode(ENCODER_CLK, INPUT);
pinMode(ENCODER_DT, INPUT);
// Interrupt for reading the rotary encoder position
rotaryEncoder.encoder0Pos = 0;
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), doEncoder, CHANGE);
display.display();
delay(1000); // Pause for 1 seconds
}
void loop() {
reUpdateButton(); // update rotary encoder button status (if pressed activate default menu)
menuUpdate(); // update or action the oled menu
dashboard();
}
void doEncoder() {
bool pinA = digitalRead(ENCODER_CLK);
bool pinB = digitalRead(ENCODER_DT);
if ( (rotaryEncoder.encoderPrevA == pinA && rotaryEncoder.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 (rotaryEncoder.encoderPrevA == 1 && rotaryEncoder.encoderPrevB == 0 && pinA == 0 && pinB == 1) rotaryEncoder.encoder0Pos -= 1;
else if (rotaryEncoder.encoderPrevA == 0 && rotaryEncoder.encoderPrevB == 1 && pinA == 1 && pinB == 0) rotaryEncoder.encoder0Pos -= 1;
else if (rotaryEncoder.encoderPrevA == 0 && rotaryEncoder.encoderPrevB == 0 && pinA == 1 && pinB == 1) rotaryEncoder.encoder0Pos += 1;
else if (rotaryEncoder.encoderPrevA == 1 && rotaryEncoder.encoderPrevB == 1 && pinA == 0 && pinB == 0) rotaryEncoder.encoder0Pos += 1;
// change of direction
else if (rotaryEncoder.encoderPrevA == 1 && rotaryEncoder.encoderPrevB == 0 && pinA == 0 && pinB == 0) rotaryEncoder.encoder0Pos += 1;
else if (rotaryEncoder.encoderPrevA == 0 && rotaryEncoder.encoderPrevB == 1 && pinA == 1 && pinB == 1) rotaryEncoder.encoder0Pos += 1;
else if (rotaryEncoder.encoderPrevA == 0 && rotaryEncoder.encoderPrevB == 0 && pinA == 1 && pinB == 0) rotaryEncoder.encoder0Pos -= 1;
else if (rotaryEncoder.encoderPrevA == 1 && rotaryEncoder.encoderPrevB == 1 && pinA == 0 && pinB == 1) rotaryEncoder.encoder0Pos -= 1;
//else if (serialDebug) Serial.println("Error: invalid rotary encoder pin state - prev=" + String(rotaryEncoder.encoderPrevA) + ","
// + String(rotaryEncoder.encoderPrevB) + " new=" + String(pinA) + "," + String(pinB));
// update previous readings
rotaryEncoder.encoderPrevA = pinA;
rotaryEncoder.encoderPrevB = pinB;
// // опрос этих событий можно проводить в условии,
// // чтобы "не тратить время" на постоянный опрос в loop
// if (enc.tick()) {
// Serial.println("doEncoder tick");
// // encoder left or rignt
// if(enc.turn()){
// // Serial.println("doEncoder turn");
// if(enc.left()){
// Serial.println("doEncoder left");
// rotaryEncoder.encoder0Pos -= 1;
// }
// if(enc.right()){
// Serial.println("doEncoder right");
// rotaryEncoder.encoder0Pos += 1;
// }
// }
// // тик вернёт отличное от нуля значение, если произошло событие:
// // 1 - left + turn
// // 2 - right + turn
// // 3 - leftH + turnH
// // 4 - rightH + turnH
// // 5 - click
// // 6 - held
// // 7 - step
// // 8 - press
// if (enc.turn()) Serial.println("doEncoder - turn");
// if (enc.turnH()) Serial.println("doEncoder - hold + turn");
// if (enc.left()) Serial.println("doEncoder - left");
// if (enc.right()) Serial.println("doEncoder - right");
// if (enc.leftH()) Serial.println("doEncoder - leftH");
// if (enc.rightH()) Serial.println("doEncoder - rightH");
// if (enc.press()) Serial.println("doEncoder - press");
// if (enc.click()) Serial.println("doEncoder - click");
// if (enc.held()) Serial.println("doEncoder - held");
// if (enc.step()) Serial.println("doEncoder - step");
// // в конце лучше вызвать resetState(), чтобы сбросить необработанные флаги!
// enc.resetState();
// }
}