Skip to content

Instantly share code, notes, and snippets.

@Spirik
Last active March 19, 2024 20:35
Show Gist options
  • Save Spirik/abf77ccd9cd309d67b5d8ead0599a06b to your computer and use it in GitHub Desktop.
Save Spirik/abf77ccd9cd309d67b5d8ead0599a06b to your computer and use it in GitHub Desktop.
GEM_encoder_additional-buttons
/*
Basic menu example using GEM library. Using rotary encoder as an input source.
U8g2lib library is used to draw menu.
KeyDetector library (version 1.2.0 or later) is used to detect rotary encoder operation.
Additional info (including the breadboard view) available on GitHub:
https://github.com/Spirik/GEM
*/
#include <GEM_u8g2.h>
#include <KeyDetector.h>
// Define signal identifiers for three outputs of encoder (channel A, channel B and a push-button)
#define ROTARY_CW_SIGNAL_KEY 1
#define ROTARY_CCW_SIGNAL_KEY 2
#define ROTARY_BUTTON_SIGNAL_KEY 3
#define GREEN_BUTTON_SIGNAL_KEY 4
#define RED_BUTTON_SIGNAL_KEY 5
// Buttons
const byte GREEN_BUTTON_PIN = 7;
const byte RED_BUTTON_PIN = 8;
// Pins encoder is connected to
const byte ROTARY_CW_PIN = 11;
const byte ROTARY_CCW_PIN = 12;
const byte ROTARY_BUTTON_PIN = 10;
// Array of Key objects that will link GEM key identifiers with dedicated pins
Key keys[] = {{ROTARY_CW_SIGNAL_KEY, ROTARY_CW_PIN}, {ROTARY_BUTTON_SIGNAL_KEY, ROTARY_BUTTON_PIN}, {GREEN_BUTTON_SIGNAL_KEY, GREEN_BUTTON_PIN}, {RED_BUTTON_SIGNAL_KEY, RED_BUTTON_PIN}};
// Key keys[] = {{ROTARY_CW_SIGNAL_KEY, ROTARY_CW_PIN}, {ROTARY_BUTTON_SIGNAL_KEY, ROTARY_BUTTON_PIN}};
// Create KeyDetector object
KeyDetector myKeyDetector(keys, sizeof(keys)/sizeof(Key), /* debounceDelay= */ 0, /* analogThreshold= */ 16, /* pullup= */ true);
bool secondaryPressed = false; // If encoder rotated while key was being pressed; used to prevent unwanted triggers
bool cancelPressed = false; // Flag indicating that Cancel action was triggered, used to prevent it from triggering multiple times
const int keyPressDelay = 1000; // How long to hold key in pressed state to trigger Cancel action, ms
long keyPressTime = 0; // Variable to hold time of the key press event
long now; // Variable to hold current time taken with millis() function at the beginning of loop()
// int greenButtonState; // the current reading from the input pin
// byte lastGreenButtonState = LOW;
// unsigned long lastGreenButtonDebounceTime = 0; // the last time the input pin was toggled
// byte lastRedButtonState = LOW;
// unsigned long lastRedButtonDebounceTime = 0; // the last time the input pin was toggled
// unsigned long debounceDelay = 100; // the debounce time; increase if the output flickers
// Create an instance of the U8g2 library.
// U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// ********************************* TIMER CONTROL *********************************
// Create variable that will be editable through option select and create associated option select
byte timerMinMinutes = 10;
SelectOptionByte selectTimerMinOptions[] = {{"0 mins", 0}, {"5 mins", 5}, {"10 mins", 10}, {"15 mins", 15}, {"30 mins", 30}, {"45 mins", 45}, {"60 mins", 60}};
GEMSelect selectTimerMinMinutes(sizeof(selectTimerMinOptions)/sizeof(SelectOptionByte), selectTimerMinOptions);
// Create menu item for option select with callback function
void applyTimerMinMinutes(); // Forward declaration
GEMItem menuItemTimerMinMinutes("Min Time:", timerMinMinutes, selectTimerMinMinutes, applyTimerMinMinutes);
// Create variable that will be editable through option select and create associated option select
byte timerMaxMinutes = 30;
SelectOptionByte selectTimerMaxOptions[] = {{"0 mins", 0}, {"5 mins", 5}, {"10 mins", 10}, {"15 mins", 15}, {"30 mins", 30}, {"45 mins", 45}, {"60 mins", 60}};
GEMSelect selectTimerMaxMinutes(sizeof(selectTimerMaxOptions)/sizeof(SelectOptionByte), selectTimerMaxOptions);
// Create menu item for option select with callback function
void applyTimerMaxMinutes(); // Forward declaration
GEMItem menuItemTimerMaxMinutes("Max Time:", timerMaxMinutes, selectTimerMaxMinutes, applyTimerMaxMinutes);
// *********************************************************************************
// ********************************* LEVEL CONTROL *********************************
// Create variable that will be editable through option select and create associated option select
byte level = 20;
SelectOptionByte selectLevelOptions[] = {{"1", 1}, {"10", 10}, {"20", 20}, {"30", 30}, {"40", 40}, {"50", 50}, {"60", 60}, {"70", 70}, {"80", 80}, {"90", 90}, {"100", 100}};
GEMSelect selectLevel(sizeof(selectLevelOptions)/sizeof(SelectOptionByte), selectLevelOptions);
// Create menu item for option select with callback function
void applyLevel(); // Forward declaration
GEMItem menuItemLevel("Level:", level, selectLevel, applyLevel);
// Create variable that will be editable through option select and create associated option select
byte duration = 5;
SelectOptionByte selectLevelDurationOptions[] = {{"1 sec", 1}, {"2 secs", 2}, {"3 secs", 3}, {"4 secs", 4}, {"5 secs", 5}, {"6 secs", 6}, {"7 secs", 7}, {"8 secs", 8}, {"9 secs", 9}, {"10 secs", 10}};
GEMSelect selectDuration(sizeof(selectLevelDurationOptions)/sizeof(SelectOptionByte), selectLevelDurationOptions);
// Create menu item for option select with callback function
void applyDuration(); // Forward declaration
GEMItem menuItemDuration("Duration:", duration, selectDuration, applyDuration);
// *********************************************************************************
// Create menu page object of class GEMPage. Menu page holds menu items (GEMItem) and represents menu level.
// Menu can have multiple menu pages (linked to each other) with multiple menu items each
GEMPage menuPageMain("Main Menu"); // Main page
GEMPage menuPageTimerControl("Timer Control"); // Timer control submenu
GEMPage menuPageLevelControl("Level Control"); // Level control submenu
GEMPage menuPageStart("Start"); // Start
// Create menu item linked to Timer and Level Control menu pages
GEMItem menuPageMainTimerControl("Timer Control", menuPageTimerControl);
GEMItem menuPageMainLevelControl("Level Control", menuPageLevelControl);
GEMItem menuPageMainStart("Start", menuPageStart);
// Create menu object of class GEM_u8g2. Supply its constructor with reference to u8g2 object we created earlier
GEM_u8g2 menu(u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO);
// Which is equivalent to the following call (you can adjust parameters to better fit your screen if necessary):
// GEM_u8g2 menu(u8g2, /* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ GEM_ITEMS_COUNT_AUTO, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86);
// Apply preset based on Timer variable value
void applyTimerMinMinutes() {
// Print variable to Serial
Serial.print(F("Timer Min Minutes option set: "));
Serial.println(timerMinMinutes);
}
// Apply preset based on Timer variable value
void applyTimerMaxMinutes() {
// Print variable to Serial
Serial.print(F("Timer Max Minutes option set: "));
Serial.println(timerMaxMinutes);
}
// Apply preset based on Timer variable value
void applyLevel() {
// Print variable to Serial
Serial.print(F("Level: "));
Serial.println(level);
}
// Apply preset based on Timer variable value
void applyDuration() {
// Print variable to Serial
Serial.print(F("Duration: "));
Serial.println(duration);
}
/* void checkGreenButton() {
int buttonState = digitalRead(GREEN_BUTTON_PIN);
if (buttonState == 0) {
test();
}
} */
/* void checkRedButton() {
int buttonState = digitalRead(RED_BUTTON_PIN);
if (buttonState == 0) {
turnOn(level, duration);
}
} */
void test() {
Serial.println(F("Testing..."));
}
void turnOn(int level, int duration) {
Serial.print(F("Transmitting...Level: "));
Serial.print(level);
Serial.print(F(", Duration: "));
Serial.println(duration);
}
void setupMenu() {
// Add menu items to Main Menu page
menuPageMain.addMenuItem(menuPageMainTimerControl);
menuPageMain.addMenuItem(menuPageMainLevelControl);
menuPageMain.addMenuItem(menuPageMainStart);
// Add menu items to Timer and Level Control menu pages
menuPageTimerControl.addMenuItem(menuItemTimerMinMinutes);
menuPageTimerControl.addMenuItem(menuItemTimerMaxMinutes);
menuPageLevelControl.addMenuItem(menuItemLevel);
menuPageLevelControl.addMenuItem(menuItemDuration);
// Specify parent menu page for the Timer and Level Control menu pages
menuPageTimerControl.setParentMenuPage(menuPageMain);
menuPageLevelControl.setParentMenuPage(menuPageMain);
menuPageStart.setParentMenuPage(menuPageMain);
// Add menu page to menu and set it as current
menu.setMenuPageCurrent(menuPageMain);
}
void setup() {
// Pin modes
pinMode(GREEN_BUTTON_PIN, INPUT_PULLUP);
pinMode(RED_BUTTON_PIN, INPUT_PULLUP);
pinMode(ROTARY_CW_PIN, INPUT_PULLUP);
pinMode(ROTARY_CCW_PIN, INPUT_PULLUP);
pinMode(ROTARY_BUTTON_PIN, INPUT_PULLUP);
// Serial communication setup
Serial.begin(115200);
// U8g2 library init
u8g2.begin();
// attachInterrupt(digitalPinToInterrupt(GREEN_BUTTON_PIN), checkGreenButton, FALLING);
// attachInterrupt(digitalPinToInterrupt(RED_BUTTON_PIN), checkRedButton, FALLING);
// Load initial menu preset selected through option select
applyTimerMinMinutes();
applyTimerMaxMinutes();
applyLevel();
applyDuration();
// Menu init, setup and draw
menu.init();
setupMenu();
menu.drawMenu();
}
void loop() {
processMenu();
}
void processMenu() {
// Get current time to use later on
now = millis();
// If menu is ready to accept button press...
if (menu.readyForKey()) {
// ...detect key press using KeyDetector library
// and pass pressed button to menu
myKeyDetector.detect();
switch (myKeyDetector.trigger) {
case ROTARY_BUTTON_SIGNAL_KEY:
// Button was pressed
Serial.println(F("Button pressed"));
// Save current time as a time of the key press event
keyPressTime = now;
break;
case GREEN_BUTTON_SIGNAL_KEY:
// Green button was pressed
Serial.println(F("GREEN Button pressed"));
test();
break;
case RED_BUTTON_SIGNAL_KEY:
// Red button was pressed
Serial.println(F("RED Button pressed"));
turnOn(level, duration);
break;
}
/* Detecting rotation of the encoder on release rather than push
(i.e. myKeyDetector.triggerRelease rather myKeyDetector.trigger)
may lead to more stable readings (without excessive signal ripple) */
switch (myKeyDetector.triggerRelease) {
case ROTARY_CW_SIGNAL_KEY:
Serial.println(F("ROTARY_CW_SIGNAL_KEY"));
// Signal from Channel A of encoder was detected
if (digitalRead(ROTARY_CCW_PIN) == LOW) {
// If channel B is low then the knob was rotated CCW
if (myKeyDetector.current == ROTARY_BUTTON_SIGNAL_KEY) {
// If push-button was pressed at that time, then treat this action as GEM_KEY_LEFT,...
Serial.println(F("Rotation CCW with button pressed (release)"));
menu.registerKeyPress(GEM_KEY_LEFT);
// Button was in a pressed state during rotation of the knob, acting as a modifier to rotation action
secondaryPressed = true;
} else {
// ...or GEM_KEY_UP otherwise
Serial.println(F("Rotation CCW (release)"));
menu.registerKeyPress(GEM_KEY_UP);
}
} else {
// If channel B is high then the knob was rotated CW
if (myKeyDetector.current == ROTARY_BUTTON_SIGNAL_KEY) {
// If push-button was pressed at that time, then treat this action as GEM_KEY_RIGHT,...
Serial.println(F("Rotation CW with button pressed (release)"));
menu.registerKeyPress(GEM_KEY_RIGHT);
// Button was in a pressed state during rotation of the knob, acting as a modifier to rotation action
secondaryPressed = true;
} else {
// ...or GEM_KEY_DOWN otherwise
Serial.println(F("Rotation CW (release)"));
menu.registerKeyPress(GEM_KEY_DOWN);
}
}
break;
case ROTARY_BUTTON_SIGNAL_KEY:
// Button was released
Serial.println(F("Button released"));
if (!secondaryPressed) {
// If button was not used as a modifier to rotation action...
if (now <= keyPressTime + keyPressDelay) {
// ...and if not enough time passed since keyPressTime,
// treat key that was pressed as Ok button
menu.registerKeyPress(GEM_KEY_OK);
}
}
secondaryPressed = false;
cancelPressed = false;
break;
case GREEN_BUTTON_SIGNAL_KEY:
// Button was released
Serial.println(F("Green button released"));
break;
case RED_BUTTON_SIGNAL_KEY:
// Button was released
Serial.println(F("Red button released"));
break;
}
// After keyPressDelay passed since keyPressTime
if (now > keyPressTime + keyPressDelay) {
switch (myKeyDetector.current) {
case ROTARY_BUTTON_SIGNAL_KEY:
if (!secondaryPressed && !cancelPressed) {
// If button was not used as a modifier to rotation action, and Cancel action was not triggered yet
Serial.println(F("Button remained pressed"));
// Treat key that was pressed as Cancel button
menu.registerKeyPress(GEM_KEY_CANCEL);
cancelPressed = true;
}
break;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment