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