328 lines
10 KiB
C++
328 lines
10 KiB
C++
#include <BLEDevice.h>
|
|
#include <BLEUtils.h>
|
|
#include <BLEServer.h>
|
|
#include <EEPROM.h>
|
|
#include "esp_sleep.h"
|
|
|
|
// ***** Configuration *****
|
|
// Pins:
|
|
#define BATTERY_PIN A0 // Battery voltage reading via a resistor divider
|
|
#define SOIL_PIN A2 // Soil moisture sensor analog reading
|
|
#define BUTTON_PIN D1 // Button on D1, active LOW (internal pull-up)
|
|
|
|
// EEPROM settings: 4 bytes (2 for dry, 2 for wet)
|
|
#define EEPROM_SIZE 4
|
|
#define EEPROM_DRY_ADDR 0
|
|
#define EEPROM_WET_ADDR 2
|
|
|
|
// Sampling settings:
|
|
#define BATTERY_SAMPLES 10
|
|
#define SOIL_SAMPLES 20
|
|
#define SAMPLE_INTERVAL 100 // 0.1 seconds between soil samples
|
|
#define NORMAL_DELAY 2000 // 2 seconds delay between normal readings
|
|
#define CAL_DELAY 10000 // 10-second delay before calibration reading
|
|
|
|
// Deep sleep settings:
|
|
#define SLEEP_DURATION_HOURS 6
|
|
#define AWAKE_MAX_MS 300000 // 5 minutes in milliseconds
|
|
|
|
// Battery voltage parameters (assuming 220kΩ divider, two equal resistors)
|
|
#define ADC_MAX 4095.0
|
|
#define VREF 3.3
|
|
#define VOLTAGE_DIVIDER_FACTOR 2.0
|
|
#define BATTERY_FULL 4.2
|
|
#define BATTERY_EMPTY 3.0
|
|
|
|
// ***** BLE Settings *****
|
|
#define SERVICE_UUID "12345678-1234-1234-1234-123456789abc"
|
|
#define MOISTURE_CHAR_UUID "abcd1234-5678-1234-5678-abcdef123456"
|
|
#define BATTERY_CHAR_UUID "fedcba98-7654-3210-0123-456789abcdef"
|
|
|
|
// ***** Default Calibration Values *****
|
|
// These defaults are used if no valid calibration exists in EEPROM.
|
|
#define DEFAULT_DRY_CAL 3000 // Higher raw reading for dry soil
|
|
#define DEFAULT_WET_CAL 1000 // Lower raw reading for wet soil
|
|
|
|
// ***** Global Calibration Variables *****
|
|
int dryCal = 0;
|
|
int wetCal = 0;
|
|
|
|
// Calibration mode states
|
|
enum CalState {
|
|
CAL_OFF,
|
|
CAL_DRY_PENDING,
|
|
CAL_WET_PENDING,
|
|
CAL_DONE
|
|
};
|
|
CalState calState = CAL_OFF;
|
|
|
|
// Debounce for button on BUTTON_PIN
|
|
bool buttonPressed = false;
|
|
unsigned long lastButtonPress = 0;
|
|
const unsigned long debounceDelay = 50; // 50 ms
|
|
|
|
// Operation mode: NORMAL_MODE or CALIBRATION_MODE
|
|
enum OperationMode {
|
|
NORMAL_MODE,
|
|
CALIBRATION_MODE
|
|
};
|
|
OperationMode opMode = NORMAL_MODE;
|
|
|
|
// ***** BLE Server Variables *****
|
|
BLEServer* pServer = nullptr;
|
|
BLECharacteristic* pMoistureCharacteristic = nullptr;
|
|
BLECharacteristic* pBatteryCharacteristic = nullptr;
|
|
|
|
// ***** Time Tracking *****
|
|
unsigned long awakeStartTime = 0;
|
|
|
|
// ***** Helper Functions *****
|
|
|
|
// Read battery voltage: average BATTERY_SAMPLES, convert reading to voltage, then map to a percentage.
|
|
float readBatteryVoltage() {
|
|
long total = 0;
|
|
for (int i = 0; i < BATTERY_SAMPLES; i++) {
|
|
total += analogRead(BATTERY_PIN);
|
|
delay(10);
|
|
}
|
|
float avg = total / (float)BATTERY_SAMPLES;
|
|
float v_adc = (avg * VREF) / ADC_MAX;
|
|
float v_bat = v_adc * VOLTAGE_DIVIDER_FACTOR;
|
|
return v_bat;
|
|
}
|
|
|
|
int batteryPercentage(float voltage) {
|
|
int percent = (int)(((voltage - BATTERY_EMPTY) / (BATTERY_FULL - BATTERY_EMPTY)) * 100);
|
|
if (percent < 0) percent = 0;
|
|
if (percent > 100) percent = 100;
|
|
return percent;
|
|
}
|
|
|
|
// Read soil moisture raw value: average SOIL_SAMPLES spaced by SAMPLE_INTERVAL ms.
|
|
int readSoilRaw() {
|
|
long sum = 0;
|
|
for (int i = 0; i < SOIL_SAMPLES; i++) {
|
|
sum += analogRead(SOIL_PIN);
|
|
delay(SAMPLE_INTERVAL);
|
|
}
|
|
return sum / SOIL_SAMPLES;
|
|
}
|
|
|
|
// Map raw soil moisture value to percentage using calibration values.
|
|
int mapSoilMoisture(int raw) {
|
|
if (dryCal == wetCal) return 0;
|
|
int percent = map(raw, dryCal, wetCal, 0, 100);
|
|
percent = constrain(percent, 0, 100);
|
|
return percent;
|
|
}
|
|
|
|
// Update BLE characteristics with current battery and soil moisture data.
|
|
void updateBLE() {
|
|
int soilRaw = readSoilRaw();
|
|
int moisturePercent = 0;
|
|
if (dryCal != 0 && wetCal != 0 && dryCal > wetCal) {
|
|
moisturePercent = mapSoilMoisture(soilRaw);
|
|
}
|
|
|
|
float v_bat = readBatteryVoltage();
|
|
int batPercent = batteryPercentage(v_bat);
|
|
|
|
String moistureStr = String(moisturePercent);
|
|
String batteryStr = String(batPercent);
|
|
|
|
pMoistureCharacteristic->setValue(moistureStr.c_str());
|
|
pMoistureCharacteristic->notify();
|
|
|
|
pBatteryCharacteristic->setValue(batteryStr.c_str());
|
|
pBatteryCharacteristic->notify();
|
|
|
|
Serial.print("Battery: ");
|
|
Serial.print(v_bat);
|
|
Serial.print(" V (");
|
|
Serial.print(batPercent);
|
|
Serial.println("%)");
|
|
Serial.print("Soil raw: ");
|
|
Serial.print(soilRaw);
|
|
Serial.print(" Moisture: ");
|
|
Serial.print(moisturePercent);
|
|
Serial.println("%");
|
|
}
|
|
|
|
// Enter deep sleep for SLEEP_DURATION_HOURS hours.
|
|
void enterDeepSleep() {
|
|
Serial.println("Entering deep sleep.");
|
|
uint64_t sleepTimeUs = SLEEP_DURATION_HOURS * 3600ULL * 1000000ULL;
|
|
esp_deep_sleep(sleepTimeUs);
|
|
}
|
|
|
|
// ----- Calibration Functions -----
|
|
// Wait CAL_DELAY ms then take SOIL_SAMPLES samples and return the average raw value.
|
|
int calibrateSensor() {
|
|
delay(CAL_DELAY);
|
|
return readSoilRaw();
|
|
}
|
|
|
|
// Check the state of the calibration button on BUTTON_PIN and update calibration state.
|
|
void checkButton() {
|
|
if (digitalRead(BUTTON_PIN) == LOW) { // Button pressed
|
|
if (!buttonPressed && (millis() - lastButtonPress > debounceDelay)) {
|
|
buttonPressed = true;
|
|
lastButtonPress = millis();
|
|
|
|
// Cycle through calibration states
|
|
if (calState == CAL_OFF) {
|
|
calState = CAL_DRY_PENDING;
|
|
Serial.println("Button pressed: Entering Dry Calibration Mode. Ensure sensor is in dry soil.");
|
|
} else if (calState == CAL_DRY_PENDING) {
|
|
calState = CAL_WET_PENDING;
|
|
Serial.println("Button pressed: Entering Wet Calibration Mode. Place sensor in wet soil.");
|
|
} else if (calState == CAL_WET_PENDING) {
|
|
calState = CAL_DONE;
|
|
Serial.println("Button pressed: Finalizing Calibration.");
|
|
}
|
|
}
|
|
} else {
|
|
buttonPressed = false;
|
|
}
|
|
}
|
|
|
|
// Runs the calibration sequence.
|
|
// In Dry mode: after delay, take sample and assign dryCal.
|
|
// In Wet mode: similarly assign wetCal.
|
|
void runCalibration() {
|
|
if (calState == CAL_DRY_PENDING) {
|
|
Serial.println("Starting Dry Calibration...");
|
|
int dryValue = calibrateSensor();
|
|
dryCal = dryValue;
|
|
Serial.print("Dry calibration value: ");
|
|
Serial.println(dryCal);
|
|
// Wait for next button press to proceed to wet calibration.
|
|
while (calState == CAL_DRY_PENDING) {
|
|
checkButton();
|
|
delay(100);
|
|
}
|
|
}
|
|
if (calState == CAL_WET_PENDING) {
|
|
Serial.println("Starting Wet Calibration...");
|
|
int wetValue = calibrateSensor();
|
|
wetCal = wetValue;
|
|
Serial.print("Wet calibration value: ");
|
|
Serial.println(wetCal);
|
|
while (calState == CAL_WET_PENDING) {
|
|
checkButton();
|
|
delay(100);
|
|
}
|
|
}
|
|
if (calState == CAL_DONE) {
|
|
// Save calibration values to EEPROM
|
|
EEPROM.write(EEPROM_DRY_ADDR, lowByte(dryCal));
|
|
EEPROM.write(EEPROM_DRY_ADDR + 1, highByte(dryCal));
|
|
EEPROM.write(EEPROM_WET_ADDR, lowByte(wetCal));
|
|
EEPROM.write(EEPROM_WET_ADDR + 1, highByte(wetCal));
|
|
EEPROM.commit();
|
|
Serial.println("Calibration saved. Exiting calibration mode.");
|
|
calState = CAL_OFF;
|
|
opMode = NORMAL_MODE;
|
|
}
|
|
}
|
|
|
|
// ----- BLE Server Setup -----
|
|
void setupBLEServer() {
|
|
BLEDevice::init("SoilSensor_XIAO");
|
|
pServer = BLEDevice::createServer();
|
|
BLEService* pService = pServer->createService(SERVICE_UUID);
|
|
|
|
pMoistureCharacteristic = pService->createCharacteristic(
|
|
MOISTURE_CHAR_UUID,
|
|
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
|
|
);
|
|
pMoistureCharacteristic->setValue("0");
|
|
|
|
pBatteryCharacteristic = pService->createCharacteristic(
|
|
BATTERY_CHAR_UUID,
|
|
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
|
|
);
|
|
pBatteryCharacteristic->setValue("0");
|
|
|
|
pService->start();
|
|
|
|
BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
|
|
pAdvertising->addServiceUUID(SERVICE_UUID);
|
|
pAdvertising->setScanResponse(true);
|
|
pAdvertising->setMinPreferred(0x06);
|
|
pAdvertising->setMinPreferred(0x12);
|
|
BLEDevice::startAdvertising();
|
|
|
|
Serial.println("BLE Server started and advertising.");
|
|
}
|
|
|
|
// ----- Main Setup and Loop -----
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
Serial.println("Booting Sensor Node...");
|
|
|
|
EEPROM.begin(EEPROM_SIZE);
|
|
|
|
pinMode(BATTERY_PIN, INPUT);
|
|
pinMode(SOIL_PIN, INPUT);
|
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
|
|
|
// Initialize BLE Server so sensor values can be advertised
|
|
setupBLEServer();
|
|
|
|
// Load calibration from EEPROM
|
|
int storedDry = EEPROM.read(EEPROM_DRY_ADDR) | (EEPROM.read(EEPROM_DRY_ADDR + 1) << 8);
|
|
int storedWet = EEPROM.read(EEPROM_WET_ADDR) | (EEPROM.read(EEPROM_WET_ADDR + 1) << 8);
|
|
// Check validity: dry value should be higher than wet value and nonzero.
|
|
if (storedDry > storedWet && storedDry > 0 && storedWet > 0) {
|
|
dryCal = storedDry;
|
|
wetCal = storedWet;
|
|
Serial.print("Loaded Calibration - Dry: ");
|
|
Serial.print(dryCal);
|
|
Serial.print(" Wet: ");
|
|
Serial.println(wetCal);
|
|
opMode = NORMAL_MODE;
|
|
} else {
|
|
// Use default calibration values if none are valid.
|
|
dryCal = DEFAULT_DRY_CAL;
|
|
wetCal = DEFAULT_WET_CAL;
|
|
Serial.println("No valid calibration found. Using default calibration values.");
|
|
Serial.print("Default Dry: ");
|
|
Serial.print(dryCal);
|
|
Serial.print(" Default Wet: ");
|
|
Serial.println(wetCal);
|
|
opMode = NORMAL_MODE; // Allow device to be usable even without calibration.
|
|
}
|
|
|
|
awakeStartTime = millis();
|
|
}
|
|
|
|
void loop() {
|
|
// Check for button press to trigger calibration mode.
|
|
checkButton();
|
|
if (digitalRead(BUTTON_PIN) == LOW && opMode == NORMAL_MODE) {
|
|
// If button is pressed while in normal mode, enter calibration mode.
|
|
Serial.println("Button pressed while awake. Entering Calibration Mode.");
|
|
opMode = CALIBRATION_MODE;
|
|
calState = CAL_DRY_PENDING;
|
|
}
|
|
|
|
if (opMode == CALIBRATION_MODE) {
|
|
runCalibration();
|
|
awakeStartTime = millis(); // reset awake time after calibration
|
|
return;
|
|
}
|
|
|
|
// NORMAL MODE
|
|
updateBLE();
|
|
|
|
// If more than 5 minutes have elapsed, go to deep sleep for 6 hours.
|
|
if (millis() - awakeStartTime >= AWAKE_MAX_MS) {
|
|
Serial.println("5 minutes elapsed; going to deep sleep.");
|
|
enterDeepSleep();
|
|
}
|
|
|
|
delay(NORMAL_DELAY);
|
|
}
|