2

I'm building a control system for three sump pumps using a single Arduino and a set of relays. I have float sensors for each one, and need the Arduino to trigger the relays when the float sensors are triggered. Sensor 1 triggers pump 1, sensor 2 triggers pump 2, etc.

Simple enough, but I also need each pump to remain on for 30 seconds after its float sensor returns to normal. This throws me for a loop since I need the other pumps and sensors to function during this delay time. I quickly found that the blocking delay() function is no longer feasible.

How can I trigger the relays, keep them on for 30 seconds after the sensors return to normal, but all the while still monitor and control the other sensors/relays? Would a non-blocking timer solution work? Would I have to delve into interrupts?

4 Answers 4

3

Would a non-blocking timer solution work?

Yes. Your "timer" doesn't have to be anything more than a start time from millis() and a flag. Then your loop just compares the start time to the current millis() value, and if it's more than the interval you wanted, do the "thing". In code:

uint32_t startTime;
bool waiting = false;
const uint32_t PUMP_DURATION = 30000UL;

void setup()
{
  // start the "timer"
  startTime = millis();
  waiting = true;
}

void loop()
{
  if (waiting && (millis() - startTime >= PUMP_DURATION)) {
    waiting = false;
    // do something now!
    foo();
  }

The flag keeps it from checking when the pump isn't running.

If you have multiple timed things, just make another set of variables (i.e., startTime2 and waiting2). Give 'em better names, eh?

There are also Arduino libraries that encapsulate this common design pattern. TimeMark is one.

Here's some sample code on the Arduino forum.

2
  • 30 seconds after the program starts the condition (millis() - startTime >= PUMP_DURATION) will always be true.
    – Octopus
    Commented Feb 22, 2016 at 23:54
  • Yes... and? This is a technique for (re)starting the timer and detecting the expiration. It should be casual to the most obvious observer that this is the minimal demonstration of the technique. Different intervals, different starts, restarting, multiple timers, etc., are left as an exercise for the reader. -_-
    – slash-dev
    Commented Feb 23, 2016 at 1:04
3

The "blink-without-delay"-pattern shows how this can be solved but with several time periods and logic this becomes complex. Some abstraction is needed. The Timemark library provides a solution. Belows is the logic for a single sensor-relay pair with a state trace to serial:

#include <Timemark.h>

const uint32_t TURN_OFF_TIMEOUT = 30000L;
Timemark turnOff(TURN_OFF_TIMEOUT);
const int sensorPin = 2;
const int relayPin = 3;

void setup()
{
  Serial.begin(9600);
  while (!Serial);

  pinMode(sensorPin, INPUT_PULLUP);
  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW);

  trace.start();
}

void loop()
{
  if (trace.expired()) {
    Serial.print(millis());
    Serial.print(F(":sensor="));
    Serial.print(digitalRead(sensorPin));
    Serial.print(F(", relay="));
    Serial.println(digitalRead(relayPin));
  }

  if (digitalRead(sensorPin) == LOW) {
    digitalWrite(relayPin, HIGH);
    turnOff.start();
  }
  else if (turnOff.expired()) {
    digitalWrite(relayPin, LOW);
  }
}

Expanding to three sensor-relays becomes:

#include <Timemark.h>

const uint32_t TURN_OFF_TIMEOUT = 30000L;
const int CONTROL_MAX = 3;
Timemark turnOff[CONTROL_MAX];
const int sensorPin[CONTROL_MAX] = { 2, 4, 6 };
const int relayPin[CONTROL_MAX] = { 3, 5, 7 };

void setup()
{
  for (int i = 0; i < CONTROL_MAX; i++) {
    turnOff[i].limitMillis(TURN_OFF_TIMEOUT);
    pinMode(sensorPin[i], INPUT_PULLUP);
    pinMode(relayPin[i], OUTPUT);
    digitalWrite(relayPin[i], LOW);
  }
}

void loop()
{   
  for (int i = 0; i < CONTROL_MAX; i++) {
    if (digitalRead(sensorPin[i]) == LOW) {
      digitalWrite(relayPin[i], HIGH);
      turnOff[i].start();
    }
    else if (turnOff[i].expired()) {
      digitalWrite(relayPin[i], LOW);
    }
  }
}

An alternative solution is to use the Scheduler library. Below is a rewrite using template functions and a controller task per sensor-relay pair.

#include <Scheduler.h>

const uint32_t TURN_OFF_TIMEOUT = 30000L;
const uint32_t DEBOUNCE_TIMEOUT = 40L;

template<const int sensorPin, const int relayPin> void setupController()
{
  pinMode(sensorPin, INPUT_PULLUP);
  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW);
}

template<const int sensorPin, const int relayPin> void loopController()
{
  if (digitalRead(sensorPin) == LOW) {
    digitalWrite(relayPin, HIGH);
    while (digitalRead(sensorPin) == LOW) delay(DEBOUNCE_TIMEOUT);
    delay(TURN_OFF_TIMEOUT);
    digitalWrite(relayPin, LOW);
  }
  yield();
}

void setup()
{
  Scheduler.start(setupController<2,3>, loopController<2,3>);
  Scheduler.start(setupController<4,5>, loopController<4,5>);
  Scheduler.start(setupController<6,7>, loopController<6,7>);
}

void loop()
{
  yield();
}

Please note that the Scheduler requires yield() or delay() to switch between the tasks.

Cheers!

0

You might use something like this:

#define AMOUNT_OF_TIMERS 10
unsigned int timer[AMOUNT_OF_TIMERS];
unsigned int sensors[AMOUNT_OF_TIMERS];
unsigned int pumps[AMOUNT_OF_TIMERS];
void setup()
{
  sensors[0] = 1;
  sensors[1] = 2;
  sensors[2...
  ......s[9] = 12;
  pumps[0] = 13;
  pumps[1....
  ....s[9] = 22;
  for(unsigned int i = 0; i< AMOUNT_OF_TIMERS; i++){
    PinMode(sensors[i],INPUT);
    PinMode(pumps[i],OUTPUT);
  }
}

void loop()
{
  checkPins();
  decreaseTimers();
  delay(1000);
}

void checkPins(){
  for(unsigned int i = 0; i< AMOUNT_OF_TIMERS; i++){
    if(digitalRead(sensors[i])
    pump[i] = 30;
  }
}

void decreaseTimers(){
  for(unsigned int i = 0; i< AMOUNT_OF_TIMERS; i++){
    if(timer[i]>0)
    timer--;
  }
}

Personally, I think, handling the timing events in an interrupt is way better. Which might also be for the sensor inputs.

It's not the neatest thing, but it will do. Using interrupts should make it even more efficient.

  • You should use interrupts.
  • You might use unsigned chars to preserve extra RAM.
  • You should adjust the timers/delay. (set timer to 300, and delay to 100ms) for better result.
0

Using info from every suggestion, I've so far come up with the following. I may not need to touch this code again for years and years (or a new house owner may need to look at it should I move out), so I tried to comment it as much as possible.

I don't like that I've repeated so much code in the readFloatSensor functions, but this is where I am so far. How can I eliminate the code duplication? I believe I could use arrays and for loops but now I've entered another grey area for my knowledge level.

// PumpController.ino

// ----CONSTANTS
// ----Edit these if you need to change the pin layout or pump duration

#define RELAY_ON 0                     // The pump relays are active-LOW; on when LOW (0)
#define RELAY_OFF 1                    // and off when HIGH (1). These #defines just help keep track.

const int pumpRelayPin1 =  12;         // Pin numbers for the pump relays
const int pumpRelayPin2 =  11;
const int pumpRelayPin3 =  10;

const int floatSensorPin1 = 7;         // Pin numbers for the float sensors
const int floatSensorPin2 = 8;
const int floatSensorPin3 = 9;

const int pumpDuration = 500;          // Number of millisecs that the pumps run
                                       // after the float sensors no longer detect water

// ----VARIABLES

byte pumpState1 = RELAY_OFF;           // Used to record whether the pumps are on or off
byte pumpState2 = RELAY_OFF;           // (default to HIGH/OFF)
byte pumpState3 = RELAY_OFF;

unsigned long currentMillis = 0;       // Stores the value of millis() in each iteration of loop()

unsigned long pumpTimerMillis1 = 0;    // Stores the times when the float sensors were last triggered
unsigned long pumpTimerMillis2 = 0;
unsigned long pumpTimerMillis3 = 0;


// ----SETUP (Sets up the pins at startup/reset)

void setup()
{

    // Debug
    Serial.begin(9600);
    Serial.println("Starting PumpController.ino");

    // Pump relays are active-LOW. Initialize pump
    // pins HIGH so the relays are inactive at startup/reset
    digitalWrite(pumpRelayPin1, RELAY_OFF);
    digitalWrite(pumpRelayPin2, RELAY_OFF);
    digitalWrite(pumpRelayPin3, RELAY_OFF);

    // THEN set the pins as outputs
    pinMode(pumpRelayPin1, OUTPUT);
    pinMode(pumpRelayPin2, OUTPUT);
    pinMode(pumpRelayPin3, OUTPUT);

    // Set the float sensor pins as inputs and use the built-in pullup resistors
    pinMode(floatSensorPin1, INPUT_PULLUP);
    pinMode(floatSensorPin2, INPUT_PULLUP);
    pinMode(floatSensorPin3, INPUT_PULLUP);

}


// ----MAIN LOOP

void loop()
{
    // Get the current clock count

    currentMillis = millis() + pumpDuration;       // We add the countdown timer duration to
                                                   // the clock to prevent the pumps from
                                                   // running at boot time, while the clock
                                                   // counter is still below the timer value

    // Call the functions that do the work

    readFloatSensor1();             // check each float sensor and decide whether
    readFloatSensor2();             // to start the pump and countdown timer
    readFloatSensor3();
    triggerPumps();                 // Actually toggles the relay pins based on
}                                   // the data from the above functions.


// ----WORKER FUNCTIONS

void readFloatSensor1()
{
    if (digitalRead(floatSensorPin1) == LOW)   // If the float sensor is tripped (pulled low)
    {
        pumpState1 = RELAY_ON;                 // then trigger the pump relay pin
        pumpTimerMillis1 = currentMillis;      // and set the pump countdown timer to the current clock time
    }

    if (currentMillis - pumpTimerMillis1 >= pumpDuration)   // If the countdown timer has expired,
    {
        pumpState1 = RELAY_OFF;                             // then turn off the pump relay pin
        pumpTimerMillis1 = 0;                               // and reset the timer to 0 for the next trigger
    }
}

//========

void readFloatSensor2()
{
    if (digitalRead(floatSensorPin2) == LOW)   // If the float sensor is tripped (pulled low)
    {
        pumpState2 = RELAY_ON;                 // then trigger the pump relay pin
        pumpTimerMillis2 = currentMillis;      // and set the pump countdown timer to the current clock time
    }

    if (currentMillis - pumpTimerMillis2 >= pumpDuration)   // If the countdown timer has expired,
    {
        pumpState2 = RELAY_OFF;                             // then turn off the pump relay pin
        pumpTimerMillis2 = 0;                               // and reset the timer to 0 for the next trigger
    }
}

//========

void readFloatSensor3()
{
    if (digitalRead(floatSensorPin3) == LOW)   // If the float sensor is tripped (pulled low)
    {
        pumpState3 = RELAY_ON;                 // then trigger the pump relay pin
        pumpTimerMillis3 = currentMillis;      // and set the pump countdown timer to the current clock time
    }

    if (currentMillis - pumpTimerMillis3 >= pumpDuration)   // If the countdown timer has expired,
    {
        pumpState3 = RELAY_OFF;                             // then turn off the pump relay pin
        pumpTimerMillis3 = 0;                               // and reset the timer to 0 for the next trigger
    }
}

//========

void triggerPumps()
{
    // Toggle the pump relay pins on and off based on
    // pumpState from the readFloatSensor functions

    digitalWrite(pumpRelayPin1, pumpState1);
    digitalWrite(pumpRelayPin2, pumpState2);
    digitalWrite(pumpRelayPin3, pumpState3);
}

//=====END
1
  • With your regard to the concern about duplication, you can make a generic sensor function and pass it the pins for the sensor of interest - see for example how so many peripheral libraries are instantiated with pin assignments in their constructor. Or you could put all of each pumps information in a struct, or go a step further and define a pump object you instantiate three of. Then you put those in an array and call the appropriate methods in turn... Commented Feb 22, 2016 at 23:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.