//Quelle für die Codestruktur vom Menü:
//http://educ8s.tv/arduino-rotary-encoder-menu/#google_vignette
//Einbindung externer Bibliotheken
#include <LiquidCrystal_I2C.h>
//Deklaration der Variablen
int menuitem = 1; //Auswahl eines Menüpunktes
int frame = 1; //Variable für die Definition, welche Menüpunkte zum aktuellen Zeitpunkt zu sehen sind (relevant zum Scrollen)
int prevFrame = 0; //Variable zur Erkennung von Veränderungen im Frame
int page = 1; //Variable für Menüebene
int prevPage = 0; //Variable zur Erkennung von Veränderungen in den Menüebenen
int lastMenuItem = 1;
//Menüpunkte
String menuItem1 = "Kontrast";
String menuItem2 = "Lautstaerke";
String menuItem3 = "Sprache";
String menuItem4 = "Schwierigkeit";
String menuItem5 = "Licht: AN";
String menuItem6 = "Reset";
//Konfigurationen in den einzelnen Untermenüs
boolean backlight = true;
int contrast=60;
int volume = 50;
String language[3] = { "DE", "EN", "FR" };
int selectedLanguage = 0;
String difficulty[2] = { "LEICHT", "SCHWER" };
int selectedDifficulty = 0;
//Pinbelegung des Drehencoders
#define ENCODER_CLK 2 // Drehencoder Puls(A)
#define ENCODER_DT 3 // Drehencoder Puls(B)
#define ENCODER_BTN 13 // Drehencoder Taster
//Variablen für die Verarbeitung der Eingaben durch den Drehencoder
boolean up = false;
boolean down = false;
boolean middle = false;
int switchState_encoder; // Variable für den aktuellen Status des Encoder-Tasters
int prevSwitchState_encoder; //Variablen für den vorherigen Status des Encoder-Tasters
int trueCursor; //Cursor zum Markieren des ausgewählten Menüpunktes
// Konfiguration des eingebundenen LCD-Displays
LiquidCrystal_I2C lcd(0x27, 20, 4);
void setup() {
//Initialisierung aller IN- und OUTPUTs
pinMode(ENCODER_CLK, INPUT);
pinMode(ENCODER_DT, INPUT);
pinMode(ENCODER_BTN, INPUT_PULLUP);
turnBacklightOn(); //Hintergrundbeleuchtung aktivieren
lcd.begin(20, 4); //Initialisierung des LCD-Displays
lcd.clear(); //Displayinhalt löschen
lcd.setContrast(contrast); //Kontrastvoreinstellungen
drawMenu(); //Darstellung des Displayinhalts
}
void loop() {
readRotaryEncoder(); //Auslesen der Drehrichtung
switchState_encoder = digitalRead(ENCODER_BTN); //Auslesen des Encoder-Buttons
/*Ist eine Veränderung des Encoder-Buttons wahrzunehmen, dann soll beim Betätigen (PullUp, deswegen LOW)
die dazugehörige Variable auf "true" (1) gesetzt und der Displayinhalt beim Öffnen eines neuen Fensters
gelöscht werden. Damit diese Funktion wiederholt funktioniert, wird die Variable für die aktuelle Seite
auf die Variable für die vorherige Seite gesetzt. */
if (switchState_encoder != prevSwitchState_encoder) {
switch (switchState_encoder) {
case LOW:
middle=true;
if(page != prevPage){
lcd.clear();
prevPage = page;
}
break;
}
}
prevSwitchState_encoder = switchState_encoder; //Zwischenspeichern des aktuellen Zustandes
//
if (middle) //Wenn der Encoder-Button gedrückt wurde:
{
middle = false; //Wert für wiederholten Vorgang zurücksetzen
if (page == 1 && menuitem==5) // Kontrolle der Hintergrundbeleuchtung
{
if (backlight)
{
backlight = false;
menuItem5 = "Licht: AUS";
turnBacklightOff();
}
else
{
backlight = true;
menuItem5 = "Licht: AN ";
turnBacklightOn();
}
}
if(page == 1 && menuitem ==6)// Reset
{
resetDefaults();
}
else if (page == 1 && menuitem<=4) //Öffnen einzelner Untermenüs von bestimmten Menüpunkten
{
page=2;
lcd.noBlink();
}
else if (page == 2) //Schließen des Untermenüs
{
page=1;
}
drawMenu(); //Ausgabe der Verarbeitung auf dem Display
}
}
//Wurde eine Drehung wahrgenommen, dann soll über die Funktion
//eine Veränderung vorgenommen werden.
void encoderChanged()
{
if (up && page == 1 ) //Der Encoder wird gegen den Uhrzeigersinn gedreht
{
up = false;
/*Beim Durchlauf der Menüpunkte von unten nach oben, sollen die Menüpunkte
wie beim Scrollen von oben auftauchen, bis man wieder beim Startpunkt ist.
Dazu wird kontrolliert, wann sich der Cursor im jeweilgen Frame in der
obersten Stellung befindet.*/
if(menuitem==2 && frame ==2)
{
frame--;
}
if(menuitem==3 && frame ==3)
{
frame--;
}
if(menuitem==4 && frame ==4)
{
frame--;
}
lastMenuItem = menuitem; //Abspeichern des letzten Menüpunktes im Hintergrund
menuitem--; //Da durch das Hochscrollen wieder die vorherigen Menüpunkte erscheinen sollen, wird der Wert der Variable dekrementiert.
/*Grenze, dass der Wert von "menuitem" nicht ins Negative gehen kann.*/
if (menuitem==0)
{
menuitem=1;
}
}
/*Je nachdem, in welchem Menü man sich befindet, soll der Wert der jeweilgen Variable drekremntiert werdem*/
else if (up && page == 2 && menuitem==1 ) //Kontrast
{
up = false;
contrast--;
lcd.setContrast(contrast);
}
else if (up && page == 2 && menuitem==2 ) //Volume
{
up = false;
volume--;
}
else if (up && page == 2 && menuitem==3 ) //Sprache
{
up = false;
selectedLanguage--;
if(selectedLanguage == -1)
{
selectedLanguage = 2;
}
}
else if (up && page == 2 && menuitem==4 ) //Schwierigkeit
{
up = false;
selectedDifficulty--;
if(selectedDifficulty == -1)
{
selectedDifficulty = 1;
}
}
if (down && page == 1) //Der Encoder wird mit den Uhrzeigersinn gedreht
{
//Gleiches Prinzip wie bei Drehen gegen den Uhrzeigersinn. Allerdings geht die Menüführung in die andere Richtung
down = false;
if(menuitem==3 && lastMenuItem == 2)
{
frame ++;
}else if(menuitem==4 && lastMenuItem == 3)
{
frame ++;
}
else if(menuitem==5 && lastMenuItem == 4 && frame!=4)
{
frame ++;
}
lastMenuItem = menuitem;
menuitem++;
if (menuitem==7)
{
menuitem--;
}
}
else if (down && page == 2 && menuitem==1) //Kontrast
{
down = false;
contrast++;
setContrast();
}
else if (down && page == 2 && menuitem==2) //Volume
{
down = false;
volume++;
}
else if (down && page == 2 && menuitem==3 ) //Sprache
{
down = false;
selectedLanguage++;
if(selectedLanguage == 3)
{
selectedLanguage = 0;
}
}
else if (down && page == 2 && menuitem==4 ) //Schwierigkeit
{
down = false;
selectedDifficulty++;
if(selectedDifficulty == 2)
{
selectedDifficulty = 0;
}
}
drawMenu(); //Ausgabe der Verarbeitung auf dem Display
}
//Funktion für die Darstellung des Menüs auf dem Display
void drawMenu()
{
if (page==1)
{
//Überschrift des Hauptmenüs
lcd.setCursor(5, 0);
lcd.print("HAUPTMENUE");
//Sobald ein neuer Frame geöffnet wird, sollen die Begriffe gelöscht werden,
//um Buchstabenreste auf dem Display bei Überschreibungen zu vermeiden.
if(frame != prevFrame){
displayMenuItem(" ", 1,false);
displayMenuItem(" ", 2,false);
displayMenuItem(" ", 3,false);
prevFrame = frame;
}
/*Je nachdem, welcher Menüpunkt in welchem Frame ausgewählt ist, dienen die
folgenden If-Funktion der Vorbereitung für die Darstellung des Menüs auf dem Display.
Da immer drei Menüpunkte zu sehen sind, wird immer dreimal pro Funktion auf die Unterfunktion
"displayMenuItemX" zugegriffen. */
if(menuitem==1 && frame ==1)
{
displayMenuItem(menuItem1, 1,true); //Variabken und Werte aus der Klammer: (1. Name des Menüpunktes, Zeileneinordnung, Ausgewählt: Ja/Nein)
displayMenuItem(menuItem2, 2,false);
displayMenuItem(menuItem3, 3,false);
}
else if(menuitem == 2 && frame == 1)
{
displayMenuItem(menuItem1, 1,false);
displayMenuItem(menuItem2, 2,true);
displayMenuItem(menuItem3, 3,false);
}
else if(menuitem == 3 && frame == 1)
{
displayMenuItem(menuItem1, 1,false);
displayMenuItem(menuItem2, 2,false);
displayMenuItem(menuItem3, 3,true);
}
else if(menuitem == 4 && frame == 2)
{
displayMenuItem(menuItem2, 1,false);
displayMenuItem(menuItem3, 2,false);
displayMenuItem(menuItem4, 3,true);
}
else if(menuitem == 3 && frame == 2)
{
displayMenuItem(menuItem2, 1,false);
displayMenuItem(menuItem3, 2,true);
displayMenuItem(menuItem4, 3,false);
}
else if(menuitem == 2 && frame == 2)
{
displayMenuItem(menuItem2, 1,true);
displayMenuItem(menuItem3, 2,false);
displayMenuItem(menuItem4, 3,false);
}
else if(menuitem == 5 && frame == 3)
{
displayMenuItem(menuItem3, 1,false);
displayMenuItem(menuItem4, 2,false);
displayMenuItem(menuItem5, 3,true);
}
else if(menuitem == 6 && frame == 4)
{
displayMenuItem(menuItem4, 1,false);
displayMenuItem(menuItem5, 2,false);
displayMenuItem(menuItem6, 3,true);
}
else if(menuitem == 5 && frame == 4)
{
displayMenuItem(menuItem4, 1,false);
displayMenuItem(menuItem5, 2,true);
displayMenuItem(menuItem6, 3,false);
}
else if(menuitem == 4 && frame == 4)
{
displayMenuItem(menuItem4, 1,true);
displayMenuItem(menuItem5, 2,false);
displayMenuItem(menuItem6, 3,false);
}
else if(menuitem == 3 && frame == 3)
{
displayMenuItem(menuItem3, 1,true);
displayMenuItem(menuItem4, 2,false);
displayMenuItem(menuItem5, 3,false);
}
else if(menuitem == 2 && frame == 2)
{
displayMenuItem(menuItem2, 1,true);
displayMenuItem(menuItem3, 2,false);
displayMenuItem(menuItem4, 3,false);
}
else if(menuitem == 4 && frame == 3)
{
displayMenuItem(menuItem3, 1,false);
displayMenuItem(menuItem4, 2,true);
displayMenuItem(menuItem5, 3,false);
}
lcd.setCursor(1, trueCursor); //Der Cursor wird an dem Punkt gesetzt, wo sich der ausgewählte Menüpunkt befindet.
lcd.blink(); //Ausgewählter Memüpunkt blinkt
lcd.display(); //Alle Inhalte fürs Display werden ausgegeben
}
//Darstellung der Einstellungen im Untermenü
else if (page==2 && menuitem == 1)
{
displayIntMenuPage(menuItem1, contrast); //Übergabeparameter
}
else if (page==2 && menuitem == 2)
{
displayIntMenuPage(menuItem2, volume);
}
else if (page==2 && menuitem == 3)
{
displayStringMenuPage(menuItem3, language[selectedLanguage]);
}
else if (page==2 && menuitem == 4)
{
displayStringMenuPage(menuItem4, difficulty[selectedDifficulty]);
}
else if (page==2 && menuitem == 4)
{
displayStringMenuPage(menuItem4, difficulty[selectedDifficulty]);
}
}
//Funktion für das Zurücksetzen aller Einstellungen
void resetDefaults()
{
//Variablen werden auf ihre Initialisierungswerte zurückgesetzt
contrast = 60;
volume = 50;
selectedLanguage = 0;
selectedDifficulty = 0;
setContrast();
backlight = true;
menuItem5 = "Licht: AN ";
turnBacklightOn();
}
//Funktion für das Einstellen des Kontrastes
void setContrast()
{
lcd.setContrast(contrast);
lcd.display();
}
//Funktionen für das Ein- bzw. Ausstellen der Hintergrundbeleuchtung
void turnBacklightOn()
{
lcd.backlight();
}
void turnBacklightOff()
{
lcd.noBacklight();
}
//Funktion zur Darstellung der Untermenüs, bei denen Werte verändert werden.
void displayIntMenuPage(String menuItem, int value)
{
//Serial.println("displayIntMenuPage");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(menuItem);
lcd.setCursor(0, 2);
lcd.print("Wert");
lcd.setCursor(0, 3);
lcd.print(value);
lcd.display();
}
//Funktion zur Darstellung der Untermenüs, bei denen Texte verändert werden.
void displayStringMenuPage(String menuItem, String value)
{
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(menuItem);
lcd.setCursor(0, 2);
lcd.print("Wert");
lcd.setCursor(0, 3);
lcd.print(value);
lcd.display();
}
//Funktion für das Festlegen des ausgewählten Menüpunktes
void displayMenuItem(String item, int position, boolean selected)
{
/*Wenn der übergebene Parameter für "selected" gleich true ist, dann soll die
Position dieses Menüpunktes zwischengespeichert werden*/
if(selected)
{
trueCursor = position;
}
lcd.setCursor(1, position);
lcd.print(">"+item);
}
int lastClk = HIGH;
//Funktion zum Auslesen der Drehrichtung vom Encoder
//Definition der Drehrichtung grafisch dargestellt unter folgenden Link:
//https://docs.wokwi.com/parts/wokwi-ky-040
void readRotaryEncoder()
{
int newClk = digitalRead(ENCODER_CLK);
if (newClk != lastClk) {
//Eine Veränderung am CLK-Pin wurde wahrgenommen
lastClk = newClk;
int dtValue = digitalRead(ENCODER_DT);
if (newClk == LOW && dtValue == HIGH) {
//Serial.println("Im Uhrzeigersinn ⏩");
down = true;
}
if (newClk == LOW && dtValue == LOW) {
//Serial.println("Gegen Uhrzeigersinn ⏪");
up = true;
}
encoderChanged();
}
}