#include <Encoder.h> //#define ENCODER_OPTIMIZE_INTERRUPTS
#include <LiquidCrystal_I2C.h>
#define cols 20
#define rows 4
LiquidCrystal_I2C lcd(0x27, cols, rows);
char *Blank;
// ********** Menu options **********
#define ShowScrollBar 1
#define ScrollLongCaptions 1
#define ScrollDelay 800
#define BacklightDelay 10000
#define MenuTriggerDelay 5000
#define ReturnFromMenu 0
enum eMenuKey {mkNull, mkBack, mkRoot, mkA, mkAA, mkAB, mkB, mkBA, mkBB, mkC, mkCA, mkCB, mkCC, mkD, mkDA, mkDB, mkE, mkEA
};
// ********** virtual encoder pins ***************
#define pin_CLK 3 // CLK A
#define pin_DT 4 // DT B
#define pin_Btn 2 // switch
unsigned long CurrentTime, PrevEncoderTime, MenuTriggeredDelay = 0;;
enum eEncoderState {eNone, eLeft, eRight, eButton};
eEncoderState EncoderState;
int EncoderA, EncoderB, EncoderAPrev, counter;
bool ButtonPrev;
// ********** Function ***************
eEncoderState GetEncoderState();
void LCDBacklight(byte v = 2);
void menuTrigger(byte z = 2);
eMenuKey DrawMenu(eMenuKey Key);
// ********** Encoder.h + old Menu options **********
Encoder myEnc(3, 4);
String timeStr = "";
long oldPosition = -999;
long initPosition = -999;
unsigned long menuTriggeredTime = 0;
const int numOfScreens = 5;
int currentScreen = -1;
String screens[numOfScreens][2] = {
{"Ignition Time","Minutes"},
{"Stabilization", "Minutes"},
{"Cleaning","Minutes"},
{"Start dose","Seconds"},
{"Cut-off Temperature","Celsius"},
};
int parameters[numOfScreens];
bool updateScreen = true;
// ********** Handlers for menu items **********
float InputValue(char* Title, float DefaultValue, float MinValue, float MaxValue) {
// function for entering a value
lcd.clear();
lcd.print(Title);
lcd.setCursor(0, 1);
lcd.print(DefaultValue);
delay(100);
while (1)
{
EncoderState = GetEncoderState();
switch (EncoderState) {
case eNone: {
LCDBacklight();
menuTrigger(0);
continue;
}
case eButton: {
LCDBacklight(1);
return DefaultValue;
}
case eLeft: {
LCDBacklight(1);
menuTrigger(1);
if (DefaultValue > MinValue) DefaultValue--;
break;
}
case eRight: {
LCDBacklight(1);
menuTrigger(1);
if (DefaultValue < MaxValue) DefaultValue++;
break;
}
}
lcd.setCursor(0, 1);
lcd.print(Blank);
lcd.setCursor(0, 1);
lcd.print(DefaultValue);
}
};
float A, B, C, Atemp,value;
void Demo() {
lcd.clear();
lcd.print("It's just a demo");
while (GetEncoderState() == eNone) LCDBacklight();
};
void InputA() {
A = InputValue("pH ",A, 1.00, 8.00);
};
void InputB() {
B = InputValue("Input B", B, -10, 10);
};
void InputC() {
C = InputValue("Input C", C, -10, 10);
};
// ******************** Menu ********************
byte ScrollUp[8] = {0x4, 0xa, 0x11, 0x1f};
byte ScrollDown[8] = {0x0, 0x0, 0x0, 0x0, 0x1f, 0x11, 0xa, 0x4};
byte ItemsOnPage = rows; // The maximum number of elements to display on the screen
unsigned long BacklightOffTime = 0;
unsigned long ScrollTime = 0;
byte ScrollPos;
byte CaptionMaxLength;
struct sMenuItem {
eMenuKey Parent; // parent key
eMenuKey Key; // Key
char *Caption; // Menu item name
void (*Handler)(); // Handler
};
sMenuItem Menu[] = {
{mkNull, mkRoot, "Menu", NULL},
{mkRoot, mkA, "pH setting", NULL},
{mkA, mkAA, "pH set Point", InputA},
{mkA, mkAB, "pH threshold", InputB},
{mkA, mkBack, "Back", NULL},
{mkRoot, mkB, "EC setting", NULL},
{mkB, mkBA, "EC set Point", InputA},
{mkB, mkBB, "EC threshold", InputB},
{mkB, mkBack, "Back", NULL},
{mkRoot, mkC, "Dosing Config", NULL},
{mkC, mkCA, "Continues", InputA},
{mkC, mkCB, "Every 4Hr", InputB},
{mkC, mkCC, "Every 8Hr", InputB},
{mkC, mkBack, "Back", NULL},
{mkRoot, mkD, "Date & Time", NULL},
{mkD, mkDA, "Time", InputA},
{mkD, mkDB, "Date", InputB},
{mkD, mkBack, "Back", NULL},
{mkRoot, mkE, "About", NULL},
{mkE, mkEA, "E! AlQubaisi", NULL},
{mkE, mkBack, "Back", NULL},
};
// ************************ Menu length ************************
const float MenuLength = sizeof(Menu) / sizeof(Menu[0]);
// ******************** Menu backlight on/off ********************
void LCDBacklight(byte v) { // Backlight control
if (v == 0) { // Turn off the backlight
BacklightOffTime = millis();
lcd.noBacklight();
}
else if (v == 1) { //Turn on backlight
BacklightOffTime = millis() + BacklightDelay;
lcd.backlight();
}
else { // Turn off if time is up
if (BacklightOffTime < millis())
lcd.noBacklight();
else
lcd.backlight();
}
}
// ******************** on display & Menu ********************
void menuTrigger(byte z) { // Backlight control
if (z == 0) { // Turn off the backlight
MenuTriggeredDelay = millis();
//CurrentTime = PrevEncoderTime;
}
else if (z == 1 || z == 2) { //Turn on backlight
MenuTriggeredDelay = millis() + MenuTriggerDelay;
}
else {
}
};
eMenuKey DrawMenu(eMenuKey Key) { // Drawing the specified menu level and navigating through it
eMenuKey Result;
int k, l, Offset, CursorPos, y;
sMenuItem **SubMenu = NULL;
bool NeedRepaint;
String S;
l = 0;
LCDBacklight(1);
// write submenu elements in SubMenu
for (byte i = 0; i < MenuLength; i++) {
if (Menu[i].Key == Key) {
k = i;
}
else if (Menu[i].Parent == Key) {
l++;
SubMenu = (sMenuItem**) realloc (SubMenu, l * sizeof(void*));
SubMenu[l - 1] = &Menu[i];
}
}
if (l == 0) { // l==0 - no submenu
if ((ReturnFromMenu == 0) and (Menu[k].Handler != NULL)) (*Menu[k].Handler)(); // Call the handler if it exists
LCDBacklight(1);
return Key; // and return the index of this menu item
}
//Otherwise, draw a submenu
CursorPos = 0;
Offset = 0;
ScrollPos = 0;
NeedRepaint = 1;
do {
if (NeedRepaint) {
NeedRepaint = 0;
lcd.clear();
y = 0;
for (int i = Offset; i < min(l, Offset + ItemsOnPage); i++) {
lcd.setCursor(1, y++);
lcd.print(String(SubMenu[i]->Caption).substring(0, CaptionMaxLength));
}
lcd.setCursor(0, CursorPos);
lcd.print(">");
if (ShowScrollBar) {
if (Offset > 0) {
lcd.setCursor(cols - 1, 0);
lcd.write(0);
}
if (Offset + ItemsOnPage < l) {
lcd.setCursor(cols - 1, ItemsOnPage - 1);
lcd.write(1);
}
}
}
EncoderState = GetEncoderState();
switch (EncoderState) {
case eLeft: {
//
LCDBacklight(1);
ScrollTime = millis() + ScrollDelay * 5;
if (CursorPos > 0) { // If possible, raise the cursor
if ((ScrollLongCaptions) and (ScrollPos)) {
// If the previous menu item was scrolled, then display it again
lcd.setCursor(1, CursorPos);
lcd.print(Blank);
lcd.setCursor(1, CursorPos);
lcd.print(String(SubMenu[Offset + CursorPos]->Caption).substring(0, CaptionMaxLength));
ScrollPos = 0;
}
// Erase the cursor in the old place, draw in the new one
lcd.setCursor(0, CursorPos--);
lcd.print(" ");
lcd.setCursor(0, CursorPos);
lcd.print(">");
}
else if (Offset > 0) {
//The cursor is already in the extreme position. If there are items above, then redraw the menu
Offset--;
NeedRepaint = 1;
}
break;
}
case eRight: {
// Scroll down the menu
LCDBacklight(1);
ScrollTime = millis() + ScrollDelay * 5;
if (CursorPos < min(l, ItemsOnPage) - 1) {// Если есть возможность, то опускаем курсор
if ((ScrollLongCaptions) and (ScrollPos)) {
// If the previous menu item was scrolled, then display it again
lcd.setCursor(1, CursorPos);
lcd.print(Blank);
lcd.setCursor(1, CursorPos);
lcd.print(String(SubMenu[Offset + CursorPos]->Caption).substring(0, CaptionMaxLength));
ScrollPos = 0;
}
// Erase the cursor in the old place, draw in the new one
lcd.setCursor(0, CursorPos++);
lcd.print(" ");
lcd.setCursor(0, CursorPos);
lcd.print(">");
}
else {
// The cursor is already in the extreme position. If there are items below, then redraw the menu
if (Offset + CursorPos + 1 < l) {
Offset++;
NeedRepaint = 1;
}
}
break;
}
case eButton: {
// Menu item selected. Pressing the Back button is handled separately
LCDBacklight(1);
ScrollTime = millis() + ScrollDelay * 5;
if (SubMenu[CursorPos + Offset]->Key == mkBack) {
free(SubMenu);
return mkBack;
}
Result = DrawMenu(SubMenu[CursorPos + Offset]->Key);
if ((Result != mkBack) and (ReturnFromMenu)) {
free(SubMenu);
return Result;
}
NeedRepaint = 1;
break;
}
case eNone: {
if (ScrollLongCaptions) {
// Scroll through long titles when idle
S = SubMenu[CursorPos + Offset]->Caption;
if (S.length() > CaptionMaxLength)
{
if (ScrollTime < millis())
{
ScrollPos++;
if (ScrollPos == S.length() - CaptionMaxLength)
ScrollTime = millis() + ScrollDelay * 2; // Slight delay when the whole name is displayed
else if (ScrollPos > S.length() - CaptionMaxLength)
{
ScrollPos = 0;
ScrollTime = millis() + ScrollDelay * 5; // Delay before scrolling starts
}
else
ScrollTime = millis() + ScrollDelay;
lcd.setCursor(1, CursorPos);
lcd.print(Blank);
lcd.setCursor(1, CursorPos);
lcd.print(S.substring(ScrollPos, ScrollPos + CaptionMaxLength));
}
}
}
LCDBacklight();
}
}
} while (1);
}
//****************************************
// ******************** START VOID SETUP ********************
void setup() {
Serial.begin(115200);
//Wire.begin(D1, D2);
pinMode(2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), triggerMenu, FALLING);
initScreen();
// ******************** Flexi window ********************
CaptionMaxLength = cols - 1;
Blank = (char*) malloc(cols * sizeof(char));
for (byte i = 0; i < CaptionMaxLength; i++)
Blank[i] = ' ';
if (ShowScrollBar) {
CaptionMaxLength--;
lcd.createChar(0, ScrollUp);
lcd.createChar(1, ScrollDown);
}
Blank[CaptionMaxLength] = 0;
// ******************** Flexi window END ********************
delay(2000);
}
// ******************** END VOID SETUP ********************
// ******************** START VOID LOOP ********************
void loop() {
eEncoderState GetEncoderState();
long newPosition = myEnc.read();
if (newPosition != oldPosition && newPosition % 4 == 0) {
Serial.println(newPosition);
printPosition(newPosition / 4);
if(menuTriggeredTime != 0 && currentScreen != -1) {
if(newPosition > oldPosition) {
parameters[currentScreen]++;
} else {
parameters[currentScreen]--;
}
//reset menu trigger time on parameter change
menuTriggeredTime = millis();
updateScreen = true;
}
oldPosition = newPosition;
}
if(menuTriggeredTime != 0 && currentScreen != -1) {
//DrawMenu(mkRoot);
//displayMenu();
eEncoderState GetEncoderState();
if(menuTriggeredTime + 4000 < millis()) {
menuTriggeredTime = 0;
currentScreen = -1;
Serial.println("Init pos:");
Serial.println(initPosition);
myEnc.write(initPosition);
oldPosition = initPosition;
newPosition = initPosition;
initPosition = -999;
printPosition(oldPosition / 2);
clearLCDLine(0);
clearLCDLine(1);
clearLCDLine(2);
lcd.setCursor(5,2);
lcd.print("Subscribe!");
lcd.setCursor(3,3);
lcd.print("Taste The Code");
}
} else {
// RtcDateTime now = Rtc.GetDateTime();
// updateTime(returnDateTime(now));
// if (!now.IsValid())
// {
// // Common Causes:
// // 1) the battery on the device is low or even missing and the power line was disconnected
// Serial.println("RTC lost confidence in the DateTime!");
// }
}
delay(10);
}
// ******************** END VOID LOOP ********************
// ******************** START init LCD ********************
void initScreen() { // Screen settings
lcd.init();
lcd.backlight();
lcd.setCursor(1,rows/2 - 1);
lcd.print("Hydroponic Master");
lcd.setCursor(0,rows/2);
lcd.print("V0.1");
lcd.setCursor(6,rows/2);
lcd.print("By");
lcd.setCursor(10,rows/2);
lcd.print("AD74710");
delay(2000);
lcd.clear();
};
// ******************** END init LCD ********************
void printPosition(long pos) {
clearLCDLine(1);
lcd.setCursor(0,1);
lcd.print("Position: ");
lcd.print(pos);
}
void clearLCDLine(int line)
{
lcd.setCursor(0,line);
for(int n = 0; n < 20; n++) // 20 indicates symbols in line. For 2x16 LCD write - 16
{
lcd.print(" ");
}
}
void updateTime(String tStr) {
if(timeStr != tStr) {
lcd.setCursor(0,0);
lcd.print(tStr);
timeStr = tStr;
}
}
#define countof(a) (sizeof(a) / sizeof(a[0]))
//String returnDateTime(const RtcDateTime& dt)
//{
// char datestring[21];
// snprintf_P(datestring,
// countof(datestring),
// PSTR("%02u/%02u/%04u %02u:%02u:%02u"),
// dt.Month(),
// dt.Day(),
// dt.Year(),
// dt.Hour(),
// dt.Minute(),
// dt.Second() );
// return datestring;
//}
void triggerMenu()
{
if(menuTriggeredTime + 50 < millis()){
if(menuTriggeredTime == 0) {
initPosition = oldPosition;
}
menuTriggeredTime = millis();
currentScreen++;
if(currentScreen >= numOfScreens) {
currentScreen = 0;
}
updateScreen = true;
}
}
void displayMenu() {
if(updateScreen) {
lcd.clear();
lcd.print(" *** SETTINGS *** ");
lcd.setCursor(0,1);
lcd.print(screens[currentScreen][0]);
lcd.setCursor(0,2);
lcd.print(parameters[currentScreen]);
lcd.print(" ");
lcd.print(screens[currentScreen][1]);
updateScreen = false;
}
}
// ******************** START Encoder with button********************
eEncoderState GetEncoderState() {
// Reading the state of the encoder
eEncoderState Result = eNone;
CurrentTime = millis();
if (CurrentTime >= (PrevEncoderTime + 5)) {
PrevEncoderTime = CurrentTime;
if (digitalRead(pin_Btn) == LOW ) {
if (ButtonPrev) {
Result = eButton; // Button pressed
ButtonPrev = 0;
}
}
else {
ButtonPrev = 1;
EncoderA = digitalRead(pin_DT);
EncoderB = digitalRead(pin_CLK);
if ((!EncoderA) && (EncoderAPrev)) { // Signal A changed from 1 to 0
if (EncoderB) Result = eRight; // B=1 => encoder rotates clockwise
else Result = eLeft; // B=0 => encoder rotates counterclockwise
}
EncoderAPrev = EncoderA; // remember the current state
}
}
return Result;
}
// ******************** END Encoder with button********************