#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#define SSD1306_NO_SPLASH // Turn off oled splashscreen
#define encoderPinA 2 // Rotary encoder GPIO pin (CLK)
#define encoderPinB 3 // Rotary encoder GPIO pin (DT)
#define encoderPress 4 // Rotary encoder GPIO button (SW)
#define OLED_ADDR 0x3C // OLED i2c address
#define SCREEN_WIDTH 128 // OLED display width in pixels
#define SCREEN_HEIGHT 64 // OLED display height in pixels
#define OLED_RESET -1 // Reset pin GPIO
#define buttonPressedState 0 // Logic level when button is pressed
#define debounceDelay 20 // Debounce delay
#define waterLevelPin A3 // Float sensor GPIO pin
#define moistureSensorPin 6 // Soil moisture sensor GPIO pin
#define pumpPin 12 // Pump GPIO pin (via transistor)
#define whiteLED 5 // White status LED GPIO pin
#define redLED 8 // Red status LED GPIO pin
#define greenLED 9 // Green status LED GPIO pin
#define buzzer 7 // Buzzer
const byte serialDebug = 0; // Serial debugging flag
const byte menuTimeout = 10; // Menu inactivity timeout (seconds)
const bool menuLargeText = 0; // Larger text
const byte maxmenuItems = 9; // Number of items used in the menus
const byte itemTrigger = 2; // Rotary encoder detents
const byte topLine = 18; // Y position of lower area display
const byte lineSpace1 = 8; // Line spacing for textsize 1 (small text)
const byte lineSpace2 = 17; // Line spacing for textsize 2 (large text)
const byte displayMaxLines = 9; // Max lines that can be displayed in lower section of display
const byte MaxmenuTitleLength = 10; // Max characters per line when using text size 2
double checkInterval = 500; // Time to check soil moisture sensor
int timeToPump = 5000; // Time to run water pump
int moistureSensor = 0; // Moisture sensor value
int waterLevelSensor = 0; // Low water level value
enum menuModes {
off, // Display off
menu, // Menu active
message, // Displaying message
};
menuModes menuMode= off; // Default mode at startup
String menuTitle = ""; // The title of active mode
byte noOfmenuItems = 0; // Number of menu items
byte selectedMenuItem = 0; // Selected menu flag
byte highlightedMenuItem = 0; // Highlighted menu item
String menuItems[maxmenuItems+1]; // Menu items
uint32_t lastMenuActivity = 0; // Time the menu last saw activity
byte waterTankEmpty = 0; // Flag to keep water pump off
byte mValueLow = 0; // Spare variable
byte mValueHigh = 0; // Spare variable
byte mValueStep = 0; // Spare variable
volatile int encoderPos = 0; // Current value selected with rotary encoder
volatile bool encoderPrevA; // Used to debounce rotary encoder
volatile bool encoderPrevB; // Used to debounce rotary encoder
uint32_t reLastButtonChange = 0; // Last time state of button changed
bool encoderPrevButton = 0; // Used to debounce button
byte reButtonDebounced = 0; // Debounced current button state (1 when pressed)
const bool rebuttonPressedState = buttonPressedState; // Logic level when the button is pressed
const byte redebounceDelay = debounceDelay; // Button debounce delay setting
bool reButtonPressed = 0; // Flag set when the button is pressed
// oled SSD1306 display connected to I2C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void defaultMenu() {
demoMenu();
}
void demoMenu() {
resetMenu();
menuMode = menu;
noOfmenuItems = 9;
menuTitle = "Menu";
menuItems[1] = "Soil H20 %";
menuItems[2] = "Frequency";
menuItems[3] = "Amount";
menuItems[4] = "Add H2O";
menuItems[5] = "Last H20";
menuItems[6] = "Warnings";
menuItems[7] = "Buzz Off";
menuItems[8] = "Reset";
menuItems[9] = "Menu Off";
}
void menuActions() {
if(selectedMenuItem == 1 && reButtonPressed == 1) {
moistLevel();
}
if(selectedMenuItem == 2 && reButtonPressed == 1) {
freqSet();
}
if(selectedMenuItem == 3 && reButtonPressed == 1) {
amountWater();
}
if(selectedMenuItem == 4 && reButtonPressed == 1) {
manualWater();
}
if(selectedMenuItem == 5 && reButtonPressed == 1) {
lastWater();
}
if(selectedMenuItem == 6 && reButtonPressed == 1) {
listWarnings();
}
if(selectedMenuItem == 7 && reButtonPressed == 1) {
buzzerState();
}
if(selectedMenuItem == 8 && reButtonPressed == 1) {
resetSettings();
}
if(selectedMenuItem == 9 && reButtonPressed == 1) {
menuOff();
}
}
void moistLevel() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,10);
display.print(F("The soil moisture level is: "));
// display.print(mValueLow);
display.print(F("%"));
display.display();
}
void freqSet() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("How often should I check the soil moisture level?"));
display.setCursor(0,30);
display.println(F(">24 hours"));
display.println(F(" 72 hours"));
display.println(F(" 120 hours"));
display.display();
}
void amountWater() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("How long should I pump water?"));
display.setCursor(0,20);
display.println(F(">5 seconds"));
display.println(F(" 10 seconds"));
display.println(F(" 15 seconds"));
display.display();
}
void manualWater() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("Manually watering the plant."));
display.display();
runPump();
}
void lastWater() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("The plant was watered "));
// display.print(mValueHigh);
display.print(F(" hours ago."));
display.display();
}
void listWarnings() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("No warnings to report."));
display.display();
}
void buzzerState() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("The buzzer is ON."));
display.display();
}
void resetSettings() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("All settings reset to default."));
display.display();
}
void menuOff() {
resetMenu();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print(F("Turning screen off."));
display.display();
}
void readMoistureSensor() {
moistureSensor = analogRead(moistureSensorPin);
if(moistureSensor >= 500 && moistureSensor <= 850 && waterTankEmpty !=0) {
runPump();
flashGreenLED();
}
}
void readWaterSensor() {
waterLevelSensor = digitalRead(waterLevelPin);
if(waterLevelSensor == 0) {
flashRedLED();
waterTankEmpty = 0;
} else {
waterTankEmpty = 1;
}
}
void runPump() {
digitalWrite(pumpPin, HIGH);
delay(timeToPump);
digitalWrite(pumpPin, LOW);
}
void soundBuzzer() {
tone(buzzer, 250);
delay(50);
noTone(buzzer);
delay(50);
}
void flashWhiteLED() {
for(int i = 0; i <= 4; i++);
digitalWrite(whiteLED, HIGH);
delay(300);
digitalWrite(whiteLED, LOW);
delay(300);
}
void flashRedLED() {
for(int i = 0; i <= 4; i++);
digitalWrite(redLED, HIGH);
delay(300);
digitalWrite(redLED, LOW);
delay(300);
}
void flashGreenLED() {
for(int i = 0; i <= 4; i++);
digitalWrite(greenLED, HIGH);
delay(300);
digitalWrite(greenLED, LOW);
delay(300);
}
void setup() {
Serial.begin(115200); // Start serial comms
pinMode(whiteLED, OUTPUT);
pinMode(redLED, OUTPUT);
pinMode(greenLED, OUTPUT);
pinMode(buzzer, OUTPUT);
pinMode(pumpPin, OUTPUT);
pinMode(encoderPress, INPUT_PULLUP);
pinMode(encoderPinA, INPUT);
pinMode(encoderPinB, INPUT);
if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
if (serialDebug) Serial.println(("\nError initialising the oled display"));
}
Wire.setClock(100000);
encoderPos = 0;
attachInterrupt(digitalPinToInterrupt(encoderPinA), readEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(encoderPress), updateButton, RISING);
defaultMenu();
flashWhiteLED();
}
void loop() {
updateButton();
menuUpdate();
// Flash onboard led - debugging
// static uint32_t ledTimer = millis();
// if ( (unsigned long)(millis() - ledTimer) > 500 ) {
// digitalWrite(LED, !digitalRead(LED));
// ledTimer = millis();
// }
}
void updateButton() {
bool tReading = digitalRead(encoderPress); // Read current button state
if (tReading != encoderPrevButton) reLastButtonChange = millis(); // If it has changed reset timer
if ( (unsigned long)(millis() - reLastButtonChange) > redebounceDelay ) { // If button state is stable
if (encoderPrevButton == rebuttonPressedState) {
if (reButtonDebounced == 0) { // If the button has been pressed
reButtonPressed = 1; // Flag set when the button has been pressed
if (menuMode == off) defaultMenu(); // If the display is off start the default menu
}
reButtonDebounced = 1; // Debounced button status (1 when pressed)
} else {
reButtonDebounced = 0;
}
}
encoderPrevButton = tReading; // Update last state read
}
void menuUpdate() {
if (menuMode == off) return; // If menu system is turned off do nothing more
// if no recent activity then turn oled off
if ( (unsigned long)(millis() - lastMenuActivity) > (menuTimeout * 1000) ) {
resetMenu();
return;
}
switch (menuMode) {
// if there is an active menu
case menu:
drawMenu();
menuActions();
break;
// if a message is being displayed
case message:
if (reButtonPressed == 1) defaultMenu(); // if button has been pressed return to default menu
break;
}
}
void drawMenu() {
// rotary encoder
if (encoderPos >= itemTrigger) {
encoderPos -= itemTrigger;
highlightedMenuItem++;
lastMenuActivity = millis(); // log time
}
if (encoderPos <= -itemTrigger) {
encoderPos += itemTrigger;
highlightedMenuItem--;
lastMenuActivity = millis(); // log time
}
if (reButtonPressed == 1) {
selectedMenuItem = highlightedMenuItem; // flag that the item has been selected
lastMenuActivity = millis(); // log time
if (serialDebug) Serial.println("menu '" + menuTitle + "' item '" + menuItems[highlightedMenuItem] + "' selected");
}
const int _centreLine = displayMaxLines / 6 + 1; // mid list point
display.clearDisplay();
display.setTextColor(WHITE);
// verify valid highlighted item
if (highlightedMenuItem > noOfmenuItems) highlightedMenuItem = noOfmenuItems;
if (highlightedMenuItem < 1) highlightedMenuItem = 1;
// title
display.setCursor(40, 0);
if (menuLargeText) {
display.setTextSize(2);
display.println(menuItems[highlightedMenuItem].substring(0, MaxmenuTitleLength));
} else {
if (menuTitle.length() > MaxmenuTitleLength) display.setTextSize(1);
else display.setTextSize(2);
display.println(menuTitle);
}
display.drawLine(0, topLine-1, display.width(), topLine-1, WHITE); // draw horizontal line under title
// menu
display.setTextSize(2);
display.setCursor(0, topLine + 4);
for (int i=1; i <= displayMaxLines; i++) {
int item = highlightedMenuItem - _centreLine + i;
if (item == highlightedMenuItem) display.setTextColor(BLACK, WHITE);
else display.setTextColor(WHITE);
if (item > 0 && item <= noOfmenuItems) display.println(menuItems[item]);
else display.println(" ");
}
display.display();
}
void resetMenu() {
menuMode = off;
selectedMenuItem = 0;
encoderPos = 0;
noOfmenuItems = 0;
menuTitle = "";
highlightedMenuItem = 0;
reButtonPressed = 0;
lastMenuActivity = millis(); // log time
display.clearDisplay();
display.display();
}
void readEncoder() {
bool pinA = digitalRead(encoderPinA);
bool pinB = digitalRead(encoderPinB);
if ( (encoderPrevA == pinA && 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 (encoderPrevA == 1 && encoderPrevB == 0 && pinA == 0 && pinB == 1) encoderPos -= 1;
else if (encoderPrevA == 0 && encoderPrevB == 1 && pinA == 1 && pinB == 0) encoderPos -= 1;
else if (encoderPrevA == 0 && encoderPrevB == 0 && pinA == 1 && pinB == 1) encoderPos += 1;
else if (encoderPrevA == 1 && encoderPrevB == 1 && pinA == 0 && pinB == 0) encoderPos += 1;
// change of direction
else if (encoderPrevA == 1 && encoderPrevB == 0 && pinA == 0 && pinB == 0) encoderPos += 1;
else if (encoderPrevA == 0 && encoderPrevB == 1 && pinA == 1 && pinB == 1) encoderPos += 1;
else if (encoderPrevA == 0 && encoderPrevB == 0 && pinA == 1 && pinB == 0) encoderPos -= 1;
else if (encoderPrevA == 1 && encoderPrevB == 1 && pinA == 0 && pinB == 1) encoderPos -= 1;
// update previous readings
encoderPrevA = pinA;
encoderPrevB = pinB;
}