Handling multiple tasks with single function

I figure that should be solved with each input having a resistor pulling either high or low?

How do I print debug msgs?

I have provisions to add pull down on the MUX outputs going to LED Rings would that be helpful?

I'm referring to serial messages.

Maybe. The signal should be pulled to the idle state of the communication signal, whichever way that is.

I have forwarded the snippet to them, they should be testing it coming Monday

How did the testing go of your sketch and hardware?

I'm going to build this up slowly; my sketch is not complete yet. In the first step, we're going to blink the rings.

First, we define a structure with the information that is needed for (the blinking of) the rings.

// structure with info of a led ring
struct RINGINFO
{
  const uint8_t address;    // mux address
  bool status;              // on or off
  uint32_t nextTime;        // next time that ring must change
};

address is the address for the mux; as it never changes, it's const.
status keeps track of the status for the blinking
nextTime keeps track of the next time that you want to change fromon to off or off to on.

Note:
I make mostly use of uint8_t (instead of byte) and uint32_t (instead of unsigned long)

This struct will be part of the struct that defines a tank; this might still change.

// structure with information about a tank
struct TANKINFO
{
  INA226 ina;                   // ina object
  const uint8_t inaAddress;     // i2c address of ina
  const uint8_t pumpAddress;    // pump address on mux
  const uint8_t levelSensorPin; // pin of level sensor

  RINGINFO ring;                // ring info

  uint8_t levelStatus;          // level sensor status

  CLEANINGSTATE cleaningState;
  uint32_t nextPumpActionTime;
};

Most fields should be obvious. cleaningState and nextPumpActionTime are for use with a statemachine for the cleaning of the tank. CLEANINGSTATE is an enum that gives sensible names to the steps of the cleaning. This enum needs to be placed before the TANKINFO struct.

// various states for cleaning tanks
enum class CLEANINGSTATE
{
  OFF,            // not cleaning
  AIRCOMP1,       // first air comp run
  PUMP,           // pump
  AIRCOMP2,       // second air comp run
};

If there is a need to expand with more states, you can simply add them.

Now you can create an array of tanks.

// all tanks
TANKINFO tanks[] =
{
  {INA226(), 0x40, 0, 2, {0, RING_OFF, 0}, HIGH, CLEANINGSTATE::OFF, 0},
  {INA226(), 0x41, 1, 3, {1, RING_ON,  0}, HIGH, CLEANINGSTATE::OFF, 0},
//                       |< ring info  >| 
};

For demonstration, the first ring is off and the second ring is on. RING_ON and RING_OFF are defined as follows.

// sensible constants for ring state
#define RING_ON true
#define RING_OFF !RING_ON

If you ever want to reverse it, you only have to change the first line of the above two. There is a similar one for outputs that are active LOW (e.g. air comp).

// sensible constants for outputs that are active LOW
#define OUTPUT_ON LOW
#define OUTPUT_OFF !OUTPUT_ON

In an earlier post, I used sizeof(tanks) / sizeof(tanks[0]); it calculates the number of elements in the tanks array. That's quite a bit of typing if you have to use it often and for another array you will have to repeat all that typing. Therefore I use a macro

// macro to calculate the number of elements in any type of array.
#define NUMELEMENTS(x) (sizeof(x) / sizeof(x[0]))

Lastly, for debugging I've added some macros that make it easy to switch on and off serial printing of debug information.

The first part of your sketch will now look like

// for debugging
#define DBG

#ifdef DBG
#define DBG_PRINT(x) Serial.print(x)
#define DBG_PRINTLN(x) Serial.println(x)
#else
#define DBG_PRINT(x)
#define DBG_PRINTLN(x)
#endif

// macro to calculate the number of elements in any type of array.
#define NUMELEMENTS(x) (sizeof(x) / sizeof(x[0]))
// sensible constants for outputs that are active LOW
#define OUTPUT_ON LOW
#define OUTPUT_OFF !OUTPUT_ON
// sensible constants for ring state
#define RING_ON true
#define RING_OFF !RING_ON

// includes
#include <FastLED.h>
#include <Wire.h>
#include <INA226.h>

// address buses
const uint8_t addressBusPumpPins[] = {41, 40, 36, 37};
const uint8_t addressBusLedPins[] = {31, 30, 33, 32};
// other pins
const uint8_t pumpsEnablePin = 34;
const uint8_t pumpPwmPin = 45;
const uint8_t ledActivePin = 29;
const uint8_t airCompPin = 39;

// various states for cleaning tanks
enum class CLEANINGSTATE
{
  OFF,            // not cleaning
  AIRCOMP1,       // first air comp run
  PUMP,           // pump
  AIRCOMP2,       // second air comp run
};

// structure with info of a led ring
struct RINGINFO
{
  const uint8_t address;    // mux address
  bool status;              // on or off
  uint32_t nextTime;        // next time that ring must change
};

// structure with information about a tank
struct TANKINFO
{
  INA226 ina;
  const uint8_t inaAddress;     // i2c address of ina
  const uint8_t pumpAddress;    // pump address on mux
  const uint8_t levelSensorPin; //

  RINGINFO ring;                // ring info

  uint8_t levelStatus;        // level sensor
  CLEANINGSTATE cleaningState;
  uint32_t nextPumpActionTime;
};

// all tanks
TANKINFO tanks[] =
{
  {INA226(), 0x40, 0, 2, {0, RING_OFF, 0}, HIGH, CLEANINGSTATE::OFF, 0},
  {INA226(), 0x41, 1, 3, {1, RING_ON, 0}, HIGH, CLEANINGSTATE::OFF, 0},
};

// the tank that is being controlled
uint8_t tankIndex;

// How many leds in your strip?
const uint16_t NUM_LEDS = 30;
// led ring connections
const uint8_t dataPin = 8;
const uint8_t clockPin = 13;

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

void setup()
{
}

void loop()
{
}

What wasn't described earlier

  1. addressBusPumpPins[]
  2. addressBusLedPins[]
  3. other pins
  4. one additional variable tankIndex that will be used to iterate through the tanks array.

If you remove #define DBG or comment it out, the DEBUG_PRINT macros will not print.

More in the next post.

1 Like

I wrote a function systemInit() that does all the initalisations.

/*
  initialise the system
*/
void systemInit()
{
  // set the pumps address bus pins to output
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(addressBusPumpPins); cnt++)
  {
    pinMode(addressBusPumpPins[cnt], OUTPUT);
  }

  // set the led rings address bus pins to output
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(addressBusLedPins); cnt++)
  {
    pinMode(addressBusLedPins[cnt], OUTPUT);
  }

  // other pins
  pinMode(pumpsEnablePin, OUTPUT);
  pinMode(pumpPwmPin, OUTPUT);
  pinMode(airCompPin, OUTPUT);
  digitalWrite(airCompPin, OUTPUT_OFF);  // air comp off

  // initialise tank related pins and INA
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    // set level pin to input with pull up
    pinMode(tanks[cnt].levelSensorPin, INPUT_PULLUP);
    // configure INA
    tanks[cnt].ina.begin(tanks[cnt].inaAddress);
    tanks[cnt].ina.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
    tanks[cnt].ina.calibrate(0.01, 0.5);
  }

  // setup fastled
  FastLED.addLeds<NEOPIXEL, dataPin>(leds, NUM_LEDS);
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    ringOff(tanks[cnt].ring);
  }

  DBG_PRINTLN(F("After ringOff"));
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    DBG_PRINT(F("Address = "));
    DBG_PRINTLN(tanks[cnt].ring.address);
    DBG_PRINT(F("ring is "));
    DBG_PRINTLN(tanks[cnt].ring.status == RING_ON ? "ON" : "OFF");
  }
}

The function does a first loops through the addressBusXXXPins arrays and sets the pins to OUTPUT. Next it handles the other pins. And lastly it initialises the 'stuff' related to each tank. It sets the level sensor pin and configuares the ina; based on your original code, all inas use the same configuration. If you had a need for different configurations, you could add settings to the TANKINFO struct.
Note the sue of NUMELEMENTS in for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++).
Next it initialises the leds in the LED ring and switches the leds of each ring of.

Lastly it prints some debug info about the leds of each tank.

There are two functions for the led rings; one to switch a led ring off and one to blink a led ring with a given colour and a given interval.

/*
  switch ring off
  In:
    ring address
*/
void ringOff(RINGINFO &ring)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Address = "));
  DBG_PRINTLN(ring.address);
  DBG_PRINT(F("ring is "));
  DBG_PRINTLN(ring.status == RING_ON ? "ON" : "OFF");

  // select the ring
  selectRing(ring.address);

  // set the colour
  for (uint8_t ledCnt = 0; ledCnt < NUM_LEDS; ledCnt++)
  {
    leds[ledCnt] = CRGB::Black;
  }

  // show the colour
  FastLED.show();

  // cleanup
  ring.status = RING_OFF;
  ring.nextTime = 0;
}

The function takes one argument, a reference (note the & in front of ring) to a RINGINFO struct.

DBG_PRINTLN(__FUNCTION__) prints the function name so it's easier to follow the flow of the program.
ring.status == RING_ON ? "ON" : "OFF" is a fancy way to implement an if/else. The same yould be achieved with

if(ring.status == RING_ON)
{
  DBG_PRINTLN("ON");
}
else
{
  DBG_PRINTLN("OFF");
}

You can research ternary operator.

To access the elements of a struct, you use the DOT. Further the function should be self-explaining. At the end we just do a slight cleanup.

The blink function

/*
  blink the ring
  In:
    ring info
    colour
    blink interval
*/
void ringBlink(RINGINFO &ring, CRGB colour, uint32_t interval)
{
  uint32_t now = millis();

  DBG_PRINTLN(__FUNCTION__);

  if (now >= ring.nextTime)
  {
    DBG_PRINT(F("Ring address = "));
    DBG_PRINTLN(ring.address);
    DBG_PRINT(F("ring is "));
    DBG_PRINTLN(ring.status == RING_ON ? "ON" : "OFF");
    
    // set the next time that the ring needs to be refreshed
    ring.nextTime += interval;

    // select the ring
    DBG_PRINT(F("next ring status change at "));
    DBG_PRINTLN(ring.nextTime);
    selectRing(ring.address);

    // select the colour; either black for OFF or the specified colour
    if (ring.status == RING_OFF)
    {
      DBG_PRINTLN(F("switching ring on"));
      ring.status = RING_ON;
    }
    else
    {
      DBG_PRINTLN(F("switching ring off"));
      ring.status = RING_OFF;
      colour = CRGB::Black;
    }

    // set the colour
    DBG_PRINTLN(F("setting the colour"));
    for (uint8_t ledCnt = 0; ledCnt < NUM_LEDS; ledCnt++)
    {
      leds[ledCnt] = colour;
    }

    // show the colour
    DBG_PRINTLN(F("applying the colour"));
    FastLED.show();
  }
}

This function takes three arguments; again a reference to a ring, the colour that you want it to flash and the flash interval.

Both ring functions makeuse from the selectRing() function that controls the multiplexer; a similar function exists for the pumps

/*
  select led ring
  In:
    address for led multiplexer.
*/
void selectRing(uint8_t address)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Setting ring address bus to "));
  DBG_PRINTLN(address);
  
  digitalWrite(addressBusLedPins[0], (address & 0x01));
  digitalWrite(addressBusLedPins[1], (address & 0x02) >> 1);
  digitalWrite(addressBusLedPins[2], (address & 0x04) >> 2);
  digitalWrite(addressBusLedPins[3], (address & 0x08) >> 3);
}

/*
  select pump
  In:
    address for multiplexer.
*/
void selectPump(uint8_t address)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Setting pump address bus to "));
  DBG_PRINTLN(address);

  digitalWrite(addressBusPumpPins[0], (address & 0x01));
  digitalWrite(addressBusPumpPins[1], (address & 0x02) >> 1);
  digitalWrite(addressBusPumpPins[2], (address & 0x04) >> 2);
  digitalWrite(addressBusPumpPins[3], (address & 0x08) >> 3);
}

nextTime is initially set to 0 so it will not take long before if (now >= ring.nextTime) evaluates to true. The first thing to do is to calculate the next time that something needs to happen; basically toggle the state of the ring from OFF to ON to OFF ...
If the ring was OFF, we set the status to ON, if it was ON, we set the status to OFF and change the colour to black.
The rest of the function should be easy to understand.

Now we can use systemInit() in setup().

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  systemInit();
}

And for testing your rings based on the sensor level you can use the following.

void loop()
{
  // read level status
  tanks[tankIndex].levelStatus = digitalRead(tanks[tankIndex].levelSensorPin);
  if (tanks[tankIndex].levelStatus == LOW)
  {
    ringBlink(tanks[tankIndex].ring, CRGB::Yellow, 250);
  }
  else
  {
    ringOff(tanks[tankIndex].ring);

  }
  tankIndex++;
  if (tankIndex >= NUMELEMENTS(tanks))
  {
    tankIndex = 0;
  }
}

Each time loop() is called, a the next tank will be checked and the ring will flash if the level is LOW.

I have a statemachine in place for the cleaning of the tanks but don't have time to post and explain. For another time :wink:

1 Like

And the full code for now, just in case I missed / messed up things. It compiles but I can not test.

// for debugging
#define DBG

#ifdef DBG
#define DBG_PRINT(x) Serial.print(x)
#define DBG_PRINTLN(x) Serial.println(x)
#else
#define DBG_PRINT(x)
#define DBG_PRINTLN(x)
#endif

// macro to calculate the number of elements in any type of array.
#define NUMELEMENTS(x) (sizeof(x) / sizeof(x[0]))
// sensible constants for outputs that are active LOW
#define OUTPUT_ON LOW
#define OUTPUT_OFF !OUTPUT_ON
// sensible constants for ring state
#define RING_ON true
#define RING_OFF !RING_ON

// includes
#include <FastLED.h>
#include <Wire.h>
#include <INA226.h>

// address buses
const uint8_t addressBusPumpPins[] = {41, 40, 36, 37};
const uint8_t addressBusLedPins[] = {31, 30, 33, 32};
// other pins
const uint8_t pumpsEnablePin = 34;
const uint8_t pumpPwmPin = 45;
const uint8_t ledActivePin = 29;
const uint8_t airCompPin = 39;

// various states for cleaning tanks
enum class CLEANINGSTATE
{
  OFF,            // not cleaning
  AIRCOMP1,       // first air comp run
  PUMP,           // pump
  AIRCOMP2,       // second air comp run
};

// structure with info of a led ring
struct RINGINFO
{
  const uint8_t address;    // mux address
  bool status;              // on or off
  uint32_t nextTime;        // next time that ring must change
};

// structure with information about a tank
struct TANKINFO
{
  INA226 ina;
  const uint8_t inaAddress;     // i2c address of ina
  const uint8_t pumpAddress;    // pump address on mux
  const uint8_t levelSensorPin; //

  RINGINFO ring;                // ring info

  uint8_t levelStatus;        // level sensor
  CLEANINGSTATE cleaningState;
  uint32_t nextPumpActionTime;
};

// all tanks
TANKINFO tanks[] =
{
  {INA226(), 0x40, 0, 2, {0, RING_OFF, 0}, HIGH, CLEANINGSTATE::OFF, 0},
  {INA226(), 0x41, 1, 3, {1, RING_ON, 0}, HIGH, CLEANINGSTATE::OFF, 0},
};

// the tank that is being controlled
uint8_t tankIndex;

// How many leds in your strip?
const uint16_t NUM_LEDS = 30;
// led ring connections
const uint8_t dataPin = 8;
const uint8_t clockPin = 13;

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

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  systemInit();

}

void loop()
{
  // read level status
  tanks[tankIndex].levelStatus = digitalRead(tanks[tankIndex].levelSensorPin);
  if (tanks[tankIndex].levelStatus == LOW)
  {
    ringBlink(tanks[tankIndex].ring, CRGB::Yellow, 250);
  }
  else
  {
    ringOff(tanks[tankIndex].ring);
  }
  tankIndex++;
  if (tankIndex >= NUMELEMENTS(tanks))
  {
    tankIndex = 0;
  }

}

/*
  initialise the system
*/
void systemInit()
{
  // set the pumps address bus pins to output
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(addressBusPumpPins); cnt++)
  {
    pinMode(addressBusPumpPins[cnt], OUTPUT);
  }

  // set the led rings address bus pins to output
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(addressBusLedPins); cnt++)
  {
    pinMode(addressBusLedPins[cnt], OUTPUT);
  }

  // other pins
  pinMode(pumpsEnablePin, OUTPUT);
  pinMode(pumpPwmPin, OUTPUT);
  pinMode(airCompPin, OUTPUT);
  digitalWrite(airCompPin, OUTPUT_OFF);  // air comp off

  // initialise tank related pins and INA
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    // set level pin to input with pull up
    pinMode(tanks[cnt].levelSensorPin, INPUT_PULLUP);
    // configure INA
    tanks[cnt].ina.begin(tanks[cnt].inaAddress);
    tanks[cnt].ina.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
    tanks[cnt].ina.calibrate(0.01, 0.5);
  }

  // setup fastled
  FastLED.addLeds<NEOPIXEL, dataPin>(leds, NUM_LEDS);
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    ringOff(tanks[cnt].ring);
  }

  DBG_PRINTLN(F("After ringOff"));
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    DBG_PRINT(F("Address = "));
    DBG_PRINTLN(tanks[cnt].ring.address);
    DBG_PRINT(F("ring is "));
    DBG_PRINTLN(tanks[cnt].ring.status == RING_ON ? "ON" : "OFF");
  }
}

/*
  blink the ring
  In:
    ring info
    colour
    blink interval
*/
void ringBlink(RINGINFO &ring, CRGB colour, uint32_t interval)
{
  uint32_t now = millis();

  DBG_PRINTLN(__FUNCTION__);

  if (now >= ring.nextTime)
  {
    DBG_PRINT(F("Ring address = "));
    DBG_PRINTLN(ring.address);
    DBG_PRINT(F("ring is "));
    DBG_PRINTLN(ring.status == RING_ON ? "ON" : "OFF");

    // set the next time that the ring needs to be refreshed
    ring.nextTime += interval;

    // select the ring
    DBG_PRINT(F("next ring status change at "));
    DBG_PRINTLN(ring.nextTime);
    selectRing(ring.address);

    // select the colour; either black for OFF or the specified colour
    if (ring.status == RING_OFF)
    {
      DBG_PRINTLN(F("switching ring on"));
      ring.status = RING_ON;
    }
    else
    {
      DBG_PRINTLN(F("switching ring off"));
      ring.status = RING_OFF;
      colour = CRGB::Black;
    }

    // set the colour
    DBG_PRINTLN(F("setting the colour"));
    for (uint8_t ledCnt = 0; ledCnt < NUM_LEDS; ledCnt++)
    {
      leds[ledCnt] = colour;
    }

    // show the colour
    DBG_PRINTLN(F("applying the colour"));
    FastLED.show();
  }
}

/*
  switch ring off
  In:
    ring address
*/
void ringOff(RINGINFO &ring)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Address = "));
  DBG_PRINTLN(ring.address);
  DBG_PRINT(F("ring is "));
  DBG_PRINTLN(ring.status == RING_ON ? "ON" : "OFF");

  // select the ring
  selectRing(ring.address);

  // set the colour
  for (uint8_t ledCnt = 0; ledCnt < NUM_LEDS; ledCnt++)
  {
    leds[ledCnt] = CRGB::Black;
  }

  // show the colour
  FastLED.show();

  // cleanup
  ring.status = RING_OFF;
  ring.nextTime = 0;
}

/*
  select led ring
  In:
    address for led multiplexer.
*/
void selectRing(uint8_t address)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Setting ring address bus to "));
  DBG_PRINTLN(address);

  digitalWrite(addressBusLedPins[0], (address & 0x01));
  digitalWrite(addressBusLedPins[1], (address & 0x02) >> 1);
  digitalWrite(addressBusLedPins[2], (address & 0x04) >> 2);
  digitalWrite(addressBusLedPins[3], (address & 0x08) >> 3);
}

/*
  select pump
  In:
    address for multiplexer.
*/
void selectPump(uint8_t address)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Setting pump address bus to "));
  DBG_PRINTLN(address);

  digitalWrite(addressBusPumpPins[0], (address & 0x01));
  digitalWrite(addressBusPumpPins[1], (address & 0x02) >> 1);
  digitalWrite(addressBusPumpPins[2], (address & 0x04) >> 2);
  digitalWrite(addressBusPumpPins[3], (address & 0x08) >> 3);
}
1 Like

Thank you so much for valuable and wonderful input. I was busy with some other project unfortunately, so I wasn't able to test my code (Some SMD Assemblies I had to deliver of other project :grimacing: :face_with_head_bandage:) Finally finished yesterday late night and today morning I was greeted by your post's notification! yay! :partying_face:

I immediately went thru the posts quick and it took me no time to realize that I'm gonna need lil time to understand and grasp that. I'll get back on this if I have any troubles or queries (I Hope I don't get it so I don't have to bother you even after your valuable help) But my heartiest thanks once again. I can also test it on the hardware I have it here for sure and let you know how it goes

For the cleaning of the tanks, the below can be the basis.

I've first defined an enum (see earlier post) and next implemented the basic framework

/*
  clean the tank
  In:
    index in tanks array
  Returns:
    false if clearing is in progress, else true
*/
bool cleanTank(uint8_t idx)
{
  // return value; defaults to indicate that it's in progress
  bool rv = false;
  // current time
  uint32_t now = millis();

  // state machine
  switch (tanks[idx].cleaningState)
  {
    case CLEANINGSTATE::OFF:
      break;
    case CLEANINGSTATE::AIRCOMP1:
      break;
    case CLEANINGSTATE::PUMP:
      break;
    case CLEANINGSTATE::AIRCOMP2:
      break;
  }

  // blink the leds
  ringBlink(tanks[idx].ring, CRGB::Red, 250);

  // indicate that process is complete or not
  return rv;
}

The function will always blink the ring (at the end).

Based on the cleaningState, one of the cases in the switch statement will be executed.

And next I filled in the states in the finite state machine. The default cleaningState is OFF so the first time that the function is called the first case (step) is executed. This step only does some setup for the next case.

  1. Sets the time when the next step needs to stop
  2. Sets the airComp pin to ON
  3. Switches the ring off
  4. Sets the next cleaningState
    case CLEANINGSTATE::OFF:
      // end time of first air comp run
      tanks[idx].nextPumpActionTime = now + 5000UL;
      // just in case ring was on, switch it off
      ringOff(tanks[tankIndex].ring);
      // switch air comp on
      digitalWrite(airCompPin, OUTPUT_ON);
      // go to next state
      tanks[idx].cleaningState = CLEANINGSTATE::AIRCOMP1;
      DBG_PRINTLN(F("Switching to AIRCOMP1"));
      break;

The next case is executed when the function is called again; it simply checks the timing and if the 5 seconds have passed it prepares for the next case (step).

  1. Sets the time when the next step needs to stop
  2. Sets the airComp pin to OFF
  3. Selects the pump
  4. Activates the pump
  5. Sets the next cleaningState.
    case CLEANINGSTATE::AIRCOMP1:
      if (now >= tanks[idx].nextPumpActionTime)
      {
        // end time of pump run
        tanks[idx].nextPumpActionTime = now + 5000UL;
        // switch air comp off
        digitalWrite(airCompPin, OUTPUT_OFF);
        // select the pump
        selectPump(tanks[idx].pumpAddress);
        // pump on
        digitalWrite(pumpsEnablePin, OUTPUT_ON);
        analogWrite(pumpPwmPin, 255); //change this number to set the speed for pumps
        // go to next state
        tanks[idx].cleaningState = CLEANINGSTATE::PUMP;
        DBG_PRINTLN(F("Switching to PUMP"));
      }
      break;

The process repeats for the next case (PUMP) which is basically the same as for the first air comp run

    case CLEANINGSTATE::PUMP:
      if (now >= tanks[idx].nextPumpActionTime)
      {
        // end time of second air comp run run
        tanks[idx].nextPumpActionTime = now + 5000UL;
        // pump off
        digitalWrite(pumpsEnablePin, OUTPUT_OFF);
        analogWrite(pumpPwmPin, 0);
        // go to next state
        DBG_PRINTLN(F("Switching to AIRCOMP2"));
        tanks[idx].cleaningState = CLEANINGSTATE::AIRCOMP2;
      }
      break;

And we reach the last case (step)

  1. switch the air comp off
  2. change to the next case (which is the first one)
  3. set rv to true so when it's returned the caller knows that the process is complete.
    case CLEANINGSTATE::AIRCOMP2:
      if (now >= tanks[idx].nextPumpActionTime)
      {
        // switch the air comp off
        digitalWrite(airCompPin, OUTPUT_OFF);
        // go back to first state
        tanks[idx].cleaningState = CLEANINGSTATE::OFF;
        DBG_PRINTLN(F("Cleaning complete, switching to OFF"));
        // indicate that process is complete
        rv = true;
      }
      break;

The above will work for sequential cleaning of the tanks. Changes need to be made for simultaneous cleaning of tanks. In that case you don't want to switch the air comp and pump off and you will have to find another way; it might be part of a state machine for the complete process.

You can use the cleanTanks() function as shown below.

void loop()
{
  // read level status
  tanks[tankIndex].levelStatus = digitalRead(tanks[tankIndex].levelSensorPin);
  if (tanks[tankIndex].levelStatus == LOW)
  {
    ringBlink(tanks[tankIndex].ring, CRGB::Yellow, 250);
  }
  else
  {
    if (cleanTank(tankIndex) == true)
    {
      DBG_PRINT(F("Cleaning of tank with address "));
      DBG_PRINT(tanks[tankIndex].pumpAddress);
      DBG_PRINTLN(F(" complete"));
      tankIndex++;
    }
  }
}

You should now have enough ammunition to write all code. You have an example of the multitasking as well as an example of a finite state machine.

And the full code. It includes the enum for an other state machine that is intended to be used for the complete tanks process.

// for debugging
#define DBG

#ifdef DBG
#define DBG_PRINT(x) Serial.print(x)
#define DBG_PRINTLN(x) Serial.println(x)
#else
#define DBG_PRINT(x)
#define DBG_PRINTLN(x)
#endif

// macro to calculate the number of elements in any type of array.
#define NUMELEMENTS(x) (sizeof(x) / sizeof(x[0]))
// sensible constants for outputs that are active LOW
#define OUTPUT_ON LOW
#define OUTPUT_OFF !OUTPUT_ON
// sensible constants for ring state
#define RING_ON true
#define RING_OFF !RING_ON

// includes
#include <FastLED.h>
#include <Wire.h>
#include <INA226.h>

// address buses
const uint8_t addressBusPumpPins[] = {41, 40, 36, 37};
const uint8_t addressBusLedPins[] = {31, 30, 33, 32};
// other pins
const uint8_t pumpsEnablePin = 34;
const uint8_t pumpPwmPin = 45;
const uint8_t ledActivePin = 29;
const uint8_t airCompPin = 39;

// various states for the tank process
enum class TANKSTATE
{
  LEVELCHECK,     // check all tanks
  CLEAN,          // clean a tank
  VALIDATECLEAN,  // check if tank is clean
  PUMP,           // pump
  READINA,        // read the INA
};

// various states for cleaning tanks
enum class CLEANINGSTATE
{
  OFF,            // not cleaning
  AIRCOMP1,       // first air comp run
  PUMP,           // pump
  AIRCOMP2,       // second air comp run
};

// structure with info of a led ring
struct RINGINFO
{
  const uint8_t address;    // mux address
  bool status;              // on or off
  uint32_t nextTime;        // next time that ring must change
};

// structure with information about a tank
struct TANKINFO
{
  INA226 ina;
  const uint8_t inaAddress;     // i2c address of ina
  const uint8_t pumpAddress;    // pump address on mux
  const uint8_t levelSensorPin; //

  RINGINFO ring;                // ring info

  uint8_t levelStatus;        // level sensor
  CLEANINGSTATE cleaningState;
  uint32_t nextPumpActionTime;
};

// all tanks
TANKINFO tanks[] =
{
  {INA226(), 0x40, 0, 2, {0, RING_OFF, 0}, HIGH, CLEANINGSTATE::OFF, 0},
  {INA226(), 0x41, 1, 3, {1, RING_ON, 0}, HIGH, CLEANINGSTATE::OFF, 0},
};

// the tank that is being controlled
uint8_t tankIndex;

// How many leds in your strip?
const uint16_t NUM_LEDS = 30;
// led ring connections
const uint8_t dataPin = 8;
const uint8_t clockPin = 13;

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

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  systemInit();

}

void loop()
{
  // read level status
  tanks[tankIndex].levelStatus = digitalRead(tanks[tankIndex].levelSensorPin);
  if (tanks[tankIndex].levelStatus == LOW)
  {
    ringBlink(tanks[tankIndex].ring, CRGB::Yellow, 250);
  }
  else
  {
    if (cleanTank(tankIndex) == true)
    {
      DBG_PRINT(F("Cleaning of tank with address "));
      DBG_PRINT(tanks[tankIndex].pumpAddress);
      DBG_PRINTLN(F(" complete"));
      tankIndex++;
    }
  }
}

void systemInit()
{
  // set the pumps address bus pins to output
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(addressBusPumpPins); cnt++)
  {
    pinMode(addressBusPumpPins[cnt], OUTPUT);
  }

  // set the led rings address bus pins to output
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(addressBusLedPins); cnt++)
  {
    pinMode(addressBusLedPins[cnt], OUTPUT);
  }

  // other pins
  pinMode(pumpsEnablePin, OUTPUT);
  pinMode(pumpPwmPin, OUTPUT);
  pinMode(airCompPin, OUTPUT);
  digitalWrite(airCompPin, OUTPUT_OFF);  // air comp off

  // initialise tank related pins and INA
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    // set level pin to input with pull up
    pinMode(tanks[cnt].levelSensorPin, INPUT_PULLUP);
    // configure INA
    tanks[cnt].ina.begin(tanks[cnt].inaAddress);
    tanks[cnt].ina.configure(INA226_AVERAGES_1, INA226_BUS_CONV_TIME_1100US, INA226_SHUNT_CONV_TIME_1100US, INA226_MODE_SHUNT_BUS_CONT);
    tanks[cnt].ina.calibrate(0.01, 0.5);
  }

  // setup fastled
  FastLED.addLeds<NEOPIXEL, dataPin>(leds, NUM_LEDS);
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    ringOff(tanks[cnt].ring);
  }

  DBG_PRINTLN(F("After ringOff"));
  for (uint8_t cnt = 0; cnt < NUMELEMENTS(tanks); cnt++)
  {
    DBG_PRINT(F("Address = "));
    DBG_PRINTLN(tanks[cnt].ring.address);
    DBG_PRINT(F("ring is "));
    DBG_PRINTLN(tanks[cnt].ring.status == RING_ON ? "ON" : "OFF");
  }
}

/*
  blink the ring
  In:
    ring info
    colour
    blink interval
*/
void ringBlink(RINGINFO &ring, CRGB colour, uint32_t interval)
{
  uint32_t now = millis();

  DBG_PRINTLN(__FUNCTION__);

  if (now >= ring.nextTime)
  {
    DBG_PRINT(F("Ring address = "));
    DBG_PRINTLN(ring.address);
    DBG_PRINT(F("ring is "));
    DBG_PRINTLN(ring.status == RING_ON ? "ON" : "OFF");

    // set the next time that the ring needs to be refreshed
    ring.nextTime += interval;

    // select the ring
    DBG_PRINT(F("next ring status change at "));
    DBG_PRINTLN(ring.nextTime);
    selectRing(ring.address);

    // select the colour; either black for OFF or the specified colour
    if (ring.status == RING_OFF)
    {
      DBG_PRINTLN(F("switching ring on"));
      ring.status = RING_ON;
    }
    else
    {
      DBG_PRINTLN(F("switching ring off"));
      ring.status = RING_OFF;
      colour = CRGB::Black;
    }

    // set the colour
    DBG_PRINTLN(F("setting the colour"));
    for (uint8_t ledCnt = 0; ledCnt < NUM_LEDS; ledCnt++)
    {
      leds[ledCnt] = colour;
    }

    // show the colour
    DBG_PRINTLN(F("applying the colour"));
    FastLED.show();
  }
}

/*
  switch ring off
  In:
    ring address
*/
void ringOff(RINGINFO &ring)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Address = "));
  DBG_PRINTLN(ring.address);
  DBG_PRINT(F("ring is "));
  DBG_PRINTLN(ring.status == RING_ON ? "ON" : "OFF");

  // select the ring
  selectRing(ring.address);

  // set the colour
  for (uint8_t ledCnt = 0; ledCnt < NUM_LEDS; ledCnt++)
  {
    leds[ledCnt] = CRGB::Black;
  }

  // show the colour
  FastLED.show();

  // cleanup
  ring.status = RING_OFF;
  ring.nextTime = 0;
}

/*
  clean the tank
  In:
    tank number
  Returns:
    false if clearing is in progress, else true
*/
bool cleanTank(uint8_t idx)
{
  // return value; defaults to indicate that cleaning is in progress
  bool rv = false;
  // current time
  uint32_t now = millis();

  // state machine
  switch (tanks[idx].cleaningState)
  {
    case CLEANINGSTATE::OFF:
      // end time of first air comp run
      tanks[idx].nextPumpActionTime = now + 5000UL;
      // just in case ring was on, switch it off
      ringOff(tanks[idx].ring);
      // switch air comp on
      digitalWrite(airCompPin, OUTPUT_ON);
      // go to next state
      tanks[idx].cleaningState = CLEANINGSTATE::AIRCOMP1;
      DBG_PRINTLN(F("Switching to AIRCOMP1"));
      break;
    case CLEANINGSTATE::AIRCOMP1:
      if (now >= tanks[idx].nextPumpActionTime)
      {
        // end time of pump run
        tanks[idx].nextPumpActionTime = now + 5000UL;
        // switch air comp off
        digitalWrite(airCompPin, OUTPUT_OFF);
        // select the pump
        selectPump(tanks[idx].pumpAddress);
        // pump on
        digitalWrite(pumpsEnablePin, OUTPUT_ON);
        analogWrite(pumpPwmPin, 255); //change this number to set the speed for pumps
        // go to next state
        tanks[idx].cleaningState = CLEANINGSTATE::PUMP;
        DBG_PRINTLN(F("Switching to PUMP"));
      }
      break;
    case CLEANINGSTATE::PUMP:
      if (now >= tanks[idx].nextPumpActionTime)
      {
        // end time of second air comp run run
        tanks[idx].nextPumpActionTime = now + 5000UL;
        // pump off
        digitalWrite(pumpsEnablePin, OUTPUT_OFF);
        analogWrite(pumpPwmPin, 0);
        // switch the air comp on
        digitalWrite(airCompPin, OUTPUT_ON);
        // go to next state
        tanks[idx].cleaningState = CLEANINGSTATE::AIRCOMP2;
        DBG_PRINTLN(F("Switching to AIRCOMP2"));
      }
      break;
    case CLEANINGSTATE::AIRCOMP2:
      if (now >= tanks[idx].nextPumpActionTime)
      {
        // switch the air comp off
        digitalWrite(airCompPin, OUTPUT_OFF);
        // go back to first state
        tanks[idx].cleaningState = CLEANINGSTATE::OFF;
        DBG_PRINTLN(F("Cleaning complete, switching to OFF"));
        // indicate that process is complete
        rv = true;
      }
      break;
  }

  // blink the leds
  ringBlink(tanks[idx].ring, CRGB::Red, 250);

  // indicate that process is complete or not
  return rv;
}

/*
  select led ring
  In:
    address for led multiplexer.
*/
void selectRing(uint8_t address)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Setting ring address bus to "));
  DBG_PRINTLN(address);

  digitalWrite(addressBusLedPins[0], (address & 0x01));
  digitalWrite(addressBusLedPins[1], (address & 0x02) >> 1);
  digitalWrite(addressBusLedPins[2], (address & 0x04) >> 2);
  digitalWrite(addressBusLedPins[3], (address & 0x08) >> 3);
}

/*
  select pump
  In:
    address for multiplexer.
*/
void selectPump(uint8_t address)
{
  DBG_PRINTLN(__FUNCTION__);
  DBG_PRINT(F("Setting pump address bus to "));
  DBG_PRINTLN(address);

  digitalWrite(addressBusPumpPins[0], (address & 0x01));
  digitalWrite(addressBusPumpPins[1], (address & 0x02) >> 1);
  digitalWrite(addressBusPumpPins[2], (address & 0x04) >> 2);
  digitalWrite(addressBusPumpPins[3], (address & 0x08) >> 3);
}

Thank you so much for your generous and valuable help. Unfortunately I'm still not able to test any of the code yet. But two things I wanna tell

  1. I completely forgot to mention about this. The 16 pumps will be pushing some liquids in two different cups (Connected internally). And each cup is fitted with some sensors. and after every pump is finished pumping the liquid into the respective cups. The sensors are to be read and pass it on to UART0

  2. The PCB for this system arrived yesterday. I still don't have the pumps or LED Rings but I have the PCB now. The PCB has ATMEGA2560 MUX and MOSFET Drivers and MOSFET circuitry. The Atmega is already uploaded with bootloader and it accpets the code from UART. But the PCB shows short circuit on my test bench power supply. The problem is wherer the INA circuit is connected to the MOSFET path. I'm attaching hand drawn schematic how it was designed.


    Is the way INA is connected, is it wrong? when the board arrived the INA part wasn't assembled but then I tried to put the Rsense and it all went into showing short circuit. The MOSFET is N-channel

I know this might not be the thread to discuss about this issue but I can't proceed till I figure out this now

I suggest that you take the hardware part to e.g. General Electronics - Arduino Forum. Provide the real schematic (export to jpg) so people can see which components are involved and how everything is connected; you should have one (used for the design of the PCB).

If anything is modules (e.g. the INA), provide a wiring diagram that will show every single connection.

Did you have a test setup for e.g. one channel as proof of concept; e.g. on proto board? If so, did it work?

I'm not a hardware person anymore so can't really advise.

working on that. I'm trying to sort it out first. I do not have anything here other than the PCB now

Yesterday, I sorted out the short circuit problem and now I'm back on the firmware part. I'll do some tests and keep you updated.

Thank you so much!