Actuating relays based on sensor values

After a long hiatus, I'm trying to get back into some programming. I decided to take on an easy project that I'm already overwhelmed with :o

My goal is to control the filling of two reservoirs for a reef tank. There will be a high and low float switch in each tank. I will have four solenoid valves on the water filter that will fill these tanks. One will be on the input line, one will be on the drain line, and the output line will be "T"'d off to each container, each having a solenoid.

My goal is if one container gets low (low float switch activated), it will activate the input solenoid valve and the drain valve for x minutes to flush the filter membrane, then close the drain valve and open the corresponding output valve until the full float is activated. If, while one tank is filling, the other tank's low float valve is activated, there is no need to open the drain line again. In theory, this seems pretty simple but I'm having problems already.

I'm trying to break this into manageable pieces. First is to check the float level status. I've written a quick sketch, but am having trouble figuring out how to pass the results back to the void loop.

int checkFloats() {
    for (address = 0; address < 4; address++) {
      status[address] = digitalRead(Float[address]);
      return status[address];
    }

I can't find a reference as to whether or not the return command will return each value of the "status" array or if this loop will only run once and return the value of the first array back to void loop. I don't have a spare arduino laying around, so I'm just compiling at the moment.

Any pointers would be appreciated!

Your function will only return the first value; and worse, it will only check the first floater. You can return an array of ints but in my opinion that it is not the correct approach in this case.

Without seeing the rest of the code, it's a bit difficult to advise. As you haven't declared status in the the function that you presented, it must be a global array so whatever your function places in status will be available anyway to the calling function.

You might have a motivation to return a value, but in that case it might be advisable to explain why.

There is really no reason to create a function for only one line of code.

One thing you might want to watch out for is that sometimes when making consecutive analogReads on different pins, the A/D converter can't keep up and will sometimes throw a garbage value.

Taking two analogReads on the same pin is one trick to be sure you are getting good results.

Hutkikz:
There is really no reason to create a function for only one line of code.

I don't quite agree; three lines of for-code in the loop() versus one line. Keeps it readable :wink:

Hutkikz:
One thing you might want to watch out for is that sometimes when making consecutive analogReads on different pins, the A/D converter can't keep up and will sometimes throw a garbage value.

Taking two analogReads on the same pin is one trick to be sure you are getting good reads.

Note that OP is using digitalRead :wink:

sterretje:
I don't quite agree; three lines of for-code in the loop() versus one line. Keeps it readable :wink:

I wanted to say for the iteration of a single statement but was trying to keep it to keep it simple.

sterretje:
Note that OP is using digitalRead :wink:

Oops my bad.

sterretje:
Your function will only return the first value; and worse, it will only check the first floater. You can return an array of ints but in my opinion that it is not the correct approach in this case.

Without seeing the rest of the code, it's a bit difficult to advise. As you haven't declared status in the the function that you presented, it must be a global array so whatever your function places in status will be available anyway to the calling function.

You might have a motivation to return a value, but in that case it might be advisable to explain why.

The portion of the sketch I posted above was just that, and the entire sketch is very basic at this point. I can't wait until I can get to the point where I can crank a sketch out in a short amount of time - I'm not there yet.

I explained what I hope to do in the first post above, but in the end, my goal is to activate/deactivate relays (4 total) based on the status of 4 float switches (two high level and two low level switches). The first part I decided to tackle was reading of the 4 float switches. I need to be able to analyze the data I receive from that reading to determine which relays should be opened/closed.

I understand what you mean about the global status array (that I did declare globally) being globally available, and no need to return the value back to the loop. I think I may be able to get a little bit further with that info in hand.

I've created a simple chart to help me understand the different possible float switch states, and am stuck on the best way to store and manipulate the readings in my code. Below is the table:

Bottom Float Top Float
Full Closed Closed
Partially Full Closed Open
Low Open Open
Fault Open Closed

I'm thinking that using binary would be the best way to implement this in the code, but not exactly sure the best way to update the binary. What is the best way to get a binary string from a boolean input from input pins? Do you need a line of code for every possible binary input, or is there a more efficient way to write the code?

You can use bitfields in a struct.

struct SENSOR
{
  byte t1Low: 1;
  byte t1Full: 1;
  byte t2Low: 1;
  byte t2Full: 1;
};

// variable to hold status of sensors
SENSOR status;

You can directly set the status bits

bool checkSensors()
{
  // read sensors and store in status 'register'
  status.t1Low = digitalRead(T1_LOW);
  status.t1Full = digitalRead(T1_FULL);
  status.t1Low = digitalRead(T2_LOW);
  status.t2Full = digitalRead(T2_FULL);
}

where T1_LOW etc represent the floaters; T1 -> tank1, LOW -> sensor that indicates that the tank is empty.

You can check the status with something like below

if (status.t1Low == TANKEMPTY || status.t2Low == TANKEMPTY)
{
  drain();
}

The below are some definitions

// Tank sensor pin numbers
#define T1_FULL  9    // tank1 full sensor
#define T1_LOW  10   // tank1 empty sensor
#define T2_FULL 11
#define T2_LOW  12

// a clearer indication what the level on the pin means
#define TANKEMPTY LOW
#define TANKFULL  LOW

Note:
your problem calls for a small state machine with three states (drain, fill, stopped). It will make life very easy at the end of the day :wink:

sterretje:
You can use bitfields in a struct.

struct SENSOR

{
  byte t1Low: 1;
  byte t1Full: 1;
  byte t2Low: 1;
  byte t2Full: 1;
};

// variable to hold status of sensors
SENSOR status;




You can directly set the status bits


bool checkSensors()
{
  // read sensors and store in status 'register'
  status.t1Low = digitalRead(T1_LOW);
  status.t1Full = digitalRead(T1_FULL);
  status.t1Low = digitalRead(T2_LOW);
  status.t2Full = digitalRead(T2_FULL);
}



where T1_LOW etc represent the floaters; T1 -> tank1, LOW -> sensor that indicates that the tank is empty.

You can check the status with something like below


if (status.t1Low == TANKEMPTY || status.t2Low == TANKEMPTY)
{
  drain();
}




The below are some definitions


// Tank sensor pin numbers
#define T1_FULL  9    // tank1 full sensor
#define T1_LOW  10  // tank1 empty sensor
#define T2_FULL 11
#define T2_LOW  12

// a clearer indication what the level on the pin means
#define TANKEMPTY LOW
#define TANKFULL  LOW




Note:
your problem calls for a small state machine with three states (drain, fill, stopped). It will make life very easy at the end of the day ;)

This is very helpful, and although I don't completely understand it, it gives me a good idea of what to research. Thanks for your input.

@Tango2, I was doing something a few days ago that may be logically similar to your problem. I was trying to work out the legal options for some model railway signals.

I kept going round in circles until it occurred to me to make a table of the different "legal" states and also the legal moves that could follow those states. I just used 3 bits in a byte to represent the states of 2 signals and a turnout.

For example 000, 001, 100 and 101 are legal and 000 could be followed by 001, or 100, but not by any other move. I suspect you could define your system in a similar way.

...R

@Robin2 - that is very similar to how I saw my problem. Unfortunately, I would not consider myself a software developer by any means (yet), so getting from what I want to do over to how to implement is usually the hard part for me. Lucky for me I have this forum and plenty of smart people on it that can help me get to my goal, and hopefully learn something along the way.

sterretje:
You can use bitfields in a struct.

struct SENSOR

{
  byte t1Low: 1;
  byte t1Full: 1;
  byte t2Low: 1;
  byte t2Full: 1;
};

@sterretje - I've read more on the struct variable (if that is the right terminology), but have not yet figured out the significance of the 1 in "byte xxx: 1;". Is this setting an initial value for each byte, and if so, is this necessary?

Once I get the float measurement part figured out, I'll be moving on to the relay actuation. I would imagine I could use a similar method (struct) to power a relay board with digitialWrite instead of digitalRead. Is this correct? As I get a little more work done, I'll try to post up my progress in case someone would be kind enough to review it for me.[/code]

The '1' indicates that it uses only 1 bit in the byte which is sufficient for a boolean value. You have four sensors; to store the status, you use e.g. 4 booleans (which equals 4 bytes of memory space); it does not matter if you use four variables, an array or a struct. The point of bitfields is that you only need 1 byte for up to 8 boolean variables, 2 bytes for up to 16 boolean variables. It can save you a lot of memory.

The below is an example how you can use structs and bitfields to describe a contact

struct PERSON
{
  char name[16];        // name
  unsigned byte age: 7; // age (up to 127 years)
  unsigned byte gender; // true for male, false for female
}

You save one byte per contact; 100 contacts and you have saved 100 bytes.

Do a search for c bitfield. Plenty info to be found, e.g. tutorialpoint C - Bit Fields

That explains it, and also gives me an idea of what to look for when researching that more. Your explanation was good enough for me to grasp the concept. One thing that didn't quite add up was when you said:

sterretje:
...to store the status, you use e.g. 4 booleans (which equals 4 bytes of memory space)...

Did you mean 4 bits of 1 byte?

So here's what I've come up with so far. If anyone would like to critique it, I would welcome the feedback.

  // This sketch is for RO/DI Control for my saltwater changing station.
  // It will monitor 4 float switches, control 4 relays that will power solenoids controlling water flow.

#define Water_Main_Pin 0
#define Fast_Flush_Pin 1
#define ATO_Fill_Pin 2
#define FSW_Fill_Pin 3
#define ATO_Low_Pin 9
#define ATO_Full_Pin 10
#define FSW_Low_Pin 11
#define FSW_Full_Pin 12


#define ACTIVE HIGH
#define INACTIVE LOW

struct SENSOR {
  byte ATOLow: 1;
  byte ATOFull: 1;
  byte FSWLow: 1;
  byte FSWFull: 1;
};

struct OUTPUTS {
  byte WaterMain: 1;
  byte FastFlush: 1;
  byte ATOFill: 1;
  byte FSWFill: 1;
};

SENSOR status;
OUTPUTS signal;

void setup() {
  
}

void loop() {
  
}

void setRelays() {
  digitalWrite(Water_Main_Pin, signal.WaterMain);
  digitalWrite(Fast_Flush_Pin, signal.FastFlush);
  digitalWrite(ATO_Fill_Pin, signal.ATOFill);
  digitalWrite(FSW_Fill_Pin, signal.FSWFill);
}
bool checkSensors() {
  status.ATOLow = digitalRead(ATO_Low_Pin);
  status.ATOFull = digitalRead(ATO_Full_Pin);
  status.FSWLow = digitalRead(FSW_Low_Pin);
  status.FSWFull = digitalRead(FSW_Full_Pin);
}

void checkStates() {

//ATO fault condition
  if (status.ATOLow == INACTIVE && status.ATOFull == ACTIVE) {
    signal.ATOFill == INACTIVE;
  }

//ATO full condition
  if (status.ATOLow == ACTIVE && status.ATOFull == ACTIVE) {
    signal.ATOFill == INACTIVE;
  }
  
//ATO low condition
  if (status.ATOLow == INACTIVE && status.ATOFull == INACTIVE) {
    signal.ATOFill == ACTIVE;
  }
//ATO partially full condition
  if (status.ATOLow == ACTIVE && status.ATOFull == INACTIVE) {
    signal.ATOFill == INACTIVE;
  }

//FSW fault condition 
  if (status.FSWLow == INACTIVE && status.FSWFull == ACTIVE) {
    signal.FSWFill == INACTIVE;
  }

//FSW full condition
  if (status.FSWLow == ACTIVE && status.FSWFull == ACTIVE) {
    signal.FSWFill == INACTIVE;
  }
  
//FSW low condition
  if (status.FSWLow == INACTIVE && status.FSWFull == INACTIVE) {
    signal.FSWFill == ACTIVE;
  }

//FSW partially full condition
  if (status.FSWLow == ACTIVE && status.FSWFull == INACTIVE) {
    signal.FSWFill == INACTIVE;
  }

//Water Main Check
  if (signal.FSWFill == ACTIVE || signal.ATOFill == ACTIVE) {
    signal.WaterMain == ACTIVE;
  }
}

I will have to make some adjustments before this will work like I need it to, but wanted to get a basic program functioning before adding the advanced features. Although it is still a work in progress, hopefully I'm on the right track.

Tango2:
Did you mean 4 bits of 1 byte?

Your question is a little confusing; if you mean '4 units of one byte', yes.

The smallest storage unit in an 8-bit processor is a byte; when you declare a boolean variable, the compiler will allocate a byte for that (8 bits).

Below will be (part of) a memory map that shows how the variables are stored in memory when you use boolean variables.

        // some variables here

          b7  b6  b5  b4  b3  b2  b1  b0
        +---+---+---+---+---+---+---+---+
t1Low   |   |   |   |   |   |   |   |   |
        +---+---+---+---+---+---+---+---+
t1High  |   |   |   |   |   |   |   |   |
        +---+---+---+---+---+---+---+---+
t1Low   |   |   |   |   |   |   |   |   |
        +---+---+---+---+---+---+---+---+
t1High  |   |   |   |   |   |   |   |   |
        +---+---+---+---+---+---+---+---+

        // more variables here

But a boolean can only have one of two values (true or false, 0 or 1, HIGH or LOW, ...) and hence fits in a bit. Bitfields make use of that; the memory map now looks like

        // some variables here

          b7  b6  b5  b4  b3  b2  b1  b0        
        +---+---+---+---+---+---+---+---+
status  |   |   |   |   |   |   |   |   |
        +---+---+---+---+---+---+---+---+
                          ^   ^   ^   ^
                          |   |   |   |
                          |   |   |   +-- t1Low
                          |   |   +------ t1High
                          |   +---------- t2Low
                          +-------------- t2High

        // more variables here

I hope that makes it a bit clearer.

sterretje:
Your question is a little confusing; if you mean '4 units of one byte', yes.

I wonder is there some confusion between my suggestion (in Reply #9) in which I do use bits in a byte to represent states, and @sterretje's suggestion (in Reply #7) which uses a separate byte for each state.

I find the use of bits makes comparisons easier because I can simply do

if (proposedState == 0b010) {

which gives me a simple visual presentation of the state of the 3 bits that I use. You could, of course, use all 8 bits in a byte or 16 bits in an int for a more complex system.

Using bits in a byte also works easily with SWITCH/CASE

However the end-result will be the same whichever method is used, and @sterretje's system may be more obvious.

...R

As the functions that you presented are not called yet, it's a bit difficult to determine if you're on the right track. Your code is currently lacking a timing for the drain and timing is part of the 'states' that you can have. This will make your 'bunch of' if statements in checkStates more complicated and possibly nobody might be able to follow it; even you might get lost if you look at your code in a year's time to do a modification (it happens to me ;))

As I see it, you can have three states

  • STOPPED: all valves are closed
  • DRAINING: the drain and fill valves are open
  • FILLING: only the fill valves are open

You can have a fourth one indicating STARTDRAIN if you want to

Let's assume you use the three above. Your flow would be

Stopped -(A)-> Draining -(B)-> Filling
   ^                              |
   |                              |
   +-------------(C)--------------+

(A), (B) and (C) represent the conditions for a change in state.

(A) will be if the code detects that one of the tanks is empty.
(B) will be if the code detects that the drain period has lapsed
(C) will be if the code detects that both tanks are full

You can write a few functions that are executed when a certain stage is reached.
Functions I see are

  • checkTankEmpty() that will check if one of the tanks is empty and if so, calls the below drain() function to start the drain process.
  • drain() that will open the valves responsible for draining and change the state from STOPPED to DRAINING. It will also check the progress (duration) of the drain process; once the drain process is finished, the drain() function will close the drain valve and open the relevant output valves and it will change the state from DRAINING to FILLING.
  • fill() that will close the relevant valve if it detects that a tank is full and it will change the state from FILLING to STOPPED if it detects that both tanks are full.

The below shows the relevant constants and variable for the state machine

// States
#define STOPPED   0 //
#define DRAINING  1 // filling and draining
#define FILLING   2 // filling only

// current state of statemachine; initial value stopped
byte state = STOPPED;

The below can be an example for the loop() function

void loop()
{
  checkSensors();

  switch (state)
  {
    case STOPPED:
      // check if one or more tanks are empty
      checkTankEmpty();
      break;
    case DRAINING:
      // continue draining
      drain();
      break;
    case FILLING:
      // stop draining, continue filling
      fill();
      break;
  }
}

Example of checkTankEmpty()

void checkTankEmpty()
{
  // check if one of the tanks is empty
  if (status.t1Low == TANKEMPTY || status.t2Low == TANKEMPTY)
  {
    // start draining; the drain function will change the state so we do not do it here
    drain();
  }
}

The framework for the drain() function

void drain()
{
  if(starttime == 0)
  {
    start time = current time
    open valves required for draining
    state = DRAINING;
  }
  else
  {
    if(current time - start time > drain duration)
    {
      close drain valve
      open relevant output valves
      start time = 0
      state = FILLING
    }
    else
    {
      // should not get here
    }
  }
}

And for the fill() function

void fill()
{
  if (tank 1 == full)
    close relevant output valve
  if (tank 2 == full)
    close relevant output valve

  if (tank 1 == full && tank 2 == full)
  {
    close input valve
    state = STOPPED
  }
}

The only point that is not taken into account in the fill function is that while one tank is still filling and the other one already had reached 'full', the 'full' of the latter might no longer be valid when the former one reaches 'full' and hence the condition that tests both might not evaluate to true. You can solve it with some additional variables.

Note:
You can maybe extend your table in post #6 (for your own purposes and if you post it (not update) for our understanding of your code) to indicate which valves should be open for each condition.

I think I understand the concept of storing the boolean into memory - maybe my confusion is on the memory itself. Not sure that part is extremely important in my application. I plan to do some more reading to attempt to wrap my head around it more.

As for my project, let me try to explain in more detail what I need to accomplish. I will install 4 solenoid valves on my water filter. One is on the supply (input from home plumbing), the next is a fast flush that opens a drain line to flush the filter membrane. This ensures the filter works more efficiently. Next there are two solenoids for each of the output lines which go to my two tanks. One is for fresh water, and the other is a tank where I mix saltwater. I will have two float valves in each container (one high and one low). I don't want the tanks to fill until the low level float is triggered. Otherwise, I would only need one float per tank.

Let's start with both tanks full. If either tank drops below the low float indicator, the main water solenoid would open, followed by the fast flush solenoid to flush the filter membrane. After a set amount of time (approx 5 minutes), the fast flush solenoid should close and the correct solenoid should open to fill the low tank. When the tank reaches the full mark, the main water solenoid should close in addition to the output solenoid. If the second tank becomes empty while the first is filling, there would be no need to open the fast flush solenoid, as that is only necessary when first using the filter after a period of being inactive.

I also realize that I have left a lot out of the sketch. I need to figure out how to call each function, as well as implement the "latches" for both the float valves and the fast flush since it is not a simple on-off state.

Thanks again for the help with this. I'm learning a lot as I go.

To clarify, the Arduino will only control the FILLING of these tanks. The draining will be done by a separate controller.

I see where you are going with the example for the state machine above. To clarify, I believe my states will be the following:

Top Sensor Bottom Sensor State Action Performed
1 1 Full Wait
0 1 if last state was 11, then draining Wait
0 1 if last state was 00, then filling Fill
0 0 Empty Fill
1 0 Fault Wait

Would it be acceptable to just set a variable to indicate weather the "01" state is either filling or draining based on what the previous state was? I think this may be along the lines of what was suggested above, but I'm not 100% sure.

I think there is a bit of confusion about what a state is.

In your 'language', you're referring to the states (status) of the inputs.

In my 'language', I'm referring to an action to be taken; if the program reaches a certain state, it performs a certain action. The status of the inputs defines what the next state will be or that the state does not change

State        Status          State      Status                    State   
STOPPED -+-- ATO empty -+--> DRAINING - drain duration lapsed --> FILLING
   ^     |              |                                            |
   |     +-- FSW empty -+                                            |
   |                                                                 |
   |                                                                 |
   +---------------- ATO full AND FSW full---------------------------+

The actions associated with the states

STOPPED

  • check if either ATO or FSW is empty. If so, it will change the state from STOPPED to DRAINING.

DRAINING

  • open the WATER_MAIN and FAST_FLUSH valves
  • start timing
  • wait for timing to lapse
  • close FAST_FLUSH valve
  • change state from DRAINING to FILLING

FILLING

  • open the output valves (I think you call them ATO_FILL and FSW_FILL)
  • check if ATO and FSW are full
    ** if one of them is full, close associated valve
    ** if both are full, close WATER_MAIN valve and change status from FILLING to STOPPED

These are the basics; you might want to move opening and closing of valves around (e.g. the before changing from DRAINING to FILLING you can open the output valves; basically to make sure that there will be no situation where only WATER_MAIN is open) and when the STOPPED state detects an empty tank you can already open the WATER_MAIN and FAST_FLUSH valves so the code doesn't have to do it when the program detects the DRAIN state. Personally I always find it a little difficult to be consistent in it.

You can add an ERROR state (e.g. your current code handles a situation where the sensors indicate that the tank is both full and empty).

It would look like

STOPPED -+-- ATO empty -+--> DRAINING -+-- drain duration lapsed --> FILLING
   ^     |              |              |                                |
   |     +-- FSW empty -+              |                                +-----+
   |     |                             |                                |     |
   |     |                             |                                |     |
   |    f&e                           f&e                              f&e    |
   |     |                             |                                |     |
   |     +-----------------------------+--------------------------------+     |
   |                                   |                                      |
   |                                   +-- ERROR                              |
   |                                                                          |
   +---------------- ATO full AND FSW full------------------------------------+

In every state you now check if a tank is full and empty (f&e) at the same time. When reaching the ERROR state, the code can e.g. close all valves and flash an LED. How you get away from the ERROR state is up to you. You can implement a test that when the error is solved, the state changes from ERROR to STOPPED and the code will continue from there again.

I must be careful not to give you an "information overload" :wink:

An additional point on the code that you posted earlier. CONSTANTS (as created with #define) are often fully in capitals so they are easily recognizable as being constant.

After re-reading through your previous posts, I understand it much more. I have gotten much farther in my code. I do have some questions, and will post them up tomorrow when I have some more time.

I am nearing information overload, but it is all starting to make more sense 8)