#include #include #include #include #include #include #include // ======== BLE UUIDs ======== #define SERVICE_UUID "12345678-1234-1234-1234-123456789abc" #define CHARACTERISTIC_UUID "abcd1234-5678-1234-5678-abcdef123456" // ======== Hardware & ADC ======== #define MOISTURE_PIN A0 #define LED_PIN 2 // Example LED pin (built-in LED on many ESP32 boards) #define BUTTON_PIN D4 // GPIO 4 for calibration button (recommended over GPIO 0) // ======== Constants ======== const int DRY_VALUE_DEFAULT = 500; const int WET_VALUE_DEFAULT = 200; const int ADC_RESOLUTION = 10; // 10-bit ADC (0–1023) // ======== Reading & Averaging ======== const int NUM_SAMPLES = 5; // Number of samples to average const int SAMPLE_DELAY_MS = 50; // Delay between each sample (ms) // ======== Connection Timeout Settings ======== const unsigned long CONNECTION_TIMEOUT_MS = 120000; // 2 minutes to establish connection unsigned long connectionStartTime = 0; bool connectionTimerStarted = false; // ======== Sleep Settings ======== const uint64_t DEEP_SLEEP_DURATION_US = 60ULL * 60ULL * 1000000ULL; // 1 hour in microseconds // ======== Calibration Variables ======== int dryValue = DRY_VALUE_DEFAULT; // Initialize with default dry value int wetValue = WET_VALUE_DEFAULT; // Initialize with default wet value // ======== Device Configuration ======== const char* deviceName = "003-SoilSensor"; // Default device name // ======== EEPROM Addresses ======== const int EEPROM_SIZE = 512; const int EEPROM_DRY_ADDRESS = 0; // Bytes 0-3 const int EEPROM_WET_ADDRESS = 4; // Bytes 4-7 // ======== BLE Objects ======== BLEServer* pServer = nullptr; BLECharacteristic* pCharacteristic = nullptr; // ======== Device State Flags ======== bool isConnected = false; bool dataSent = false; // ======== Calibration Variables ======== enum DeviceState { STATE_NORMAL, STATE_CALIBRATE_NOT_IN_SOIL, STATE_CALIBRATE_IN_SOIL }; DeviceState currentState = STATE_NORMAL; int calibrationCount = 0; // ======== Button Handling ======== const unsigned long DEBOUNCE_DELAY = 50; // 50ms debounce delay bool lastButtonState = HIGH; // Assuming pull-up resistor bool buttonPressed = false; unsigned long lastDebounceTime = 0; // ======== LED Blinking Parameters ======== unsigned long previousBlinkTime = 0; const unsigned long BLINK_ON_DURATION = 200; // 200ms ON const unsigned long BLINK_OFF_DURATION = 100; // 100ms OFF bool ledState = false; // ======== Function Prototypes ======== int readSoilMoisture(); int getAveragedMoisture(int numSamples); void enterDeepSleep(); void handleCalibrationButtonPress(); void readCalibrationValues(); void writeCalibrationValues(int dry, int wet); void checkButtonPress(); void updateLED(); // ======== Custom Server Callbacks ======== class MyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer* pServer) override { Serial.println("Client connected."); isConnected = true; // Turn on LED to indicate active state digitalWrite(LED_PIN, HIGH); // Stop connection timeout timer connectionTimerStarted = false; connectionStartTime = 0; } void onDisconnect(BLEServer* pServer) override { Serial.println("Client disconnected."); isConnected = false; dataSent = false; // Restart advertising to allow new connections pServer->startAdvertising(); // Restart connection timeout timer connectionTimerStarted = true; connectionStartTime = millis(); // Turn off LED to indicate sleep state digitalWrite(LED_PIN, LOW); } }; // ======== Sensor Reading Functions ======== /** * Perform a single soil moisture reading (0–100%). */ int readSoilMoisture() { int rawValue = analogRead(MOISTURE_PIN); int percent = map(rawValue, dryValue, wetValue, 0, 100); return constrain(percent, 0, 100); } /** * Gathers multiple samples, applies a small delay between each, * averages them, and returns the final moisture value. */ int getAveragedMoisture(int numSamples) { long sum = 0; for (int i = 0; i < numSamples; i++) { sum += readSoilMoisture(); delay(SAMPLE_DELAY_MS); // Small delay between samples } int avg = (int)(sum / numSamples); return avg; } /** * Enter deep sleep mode for the defined duration. */ void enterDeepSleep() { Serial.println("Entering deep sleep for 1 hour..."); // Turn off LED to indicate sleep state digitalWrite(LED_PIN, LOW); // Disable BLE before sleeping BLEDevice::deinit(true); // Configure deep sleep wake-up time esp_sleep_enable_timer_wakeup(DEEP_SLEEP_DURATION_US); // Enter deep sleep esp_deep_sleep_start(); } /** * Handle calibration button presses to manage calibration steps. */ void handleCalibrationButtonPress() { calibrationCount++; switch (calibrationCount) { case 1: // Calibration Step 1: Record Dry Value Serial.println("Calibration Step 1: Place device in dry soil and press button."); currentState = STATE_CALIBRATE_NOT_IN_SOIL; break; case 2: // Calibration Step 2: Record Wet Value Serial.println("Calibration Step 2: Place device in wet soil and press button."); currentState = STATE_CALIBRATE_IN_SOIL; break; case 3: // Calibration Complete Serial.println("Calibration Complete. Returning to normal operation."); currentState = STATE_NORMAL; calibrationCount = 0; // Reset for next calibration break; default: // Reset Calibration Serial.println("Calibration Reset. Returning to normal operation."); currentState = STATE_NORMAL; calibrationCount = 0; break; } updateLED(); } /** * Read calibration values from EEPROM. */ void readCalibrationValues() { EEPROM.get(EEPROM_DRY_ADDRESS, dryValue); EEPROM.get(EEPROM_WET_ADDRESS, wetValue); // Validate calibration values if (dryValue == 0xFFFFFFFF || wetValue == 0xFFFFFFFF) { dryValue = DRY_VALUE_DEFAULT; wetValue = WET_VALUE_DEFAULT; } } /** * Write calibration values to EEPROM. */ void writeCalibrationValues(int dry, int wet) { EEPROM.put(EEPROM_DRY_ADDRESS, dry); EEPROM.put(EEPROM_WET_ADDRESS, wet); EEPROM.commit(); } /** * Check for button presses with debouncing. */ void checkButtonPress() { bool reading = digitalRead(BUTTON_PIN); if (reading != lastButtonState) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) { if (reading == LOW && lastButtonState == HIGH) { buttonPressed = true; } } lastButtonState = reading; } /** * Update LED based on the current state. */ void updateLED() { // Reset LED parameters previousBlinkTime = millis(); ledState = false; switch (currentState) { case STATE_NORMAL: // LED is handled by BLE connection callbacks digitalWrite(LED_PIN, LOW); // Ensure LED is off in normal mode break; case STATE_CALIBRATE_NOT_IN_SOIL: // Solid LED to indicate calibration for dry soil digitalWrite(LED_PIN, HIGH); break; case STATE_CALIBRATE_IN_SOIL: // Start blinking in 2:1 ratio for wet soil calibration digitalWrite(LED_PIN, HIGH); // Start with LED ON ledState = true; break; } } /** * Initialize EEPROM and read calibration values. */ void initializeEEPROM() { if (!EEPROM.begin(EEPROM_SIZE)) { Serial.println("Failed to initialize EEPROM"); // Handle EEPROM initialization failure if necessary } readCalibrationValues(); } /** * Update mapping based on calibrated dry and wet values. */ int mapMoisture(int rawValue) { int percent = map(rawValue, dryValue, wetValue, 0, 100); return constrain(percent, 0, 100); } // ======== Setup Function ======== void setup() { // Initialize Serial Serial.begin(115200); delay(1000); // Allow time for serial to initialize // Initialize LED pin pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // LED off initially // Initialize Button pin pinMode(BUTTON_PIN, INPUT_PULLUP); // Use INPUT_PULLUP if using internal resistor // If using external resistor, use INPUT instead: // pinMode(BUTTON_PIN, INPUT); // Initialize EEPROM initializeEEPROM(); Serial.printf("Calibrated Dry Value: %d\n", dryValue); Serial.printf("Calibrated Wet Value: %d\n", wetValue); // Initialize BLE with deviceName variable BLEDevice::init(deviceName); // Initialize BLE with the variable device name pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); // Create BLE service BLEService* pService = pServer->createService(SERVICE_UUID); // Create BLE characteristic pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY ); // Add descriptor for notifications pCharacteristic->addDescriptor(new BLE2902()); // Start the service pService->start(); // Start advertising pServer->getAdvertising()->addServiceUUID(SERVICE_UUID); pServer->getAdvertising()->setScanResponse(true); pServer->getAdvertising()->start(); Serial.println("BLE server is running..."); // Start connection timeout timer connectionTimerStarted = true; connectionStartTime = millis(); // Initialize state and LED currentState = STATE_NORMAL; calibrationCount = 0; updateLED(); } // ======== Loop Function ======== void loop() { // Check for button presses checkButtonPress(); if (buttonPressed) { buttonPressed = false; if (currentState != STATE_NORMAL) { // Handle calibration step handleCalibrationButtonPress(); } else { // Initiate calibration Serial.println("Button pressed. Initiating calibration mode."); currentState = STATE_CALIBRATE_NOT_IN_SOIL; calibrationCount = 1; updateLED(); } } unsigned long currentTime = millis(); // ======== Connection Timeout Logic ======== if (connectionTimerStarted) { if (currentTime - connectionStartTime >= CONNECTION_TIMEOUT_MS) { Serial.println("No connection within 2 minutes. Entering deep sleep."); enterDeepSleep(); } } // ======== BLE Connected Logic ======== if (isConnected && !dataSent && currentState == STATE_NORMAL) { // Take sensor readings int rawMoisture = analogRead(MOISTURE_PIN); int moistureAvg = mapMoisture(rawMoisture); Serial.printf("Averaged Soil Moisture: %d%%\n", moistureAvg); // Update the characteristic and notify the client pCharacteristic->setValue(moistureAvg); pCharacteristic->notify(); dataSent = true; // Immediately enter deep sleep after sending data enterDeepSleep(); } // ======== LED Update ======== updateLED(); // ======== LED Blinking Logic ======== if (currentState == STATE_CALIBRATE_IN_SOIL) { if (millis() - previousBlinkTime >= (ledState ? BLINK_ON_DURATION : BLINK_OFF_DURATION)) { ledState = !ledState; digitalWrite(LED_PIN, ledState ? HIGH : LOW); previousBlinkTime = millis(); } } // Short delay to prevent watchdog resets delay(100); }