Handling multiple tasks with single function

Greetings! wish u a very happy Christmas and Wishes for new year in advance!

I'm working on system with Arduino Mega2560. It has 16 peristaltic pumps, 16 INA226 sensors to keep track of the condition of these pumps and 16 LED rings based on WS2812 LEDs and 16 level sensors.
Each of these pumps are fitted into 1 ltr tanks. So there are total 16 Tanks

I have a particular sequence in which I want to operate all these devices:

  1. Turn on the pump for 5 secs
  2. Turn on the LED ring and keep it blinking in blue with delay of 500mSecs for 5secs
  3. Keep checking on the liquid level sensor every 1sec until the pump is ON
  4. Keep on checking the INA226 every 1sec till the pumps are ON
  5. If liquid level sensor senses empty tank stop the Pump and Start blinking LED Ring in red with delay of 250msecs till the tank is refilled (liquid level senses tank full)
  6. Blink the LED Ring in Red with delay of 250mSecs, if INA226 readings are below or above the predefines levels (something wrong happened with Pump) until the problem is cleared.

This sequence is a improved version of what I initially wrote. Here's what I wrote initially

void Select_PUMP(int A0, int A1, int A2, int A3)
{
  digitalWrite(ADDR0,A0);
  digitalWrite(ADDR1,A1);
  digitalWrite(ADDR2,A2);
  digitalWrite(ADDR3,A3);

  digitalWrite(SEL0,A0);
  digitalWrite(SEL1,A1);
  digitalWrite(SEL2,A2);
  digitalWrite(SEL3,A3);

  digitalWrite(LED_ACTIVE, HIGH);
}

The Pumps and the LED Rings are driven via two different MUX. 16 Liquid level sensors are connected to independent 16 different GPIOS and 16 INA226 communicate thru I2C with Mega with each having different address

While writing the above snippet the idea was I would just pass 0s and 1s corresponding to which Pump I have to turn ON (It was back then when the level sensors and current sensor was not in picture)

Now I want to keep the idea similar like lets say I would pass the hex value to a function to turn on the particular Pump. The related function takes that hex value and makes the GPIOs HIGH and LOW same goes for the LEDD rings and one integer for level sensor. I believe it wont be possible to include the address of the INA226 since it doesn't match with the other devices.

So is it possible to do so? and if yes how should I do it? Any suggestions? Thanks in advance

1 Like

Any time you have a bunch of variables/constants called something like SEL2, it's a pretty good bet that you could improve (and shorten) your code by using arrays.

can you share me with an example please that will make me understand more easier.

Here's the entire code

/*---------PUMPS ASSOCIATED WITH THE SOLUTION IN SEQUENCE-----
 * Container1 -> PUMP1 -> Imacimus conditioning solution
 * Container2 -> PUMP2 -> PH4
 * Container3 -> PUMP3 -> PH7
 * 
 * Following lines are to be set/reset to control pumps:
 * SelectP0, SelectP1, SelectP2, SelectP3, Enable PUMPS and PWM IN
 * 
 */
 
//-----------INA226-----------------------
#include <Wire.h>
#include <INA226.h>
INA226 ina_1;
INA226 ina_2; 
INA226 ina_3; 
INA226 ina_4; 
INA226 ina_5; 
INA226 ina_6;
INA226 ina_7; 
INA226 ina_8;
INA226 ina_9; 
INA226 ina_10;
INA226 ina_11;
INA226 ina_12;
INA226 ina_13;
INA226 ina_14;
INA226 ina_15;
INA226 ina_16;

float Current1 = 0.0;
float Current2 = 0.0;
//-----------------------------------------

#define PUMPS_ENABLE 34// ENABLE PIN ON MUX FOR PUMPS
#define ADDR0 41
#define ADDR1 40
#define ADDR2 36
#define ADDR3 37
#define PUMPS_PWM 45

#define AIR_COMP 39

#define LEDRing_Enable 
#define SEL0 31
#define SEL1 30
#define SEL2 33
#define SEL3 32
#define LED_ACTIVE 29

//-----------------IMACIMUS BOARD VARIABLES---------------------------//
char toSend[4] = {0x02, 0x00, 0x00, 0x00};
/*
toSend[0] = 0x02; // the value 02 represents the measure instruction.
toSend[1] = 0x00;
toSend[2] = 0x00;
toSend[3] = 0x00;   */
byte toRead[36];
byte toRecieve[4];
boolean measureState;
int i,len;
signed long tmp;
signed long m[8];
int num_ions=8;

//--------------------DO SENSOR-------------------------------------------//
int DO_Out;
#define DO_PIN A14//Pin number has been changed as per the PCB layout
#define VREF 5000    //VREF (mv)
#define ADC_RES 1024 //ADC Resolution
 
//Single-point calibration Mode=0
//Two-point calibration Mode=1
#define TWO_POINT_CALIBRATION 0
 
#define READ_TEMP (30) //Current water temperature ℃, Or temperature sensor function
 
//Single point calibration needs to be filled CAL1_V and CAL1_T
#define CAL1_V (1771) //mv
#define CAL1_T (30)   //℃
//Two-point calibration needs to be filled CAL2_V and CAL2_T
//CAL1 High temperature point, CAL2 Low temperature point
#define CAL2_V (1300) //mv
#define CAL2_T (15)   //℃
 
const uint16_t DO_Table[41] = {
    14460, 14220, 13820, 13440, 13090, 12740, 12420, 12110, 11810, 11530,
    11260, 11010, 10770, 10530, 10300, 10080, 9860, 9660, 9460, 9270,
    9080, 8900, 8730, 8570, 8410, 8250, 8110, 7960, 7820, 7690,
    7560, 7430, 7300, 7180, 7070, 6950, 6840, 6730, 6630, 6530, 6410};
 
uint8_t Temperaturet;
uint16_t ADC_Raw;
uint16_t ADC_Voltage;
uint16_t DO;
 
int16_t readDO(uint32_t voltage_mv, uint8_t temperature_c)
{
#if TWO_POINT_CALIBRATION == 00
  uint16_t V_saturation = (uint32_t)CAL1_V + (uint32_t)35 * temperature_c - (uint32_t)CAL1_T * 35;
  return (voltage_mv * DO_Table[temperature_c] / V_saturation);
#else
  uint16_t V_saturation = (int16_t)((int8_t)temperature_c - CAL2_T) * ((uint16_t)CAL1_V - CAL2_V) / ((uint8_t)CAL1_T - CAL2_T) + CAL2_V;
  return (voltage_mv * DO_Table[temperature_c] / V_saturation);
#endif
}

//----------------------LED RINGS--------------------------------------------//
#include <FastLED.h>

// How many leds in your strip?
#define NUM_LEDS 30

#define DATA_PIN 8
#define CLOCK_PIN 13

// Define the array of leds
CRGB leds[NUM_LEDS];


void setup() 
{
  pinInit();

//--------------IMACIMUS Board------------------------------//
 measureState = false;
 Serial1.begin(9600);

//---------------INA226--------------------------------------
 Initialize_INA();

//----------------LED RINGS----------------------------------
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);  // RED

}

void loop() 
{
  Calibration_Process();
  Water_Analysis();
  
}

void pinInit()
{
  pinMode(PUMPS_ENABLE, OUTPUT);
  pinMode(ADDR0, OUTPUT);
  pinMode(ADDR1, OUTPUT);
  pinMode(ADDR2, OUTPUT);
  pinMode(ADDR3, OUTPUT);
  pinMode(PUMPS_PWM, OUTPUT);

  pinMode(AIR_COMP, OUTPUT);
  digitalWrite(AIR_COMP, HIGH);
}

void Start_Pumps()
{
  digitalWrite(PUMPS_ENABLE, LOW);
  analogWrite(PUMPS_PWM, 255);//change this number to set the speed for pumps
}


void Stop_Pumps()
{
  digitalWrite(PUMPS_ENABLE, HIGH);
}

void Select_PUMP(int A0, int A1, int A2, int A3)
{
  digitalWrite(ADDR0,A0);
  digitalWrite(ADDR1,A1);
  digitalWrite(ADDR2,A2);
  digitalWrite(ADDR3,A3);

  digitalWrite(SEL0,A0);
  digitalWrite(SEL1,A1);
  digitalWrite(SEL2,A2);
  digitalWrite(SEL3,A3);

  digitalWrite(LED_ACTIVE, HIGH);
}

void AIR_Comp()
{
  Stop_Pumps();
  digitalWrite(AIR_COMP, LOW);//Turn ON the Air comp for 5 secs
  delay(5000);
  digitalWrite(AIR_COMP, HIGH);//Turn OFF the Air comp for 5 secs
  Start_Pumps();
}


void Calibration_Process()
{
  AIR_Comp();
  Select_PUMP(0,0,0,1);//push the PH4 solution
  Current1 = ((ina_1.readShuntCurrent() - 0.0765)*1000, 5);//read first Current sensor
  Imacimus();//read pH sensor from Imacimus board
  
  AIR_Comp();
  Select_PUMP(0,1,0,1);//push the Distilled water PUMP5
  Select_PUMP(1,0,0,1);//Push the EC Std 1 PUMP9
  Current1 = ((ina_5.readShuntCurrent() - 0.0765)*1000, 5);//read 5th Current sensor
  Current2 = ((ina_9.readShuntCurrent() - 0.0765)*1000, 5);//read 9th Current sensor
  Imacimus();//read EC Std 1 from Imacimus Board
  
  AIR_Comp();
  Select_PUMP(0,1,0,1);//push the Distilled water PUMP5
  Current1 = ((ina_5.readShuntCurrent() - 0.0765)*1000, 5);//read 5th Current sensor
  
  AIR_Comp();
  Select_PUMP(0,0,0,0);//push the Imacimus conditioning solution PUMP0
  Current1 = ((ina_1.readShuntCurrent() - 0.0765)*1000, 5);//read 1st Current sensor(needs corerction in numbering)
  Imacimus();//read multiIon from Imacimus
  
  AIR_Comp();
  Select_PUMP(0,1,0,1);//push the Distilled water PUMP5
  Current1 = ((ina_5.readShuntCurrent() - 0.0765)*1000, 5);//read 5th Current sensor
  
  AIR_Comp();
  Select_PUMP(1,0,0,1);//Push the EC Std 1 PUMP9
  Current1 = ((ina_9.readShuntCurrent() - 0.0765)*1000, 5);//read 9th Current sensor
  Imacimus();//Read EC Std 1 from Imacimus
  
  AIR_Comp();
  Select_PUMP(1,0,1,0);//Push the EC Std 2 PUMP10
  Current1 = ((ina_10.readShuntCurrent() - 0.0765)*1000, 5);//read 10th Current sensor
  Imacimus();//Read EC Std 2 from Imacimus
  
  AIR_Comp();
  Select_PUMP(0,1,0,1);//push the Distilled water PUMP5
  Current1 = ((ina_5.readShuntCurrent() - 0.0765)*1000, 5);//read 5th Current sensor
  
  AIR_Comp();
  Select_PUMP(0,1,1,1);//push the Nitrate 1 PUMP7
  Current1 = ((ina_7.readShuntCurrent() - 0.0765)*1000, 5);//read 7th Current sensor
  Imacimus();//Read Nitrate 1 from Imacimus
  
  AIR_Comp();
  Select_PUMP(1,0,0,0);//push the Nitrate 2 PUMP8
  Current1 = ((ina_8.readShuntCurrent() - 0.0765)*1000, 5);//read 8th Current sensor
  Imacimus();//Read Nitrate 2 from Imacimus
  
  AIR_Comp();
  Select_PUMP(0,1,0,1);//push the Distilled water PUMP5
  Current1 = ((ina_5.readShuntCurrent() - 0.0765)*1000, 5);//read 5th Current sensor
  
  AIR_Comp();
}

void Water_Analysis()
{
  AIR_Comp();
  Select_PUMP(1,1,0,0);//push the water Under Test PUMP12
  
  AIR_Comp();
  Select_PUMP(0,0,1,1);//push the KCL PUMP3
  Select_PUMP(0,1,0,1);//push the Distilled water PUMP5
  Get_DO();
  Imacimus();
}

void Cleaning_Process()
{
  //Stop the pumps
  //Push distilled water from both the pumps
  //keep the pumps running for say 5 secs
  //then stop the pumps
  //then push the Air for say 5 secs
  //then Stop the Air_Comp
}

void Initialize_INA()
{
  //--------------INA226--------------------------------------
 ina_1.begin(0X40);
 ina_2.begin(0X41);
 ina_3.begin(0X42);
 ina_4.begin(0X43);
 ina_5.begin(0X44);
 ina_6.begin(0X45);
 ina_7.begin(0X46);
 ina_8.begin(0X47);
 ina_9.begin(0X48);
 ina_10.begin(0X49);
 ina_11.begin(0X4A);
 ina_12.begin(0X4B);
 ina_13.begin(0X4C);
 ina_14.begin(0X4D);
 ina_15.begin(0X4E);
 ina_16.begin(0X4F);
 
  // Configure INA226
  ina_1.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_2.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_3.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_4.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_5.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_6.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_7.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_8.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_9.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_10.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_11.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_12.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_13.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_14.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_15.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
  ina_16.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
   
  // Calibrate INA226. Rshunt = 0.01 ohm, Max excepted current = 4A
  ina_1.calibrate(0.01, 0.5);
  ina_2.calibrate(0.01, 0.5);
  ina_3.calibrate(0.01, 0.5);
  ina_4.calibrate(0.01, 0.5);
  ina_5.calibrate(0.01, 0.5);
  ina_6.calibrate(0.01, 0.5);
  ina_7.calibrate(0.01, 0.5);
  ina_8.calibrate(0.01, 0.5);
  ina_9.calibrate(0.01, 0.5);
  ina_10.calibrate(0.01, 0.5);
  ina_11.calibrate(0.01, 0.5);
  ina_12.calibrate(0.01, 0.5);
  ina_13.calibrate(0.01, 0.5);
  ina_14.calibrate(0.01, 0.5);
  ina_15.calibrate(0.01, 0.5);
  ina_16.calibrate(0.01, 0.5);
  
}

void Imacimus()
{
  Send_Query();
  while (measureState == true)
    {
      rebre_Dades();
      delay(10);
    }
   delay(10);
}

void Send_Query()
{
 for(int i=0;i<4;i++)
 {
  Serial1.print(toSend[i]);
 }
 measureState=true;
}
void rebre_Dades()
{
   if (Serial1.available()>0)
      {
        len = Serial1.readBytes(toRead,36 );
        //Serial.print("HEX code: ");
        /*for (i=0; i<len; i++)
          {
            Serial.print(toRead[i], HEX);
            Serial.print(" ");
          }*/
          if (toRead[0]==0x01 && toRead[1]==0x02 && toRead[34]==0x03 && toRead[35]==0x04)//
             {
              for (i = 0; i < num_ions; i++)
                  {
                    tmp = toRead[2 + 4 * i]; // int tmp
                    tmp <<= 8;
                    tmp |= toRead[3 + 4 * i];
                    tmp <<= 8;
                    tmp |= toRead[4 + 4 * i];
                    tmp <<= 8;
                    tmp |= toRead[5 + 4 * i];
                    m[i] = tmp/100;
                    //Serial.println();
                    //Serial.print("ION number ");
                    //Serial.print(i+1);
                    //Serial.print("\t");
                    //Serial.print("value: ");
                    //Serial.print(m[i]);
                    //Serial.print(" mV");
                  }
              measureState=false;
              Serial.println();
            }
     }
}

void Get_DO()
{
  Temperaturet = (uint8_t)READ_TEMP;
  ADC_Raw = analogRead(DO_PIN);
  ADC_Voltage = uint32_t(VREF) * ADC_Raw / ADC_RES;
  DO_Out = readDO(ADC_Voltage, Temperaturet);
}

void Push_Liquid_Effect()
{
 // Turn the LED on, then pause
  for (int i=0;i<=NUM_LEDS;i++)
  {
  leds[i] = CRGB::Blue;  
  }
  FastLED.show();
  delay(500);
  //For blinking uncomment the following code
  // Now turn the LED off, then pause
   for (int i=0;i<=8;i++)
   {
    leds[i] = CRGB::Black;
   }
  FastLED.show();
  delay(500);
}

void Overload_Consuptiom_Effect()
{
  // Turn the LED on, then pause
  for (int i=0;i<=NUM_LEDS;i++)
  {
  leds[i] = CRGB::Orange;  
  }
  FastLED.show();
  delay(500);
  //For blinking uncomment the following code
  // Now turn the LED off, then pause
   for (int i=0;i<=8;i++)
   {
    leds[i] = CRGB::Black;
   }
  FastLED.show();
  delay(500);
}

void Broken_Effect()
{
  // Turn the LED on, then pause
  for (int i=0;i<=NUM_LEDS;i++){
  leds[i] = CRGB::Red;  
  }
  FastLED.show();
}

In case it helps

Its not written to handle multiple tasks. I'm seeking help to do so. the code is which I wrote initially

Any ideas how it can be done? I'm really stuck with this

Which ina226 library did you install? I used GitHub - RobTillaart/INA226: Arduino library for INA266 power sensor and your sketch does not compile.

This would be a simple solution; I only implemented the part with ADDRx, you can do the SELx.

/*
  select pump and led ring
  In:
    address for multiplexer.
*/
void Select_PUMP(uint8_t address) {
  digitalWrite(ADDR0, (address & 0x01));
  digitalWrite(ADDR1, (address & 0x02) >> 1);
  digitalWrite(ADDR2, (address & 0x04) >> 2);
  digitalWrite(ADDR3, (address & 0x08) >> 3);

  // for you to do
  digitalWrite(SEL0, A0);
  digitalWrite(SEL1, A1);
  digitalWrite(SEL2, A2);
  digitalWrite(SEL3, A3);

  digitalWrite(LED_ACTIVE, HIGH);
}

I might not understand the code (or your requirements correctly), but why do you do the below

  Select_PUMP(0, 1, 0, 1);                                     //push the Distilled water PUMP5
  Select_PUMP(1, 0, 0, 1);                                     //Push the EC Std 1 PUMP9

Does your system indeed only need to activate the pump and it stays activated? Else I suspect this to be a bug.

Further, you are aware that you usually can put your led rings in series; so they only use one pin for all of them. In that case e.g. leds[0]..leds[15] are the first ring, leds[16]..leds[31] the second ring and so on.
Controlling them through a multiplexer might have side effects, not sure.

Because you have a number of items that relate to each other, you should consider using structs or classes in which you combine them. This could be a start (based on Rob Tillaart's library); I'm more a C programmer than a C++ programmer so I will use a struct.

#include <Wire.h>
#include <INA226.h>

#include <FastLED.h>

// How many leds in your strip?
// 16 rings of 16 pixels; I don't know for sure if your rings are 30 pixels
#define NUM_LEDS 256

#define DATA_PIN 8
#define CLOCK_PIN 13

// Define the array of leds
CRGB leds[NUM_LEDS];


// info for the tank
struct TANKINFO
{
  const uint8_t pumpNo;     // mux setting for pump
  const uint8_t sensorNo;   // sensor pin
  INA226 ina;               // ina
  uint16_t ledStart;        // the first led of the ring
  uint16_t ledEnd;          // the last led of the ring
};

TANKINFO tanks[] = 
{
  {1, 1, INA226(0x40), 0,  15},
  {2, 2, INA226(0x41), 16, 31},
  // for you to complete
};



void setup()
{

  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);

  for (uint8_t tankCnt=0; tankCnt < sizeof(tanks) / sizeof(tanks[0]); tankCnt++)
  {
    // start up ina
    tanks[tankCnt].ina.begin();
    // set all leds to orange
    for (uint16_t ledCnt = tanks[tankCnt].ledStart; ledCnt < tanks[tankCnt].ledEnd; ledCnt++)
    {
      leds[ledCnt] = CRGB::Orange;
    }
  }
  FastLED.show();
}

void loop()
{
  // loop through tanks array and do what needs to be done
}

To do multitasking, you will have to switch to a millis() based approach; see e.g. Using millis() for timing. A beginners guide and the blink-without-delay example.
You can e.g. add the interval and the 'next' time of the blink as fields in the struct and when looping through the tanks array use those values to blink if necessary. Same for the readings of the inas.

Notes:

  1. This is only for demonstration purposes; it e.g. only has two tanks
  2. Not forcing you to use Rob Tillaarts library, it's just that that is what I had.

If your MCU is an ESP32 it has a built-in multi-tasking OS called freeRTOS.

Here I have 10 tasks running at the 'same' time:

#include <WiFi.h>
#include <PubSubClient.h>
#include "certs.h" // include the connection infor for WiFi and MQTT
#include "sdkconfig.h" // used for log printing
#include "esp_system.h"
#include "freertos/FreeRTOS.h" //freeRTOS items to be used
#include "freertos/task.h"
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <driver/adc.h>
#include "esp32-hal-ledc.h"
#include <HardwareSerial.h>
#include <SimpleKalmanFilter.h>
#include "MHZ19.h"
#include <ESP32Time.h>
#include <SolarCalculator.h>
////
ESP32Time rtc;
MHZ19 myMHZ19;
////
Adafruit_BME680 bme( GPIO_NUM_5 ); // use hardware SPI, set GPIO pin to use
//Adafruit_ST7789 tft = Adafruit_ST7789( TFT_CS     , TFT_DC    , TFT_MOSI   , TFT_SCLK   , TFT_RST     );
Adafruit_ST7789 tft   = Adafruit_ST7789( GPIO_NUM_15, GPIO_NUM_0, GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_22 );
WiFiClient   wifiClient; // do the WiFi instantiation thing
PubSubClient MQTTclient( mqtt_server, mqtt_port, wifiClient ); //do the MQTT instantiation thing
//////
#define evtDoParticleRead     ( 1 << 0 ) // declare an event
#define evtWaitForBME         ( 1 << 1 )
#define evtParseMQTT          ( 1 << 3 )
EventGroupHandle_t eg; // variable for the event group handle
//////
QueueHandle_t xQ_WindChillDewPoint;
QueueHandle_t xQ_eData; // environmental data to be displayed on the screen
struct stu_eData
{
  float  Temperature = 0.0f;
  float  Pressure    = 0.0f;
  float  Humidity    = 0.0f;
  float  IAQ         = 0.0f; // Index Air Quality
  float  RM0         = 0.0f; // Remaining Moisture from sensor 0
  float  PM2         = 0.0f; // particles in air
  float  WS          = 0.0f; // wind speed
  String WD          = "";   // wind direction
  float  RF          = 0.0f; // rainfall
  float  WSV         = 0.0f; // weather station volts
  float  WSC         = 0.0f; // weather station current
  float  WSP         = 0.0f; // weather station power
  float  WindChill   = 0.0f; //windchill
  float  DewPoint    = 0.0f; //dew point or dew index
  int    SunRiseHr   = 0;    // sunrise hour
  int    SunRiseMin  = 0;    //sunrise minute
  int    SunSetHr    = 0;    //sunset hour
  int    SunSetMin   = 0;    //sunset minute
  int    DuskHr      = 0;    //dusk
  int    DuskMin     = 0;    //dusk
  int    DawnHr      = 0;    // dawn
  int    DawnMin     = 0;    // dawn
  int    TransitHr   = 0;    // 'noon' time
  int    TransitMin  = 0;    // 'noon' time
  double azimuth     = 0.0f;   // Sun's azimuth, in degrees
  double elevation   = 0.0f;     // Sun's elevation, in degrees
  float  CO2         = 0.0f;
} x_eData; // environmental data
QueueHandle_t xQ_Message; // payload and topic queue of MQTT payload and topic
const int payloadSize = 100;
struct stu_message
{
  char payload [payloadSize] = {'\0'};
  String topic ;
} x_message;
////
const float oGasResistanceBaseLine = 149598.0f;
int mqttOK = 0;
volatile bool TimeSet = false;
//////
esp_timer_handle_t oneshot_timer; //veriable to store the hardware timer handle
//////
SemaphoreHandle_t sema_MQTT_KeepAlive;
SemaphoreHandle_t sema_PublishPM;
SemaphoreHandle_t sema_mqttOK;
////
//serial(2) = pin25 RX, pin26 TX
HardwareSerial co2Serial ( 2 );
//////
// interrupt service routine for WiFi events put into IRAM
void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
{
  switch (event) {
    case SYSTEM_EVENT_STA_CONNECTED:
      log_i("Connected to WiFi access point");
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      log_i("Disconnected from WiFi access point");
      break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
      log_i("WiFi client disconnected");
      break;
    default: break;
  }
} // void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
//////
void IRAM_ATTR oneshot_timer_callback( void* arg )
{
  BaseType_t xHigherPriorityTaskWoken;
  xEventGroupSetBitsFromISR( eg, evtDoParticleRead, &xHigherPriorityTaskWoken );
} //void IRAM_ATTR oneshot_timer_callback( void* arg )
//////
void IRAM_ATTR mqttCallback(char* topic, byte * payload, unsigned int length)
{
  memset( x_message.payload, '\0', payloadSize ); // clear payload char buffer
  x_message.topic = ""; //clear topic string buffer
  x_message.topic = topic; //store new topic
  int i = 0; // extract payload
  for ( i; i < length; i++)
  {
    x_message.payload[i] = ((char)payload[i]);
  }
  x_message.payload[i] = '\0';
  xQueueOverwrite( xQ_Message, (void *) &x_message );// send data to queue
} // void mqttCallback(char* topic, byte* payload, unsigned int length)
////
void setup()
{
  co2Serial.begin( 9600 , SERIAL_8N1, 25, 26 ); // pin25 RX, pin26 TX
  x_eData.WD.reserve(50);
  x_message.topic.reserve( payloadSize );
  xQ_WindChillDewPoint = xQueueCreate( 1, sizeof(stu_eData) );
  xQ_Message  = xQueueCreate( 1, sizeof(stu_message) );
  xQ_eData    = xQueueCreate( 1, sizeof(stu_eData) ); // sends a queue copy of the structure
  //
  sema_PublishPM = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_PublishPM );
  sema_mqttOK    =  xSemaphoreCreateBinary();
  xSemaphoreGive( sema_mqttOK );
  //
  ledcSetup( 4, 12000, 8 ); // ledc: 4  => Group: 0, Channel: 2, Timer: 1, led frequency, resolution  bits
  ledcAttachPin( GPIO_NUM_12, 4 );   // gpio number and channel
  ledcWrite( 4, 0 ); // write to channel number 4
  //
  eg = xEventGroupCreate(); // get an event group handle
  // output mode
  gpio_config_t io_cfg = {}; // initialize the gpio configuration structure
  io_cfg.mode = GPIO_MODE_OUTPUT; // set gpio mode
  io_cfg.pin_bit_mask = ( (1ULL << GPIO_NUM_4) ); //bit mask of the pins to set
  gpio_config(&io_cfg); // configure the gpio based upon the parameters as set in the configuration structure
  gpio_set_level( GPIO_NUM_4, LOW); // set air particle sensor trigger pin to LOW
  // input mode
  io_cfg = {}; // reinitialize the gpio configuration structure
  io_cfg.mode = GPIO_MODE_INPUT; // set gpio mode. GPIO_NUM_0 input from water level sensor
  io_cfg.pin_bit_mask = ( (1ULL << GPIO_NUM_0) | (1ULL << GPIO_NUM_27)  ); //bit mask of the pins to set, assign gpio number to be configured
  gpio_config(&io_cfg); // configure the gpio based upon the parameters as set in the configuration structure
  // set up A:D channels, refer: https://dl.espressif.com/doc/esp-idf/latest/api-reference/peripherals/adc.html
  adc1_config_width(ADC_WIDTH_12Bit);
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);// using GPIO 36
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html?highlight=hardware%20timer High Resoultion Timer API
  esp_timer_create_args_t oneshot_timer_args = {}; // initialize High Resoulition Timer (HRT) configuration structure
  oneshot_timer_args.callback = &oneshot_timer_callback; // configure for callback, name of callback function
  esp_timer_create( &oneshot_timer_args, &oneshot_timer ); // assign configuration to the HRT, receive timer handle
  //
  xTaskCreatePinnedToCore( fparseMQTT, "fparseMQTT", 7000,  NULL, 5, NULL, 1 );
  xTaskCreatePinnedToCore( MQTTkeepalive, "MQTTkeepalive", 5000, NULL, 6, NULL, 1 );
  xTaskCreatePinnedToCore( DoTheBME680Thing, "DoTheBME280Thing", 20000, NULL, 5, NULL, 1);
  xTaskCreatePinnedToCore( fDoParticleDetector, "fDoParticleDetector", 6000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fmqttWatchDog, "fmqttWatchDog", 5000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fDoTheDisplayThing, "fDoTheDisplayThing", 23000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fScreenBlanking, "fScreenBlanking", 2000, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fGetCO2, "fGetCO2", 4500, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fParseDewPointWindChill, "fParseDewPointWindChill", 4500, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fSolarCalculations, "fSolarCalculations", 10000, NULL, 2, NULL, 1 );
} //void setup()
////
void fSolarCalculations ( void *pvParameters )
{
  double sunrise;  // Sunrise, in hours (UTC)
  double transit;  // Solar noon, in hours (UTC)
  double sunset;   // Sunset, in hours (UTC)
  double dawn;     // Civil dawn, in hours (UTC)
  double dusk;     // Civil dusk, in hours (UTC)
  //double rt_ascension;  // Sun's right ascension, in degrees
  //double declination;   // Sun's declination, in degrees
  const  float time_zone = -7.0f;
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 1000; //delay for mS
  int count  = 3590;
  int monthX = 1;
  int dayX   = 1;
  for (;;)
  {
    if ( count % 60 == 0 )
    {
      if ( (rtc.getHour(true) >= 12) & (rtc.getHour(true) <= 23) )
      {
        dayX = 0;
      } else {
        dayX = 1;
      }
      calcSunriseSunset( rtc.getYear(), (rtc.getMonth() + monthX) , (rtc.getDay() + dayX), latitude, longitude, transit, sunrise, sunset );  // Calculate the times of sunrise, transit and sunset
      sunrise += time_zone;
      sunset  += time_zone;
      SolarTimeFormat( sunrise, 0 );
      SolarTimeFormat( sunset, 1 );      
      calcCivilDawnDusk( rtc.getYear(), (rtc.getMonth() + monthX) , rtc.getDay(), latitude, longitude, transit, dawn, dusk); // Calculate the times of civil dawn and dusk (UTC)
      transit += time_zone;
      dawn    += time_zone;
      dusk    += time_zone;
      SolarTimeFormat( dawn, 2 );
      SolarTimeFormat( dusk, 3 );
      SolarTimeFormat( transit, 4 );      
      calcHorizontalCoordinates( rtc.getYear(), (rtc.getMonth() + monthX) , rtc.getDay(),  rtc.getHour(true) , rtc.getMinute(), rtc.getSecond(), latitude, longitude, x_eData.azimuth, x_eData.elevation );
      x_eData.azimuth   = double(round(x_eData.azimuth * 100)) / 100; // Round to two decimal places
      x_eData.elevation = double(round(x_eData.elevation * 100)) / 100;
      if (count >= 3600 )
      {
        SolarTimeFormat( 0.0f, 5 ); // publish MQTT
        log_i( "Hour:%d Azimuth %f, elevation %f, transit %dhr %dmin, dawn %dhr %dmin, dusk %dhr %dmin", rtc.getHour(true), x_eData.azimuth, x_eData.elevation, x_eData.TransitHr, x_eData.TransitMin, x_eData.DawnHr, x_eData.DawnMin, x_eData.DuskHr, x_eData.DuskMin );
        count = 0;
      }
    }
    //log_i( " high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    count++;
  } //for (;;)
  vTaskDelete( NULL );
} //void fSolarCalculations ( )
////
void SolarTimeFormat( double h, int i  )
{
  int hours   = 0;
  int minutes = 0;
  if ( h != 0.0f )
  {
    int m = int(round(h * 60));
    hours = (m / 60) % 24;
    minutes = m % 60;
  }
  switch ( i )
  {
    case 0:
      x_eData.SunRiseHr = hours;
      x_eData.SunRiseMin = minutes;
      break;
    case 1:
      x_eData.SunSetHr = hours;
      x_eData.SunSetMin = minutes;
      break;
    case 2:
      x_eData.DawnHr = hours;
      x_eData.DawnMin = minutes;
      break;
    case 3:
      x_eData.DuskHr = hours;
      x_eData.DawnMin = minutes;
      break;
    case 4:
      x_eData.TransitHr = hours;
      x_eData.TransitMin = minutes;
      break;
    case 5:
      String sTopic = "";
      sTopic.reserve( 35 );
      sTopic.concat( String(x_eData.SunRiseHr) + "," );
      sTopic.concat( String(x_eData.SunRiseMin) + "," );
      sTopic.concat( String(x_eData.SunSetHr) + "," );
      sTopic.concat( String(x_eData.SunSetMin) + "," );
      sTopic.concat( String(x_eData.DawnHr) + "," );
      sTopic.concat( String(x_eData.DawnMin) + "," );
      sTopic.concat( String(x_eData.TransitHr) + "," );
      sTopic.concat( String(x_eData.TransitMin) );
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
      MQTTclient.publish( topicSRSSDDT, sTopic.c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
      sTopic = "";
      sTopic.concat( String(x_eData.azimuth) + "," + String(x_eData.azimuth) );
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
      MQTTclient.publish( topicAzEle, sTopic.c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
      sTopic = "";
      break;
  } // switch ( i ) {
} // void SolarTimeFormat( double h, int i  )
/*
  250-400ppm Normal background concentration in outdoor ambient air
  400-1,000ppm  Concentrations typical of occupied indoor spaces with good air exchange
  1,000-2,000ppm  Complaints of drowsiness and poor air.
  2,000-5,000 ppm Headaches, sleepiness and stagnant, stale, stuffy air. Poor concentration, loss of attention, increased heart rate and slight nausea may also be present.
  5,000 Workplace exposure limit (as 8-hour TWA) in most jurisdictions.
  >40,000 ppm Exposure may lead to serious oxygen deprivation resulting in permanent brain damage, coma, even death.
*/
void fParseDewPointWindChill( void *pvParameters )
{
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  struct stu_message px_message;
  String sDewPoint = "";
  String sWindChill = "";
  sDewPoint.reserve( payloadSize );
  sWindChill.reserve( payloadSize );
  for (;;)
  {
    if ( xQueueReceive(xQ_WindChillDewPoint, &px_message, portMAX_DELAY) == pdTRUE )
    {
      sDewPoint = px_message.payload;
      int commaIndex = sDewPoint.indexOf(',');
      sWindChill.concat ( sDewPoint.substring(0, commaIndex) );
      sDewPoint.remove( 0, (commaIndex + 1) );
      x_eData.WindChill = sWindChill.toFloat();
      x_eData.DewPoint = sDewPoint.toFloat();
      sDewPoint = "";
      sWindChill = "";
    }
    //log_i( " high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
  }
  vTaskDelete( NULL );
}
////
void fGetCO2 ( void *pvParameters )
{
  uint64_t TimePastKalman  = esp_timer_get_time();
  myMHZ19.begin( co2Serial );
  myMHZ19.autoCalibration();
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 1000; //delay for mS
  SimpleKalmanFilter KF_CO2( 1.0f, 1.0f, .01f );
  for ( ;; )
  {
    KF_CO2.setProcessNoise( (esp_timer_get_time() - TimePastKalman) / 1000000.0f );
    x_eData.CO2 = KF_CO2.updateEstimate( myMHZ19.getCO2() ); // apply simple Kalman filter
    TimePastKalman = esp_timer_get_time();
    xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
    MQTTclient.publish( topicCO2, String(round(x_eData.CO2)).c_str() );
    xSemaphoreGive( sema_MQTT_KeepAlive );
    // process wind chill and dew point
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    //log_i( " high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
  }
  vTaskDelete( NULL );
} //void fMHZ19B ( void *pvParameters )
////
void fScreenBlanking( void *pvParameters )
{
  int       TimeOfPause = 10000 * 1000;
  uint64_t  PauseStartTime = esp_timer_get_time();
  bool      Pause = false;
  const int brightness = 250;
  int       countUpDown = brightness;
  for ( ;; )
  {
    if (!Pause )
    {
      //if motion detect then show display otherwise blank display
      if ( !(gpio_get_level( GPIO_NUM_27)) )
      {
        for ( countUpDown; countUpDown-- > 0; )
        {
          ledcWrite( 4, countUpDown ); // write to channel number 4, dim backlight
          vTaskDelay( 7 );
        }
      } else {
        Pause = true;
        PauseStartTime = esp_timer_get_time();
        ledcWrite( 4, brightness );
        countUpDown = brightness;
      }
    } else {
      // still detecting movement reset blanking pause time
      if ( gpio_get_level( GPIO_NUM_27) )
      {
        PauseStartTime = esp_timer_get_time(); // extend pause blanking time
      }
      if ( (esp_timer_get_time() - PauseStartTime) >= TimeOfPause )
      {
        Pause = false;
      }
    }
    vTaskDelay( 250 );
  }
  vTaskDelete( NULL );
} //void fScreenBlanking( void *pvParameters )
//////
void fparseMQTT( void *pvParameters )
{
  struct stu_message px_message;
  for (;;)
  {
    if ( xQueueReceive(xQ_Message, &px_message, portMAX_DELAY) == pdTRUE )
    {
      xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
      mqttOK = 0;
      xSemaphoreGive( sema_mqttOK );
      if ( px_message.topic == topicRemainingMoisture_0 )
      {
        x_eData.RM0  = String(px_message.payload).toFloat();
      }
      if ( px_message.topic == topicWindSpeed )
      {
        x_eData.WS = String(px_message.payload).toFloat();
      }
      if ( px_message.topic == topicWindDirection )
      {
        x_eData.WD = "";
        x_eData.WD = String(px_message.payload);
      }
      if ( px_message.topic == topicRainfall )
      {
        x_eData.RF = String(px_message.payload).toFloat();
      }
      if ( px_message.topic == topicWSVolts )
      {
        x_eData.WSV = String(px_message.payload).toFloat();
      }
      if ( px_message.topic == topicWSCurrent )
      {
        x_eData.WSC = String(px_message.payload).toFloat();
      }
      if ( px_message.topic == topicWSPower )
      {
        x_eData.WSP = String(px_message.payload).toFloat();
      }
      if ( px_message.topic == topicDPnWI )
      {
        xQueueSend( xQ_WindChillDewPoint, (void *) &px_message, 1 );
      }
      if ( String(px_message.topic) == topicOK )
      {
        if ( !TimeSet)
        {
          String temp = "";
          temp.reserve(50);
          temp.concat( String(px_message.payload[0]) );
          temp.concat( String(px_message.payload[1]) );
          temp.concat( String(px_message.payload[2]) );
          temp.concat( String(px_message.payload[3]) );
          int year =  temp.toInt();
          temp = "";
          temp.concat( String(px_message.payload[5]) + String(px_message.payload[6]) );
          int month =  temp.toInt();
          temp =  "";
          temp.concat(String(px_message.payload[8]) + String(px_message.payload[9]) );
          int day =  temp.toInt();
          temp = "";
          temp.concat( String(px_message.payload[11]) + String(px_message.payload[12]) );
          int hour =  temp.toInt();
          temp = "";
          temp.concat( String(px_message.payload[14]) + String(px_message.payload[15]) );
          int min =  temp.toInt();
          rtc.setTime( 0, min, hour, day, month, year );
          log_i( "rtc  %s Year %d month %d day %d", rtc.getTime(), rtc.getYear(), (rtc.getMonth() + 1), rtc.getDay() );
          TimeSet = true;
        }
      }
    } //if ( xQueueReceive(xQ_Message, &px_message, portMAX_DELAY) == pdTRUE )
  } //for(;;)
  vTaskDelete( NULL );
} // void fparseMQTT( void *pvParameters )
////
void fDoTheDisplayThing( void * parameter )
{
  tft.init( 240, 320 ); // Init ST7789 320x240
  tft.setRotation( 3 );
  tft.setTextSize( 3 );
  tft.fillScreen( ST77XX_BLACK );
  tft.setTextWrap( false );
  struct stu_eData px_eData;
  const int brightness = 250;
  ledcWrite( 4, brightness ); //backlight set
  const int MaxString      = 20;
  String oldTempString     = "";
  String oldHumidityString = "";
  String oldAQIString      = "";
  String oldRainfall       = "";
  String oldWindDirection  = "";
  String oldAirPressure    = "";
  String oldRMO            = "";
  String oldPM2            = "";
  //String oldPower          = "";
  String oldC02 = "";
  oldHumidityString.reserve( MaxString );
  oldWindDirection.reserve( MaxString );
  oldAirPressure.reserve( MaxString );
  oldTempString.reserve( MaxString );
  oldAQIString.reserve( MaxString );
  oldRainfall.reserve( MaxString );
  //oldPower.reserve( MaxString );
  oldRMO.reserve( MaxString );
  oldPM2.reserve( MaxString );
  oldC02.reserve( MaxString );
  bool Tick = true;
  const int numOfColors = 40;
  /* https://chrishewett.com/blog/true-rgb565-colour-picker/#:~:text=A%20true%20RGB565%20colour%20picker%2021st%20Oct%202017,in%205%20bits%20and%20green%20in%206%20bits. */
  int colors[numOfColors] = { ST77XX_BLACK, ST77XX_RED, ST77XX_WHITE, ST77XX_BLUE, ST77XX_GREEN, ST77XX_CYAN, ST77XX_MAGENTA, ST77XX_YELLOW, 0xd55b, 0xee09,
                              0x2e15, 0xcb43, 0x6bad, 0x126f, 0x1264, 0xe264, 0xe7e4, 0x87e4, 0x87fe, 0x876a,
                              0xe304, 0x1cc4, 0xf4c4, 0xf4da, 0xcf66, 0xa879, 0x7f28, 0x4f37, 0xfa97, 0x6195,
                              0X8162, 0xc962, 0x517b, 0x325b, 0xea5b, 0x179b, 0xff80, 0xf960, 0x416d, 0x7bd1
                            };
  int colorCounter = 1;
  for (;;)
  {
    if ( xQueueReceive(xQ_eData, &px_eData, portMAX_DELAY) == pdTRUE )
    {
      tft.setCursor( 0, 0 );
      tft.setTextColor( colors[0] );
      tft.print( oldTempString );
      tft.setCursor( 0, 0 );
      tft.setTextColor( colors[colorCounter] );
      oldTempString = "";
      if ( Tick )
      {
        oldTempString.concat( "iTemp " + String(px_eData.Temperature) + "F" );
      } else {
        oldTempString.concat( "Wind Chill " + String(px_eData.WindChill) + "F" );
      }
      tft.println( oldTempString );
      tft.setCursor( 0, 30 );
      tft.setTextColor( colors[0] );
      tft.print( oldHumidityString );
      tft.setCursor( 0, 30 );
      tft.setTextColor( colors[colorCounter] );
      oldHumidityString = "";
      if ( Tick )
      {
        oldHumidityString.concat( "iHum  " + String(px_eData.Humidity) + "%" );
      } else {
        if ( (px_eData.SunRiseHr < 10) & (px_eData.SunRiseMin < 10) )
        {
          oldHumidityString.concat( "sRise 0" + String(px_eData.SunRiseHr) + "0" + String(px_eData.SunRiseMin) );
        }
        if ( (px_eData.SunRiseHr >= 10) & (px_eData.SunRiseMin < 10) )
        {
          oldHumidityString.concat( "sRise " + String(px_eData.SunRiseHr) + "0" + String(px_eData.SunRiseMin) );
        }
        if ( (px_eData.SunRiseHr < 10) & (px_eData.SunRiseMin >= 10) )
        {
          oldHumidityString.concat( "sRise 0" + String(px_eData.SunRiseHr) + String(px_eData.SunRiseMin) );
        }
        if ( (px_eData.SunRiseHr >= 10) & (px_eData.SunRiseMin >= 10) )
        {
          oldHumidityString.concat( "sRise " + String(px_eData.SunRiseHr) + String(px_eData.SunRiseMin) );
        }
      }
      tft.println( oldHumidityString );
      tft.setCursor( 0, 60 );
      tft.setTextColor( colors[0] );
      tft.print( oldAirPressure );
      tft.setCursor( 0, 60 );
      tft.setTextColor( colors[colorCounter] );
      oldAirPressure = "";
      if ( Tick )
      {
        //oldAirPressure.concat( "Pres " + String(px_eData.Pressure) + "mmHg" );
        oldAirPressure.concat( "Dew Pt. " + String(px_eData.DewPoint) + "F" );
      } else {
        if ( px_eData.SunSetMin < 10 )
        {
          oldAirPressure.concat( "sSet " + String(px_eData.SunSetHr) + "0" + String(px_eData.SunSetMin) );
        }
        if ( px_eData.SunRiseMin >= 10 )
        {
          oldAirPressure.concat( "sSet " + String(px_eData.SunSetHr) + String(px_eData.SunSetMin) );
        }
      }
      tft.println( oldAirPressure );
      tft.setCursor( 0, 90 );
      tft.setTextColor( colors[0] );
      tft.print( oldAQIString );
      tft.setCursor( 0, 90 );
      tft.setTextColor( colors[colorCounter] );
      oldAQIString = "";
      oldAQIString.concat( "iAQI " + String(px_eData.IAQ) + "%" );
      tft.println( oldAQIString );
      tft.setCursor( 0, 120 );
      tft.setTextColor( colors[0] );
      tft.print( oldRMO );
      tft.setCursor( 0, 120 );
      tft.setTextColor( colors[colorCounter] );
      oldRMO = "";
      //if ( Tick )
      //{
        oldRMO.concat( "iRM0 " + String(px_eData.RM0) + "%" );
      //} else {
      //  oldRMO.concat( "iCO2 " + String(CO2) + "ppm" );
      //}
      tft.println( oldRMO );
      tft.setCursor( 0, 150 );
      tft.setTextColor( colors[0] );
      tft.print( oldPM2 );
      tft.setCursor( 0, 150 );
      tft.setTextColor( colors[colorCounter] );
      oldPM2 = "";
      oldPM2.concat( "PM2 " + String(px_eData.PM2) + "ug/m3" );
      tft.println( oldPM2 );
      tft.setCursor( 0, 180 );
      tft.setTextColor( colors[0] );
      //tft.print( oldPower );
      tft.print( oldC02 );
      tft.setCursor( 0, 180 );
      tft.setTextColor( colors[colorCounter] );
      //oldPower = "";
      oldC02 = "";
      //oldPower.concat(  String(px_eData.WSV) + " Volts" );
      oldC02.concat(  "iCO2 " + String(x_eData.CO2) + "ppm" );
      //oldPower.concat(  String(px_eData.WSV) + "V " + String(int(px_eData.WSC * 1000.0f)) + "mA " + String((int(px_eData.WSP * 1000.0f))) + "mW" );
      //tft.println( oldPower );
      tft.println( oldC02 );
      colorCounter++;
      if ( colorCounter > (numOfColors - 1) )
      {
        colorCounter = 1;
      }
      Tick = !Tick;
      //log_i( " high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
    } //if ( xQueueReceive(xQ_eData, &px_eData, portMAX_DELAY) == pdTRUE )
  } //for (;;)
  vTaskDelete( NULL );
} //void fDoTheDisplayTHing( void * parameter )
////
void fmqttWatchDog( void * paramater )
{
  int UpdateImeTrigger = 86400; //seconds in a day
  int UpdateTimeInterval = 86300; // 1st time update in 100 counts
  int maxNonMQTTresponse = 15;
  for (;;)
  {
    vTaskDelay( 1000 );
    if ( mqttOK >= maxNonMQTTresponse )
    {
      ESP.restart();
    }
    xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
    mqttOK++;
    xSemaphoreGive( sema_mqttOK );
    UpdateTimeInterval++; // trigger new time get
    if ( UpdateTimeInterval >= UpdateImeTrigger )
    {
      TimeSet = false; // sets doneTime to false to get an updated time after a days count of seconds
      UpdateTimeInterval = 0;
    }
  }
  vTaskDelete( NULL );
}
////
float fCalulate_IAQ_Index( int gasResistance, float Humidity)
{
  float hum_baseline = 40.0f;
  float hum_weighting = 0.25f;
  float gas_offset = 0.0f;
  float hum_offset = 0.0f;
  float hum_score = 0.0f;
  float gas_score = 0.0f;
  gas_offset = oGasResistanceBaseLine - float( gasResistance );
  hum_offset = float( Humidity ) - hum_baseline;
  // calculate hum_score as distance from hum_baseline
  if ( hum_offset > 0.0f )
  {
    hum_score = 100.0f - hum_baseline - hum_offset;
    hum_score /= ( 100.0f - hum_baseline );
    hum_score *= ( hum_weighting * 100.0f );
  } else {
    hum_score = hum_baseline + hum_offset;
    hum_score /= hum_baseline;
    hum_score *= ( 100.0f - (hum_weighting * 100.0f) );
  }
  //calculate gas score as distance from baseline
  if ( gas_offset > 0.0f )
  {
    gas_score = float( gasResistance ) / oGasResistanceBaseLine;
    gas_score *= ( 100.0f - (hum_weighting * 100.0f ) );
  } else {
    gas_score = 100.0f - ( hum_weighting * 100.0f );
  }
  return ( hum_score + gas_score );
} //void fCalulate_IAQ_Index( int gasResistance, float Humidity):
////
void fDoParticleDetector( void * parameter )
{
  /*
    ug/m3     AQI                 Lvl AQ (Air Quality)
    (air Quality Index)
    0-35     0-50                1   Excellent
    35-75    51-100              2   Average
    75-115   101-150             3   Light pollution
    115-150  151-200             4   moderate
    150-250  201-300             5   heavy
    250-500  >=300               6   serious
  */
  float ADbits = 4095.0f;
  float uPvolts = 3.3f;
  float adcValue = 0.0f;
  float dustDensity = 0.0f;
  float Voc = 0.6f; // Set the typical output voltage, when there is zero dust.
  const float K = 0.5f; // Use the typical sensitivity in units of V per 100ug/m3.
  xEventGroupWaitBits (eg, evtWaitForBME, pdTRUE, pdTRUE, portMAX_DELAY );
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 100; //delay for mS
  for (;;)
  {
    //enable sensor led
    gpio_set_level( GPIO_NUM_4, HIGH ); // set gpio 4 to high to turn on sensor internal led for measurement
    esp_timer_start_once( oneshot_timer, 280 ); // trigger one shot timer for a 280us timeout, warm up time.
    xEventGroupWaitBits (eg, evtDoParticleRead, pdTRUE, pdTRUE, portMAX_DELAY ); // event will be triggered by the timer expiring, wait here for the 280uS
    adcValue = float( adc1_get_raw(ADC1_CHANNEL_0) ); //take a raw ADC reading from the dust sensor
    gpio_set_level( GPIO_NUM_4, LOW );//Shut off the sensor LED
    adcValue = ( adcValue * uPvolts ) / ADbits; //calculate voltage
    dustDensity = (adcValue / K) * 100.0; //convert volts to dust density
    if ( dustDensity < 0.0f )
    {
      dustDensity = 0.00f; // make negative values a 0
    }
    if ( xSemaphoreTake( sema_PublishPM, 0 ) == pdTRUE )  // don't wait for semaphore to be available
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
      //log_i( "ADC volts %f Dust Density = %ug / m3 ", adcValue, dustDensity ); // print the calculated voltage and dustdensity
      MQTTclient.publish( topicInsidePM, String(dustDensity).c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
      x_eData.PM2 = dustDensity;
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    //log_i( " high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
  }
  vTaskDelete( NULL );
}// end fDoParticleDetector()
////
void DoTheBME680Thing( void *pvParameters )
{
  SPI.begin(); // initialize the SPI library
  vTaskDelay( 10 );
  if (!bme.begin()) {
    log_i("Could not find a valid BME680 sensor, check wiring!");
    while (1);
  }
  // Set up oversampling and filter initialization
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150); // 320*C for 150 ms
  //wait for a mqtt connection
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  xEventGroupSetBits( eg, evtWaitForBME );
  TickType_t xLastWakeTime    = xTaskGetTickCount();
  const TickType_t xFrequency = 1000 * 15; //delay for mS
  String bmeInfo = "";
  bmeInfo.reserve( 100 );
  for (;;)
  {
    x_eData.Temperature  = bme.readTemperature();
    x_eData.Temperature  = ( x_eData.Temperature * 1.8f ) + 32.0f; // (Celsius x 1.8) + 32
    x_eData.Pressure     = bme.readPressure();
    x_eData.Pressure     = x_eData.Pressure / 133.3223684f; //converts to mmHg
    x_eData.Humidity     = bme.readHumidity();
    x_eData.IAQ          = fCalulate_IAQ_Index( bme.readGas(), x_eData.Humidity );
    //log_i( " temperature % f, Pressure % f, Humidity % f IAQ % f", x_eData.Temperature, x_eData.Pressure, x_eData.Humidity, x_eData.IAQ);
    bmeInfo.concat( String(x_eData.Temperature, 2) );
    bmeInfo.concat( "," );
    bmeInfo.concat( String(x_eData.Pressure, 2) );
    bmeInfo.concat( "," );
    bmeInfo.concat( String(x_eData.Humidity, 2) );
    bmeInfo.concat( "," );
    bmeInfo.concat( String(x_eData.IAQ, 2) );
    xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
    if ( MQTTclient.connected() )
    {
      MQTTclient.publish( topicInsideInfo, bmeInfo.c_str() );
    }
    xSemaphoreGive( sema_MQTT_KeepAlive );
    xSemaphoreGive( sema_PublishPM ); // release publish of dust density
    xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
    mqttOK ++;
    xSemaphoreGive( sema_mqttOK );
    xQueueOverwrite( xQ_eData, (void *) &x_eData );// send data to display
    //
    bmeInfo = ""; // empty the string buffer
    findDewPointWithHumidity( x_eData.Humidity, x_eData.Temperature );
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    // log_i( "DoTheBME280Thing high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
  }
  vTaskDelete ( NULL );
}
////
/*
  Important to not set vTaskDelay/vTaskDelayUntil to less then 10. Errors begin to develop with the MQTT and network connection.
  makes the initial wifi/mqtt connection and works to keeps those connections open.
*/
void MQTTkeepalive( void *pvParameters )
{
  sema_MQTT_KeepAlive   = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_MQTT_KeepAlive ); // found keep alive can mess with a publish, stop keep alive during publish
  MQTTclient.setKeepAlive( 90 ); // setting keep alive to 90 seconds makes for a very reliable connection, must be set before the 1st connection is made.
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 250; //delay for ms
  for (;;)
  {
    //check for a is-connected and if the WiFi 'thinks' its connected, found checking on both is more realible than just a single check
    if ( (wifiClient.connected()) && (WiFi.status() == WL_CONNECTED) )
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY ); // whiles MQTTlient.loop() is running no other mqtt operations should be in process
      MQTTclient.loop();
      xSemaphoreGive( sema_MQTT_KeepAlive );
    }
    else {
      log_i( "MQTT keep alive found MQTT status % s WiFi status % s", String(wifiClient.connected()), String(WiFi.status()) );
      if ( !(wifiClient.connected()) || !(WiFi.status() == WL_CONNECTED) )
      {
        connectToWiFi();
      }
      connectToMQTT();
    }
    //log_i( " high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete ( NULL );
}
////
void connectToMQTT()
{
  byte mac[5]; // create client ID from mac address
  WiFi.macAddress(mac); // get mac address
  String clientID = String(mac[0]) + String(mac[4]) ; // use mac address to create clientID
  while ( !MQTTclient.connected() )
  {
    MQTTclient.connect( clientID.c_str(), mqtt_username, mqtt_password );
    vTaskDelay( 250 );
  }
  MQTTclient.setCallback( mqttCallback );
  MQTTclient.subscribe  ( topicOK );
  MQTTclient.subscribe  ( topicRemainingMoisture_0 );
  MQTTclient.subscribe  ( topicWindSpeed );
  MQTTclient.subscribe  ( topicWindDirection );
  MQTTclient.subscribe  ( topicRainfall );
  MQTTclient.subscribe  ( topicWSVolts );
  MQTTclient.subscribe  ( topicWSCurrent );
  MQTTclient.subscribe  ( topicWSPower );
  MQTTclient.subscribe  ( topicDPnWI );
} //void connectToMQTT()
void connectToWiFi()
{
  int TryCount = 0;
  while ( WiFi.status() != WL_CONNECTED )
  {
    TryCount++;
    WiFi.disconnect();
    WiFi.begin( SSID, PASSWORD );
    vTaskDelay( 4000 );
    if ( TryCount == 10 )
    {
      ESP.restart();
    }
  }
  WiFi.onEvent( WiFiEvent );
}
////
float findDewPointWithHumidity( float humi, float temperature )
{
  //Celcius
  float ans =  (temperature - (14.55 + 0.114 * temperature) * (1 - (0.01 * humi)) - pow(((2.5 + 0.007 * temperature) * (1 - (0.01 * humi))), 3) - (15.9 + 0.117 * temperature) * pow((1 - (0.01 * humi)), 14));
  //log_i( "%f", ans );
  return ans;
}
void loop() { }
////

Notice the LOOP() function is empty.

A Mega could do a state machine.

This code also uses freeRTOS:

#include <ESP32Time.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include "certs.h" // include the connection info for WiFi and MQTT
#include "sdkconfig.h" // used for log printing
#include "esp_system.h"
#include "freertos/FreeRTOS.h" //freeRTOS items to be used
#include "freertos/task.h"
#include <driver/adc.h>
#include <SimpleKalmanFilter.h>
////
WiFiClient      wifiClient; // do the WiFi instantiation thing
PubSubClient    MQTTclient( mqtt_server, mqtt_port, wifiClient ); //do the MQTT instantiation thing
ESP32Time       rtc;
////
#define evtDoParticleRead  ( 1 << 0 ) // declare an event
#define evtADCreading      ( 1 << 3 )
EventGroupHandle_t eg; // variable for the event group handle
////
SemaphoreHandle_t sema_MQTT_KeepAlive;
SemaphoreHandle_t sema_mqttOK;
////
QueueHandle_t xQ_RemainingMoistureMQTT;
QueueHandle_t xQ_RM;
QueueHandle_t xQ_Message;
////
struct stu_message
{
  char payload [150] = {'\0'};
  String topic;
} x_message;
////
int    mqttOK = 0;
bool   TimeSet = false;
bool   manualPumpOn = false;
////
// interrupt service routine for WiFi events put into IRAM
void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
{
  switch (event) {
    case SYSTEM_EVENT_STA_CONNECTED:
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      log_i("Disconnected from WiFi access point");
      break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
      log_i("WiFi client disconnected");
      break;
    default: break;
  }
} // void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
////
void IRAM_ATTR mqttCallback(char* topic, byte * payload, unsigned int length)
{
  // clear locations
  memset( x_message.payload, '\0', 150 );
  x_message.topic = ""; //clear string buffer
  x_message.topic = topic;
  int i = 0;
  for ( i; i < length; i++)
  {
    x_message.payload[i] = ((char)payload[i]);
  }
  x_message.payload[i] = '\0';
  xQueueOverwrite( xQ_Message, (void *) &x_message );// send data
} // void mqttCallback(char* topic, byte* payload, unsigned int length)
////
void setup()
{
  x_message.topic.reserve(150);
  //
  xQ_Message = xQueueCreate( 1, sizeof(stu_message) );
  xQ_RemainingMoistureMQTT = xQueueCreate( 1, sizeof(float) ); // sends a queue copy
  xQ_RM = xQueueCreate( 1, sizeof(float) );
  //
  eg = xEventGroupCreate(); // get an event group handle
  //
  sema_mqttOK =  xSemaphoreCreateBinary();
  xSemaphoreGive( sema_mqttOK );
  //
  gpio_config_t io_cfg = {}; // initialize the gpio configuration structure
  io_cfg.mode = GPIO_MODE_INPUT; // set gpio mode. GPIO_NUM_0 input from water level sensor
  io_cfg.pull_down_en = GPIO_PULLDOWN_ENABLE; // enable pull down
  io_cfg.pin_bit_mask = ( (1ULL << GPIO_NUM_0) ); //bit mask of the pins to set, assign gpio number to be configured
  gpio_config(&io_cfg); // configure the gpio based upon the parameters as set in the configuration structure
  //
  io_cfg = {}; //set configuration structure back to default values
  io_cfg.mode = GPIO_MODE_OUTPUT;
  io_cfg.pin_bit_mask = ( (1ULL << GPIO_NUM_4) | (1ULL << GPIO_NUM_5) ); //bit mask of the pins to set, assign gpio number to be configured
  gpio_config(&io_cfg);
  gpio_set_level( GPIO_NUM_4, LOW); // deenergize relay module
  gpio_set_level( GPIO_NUM_5, LOW); // deenergize valve
  // set up A:D channels  https://dl.espressif.com/doc/esp-idf/latest/api-reference/peripherals/adc.html
  adc1_config_width(ADC_WIDTH_12Bit);
  adc1_config_channel_atten(ADC1_CHANNEL_3, ADC_ATTEN_DB_11);// using GPIO 39
  //
  xTaskCreatePinnedToCore( MQTTkeepalive, "MQTTkeepalive", 10000, NULL, 6, NULL, 1 );
  xTaskCreatePinnedToCore( fparseMQTT, "fparseMQTT", 10000, NULL, 5, NULL, 1 ); // assign all to core 1, WiFi in use.
  xTaskCreatePinnedToCore( fPublish, "fPublish", 9000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fReadAD, "fReadAD", 9000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fDoMoistureDetector, "fDoMoistureDetector", 70000, NULL, 4, NULL, 1 );
  xTaskCreatePinnedToCore( fmqttWatchDog, "fmqttWatchDog", 3000, NULL, 2, NULL, 1 );
} //void setup()
////
void fReadAD( void * parameter )
{
  float    ADbits = 4096.0f;
  float    uPvolts = 3.3f;
  float    adcValue_b = 0.0f; //plant in yellow pot
  uint64_t TimePastKalman  = esp_timer_get_time(); // used by the Kalman filter UpdateProcessNoise, time since last kalman calculation
  float    WetValue = 1.07f; // value found by putting sensor in water
  float    DryValue = 2.732f; // value of probe when held in air
  float    Range = DryValue - WetValue;
  float    RemainingMoisture = 100.0f;
  SimpleKalmanFilter KF_ADC_b( 1.0f, 1.0f, .01f );
  for (;;)
  {
    xEventGroupWaitBits (eg, evtADCreading, pdTRUE, pdTRUE, portMAX_DELAY ); //
    adcValue_b = float( adc1_get_raw(ADC1_CHANNEL_3) ); //take a raw ADC reading
    adcValue_b = ( adcValue_b * uPvolts ) / ADbits; //calculate voltage
    KF_ADC_b.setProcessNoise( (esp_timer_get_time() - TimePastKalman) / 1000000.0f ); //get time, in microsecods, since last readings
    adcValue_b = KF_ADC_b.updateEstimate( adcValue_b ); // apply simple Kalman filter
    TimePastKalman = esp_timer_get_time(); // time of update complete
    RemainingMoisture = 100.0f * (1 - ((adcValue_b - WetValue) / (DryValue - WetValue))); //remaining moisture =  1-(xTarget - xMin) / (xMax - xMin) as a percentage of the sensor wet dry volatges
    xQueueOverwrite( xQ_RM, (void *) &RemainingMoisture );
    //log_i( "adcValue_b = %f remaining moisture %f%", adcValue_b, RemainingMoisture );
  }
  vTaskDelete( NULL );
}
////
void fPublish( void * parameter )
{
  float  RemainingMoisture = 100.0f;
  for (;;)
  {
    if ( xQueueReceive(xQ_RemainingMoistureMQTT, &RemainingMoisture, portMAX_DELAY) == pdTRUE )
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY ); // whiles MQTTlient.loop() is running no other mqtt operations should be in process
      MQTTclient.publish( topicRemainingMoisture_0, String(RemainingMoisture).c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
    }
  } // for (;;)
  vTaskDelete( NULL );
} //void fPublish( void * parameter )
////
void WaterPump0_off()
{
  gpio_set_level( GPIO_NUM_4, LOW); //denergize relay module
  vTaskDelay( 1 );
  gpio_set_level( GPIO_NUM_5, LOW); //denergize/close valve
}
////
void WaterPump0_on()
{
  gpio_set_level( GPIO_NUM_5, HIGH); //energize/open valve
  vTaskDelay( 1 );
  gpio_set_level( GPIO_NUM_4, HIGH); //energize relay module
}
////
void fmqttWatchDog( void * paramater )
{
  int UpdateImeTrigger = 86400; //seconds in a day
  int UpdateTimeInterval = 85000; // get another reading when = UpdateTimeTrigger
  int maxNonMQTTresponse = 12;
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 5000; //delay for mS
  for (;;)
  {
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    xSemaphoreTake( sema_mqttOK, portMAX_DELAY ); // update mqttOK
    mqttOK++;
    xSemaphoreGive( sema_mqttOK );
    if ( mqttOK >= maxNonMQTTresponse )
    {
      ESP.restart();
    }
    UpdateTimeInterval++; // trigger new time get
    if ( UpdateTimeInterval >= UpdateImeTrigger )
    {
      TimeSet = false; // sets doneTime to false to get an updated time after a days count of seconds
      UpdateTimeInterval = 0;
    }
  }
  vTaskDelete( NULL );
} //void fmqttWatchDog( void * paramater )
////
void fDoMoistureDetector( void * parameter )
{
  //wait for a mqtt connection
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  int      TimeToPublish = 5000000; //5000000uS
  int      TimeForADreading = 100 * 1000; // 100mS
  uint64_t TimePastPublish = esp_timer_get_time(); // used by publish
  uint64_t TimeADreading   = esp_timer_get_time();
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 10; //delay for 10mS
  float    RemainingMoisture = 100.0f; //prevents pump turn on during start up
  bool     pumpOn = false;
  uint64_t PumpOnTime = esp_timer_get_time();
  int      PumpRunTime = 11000000;
  uint64_t PumpOffWait = esp_timer_get_time();
  uint64_t PumpOffWaitFor = 60000000; //one minute
  float    lowMoisture = 23.0f;
  float    highMoisture = 40.0f;
  for (;;)
  {
    //read AD values every 100mS.
    if ( (esp_timer_get_time() - TimeADreading) >= TimeForADreading )
    {
      xEventGroupSetBits( eg, evtADCreading );
      TimeADreading = esp_timer_get_time();
    }
    xQueueReceive(xQ_RM, &RemainingMoisture, 0 ); //receive queue stuff no waiting
    //read gpio 0 is water level good. Yes: OK to run pump : no pump off.   remaining moisture good, denergize water pump otherwise energize water pump.
    if ( RemainingMoisture >= highMoisture )
    {
      WaterPump0_off();
    }
    if ( !pumpOn )
    {
      log_i( "not pump on ");
      if ( gpio_get_level( GPIO_NUM_0 ) )
      {
        if ( RemainingMoisture <= lowMoisture )
        {
          //has one minute passed since last pump energize, if so then allow motor to run
          if ( (esp_timer_get_time() - PumpOffWait) >= PumpOffWaitFor )
          {
            WaterPump0_on();
            log_i( "pump on " );
            pumpOn = !pumpOn;
            PumpOnTime = esp_timer_get_time();
          }
        }
        //xSemaphoreGive( sema_RemainingMoisture );
      } else {
        log_i( "water level bad " );
        WaterPump0_off();
        PumpOffWait = esp_timer_get_time();
      }
    } else {
      /*
         pump goes on runs for X seconds then turn off, then wait PumpOffWaitTime before being allowed to energize again
      */
      if ( (esp_timer_get_time() - PumpOnTime) >= PumpRunTime )
      {
        log_i( "pump off " );
        WaterPump0_off(); // after 5 seconds turn pump off
        pumpOn = !pumpOn;
        PumpOffWait = esp_timer_get_time();
      }
    }
    // publish to MQTT every 5000000uS
    if ( (esp_timer_get_time() - TimePastPublish) >= TimeToPublish )
    {
      xQueueOverwrite( xQ_RemainingMoistureMQTT, (void *) &RemainingMoisture );// data for mqtt publish
      TimePastPublish = esp_timer_get_time(); // get next publish time
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete( NULL );
}// end fDoMoistureDetector()
////
void MQTTkeepalive( void *pvParameters )
{
  sema_MQTT_KeepAlive   = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_MQTT_KeepAlive ); // found keep alive can mess with a publish, stop keep alive during publish
  MQTTclient.setKeepAlive( 90 ); // setting keep alive to 90 seconds makes for a very reliable connection, must be set before the 1st connection is made.
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 250; // 250mS
  for (;;)
  {
    //check for a is-connected and if the WiFi 'thinks' its connected, found checking on both is more realible than just a single check
    if ( (wifiClient.connected()) && (WiFi.status() == WL_CONNECTED) )
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY ); // whiles MQTTlient.loop() is running no other mqtt operations should be in process
      MQTTclient.loop();
      xSemaphoreGive( sema_MQTT_KeepAlive );
    }
    else {
      log_i( "MQTT keep alive found MQTT status %s WiFi status %s", String(wifiClient.connected()), String(WiFi.status()) );
      if ( !(wifiClient.connected()) || !(WiFi.status() == WL_CONNECTED) )
      {
        connectToWiFi();
      }
      connectToMQTT();
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete ( NULL );
}
////
void connectToMQTT()
{
  // create client ID from mac address
  byte mac[5];
  int count = 0;
  WiFi.macAddress(mac); // get mac address
  String clientID = String(mac[0]) + String(mac[4]);
  log_i( "connect to mqtt as client %s", clientID );
  while ( !MQTTclient.connected() )
  {
    MQTTclient.disconnect();
    MQTTclient.connect( clientID.c_str(), mqtt_username, mqtt_password );
    vTaskDelay( 250 );
    count++;
    if ( count == 5 )
    {
      ESP.restart();
    }
  }
  MQTTclient.setCallback( mqttCallback );
  MQTTclient.subscribe( topicOK );
}
////
void connectToWiFi()
{
  int TryCount = 0;
  while ( WiFi.status() != WL_CONNECTED )
  {
    TryCount++;
    WiFi.disconnect();
    WiFi.begin( SSID, PASSWORD );
    vTaskDelay( 4000 );
    if ( TryCount == 10 )
    {
      ESP.restart();
    }
  }
  WiFi.onEvent( WiFiEvent );
} // void connectToWiFi()
//////
void fparseMQTT( void *pvParameters )
{
  struct stu_message px_message;
  for (;;)
  {
    if ( xQueueReceive(xQ_Message, &px_message, portMAX_DELAY) == pdTRUE )
    {
      if ( px_message.topic == topicOK )
      {
        xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
        mqttOK = 0; // clear mqtt ok count
        xSemaphoreGive( sema_mqttOK );
      }
      if ( !TimeSet )
      {
        String temp = "";
        temp = px_message.payload[0];
        temp += px_message.payload[1];
        temp += px_message.payload[2];
        temp += px_message.payload[3];
        int year =  temp.toInt();
        temp = "";
        temp = px_message.payload[5];
        temp += px_message.payload[6];
        int month =  temp.toInt();
        temp = "";
        temp = px_message.payload[8];
        temp += px_message.payload[9];
        int day =  temp.toInt();
        temp = "";
        temp = px_message.payload[11];
        temp += px_message.payload[12];
        int hour =  temp.toInt();
        temp = "";
        temp = px_message.payload[14];
        temp += px_message.payload[15];
        int min =  temp.toInt();
        rtc.setTime( 0, min, hour, day, month, year );
        log_i( "%s  ", rtc.getTime() );
        TimeSet = true;
      }
      // manual pump control
      if ( str_eTopic == topicPumpState )
      {
        if ( String(strPayload) == "off" )
        {
          WaterPump0_off();
          manualPumpOn = false;
        }
        if ( String(strPayload) == "on" )
        {
          WaterPump0_on();
          manualPumpOn = true;
        }
      }
    }
  } //for(;;)
  vTaskDelete ( NULL );
} // void fparseMQTT( void *pvParameters )
////
void loop() {}

Notice the task

void fDoMoistureDetector( void * parameter )
{
  //wait for a mqtt connection
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  int      TimeToPublish = 5000000; //5000000uS
  int      TimeForADreading = 100 * 1000; // 100mS
  uint64_t TimePastPublish = esp_timer_get_time(); // used by publish
  uint64_t TimeADreading   = esp_timer_get_time();
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 10; //delay for 10mS
  float    RemainingMoisture = 100.0f; //prevents pump turn on during start up
  bool     pumpOn = false;
  uint64_t PumpOnTime = esp_timer_get_time();
  int      PumpRunTime = 11000000;
  uint64_t PumpOffWait = esp_timer_get_time();
  uint64_t PumpOffWaitFor = 60000000; //one minute
  float    lowMoisture = 23.0f;
  float    highMoisture = 40.0f;
  for (;;)
  {
    //read AD values every 100mS.
    if ( (esp_timer_get_time() - TimeADreading) >= TimeForADreading )
    {
      xEventGroupSetBits( eg, evtADCreading );
      TimeADreading = esp_timer_get_time();
    }
    xQueueReceive(xQ_RM, &RemainingMoisture, 0 ); //receive queue stuff no waiting
    //read gpio 0 is water level good. Yes: OK to run pump : no pump off.   remaining moisture good, denergize water pump otherwise energize water pump.
    if ( RemainingMoisture >= highMoisture )
    {
      WaterPump0_off();
    }
    if ( !pumpOn )
    {
      log_i( "not pump on ");
      if ( gpio_get_level( GPIO_NUM_0 ) )
      {
        if ( RemainingMoisture <= lowMoisture )
        {
          //has one minute passed since last pump energize, if so then allow motor to run
          if ( (esp_timer_get_time() - PumpOffWait) >= PumpOffWaitFor )
          {
            WaterPump0_on();
            log_i( "pump on " );
            pumpOn = !pumpOn;
            PumpOnTime = esp_timer_get_time();
          }
        }
        //xSemaphoreGive( sema_RemainingMoisture );
      } else {
        log_i( "water level bad " );
        WaterPump0_off();
        PumpOffWait = esp_timer_get_time();
      }
    } else {
      /*
         pump goes on runs for X seconds then turn off, then wait PumpOffWaitTime before being allowed to energize again
      */
      if ( (esp_timer_get_time() - PumpOnTime) >= PumpRunTime )
      {
        log_i( "pump off " );
        WaterPump0_off(); // after 5 seconds turn pump off
        pumpOn = !pumpOn;
        PumpOffWait = esp_timer_get_time();
      }
    }
    // publish to MQTT every 5000000uS
    if ( (esp_timer_get_time() - TimePastPublish) >= TimeToPublish )
    {
      xQueueOverwrite( xQ_RemainingMoistureMQTT, (void *) &RemainingMoisture );// data for mqtt publish
      TimePastPublish = esp_timer_get_time(); // get next publish time
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete( NULL );
}// end fDoMoistureDetector()

It is a single task running several other tasks. Uses the ESP32 version of millis() to check on states and run the taskings as per the state.

1 Like

:smiley:

First of all my heartiest thanks to you for taking time and replying to me. It matters to me alot.

I have 16 Pumps and 16 LED Rings. The LED Rings are associated with each pumps (to the tanks actually)

The 16 Pumps and 16 LED Rings are accessed thru two different MUXs one for each to access the first PUMP I have to set the address lines as (A1 = 0, A2 = 0, A3 = 0 and A4 = 1) for example and same goes with the LED rings

So with

  Select_PUMP(0, 1, 0, 1);                                     //push the Distilled water PUMP5
  Select_PUMP(1, 0, 0, 1);                                     //Push the EC Std 1 PUMP9

I do this. There's a sequence in which I have to turn on these Pumps for 5 secs and shut them down afterwards in each cycle and that's why I use this.

The Select PUMP () function takes 1s and 0s as argument and sets/resets the GPIOs inside the function accordingly as the function is as follows:

void Select_PUMP(int A0, int A1, int A2, int A3)
{
  digitalWrite(ADDR0,A0);
  digitalWrite(ADDR1,A1);
  digitalWrite(ADDR2,A2);
  digitalWrite(ADDR3,A3);

  digitalWrite(SEL0,A0);
  digitalWrite(SEL1,A1);
  digitalWrite(SEL2,A2);
  digitalWrite(SEL3,A3);

  digitalWrite(LED_ACTIVE, HIGH);
}

But now, What I really want to do is :

Select_PUMP (some HEX value); // HEX Value defining which PUMP to turn ON and simultaneously which LED Ring turn ON and which Level sensor to Poll for example to turn ON the first PUMP it would be 0X01 and likewise

when the Function Select_PUMP is called it sets ADR1, ADR2, ADR3 to 0 and ADR4 to 1 same goes for LED Ring and for Level sensor

Important thing to note here is unlike LED Rings and PUMPs the level sensors are not driven thru MUX but are directly connected to GPIOs

So is this possible to do?

The code which you gave here looks wonderful! but my concern is that INA doesn't have the same address as the PUMPs and LED Rings and Level sensors.

I'm aware that LED Rings can be cascaded but I want it to be done thru MUX. I'm also aware that I'll have to go for millis and not for delays when it comes to multitasking

Can you explain me the following part please? I really can't understand it :frowning:

void Select_PUMP(uint8_t address) {
  digitalWrite(ADDR0, (address & 0x01));
  digitalWrite(ADDR1, (address & 0x02) >> 1);
  digitalWrite(ADDR2, (address & 0x04) >> 2);
  digitalWrite(ADDR3, (address & 0x08) >> 3);

Unfortunately I'm working with MEGA and not ESP32.

U talked about state machines. I believe it is a process of dividing the entire task into smaller tasks which makes sense and then writing separate function for each smaller tasks and make 'jumps' from one function to another to carry out the entire task is that correct? Please pardon me if I'm wrong about it

Have a look at this.

1 Like

In particular, have a look at state diagrams. I find it easy to get the states clear in my head with such diagrams, where it's easy to define what happens in each state and to define what causes the switch from one to another.

Yes Definitely! It is helpful. I'm aware that I will have to get in state machines for the execution flow. But rn I'm more concerned about turning ON/Turning OFF and polling the devices in more better and easy way, which is not messy at all.

Thank u for the valuable input. I appreciate it so much

WOW!! That was AMAZING!! The HEX value part when I understood it gave me goosebumps!

I was wondering is it possible to combine these two things? like Clubbing the Pumps, Sensors and LED rings using Struct but setting the MUX address lines for PUMPS and LED Rings using HEX value passing technic? or it can be done without the HEX value technic?

I tried to have an attempt but I got completely blank in the middle:

/*
 * There are eight tanks considered. Each tank has a pump driven thru MUX, LED Ring driven thru MUX and a level sensor
 * Tank1:
 * Pump1, Level sensor 1 and LED Ring 1 
 * 
 * Tank2:
 * Pump2, Level Sensor 2 and LED Ring 2
 * 
 * and so on...
 */

//Define Structure
struct TANK_CONTENTS
{
  const unit8_t PumpNo;  //Mux setting of Pumps
  const unit8_t Sensor;  //Number of Level Sensors
  const unit8_t LedRing; //Mux Setting of LED Rings
}

//create structure variables as an Array
TANK_CONTENTS Tanks[]
{
  {1,1,1},//Tank 1
  {2,2,2},//
  {3,3,3},//
  {4,4,4},//
  {5,5,5},//
  {6,6,6},//
  {7,7,7},//
  {8,8,8}//Tank 8
  
}

void setup() 
{
  Activate_Tank(Tanks[1]);

}

void loop() {
  // put your main code here, to run repeatedly:

}

void Acitvate_Tank(struct TANK_CONTENTS Tanks[])
{
  
}

I never really worked with Structs or passing the HEX value and then using it to toggle the GPIOs so i'm lil confused

The & is a bitwise AND and used for masking, the >> is a right shift and the number after it says how many positions to shift.

Lets say your address is 5; in binary it's 0101

mask with 0001      shift none
addr 0101
mask 0001
     ----
     0001           0001 (1 dec)

mask with 0010      shift one
addr 0101
mask 0010
     ----
     0000           0000 (0 dec)

mask with 0100      shift two
addr 0101
     0100
     ----
     0100           0001 (1 dec)

mask with 1000      shift three
addr 0101
mask 1000
     ----
     0000           0000 (0 dec)

The result of the shift is used in the respective digitalWrites

1 Like

It gave me goosebumps when I understood! AMAZING!!

But now how should be my approach, should I combine these two thing? Like the struct (which makes the combining easy and the HEX thing)