Smart Irrigation System With ESP32-S3 Software

 

Smart Irrigation System With ESP32-S3 Software

Please read Liability Disclaimer and License Agreement CAREFULLY

#include <Wire.h>
#include <EEPROM.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include "SparkFun_BMP581_Arduino_Library.h"
//#define DEBUG

#if defined DEBUG
   #define debug_begin(x) Serial.begin(x)
   #define debug(x)       Serial.print(x)
   #define debugln(x)     Serial.println(x)
#else
   #define debug_begin(x)
   #define debug(x)
   #define debugln(x)
#endif

// Pins used
constexpr uint8_t BTN_PIN     = 1;
constexpr uint8_t LED_PIN     = 4;
constexpr uint8_t LEVEL_PIN   = 7;
constexpr uint8_t CTRL_PIN2   = 15;

// WiFi Connect period in ms
constexpr uint8_t WiFi_Period = 1000;
// Max. no. of devices
constexpr uint8_t DEV_MAX     = 10;
// Send data to MQTT brocker every 30 seconds
constexpr uint16_t mqttPeriod = 30; 
// Min. water level in the well in m
constexpr float minWellLevel  = 0.5;
// Start and End marker for serial message from VL53L4CX/BluePill board
constexpr uint8_t sMarker     = 60;//60;
constexpr uint8_t eMarker     = 62;
// Delay for MQTT functions
constexpr uint8_t delayMQTT   = 50;
// WiFi Connect period
constexpr uint8_t VL53L4CX_Wait = 6000;
// Debounce time in milliseconds
constexpr uint8_t DEBOUNCE_DELAY = 100;
// ADC Values for buttons 1 to 10
// Assembly 1
// constexpr uint16_t BTN_ADC_MIN[] = {133, 296, 450, 591, 719, 838, 949, 1055, 1153, 1244};
// constexpr uint16_t BTN_ADC_MAX[] = {173, 336, 490, 631, 759, 878, 989, 1095, 1193, 1284};
// Assembly 2
constexpr uint16_t BTN_ADC_MIN[] = {133, 294, 444, 582, 709, 828, 935, 1041, 1137, 1226};
constexpr uint16_t BTN_ADC_MAX[] = {173, 334, 484, 622, 749, 868, 975, 1081, 1177, 1266};

enum Button { None, Btn1, Btn2, Btn3, Btn4, Btn5, Btn6, Btn7, Btn8, Btn9, Btn10 };
enum DeviceStatus { Off, On };
enum SensorNumber { Level, Temp, Press };

typedef struct ButtonObject {
  const char* topicState;
  const char* topicTime;
  uint8_t pin;
  uint8_t pinState;
  uint8_t timeTarget;
  volatile uint16_t timeSeconds;
};

typedef struct SensorObject {
  const char* name;
  float val;
};

ButtonObject myDevices[DEV_MAX];
SensorObject mySensors[3];

// MQTT Server IP
constexpr char *mqtt_server		= "10.0.1.2";
// MQTT Server username
constexpr char *mqtt_username	= "john";
// MQTT Server password
constexpr char *mqtt_password = "LenovoY50-70!";
// Client name
constexpr char *clientID      = "Valves";
// Number of devices to be used
constexpr char *maxDevNo      = "water/MaxDevNo";
// MQTT Server port
constexpr uint16_t MQTT_Port   = 1883;

volatile uint8_t interruptBMP = 0;

// Array stored in RTC memory to hold:
// 0-9 Time for each device in Min, 10 Maximum number of devices used
uint8_t DEV_IN_USE = 1;
// Stores the time when the sensors must be read
volatile uint16_t mqttTime;

// Current button
uint8_t crtButton = None;

hw_timer_t * timer = NULL;

BMP581 pressureSensor;
WiFiClient wifi_Client;
PubSubClient mqtt_Client(wifi_Client);

// Interrupt callback function for timer
void IRAM_ATTR onTimer() {
  mqttTime++;
  for (int cnt = 0; cnt < DEV_IN_USE; cnt++) {
    // If the device is On, increment the timer
    if(myDevices[cnt].pinState){
      myDevices[cnt].timeSeconds++;
    }
  }
}

// Interrupt callback function for BMP581
void bmp581InterruptHandler() {
  interruptBMP = 1;
}

// Signal WiFi error
void ledSignal(uint8_t err) {
  // Delay for MQTT functions
  constexpr uint8_t stateDelay = 100;
  constexpr uint8_t breakDelay = 400;
  uint8_t blinkCnt = 0;
  switch(err) {
    case 1:
      blinkCnt = 3;
      break;
    case 2:
      blinkCnt = 4;
      break;
    default:
      blinkCnt = 2;
  }
}

void writeDefaultEEPROM(){
  for(uint8_t cnt = 0; cnt < DEV_MAX; cnt++){
    EEPROM.write(cnt, 0);
  }
  EEPROM.write(DEV_MAX, 1);
  EEPROM.commit();
  delay(delayMQTT);
}

void setup() {
  constexpr uint8_t TX1_PIN = 17;
  constexpr uint8_t RX1_PIN = 18;

  analogReadResolution(12);
  Serial1.begin(9600, SERIAL_8N1, RX1_PIN, TX1_PIN);
  debug_begin(9600);
  // Serial.begin(9600);
  EEPROM.begin(DEV_MAX + 1);
  // writeDefaultEEPROM();
  pinMode(LED_PIN, OUTPUT);
  pinMode(LEVEL_PIN, OUTPUT);
  pinMode(CTRL_PIN2, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  digitalWrite(LEVEL_PIN, LOW);
  digitalWrite(CTRL_PIN2, LOW);
  // Initialize valves and sensors
  initObjects();
  // Load number of devices from EEPROM
  DEV_IN_USE = EEPROM.read(DEV_MAX);
  // Setup BMP581 temperature and pressure sensor
  bmpInit();
  // Setup and connect to WiFi
  startWiFi();
  // Set MQTT server IP and port
  mqtt_Client.setServer(mqtt_server, MQTT_Port);
  // Attach the MQTT callback function
  mqtt_Client.setCallback(mqttCallback);
  // Connect to MQTT broker
  ConnectToMQTT();
  if (!mqtt_Client.connected()) {
    mqtt_reconnect();
  }
  // Timer initialization
  // Set timer frequency to 1Mhz (default timer frequency is 80MHz)
  timer = timerBegin(0, 80, true);
  // Attach onTimer function to our timer.
  timerAttachInterrupt(timer, &onTimer, true);
  // Set alarm to call onTimer function every second (value in microseconds).
  // Repeat the alarm (third parameter) with unlimited count = 0 (fourth parameter).
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);
  ledSignal(0);
  crtButton = None;

}

void loop() {
  mqtt_Client.loop();
  // Is any button pressed?
  getButtons();
  if (crtButton) {
    uint8_t tmp = crtButton - 1;
    crtButton = None;
    // Accept button press events only for devices that are used
    if(tmp < DEV_IN_USE){
      // Is the device already turned ON?
      if(myDevices[tmp].pinState){
        // If a button is pressed and device is ON then turn off the device, set the time and publish MQTT
        deviceOnOff(tmp, Off, 1);
      } else {
        // If a button is pressed turn on the device, set the time and publish MQTT
        deviceOnOff(tmp, On, 1);
      }
    }
  }

  // Check if any device needs to be turned Off
  for (uint8_t cnt = 0; cnt < DEV_IN_USE; cnt++) {
    // Target time is in Minutes, sMarker has a value of 60, so...
    // If timeSeconds is not 0 and timeSeconds is equal or larger than target time in seconds
    if ((myDevices[cnt].timeSeconds) && (myDevices[cnt].timeSeconds >= (myDevices[cnt].timeTarget * sMarker))) {
      deviceOnOff(cnt, Off, 1);
    }
  }
  // Read temperature, pressure and water level
  if (mqttTime >= mqttPeriod) {
    digitalWrite(LEVEL_PIN, HIGH);
    delay(WiFi_Period);
    getTempPress();
    CheckLevelData();
    digitalWrite(LEVEL_PIN, LOW);
    publishSensors();
    // If water level is too low turn Off all devices, set the time to 0 and publish MQTT
    if (mySensors[Level].val <= minWellLevel) {
      for (int cnt = 0; cnt < DEV_IN_USE; cnt++) {
        deviceOnOff(cnt, Off, 1);
      }
    }
    mqttTime = 0;
  }
}

// Function to debounce the button
void getButtons() {
  uint16_t adc_val = analogRead(BTN_PIN);
  crtButton = None;
  // Reset current button if adc_val is greater than a threshold
  if (adc_val > BTN_ADC_MAX[9]) {
    return;
  } else {
    delay(DEBOUNCE_DELAY);
    adc_val = analogRead(BTN_PIN);
    for (uint8_t cnt = 0; cnt < DEV_MAX; cnt++) {
      if((adc_val > BTN_ADC_MIN[cnt]) && (adc_val < BTN_ADC_MAX[cnt])) {
        crtButton = (cnt + 1);
        return;
      }
    }
  }
}

// Get the serial data from VL53L4CX/BluePill
void CheckLevelData() {
  constexpr uint8_t checkLen = 4; // Wait for 4 bytes
  const uint32_t endTime = millis() + VL53L4CX_Wait; // Store the end time

  while (Serial1.available() < checkLen) {
    if (millis() >= endTime) {
      // Timeout reached (6 seconds)
      return; // Exit the function
    }
    // Wait for more data to arrive
  }

  uint8_t highByte, lowByte;
  uint8_t marker = Serial1.read();
  // This is the message start
  if (marker == sMarker) {
    highByte = Serial1.read();
    lowByte = Serial1.read();
    marker = Serial1.read();
    // We have a valid message
    if (marker == eMarker) {
      mySensors[Level].val = (float)(((uint16_t)highByte << 8) | lowByte);
    } // else do nothing, keep previous value
  }
}

//Turns On or Off a device and updates MQTT topic
void deviceOnOff(uint8_t idx, uint8_t State, uint8_t sendMessage) {
  myDevices[idx].pinState = State;
  // If the state is On
  if (State) {
    digitalWrite(myDevices[idx].pin, HIGH);
  } else {
    digitalWrite(myDevices[idx].pin, LOW);
  }
  myDevices[idx].timeSeconds = 0; // resetTimer
  if (sendMessage) {
    publishToMqtt(myDevices[idx].topicState, String(myDevices[idx].pinState));
  }
}

//Publish value to MQTT topic
void publishToMqtt(const char* mqtt_topic, String mqtt_value) {
  if (!mqtt_Client.publish(mqtt_topic, mqtt_value.c_str())) {
    mqtt_Client.connect(clientID, mqtt_username, mqtt_password);
    delay(delayMQTT);
    mqtt_Client.publish(mqtt_topic, mqtt_value.c_str());
  }
  delay(delayMQTT);
}

void publishTopics(){
  for (uint8_t cnt = 0; cnt < DEV_IN_USE; cnt++) {
    publishToMqtt(myDevices[cnt].topicState, String(myDevices[cnt].pinState));
    delay(delayMQTT);
    mqtt_Client.subscribe(myDevices[cnt].topicState);
    delay(delayMQTT);
    publishToMqtt(myDevices[cnt].topicTime, String(myDevices[cnt].timeTarget));
    delay(delayMQTT);
    mqtt_Client.subscribe(myDevices[cnt].topicTime);
    delay(delayMQTT);
  }
}

void publishSensors(){
  for (uint8_t cnt = 0; cnt < 3; cnt++) {
    publishToMqtt(mySensors[cnt].name, String(mySensors[cnt].val));
  }
}

// Connect to MQTT brocker
void ConnectToMQTT() {
  if (mqtt_Client.connect(clientID, mqtt_username, mqtt_password)) {
    publishSensors();
    publishTopics();
    publishToMqtt(maxDevNo, String(DEV_IN_USE));
    delay(delayMQTT);
    mqtt_Client.subscribe(maxDevNo);
  } else {
    ledSignal(2);
  }
}

//Reconnect to MQTT brocker
void mqtt_reconnect() {
  uint8_t retry = 10;
  while (!mqtt_Client.connected() && retry) {
    ConnectToMQTT();
    retry--;
  }
}

void setNewDevNo() {
  EEPROM.write(DEV_MAX, DEV_IN_USE);
  EEPROM.commit();
  for(uint8_t cnt = DEV_IN_USE; cnt < DEV_MAX; cnt++){
    deviceOnOff(cnt, Off, 0);
    mqtt_Client.unsubscribe(myDevices[cnt].topicState);
    mqtt_Client.unsubscribe(myDevices[cnt].topicTime);
    delay(delayMQTT);
  }
  // To update the devices
  publishTopics();
}
// MQTT callback function
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  debug("Message arrived [");
  debug(topic);
  debugln("]");
  
  String messageTemp;
  for (int i = 0; i < length; i++) {
    messageTemp += (char)payload[i];
  }
  // If we change the number of devices
  if (strcmp(topic, maxDevNo) == 0) {
    DEV_IN_USE = messageTemp.toInt();
    if(DEV_IN_USE > DEV_MAX) DEV_IN_USE = DEV_MAX;
    if(DEV_IN_USE < 1) DEV_IN_USE = 1;
    setNewDevNo();
  } else {
    for (uint8_t cnt = 0; cnt < DEV_IN_USE; cnt++) {
      // If the device state is changed
      if (strcmp(topic, myDevices[cnt].topicState) == 0) {
        myDevices[cnt].pinState = messageTemp.toInt();
        deviceOnOff(cnt, myDevices[cnt].pinState, 0);
      }
      // If the device time is changed
      if (strcmp(topic, myDevices[cnt].topicTime) == 0) {
        myDevices[cnt].timeTarget = messageTemp.toInt();
        EEPROM.write(cnt, myDevices[cnt].timeTarget);
        EEPROM.commit();
        delay(delayMQTT);
      }
    }
  }
}

// Wi-Fi connection SetUp - Wasted a day with connection issues because of buggy library
void startWiFi(){
  // WiFi network name
  constexpr char *ssid = "GVI";
  // WiFi network password
  constexpr char *password = "LenovoY50";
  WiFi.mode(WIFI_STA);
  WiFi.hostname(clientID);
  // debugln(WiFi.macAddress());
  WiFi.useStaticBuffers(true);
  // WiFi.setMinSecurity(WIFI_AUTH_WPA_PSK);   //WIFI_AUTH_WEP
  WiFi.disconnect();
  delay(WiFi_Period);
  WiFi.begin(ssid, password);
  // Wait to connect to WiFi 
  // WiFi.waitForConnectResult(1000);
  while (WiFi.waitForConnectResult(WiFi_Period) != WL_CONNECTED) {
    ledSignal(1);
  }
}

// Initialize devices, sensors
void initObjects() {
  constexpr char* deviceTopics[] = {
    "water/State1", "water/State2", "water/State3", "water/State4", "water/State5",
    "water/State6", "water/State7", "water/State8", "water/State9", "water/State10",
    "water/Time1", "water/Time2", "water/Time3", "water/Time4", "water/Time5",
    "water/Time6", "water/Time7", "water/Time8", "water/Time9", "water/Time10"
  };

  constexpr uint8_t pins[] = {10, 11, 12, 13, 14, 21, 47, 48, 35, 36};

  for (uint8_t cnt = 0; cnt < DEV_MAX; cnt++) {
    myDevices[cnt].topicState = deviceTopics[cnt];
    myDevices[cnt].topicTime = deviceTopics[cnt + 10];
    myDevices[cnt].timeTarget = EEPROM.read(cnt);
    myDevices[cnt].timeSeconds = 0;
    myDevices[cnt].pin = pins[cnt];
    pinMode(myDevices[cnt].pin, OUTPUT);
    digitalWrite(myDevices[cnt].pin, LOW);
  }

  constexpr char* sensorNames[] = {
    "water/Level", "water/Temperature", "water/Pressure"
  };

  for (uint8_t cnt = 0; cnt < 3; cnt++) {
    mySensors[cnt].name = sensorNames[cnt];
    mySensors[cnt].val = 0.0f;
  }
}

// Initialize BMP581 sensor
void bmpInit() {
  constexpr uint32_t oorCenter  = 101325;
  constexpr uint8_t oorWindow   = 255;
  constexpr uint8_t SDA_Pin     = 8;
  constexpr uint8_t SCL_Pin     = 9;
  constexpr uint8_t INT_PIN     = 16;
  Wire.begin(SDA_Pin, SCL_Pin);
  while (pressureSensor.beginI2C(BMP581_I2C_ADDRESS_SECONDARY) != BMP5_OK) {
    debugln("Error: BMP581 not connected, check wiring and I2C address!");
    delay(WiFi_Period);
  }

  int8_t err = BMP5_OK;
  err = pressureSensor.setODRFrequency(BMP5_ODR_01_HZ);
  if (err != BMP5_OK) {
    debugln("ODR setting failed! Error code: ");
    debugln(err);
  }

  bmp5_oor_press_configuration oorConfig {
    .oor_thr_p     = oorCenter,
    .oor_range_p   = oorWindow,
    .cnt_lim       = BMP5_OOR_COUNT_LIMIT_3,
    .oor_sel_iir_p = BMP5_DISABLE
  };
  pressureSensor.setOORConfig(&oorConfig);

  BMP581_InterruptConfig interruptConfig = {
    .enable   = BMP5_INTR_ENABLE,
    .drive    = BMP5_INTR_PUSH_PULL,
    .polarity = BMP5_ACTIVE_HIGH,
    .mode     = BMP5_PULSED,
    .sources  = {
      .drdy_en = BMP5_ENABLE,
      .fifo_full_en = BMP5_DISABLE,
      .fifo_thres_en = BMP5_DISABLE,
      .oor_press_en = BMP5_DISABLE
    }
  };
  err = pressureSensor.setInterruptConfig(&interruptConfig);

  if (err != BMP5_OK) {
    debugln("Interrupt settings failed! Error code: ");
    debugln(err);
  }

  attachInterrupt(INT_PIN, bmp581InterruptHandler, RISING);
}

// Get temperature and pressure  from BMP581
void getTempPress() {
  if (interruptBMP) {
    interruptBMP = 0;
    int8_t err = BMP5_OK;
    uint8_t interruptStatus = 0;
    err = pressureSensor.getInterruptStatus(&interruptStatus);
    
    if (err != BMP5_OK) {
      debugln("Get interrupt status failed! Error code: ");
      debugln(err);
      return;
    }

    if (interruptStatus & BMP5_INT_ASSERTED_DRDY) {
      bmp5_sensor_data data = {0, 0};
      err = pressureSensor.getSensorData(&data);

      if (err == BMP5_OK) {
        mySensors[Temp].val = data.temperature;
        mySensors[Press].val = data.pressure;
      } else {
        debug("Error getting data from sensor! Error code: ");
        debugln(err);
      }
    }

    if (interruptStatus & BMP5_INT_ASSERTED_PRESSURE_OOR) {
      debugln("Out of range condition triggered!");
    }

    if (!(interruptStatus & (BMP5_INT_ASSERTED_PRESSURE_OOR | BMP5_INT_ASSERTED_DRDY))) {
      debugln("Wrong interrupt condition!");
    }
  }
}

Comments powered by CComment

Who’s online

We have 228 guests and no members online