#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <EEPROM.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Pins
#define SOIL_PIN A3
#define DRY_BUTTON_PIN 4
#define WET_BUTTON_PIN 5
// EEPROM addresses for storing calibration
#define EEPROM_DRY_HIGH 0
#define EEPROM_DRY_LOW 1
#define EEPROM_WET_HIGH 2
#define EEPROM_WET_LOW 3
#define EEPROM_MAGIC 4 // To check if EEPROM has been initialized
// Default calibration values
int dryValue = 800;
int wetValue = 300;
// Button debouncing
unsigned long lastDryPress = 0;
unsigned long lastWetPress = 0;
const unsigned long debounceDelay = 300;
bool lastDryState = HIGH;
bool lastWetState = HIGH;
// Plant bitmap dimensions
#define plant_width 32
#define plant_height 48
// ===== PASTE ALL YOUR BITMAP ARRAYS HERE =====
// Use https://berrydev-ai.github.io/berry-tools/bitmap-converter.html
// Generated bitmap arrays for 27 image(s)
// Generated on 11/27/2025, 9:28:41 AM
#include "plant_bitmaps.h"
void setup() {
Serial.begin(9600);
// Setup buttons with internal pullup resistors
pinMode(DRY_BUTTON_PIN, INPUT_PULLUP);
pinMode(WET_BUTTON_PIN, INPUT_PULLUP);
// Load calibration from EEPROM
loadCalibration();
// Initialize display
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("Soil Monitor"));
display.println(F("Starting..."));
display.display();
delay(1000);
Serial.println(F("Soil Monitor Ready"));
Serial.print(F("Dry value: "));
Serial.println(dryValue);
Serial.print(F("Wet value: "));
Serial.println(wetValue);
}
void loop() {
// Check for button presses
checkCalibrationButtons();
// Read soil moisture
int soilValue = analogRead(SOIL_PIN);
// Map sensor value to 0-100% (inverted because lower reading = wetter)
int moisturePercent = map(soilValue, dryValue, wetValue, 0, 100);
moisturePercent = constrain(moisturePercent, 0, 100);
// Map moisture to plant image index (0-26)
int imageIndex = map(moisturePercent, 0, 100, 0, 26);
imageIndex = constrain(imageIndex, 0, 26);
// Clear display
display.clearDisplay();
// Draw text on left side
display.setCursor(0, 0);
display.setTextSize(1);
display.println(F("Soil Monitor"));
display.setCursor(0, 16);
display.setTextSize(2);
display.print(moisturePercent);
display.println(F("%"));
display.setCursor(0, 40);
display.setTextSize(1);
display.print(F("Raw:"));
display.println(soilValue);
if (moisturePercent < 30) {
display.println(F("DRY!"));
} else if (moisturePercent < 60) {
display.println(F("OK"));
} else {
display.println(F("WET"));
}
// Draw plant image on right side
display.drawBitmap(96, 8, plant_images[imageIndex], plant_width, plant_height, SSD1306_WHITE);
// Update display
display.display();
// Debug output
// Serial.print("Raw: ");
// Serial.print(soilValue);
// Serial.print(" | Moisture: ");
// Serial.print(moisturePercent);
// Serial.print("% | Image: ");
// Serial.print(imageIndex);
// Serial.print(" | DRY btn: ");
// Serial.print(digitalRead(DRY_BUTTON_PIN));
// Serial.print(" | WET btn: ");
// Serial.println(digitalRead(WET_BUTTON_PIN));
delay(500);
}
void checkCalibrationButtons() {
unsigned long currentTime = millis();
int soilValue = analogRead(SOIL_PIN);
// Read current button states
bool dryState = digitalRead(DRY_BUTTON_PIN);
bool wetState = digitalRead(WET_BUTTON_PIN);
// Check DRY button (trigger on button press, not release)
// Only trigger if button went from HIGH to LOW (button was just pressed)
if (dryState == LOW && lastDryState == HIGH &&
(currentTime - lastDryPress) > debounceDelay) {
lastDryPress = currentTime;
// Set dry calibration to current reading
dryValue = soilValue;
saveCalibration();
// Show confirmation
showCalibrationMessage("DRY", dryValue);
Serial.print(F("DRY calibrated to: "));
Serial.println(dryValue);
}
// Check WET button (trigger on button press, not release)
// Only trigger if button went from HIGH to LOW (button was just pressed)
if (wetState == LOW && lastWetState == HIGH &&
(currentTime - lastWetPress) > debounceDelay) {
lastWetPress = currentTime;
// Set wet calibration to current reading
wetValue = soilValue;
saveCalibration();
// Show confirmation
showCalibrationMessage("WET", wetValue);
Serial.print(F("WET calibrated to: "));
Serial.println(wetValue);
}
// Update last states
lastDryState = dryState;
lastWetState = wetState;
}
void showCalibrationMessage(const char* type, int value) {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(10, 10);
display.println(type);
display.println(F("SAVED!"));
display.setTextSize(1);
display.setCursor(10, 45);
display.print(F("Value: "));
display.println(value);
display.display();
delay(2000);
}
void saveCalibration() {
// Save calibration values to EEPROM
EEPROM.write(EEPROM_DRY_HIGH, highByte(dryValue));
EEPROM.write(EEPROM_DRY_LOW, lowByte(dryValue));
EEPROM.write(EEPROM_WET_HIGH, highByte(wetValue));
EEPROM.write(EEPROM_WET_LOW, lowByte(wetValue));
EEPROM.write(EEPROM_MAGIC, 42); // Magic number to indicate EEPROM is initialized
}
void loadCalibration() {
// Check if EEPROM has been initialized
if (EEPROM.read(EEPROM_MAGIC) == 42) {
// Load saved calibration
dryValue = word(EEPROM.read(EEPROM_DRY_HIGH), EEPROM.read(EEPROM_DRY_LOW));
wetValue = word(EEPROM.read(EEPROM_WET_HIGH), EEPROM.read(EEPROM_WET_LOW));
Serial.println(F("Loaded calibration from EEPROM"));
} else {
// First time - use defaults and save them
saveCalibration();
Serial.println(F("Using default calibration"));
}
}