// Light Controller Type 1
// ***************************
#include <GEM_u8g2.h> // GEM library written by Spirik: https://github.com/Spirik/GEM
#include <KeyDetector.h> // Used for detecting button presses
// Define signal identifiers for our outputs
#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
// Button pins
#define GREEN_BUTTON_PIN 7
#define RED_BUTTON_PIN 8
// Pins encoder is connected to
#define ROTARY_CW_PIN 11
#define ROTARY_CCW_PIN 12
#define ROTARY_BUTTON_PIN 10
#define splash_width 12
#define splash_height 12
static const unsigned char splash_bits [] U8X8_PROGMEM = {
0xc0,0xf7,0x60,0xfe,0x30,0xfe,0x18,0xff,0x8c,0xff,0xc6,0xff,
0xe3,0xff,0xf1,0xff,0x19,0xff,0x7f,0xfc,0xff,0xfd,0xfe,0xf7
};
// 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}};
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}};
// Create KeyDetector object
KeyDetector myKeyDetector(keys, sizeof(keys)/sizeof(Key), /* debounceDelay= */ 5, /* 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()
bool countdownRunning = false;
int countdownRefreshInterval = 1000; // Countdown will refresh every n milliseconds
unsigned long countdownPreviousMillis = 0; // Used to show the countdown timer
int currentCountdownSeconds = 0;
// Create an instance of the U8g2 library.
//U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// ********************************* TIMER CONTROL *********************************
// Create variable that will be editable through option select and create associated option select
//MIN TIME
byte timerMinMinutes = 1; // Default to n minutes
//SelectOptionByte selectTimerMinOptions[] = {{"0 mins", 0}, {"5 mins", 5}, {"10 mins", 10}, {"15 mins", 15}, {"30 mins", 30}, {"45 mins", 45}};
//SelectOptionByte selectTimerMinOptions[] = {{"1 min", 1}, {"2 min", 2}, {"3 min", 3},{"4 min", 4}, {"5 min", 5}, {"6 min", 6}, {"7 min", 7}, {"8 min", 8}, {"9 min", 9}, {"10 min", 10},
//{"11 min", 11}, {"12 min", 12}, {"13 min", 13},{"14 min", 14}, {"15 min", 15}, {"16 min", 16}, {"17 min", 17}, {"18 min", 18}, {"19 min", 19}, {"20 min", 20},
//{"21 min", 21}, {"22 min", 22}, {"23 min", 23},{"24 min", 24}, {"25 min", 25}, {"26 min", 26}, {"27 min", 27}, {"28 min", 28}, {"29 min", 29}, {"30 min", 30} };
//SelectOptionByte selectTimerMinOptions[] = {{"1 min", 1}, {"2 min", 2}, {"3 min", 3},{"4 min", 4}, {"5 min", 5}, {"6 min", 6}, {"7 min", 7}, {"8 min", 8}, {"9 min", 9}, {"10 min", 10},
//{"11 min", 11}, {"12 min", 12}, {"13 min", 13},{"14 min", 14}, {"15 min", 15} };
SelectOptionByte selectTimerMinOptions[] = {{"5 secs", 5}, {"8 secs", 8}, {"10 secs", 10}, {"20 secs", 20}, {"30 secs", 30}, {"40 secs", 40}, {"50 secs", 50}, {"1 mins", 60}, {"2 mins", 120}, {"3 mins", 180},{"4 mins", 240}, {"5 mins", 300}, {"8 mins", 480},{"10 mins", 600}, {"15 mins", 900}, {"30 mins", 1800}, {"45 mins", 2700}};
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);
//// MAX TIME
// Create variable that will be editable through option select and create associated option select
byte timerMaxMinutes = 2; // Default to n minutes
//SelectOptionByte selectTimerMaxOptions[] = {{"5 mins", 5}, {"10 mins", 10}, {"15 mins", 15}, {"30 mins", 30}, {"45 mins", 45}, {"60 mins", 60}};
//SelectOptionByte selectTimerMaxOptions[] = {{"1 min", 1}, {"2 min", 2}, {"3 min", 3},{"4 min", 4}, {"5 min", 5}, {"6 min", 6}, {"7 min", 7}, {"8 min", 8}, {"9 min", 9}, {"10 min", 10},
//{"11 min", 11}, {"12 min", 12}, {"13 min", 13},{"14 min", 14}, {"15 min", 15} };
SelectOptionByte selectTimerMaxOptions[] = {{"5 secs", 5}, {"8 secs", 8}, {"10 secs", 10}, {"20 secs", 20}, {"30 secs", 30}, {"40 secs", 40}, {"50 secs", 50}, {"1 mins", 60}, {"2 mins", 120}, {"3 mins", 180},{"4 mins", 240}, {"5 mins", 300}, {"8 mins", 480},{"10 mins", 600}, {"15 mins", 900}, {"30 mins", 1800}, {"45 mins", 2700}};
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; // Default to n level
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; // Default to n duration
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);
void start(); // Forward declaration
GEMItem menuItemStartButton("Start", start);
// 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= */ 18, /* 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 turnOn(byte level, byte duration) {
/*Serial.print(F("Turn On...Level: "));
Serial.print(level);
Serial.print(F(", Duration: "));
Serial.println(duration);*/
}
void runTest() {
// Serial.println(F("Testing..."));
}
void setupMenu() {
// Add menu items to Main Menu page
menuPageMain.addMenuItem(menuPageMainTimerControl);
menuPageMain.addMenuItem(menuPageMainLevelControl);
// menuPageMain.addMenuItem(menuPageMainStart);
menuPageMain.addMenuItem(menuItemStartButton);
// 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 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"));
break;
case RED_BUTTON_SIGNAL_KEY:
// Red button was pressed
// Serial.println(F("RED Button pressed"));
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"));
runTest();
break;
case RED_BUTTON_SIGNAL_KEY:
// Button was released
// Serial.println(F("Red button released"));
turnOn(level, duration);
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;
}
}
}
}
void getRandomMinute() {
// Make sure the min number is 1 minute and then multiple by 60 for total seconds
//currentCountdownSeconds = random(max(timerMinMinutes, 1), timerMaxMinutes) * 60; // This will give us total number of seconds to count down
currentCountdownSeconds = random(max(timerMinMinutes, 1), timerMaxMinutes) ; // This will give us total number of seconds to count down
Serial.print(F(" Random CountdownSeconds "));
Serial.println(currentCountdownSeconds);
}
void start() {
menu.context.loop = startLoop;
menu.context.enter = startEnter;
menu.context.exit = startExit;
menu.context.allowExit = false; // Setting to false will require manual exit from the loop
menu.context.enter();
}
void startLoop() {
unsigned long now = millis();
myKeyDetector.detect();
if (myKeyDetector.trigger == ROTARY_BUTTON_SIGNAL_KEY) {
menu.context.exit();
} else {
if (now - countdownPreviousMillis >= countdownRefreshInterval) {
byte actualMinutes = (currentCountdownSeconds / 60) % 60;
byte actualSeconds = currentCountdownSeconds % 60;
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_6x12_tr);
if (currentCountdownSeconds >= 0 && actualSeconds <= 60) {
// Show the countdown
u8g2.setCursor(36, 5);
u8g2.print("COUNTDOWN");
u8g2.setFont(u8g2_font_profont29_mn); // Use a larger font for the countdown timer
// Minutes
u8g2.setCursor(27, 22);
if (actualMinutes < 10) {
u8g2.print("0");
}
u8g2.print(actualMinutes);
u8g2.setCursor(57, 22);
u8g2.print(":");
// Seconds
u8g2.setCursor(71, 22);
if (actualSeconds < 10) {
u8g2.print("0");
}
u8g2.print(actualSeconds);
} else if (currentCountdownSeconds < 0 && currentCountdownSeconds >= -2) {
// Briefly let the user know we are transmitting
u8g2.setCursor(25, 5);
u8g2.print("TRANSMITTING...");
//Serial.println(F("Transmit "));
} else {
// Transmit and then restart countdown
turnOn(level, duration);
getRandomMinute(); // Start timer again
break;
}
u8g2.setFont(u8g2_font_6x12_tr);
u8g2.setCursor(14, 50);
u8g2.print("Push knob to exit");
} while (u8g2.nextPage());
countdownPreviousMillis = now;
currentCountdownSeconds--;
}
}
}
void startEnter() {
// Clear sreen
u8g2.clear();
getRandomMinute();
}
// Invoked once when the GEM_KEY_CANCEL key is pressed
void startExit() {
// Reset variables
// previousMillis = 0;
// currentFrame = framesCount;
// Draw menu back on screen and clear context
menu.reInit();
menu.drawMenu();
menu.clearContext();
}
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();
// Load initial menu preset selected through option select
applyTimerMinMinutes();
applyTimerMaxMinutes();
applyLevel();
applyDuration();
// Menu init, setup and draw
menu.setSplash(splash_width, splash_height, splash_bits);
menu.init();
setupMenu();
menu.drawMenu();
}
void loop() {
processMenu();
}