Trouble finding timer issue in Sprinkler Controller program

Howdy everyone,
I’m deep into a project, basically completed, but running into a major issue concerning my timers. My project is a sprinkler controller, modeled to emulate the functionality of a standard off-the-shelf sprinkler controller, with the ability to program the schedule and manually operate different zones from the app. I have my app and code built, but am running into an issue that I think is linked to timers. I’ve been through my code a hundred times and the error doesn’t make sense.

The user inputs the start time for the program to run, time/days. The user also inputs how many minutes each zone should run. A function on an interval timer (checkSchedule()) constantly monitors the current time, and when it is the time that the program is scheduled to run, a function is called to runDailyProgram(). This function looks at each zone’s runtime, and calculates how many millis until each zone should turn on from the start of the program, with a 3 second gap to shutdown one solenoid and power another. These milli times are then used to set timeout timers for a function to run each zone. Each zone function is then called at the appropriate time to START a zone. Inside each zone function, another timeout timer is set to turn the zone off.

My issue: When runDailyProgram() function is called by the checkSchedule() function it starts up fine. Each cycle runs as expected until Zone 4. It starts running, the relay powers, and then immediately powers off. It’s like the last timeout timer inside the zone4TurnOnProgram() is running too fast. The only thing not working is the timed gap when the zone should remain on.

In playing with this I created a function that I can call with a blynk button on my app. If I hit the button it just calls the runDailyProgram() function manually. When it runs this way, the program runs correctly, and Zone 4 will remain on the correct amount of time.

I may be missing something obvious, or if this is a timer bug, I would appreciate any advice for overcoming it. This is the last hurdle before I can really start having fun with this project (Google Assistant integration, webhooks for rain delay, etc). Any help would be greatly appreciated, and if someone wants to trade code reviews, I would be more than willing to try it out!

Setup:
• Hardware model + communication type: NodeMCU with Wifi
• Smartphone OS (iOS or Android) + version : Android, newest?
• Blynk server or local server: Blynk
• Blynk Library version: Current
• Controls relay module


#include <Arduino.h>
#include <SPI.h>
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
#include <TimeLib.h>
#include <WidgetRTC.h>

/* Comment this out to disable prints and save space */
#define BLYNK_PRINT Serial

#define WIFI_LED_PIN 2           // Arduino pin 2 = NodeMCU pin D4 = WiFi LED

#define ZONE_1_RELAY_PIN 5       // Arduino pin 5 = NodeMCU pin D1 = Zone 1 Relay
#define ZONE_2_RELAY_PIN 4       // Arduino pin 4 = NodeMCU pin D2 = Zone 2 Relay
#define ZONE_3_RELAY_PIN 12      // Arduino pin 12 = NodeMCU pin D6 = Zone 3 Relay
#define ZONE_4_RELAY_PIN 14      // Arduino pin 14 = NodeMCU pin D5 = Zone 4 Relay

#define ZONE_1_BUTTON 0          // Blynk zone button V0
#define ZONE_2_BUTTON 1          // Blynk zone button V1
#define ZONE_3_BUTTON 2          // Blynk zone button V2
#define ZONE_4_BUTTON 3          // Blynk zone button V3

#define BLYNK_TEST_BUTTON 69     // Blynk button used in testing V69

#define TIME_INPUT_WIDGET 7      // Blynk Time Input widget V7
#define ZONE_1_MINUTE_INPUT 8    // Blynk numeric input for zone 1 run time V8
#define ZONE_2_MINUTE_INPUT 9    // Blynk numeric input for zone 2 run time V9
#define ZONE_3_MINUTE_INPUT 10   // Blynk numeric input for zone 3 run time V10
#define ZONE_4_MINUTE_INPUT 11   // Blynk numeric input for zone 4 run time V11

#define PROGRAM_GAP_SECS 3       // Time between program cycles

// Prototypes (Not necesary for Arduino IDE.)
void turnOffAllZones();
void zoneTurnOnManual(int, int, unsigned long);
void checkSchedule();
void runDailyProgram();
void programOver();


// You should get Auth Token in the Blynk App.
// char auth[] = "xxx"; // local 
char auth[] = "xxx"; // online

// WiFi credentials.
// Set password to "" for open networks.
char ssid[] = "xxx";
char pass[] = "xxx";

// Global variables and things
BlynkTimer killTimer;
BlynkTimer programTimer;
BlynkTimer scheduler;
WidgetRTC rtc;


// Timer variables
int startHour = 22;
int startMin = 50;   // have to set these or get weird error where doesn't update in BLYNK_WRITE(V7)
unsigned long zone1mil, zone2mil, zone3mil, zone4mil;  // Zone run time stored in millis
int dailySchedule[7];
bool scheduleSet = false;
int pTimer1, pTimer2, pTimer3, pTimer4, pTimer5;
bool programRunning = false;

void setup()
{
  // Pin Setup 
  pinMode(WIFI_LED_PIN, OUTPUT);
  pinMode(ZONE_1_RELAY_PIN, OUTPUT); 
  pinMode(ZONE_2_RELAY_PIN, OUTPUT); 
  pinMode(ZONE_3_RELAY_PIN, OUTPUT); 
  pinMode(ZONE_4_RELAY_PIN, OUTPUT);

  // Start pins high right away to keep relays from being active during boot, etc.
  digitalWrite(ZONE_1_RELAY_PIN, HIGH);
  digitalWrite(ZONE_2_RELAY_PIN, HIGH);
  digitalWrite(ZONE_3_RELAY_PIN, HIGH);
  digitalWrite(ZONE_4_RELAY_PIN, HIGH);

  // Debug console
  Serial.begin(9600);

  // Blynk startup
  Blynk.begin(auth, ssid, pass);
  //Blynk.begin(auth, ssid, pass, IPAddress(192,168,50,145), 8080);
  
  
  turnOffAllZones();
  Blynk.syncVirtual(V7, V8, V9, V10, V11);     // Only reset vPins necesary for scheduling (RTC, time input, minute inputs)

  scheduler.setInterval(2*1000, checkSchedule);
  
}



void loop()
{
  Blynk.run();
  killTimer.run();
  programTimer.run();
  scheduler.run();
}

void checkSchedule() {
  Serial.println("\nChecking schedule.");
  int nowHour = hour();
  int nowMin = minute();
  Serial.print("Time: "); // FIX IDITO
  Serial.print(nowHour);
  Serial.print(":");
  Serial.print(nowMin);
  Serial.println();
  if (nowHour == startHour && nowMin == startMin) {
    Serial.println("TIME GOOD"); 
    if (programRunning == false) {                //!programTimer.isEnabled(pTimer1) && !programTimer.isEnabled(pTimer2) && !programTimer.isEnabled(pTimer3) && !programTimer.isEnabled(pTimer4)) {
      Serial.println("CheckSchedule() calling runDailyProgram().");
      programRunning = true;
      runDailyProgram();
    }
    else {
      Serial.println("Time to run, but already running.");
    }
  }
  else {
    Serial.println("NOT TIME\n");
  }
}



/*
* Turns off all Zones. Always runs before starting a zone to ensure that two zones are never on simultaneously. Also updates all app buttons to off.
*/
void turnOffAllZones() {
  killTimer.disableAll();         // Kill all other timers to get rid of any overplapping commands.
  Serial.println("\nTurning off water.");
  digitalWrite(ZONE_1_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_1_BUTTON, HIGH);
  digitalWrite(ZONE_2_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_2_BUTTON, HIGH);
  digitalWrite(ZONE_3_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_3_BUTTON, HIGH);
  digitalWrite(ZONE_4_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_4_BUTTON, HIGH);
}

void turnOffAllZonesProgram() {
  killTimer.disableAll();         // Kill all other timers to get rid of any overplapping commands.
  Serial.println("Successfully completed cycle.");
  digitalWrite(ZONE_1_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_1_BUTTON, HIGH);
  digitalWrite(ZONE_2_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_2_BUTTON, HIGH);
  digitalWrite(ZONE_3_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_3_BUTTON, HIGH);
  digitalWrite(ZONE_4_RELAY_PIN, HIGH);
  Blynk.virtualWrite(ZONE_4_BUTTON, HIGH);
}




/*
* Turns on selected zone for specified amount of time, then turns zone off automatically to prevent user from leaving the water running.
* @param zonePin - The pin of the zone to be turned on 
* @param buttonPin - The virtual pin of the button to change the state to ON
* @param time - The time in minutes to run the zone
*/
void zoneTurnOnManual(int zonePin, int buttonPin, unsigned long time) {
  // Shut all zones off, only one zone should run at a time, turn app button back on
  turnOffAllZones();                               
  Blynk.virtualWrite(buttonPin, LOW);

  // Turn the zone on
  digitalWrite(zonePin, LOW);  

  // Set zone to go off after specified amount of time       
  killTimer.setTimeout(time, turnOffAllZones);
}

void zone1TurnOnProgram() {
  // Shut all zones off, only one zone should run at a time, turn app button back on
  turnOffAllZones();                               
  Blynk.virtualWrite(ZONE_1_BUTTON, LOW);

  // Turn the zone on
  digitalWrite(ZONE_1_RELAY_PIN, LOW);

  // Set zone to go off after specified amount of time       
  killTimer.setTimeout(zone1mil, turnOffAllZonesProgram);
  Serial.println("Zone 1 running.");
}

void zone2TurnOnProgram() {
  // Shut all zones off, only one zone should run at a time, turn button back on
  turnOffAllZones();                               
  Blynk.virtualWrite(ZONE_2_BUTTON, LOW);

  // Turn the zone on
  digitalWrite(ZONE_2_RELAY_PIN, LOW);

  // Set zone to go off after specified amount of time       
  killTimer.setTimeout(zone2mil, turnOffAllZonesProgram);
  Serial.println("Zone 2 running.");
}

void zone3TurnOnProgram() {
  // Shut all zones off, only one zone should run at a time, turn button back on
  turnOffAllZones();                               
  Blynk.virtualWrite(ZONE_3_BUTTON, LOW);

  // Turn the zone on
  digitalWrite(ZONE_3_RELAY_PIN, LOW);

  // Set zone to go off after specified amount of time       
  killTimer.setTimeout(zone3mil, turnOffAllZonesProgram);
  Serial.println("Zone 3 running.");
}

void zone4TurnOnProgram() {
  // Shut all zones off, only one zone should run at a time, turn button back on
  turnOffAllZones();                               
  Blynk.virtualWrite(ZONE_4_BUTTON, LOW);

  // Turn the zone on
  digitalWrite(ZONE_4_RELAY_PIN, LOW);

  // Set zone to go off after specified amount of time       
  killTimer.setTimeout(zone4mil, turnOffAllZonesProgram);
  Serial.println("Zone 4 running.");
  programRunning = false;
  Serial.println("Program finito!");
}


void runDailyProgram() {
  // Calculate times:
  unsigned long zone2start = zone1mil + (PROGRAM_GAP_SECS * 1000);    
  Serial.println("Zone 2 start: " + String(zone2start));

  unsigned long zone3start = zone2start + zone2mil + (PROGRAM_GAP_SECS * 1000);
  Serial.println("Zone 3 start: " + String(zone3start));

  unsigned long zone4start = zone3start + zone3mil + (PROGRAM_GAP_SECS * 1000);
  Serial.println("Zone 4 start: " + String(zone4start));

 

  // Run program
  zone1TurnOnProgram();
  pTimer2 = programTimer.setTimeout(zone2start, zone2TurnOnProgram);
  pTimer3 = programTimer.setTimeout(zone3start, zone3TurnOnProgram);
  pTimer4 = programTimer.setTimeout(zone4start, zone4TurnOnProgram);
  
}


/*
* Synchronize time on connection.
*/
BLYNK_CONNECTED() {
  rtc.begin();
}

/*
* Process Zone 1 manual activation, max 10 minutes.
*/
BLYNK_WRITE(ZONE_1_BUTTON) {
  if(param.asInt()) {
    Serial.println("You turned off Zone 1.");
    turnOffAllZones();
  }
  else {
    Serial.println("You turned on Zone 1, it will auto-off in 10 minutes.");
    zoneTurnOnManual(ZONE_1_RELAY_PIN, ZONE_1_BUTTON, 10 * 1000);
  }
}


/*
* Process Zone 2 manual activation, max 10 minutes.
*/
BLYNK_WRITE(ZONE_2_BUTTON) {
  if(param.asInt()) {
    Serial.println("You turned off Zone 2.");
    turnOffAllZones();
  }
  else {
    Serial.println("You turned on Zone 2, it will auto-off in 10 minutes.");
    zoneTurnOnManual(ZONE_2_RELAY_PIN, ZONE_2_BUTTON, 10 * 1000);
  }
}


/*
* Process Zone 3 manual activation, max 10 minutes.
*/
BLYNK_WRITE(ZONE_3_BUTTON) {
  if(param.asInt()) {
    Serial.println("You turned off Zone 3.");
    turnOffAllZones();
  }
  else {
    Serial.println("You turned on Zone 3, it will auto-off in 10 minutes.");
    zoneTurnOnManual(ZONE_3_RELAY_PIN, ZONE_3_BUTTON, 10 * 1000);
  }
}


/*
* Process Zone 4 manual activation, max 10 minutes.
*/
BLYNK_WRITE(ZONE_4_BUTTON) {
  if(param.asInt()) {
    Serial.println("You turned off Zone 4.");
    turnOffAllZones();
  }
  else {
    Serial.println("You turned on Zone 4, it will auto-off in 10 minutes.");
    zoneTurnOnManual(ZONE_4_RELAY_PIN, ZONE_4_BUTTON, 10 * 1000);
  }
}


/*
 * Processes time from Blynk time set widget
 */
BLYNK_WRITE(TIME_INPUT_WIDGET) {      
  TimeInputParam t(param);

  // Process start times
  if (t.hasStartTime())
  {
    startHour = t.getStartHour();
    startMin = t.getStartMinute();
    Serial.println(String("Start: ") +
                   t.getStartHour() + ":" +
                   t.getStartMinute());
    
  }
 
  // Process weekdays (1. Mon, 2. Tue, 3. Wed, ...)
  for (int i = 1; i <= 7; i++) {
    if (t.isWeekdaySelected(i)) {
      Serial.println(String("Day ") + i + " is selected");
      dailySchedule[i] = 1;
    }
    else
      dailySchedule[i] = 0;
  }

  scheduleSet = true;
  Serial.println("Schedule has been programmed.");
}


/*
 * Set the run time for Zone 1 from Blynk
 */
BLYNK_WRITE(ZONE_1_MINUTE_INPUT) {
  int minutes = param.asInt();
  zone1mil = minutes * 1000;
  Serial.println("Zone 1 set to " + String(minutes) + " minutes.");
}


/*
 * Set the run time for Zone 2 from Blynk
 */
BLYNK_WRITE(ZONE_2_MINUTE_INPUT) {
  int minutes = param.asInt();
  zone2mil = minutes * 1000;
  Serial.println("Zone 2 set to " + String(minutes) + " minutes.");
}


/*
 * Set the run time for Zone 3 from Blynk
 */
BLYNK_WRITE(ZONE_3_MINUTE_INPUT) {
  int minutes = param.asInt();
  zone3mil = minutes * 1000;
  Serial.println("Zone 3 set to " + String(minutes) + " minutes.");
}


/*
 * Set the run time for Zone 4 from Blynk
 */
BLYNK_WRITE(ZONE_4_MINUTE_INPUT) {
  int minutes = param.asInt();
  zone4mil = minutes * 1000;
  Serial.println("Zone 4 set to " + String(minutes) + " minutes.");   
}

/*
 * A V pin to test functions
 */
BLYNK_WRITE(BLYNK_TEST_BUTTON) {
  Serial.println("BLYNK TEST BUTTON PUSHED");
  runDailyProgram();
}

// NEED: BLYNK_WRITE for webhooks (make standalone)

Schedule Program App Screen

I wanted to add, that while I have a lot of time invested in this design, if there is a way better method to accomplish something I’m doing, I’m open to rewriting major portions. Cheers!

You should seriously consider using ONE timer object and three timer iterations (each timer can have up to 16) instead of three separate timer objects. Otherwise you are taking up extra system resources for the separate timer objects that might be contributing to your issue :thinking:

The difference between your timers for zones 1,2 & 3 and the one for zone 4 are these three lines after you start your timeout timer:

I think you’re forgetting that the timeout timer is non-blocking. This means that as soon as you begin the timer the code will continue to the next three lines while the timeout timer is executing in the background.

Personally, I think I’d take a different approach…
Rather than doing all the Millis calculations and using the timeout timers I’d simply calculate the start and stop time of each zone then have a timed function that checks to see if one of these start/stop times has been met and take the appropriate action.

Pete.

2 Likes

I started with this approach originally, but I cancel timers throughout the operation to make sure that new timer creations do not overlap with others that are supposed to be finished. This required the need for multiple timers, so some can be wiped out while others continue running. I understand trying to use the single timer, but ran into a nightmare trying to interact with and delete named timers. Going to follow Pete’s advice and rework, will also try to keep it to one timer in the new design.

Thank you for your input!

Pete,
I do understand the none-blocking issue, and figured that was the root of the problem, but still wanted to figure out how timers could be getting wiped. I removed the lines in the code you mentioned to make it the same as the other zones previously, as it only affects future scheduled runs, but it didn’t sort the issue.

I spent a lot of time trying to make this work since it does work in one case, just not another, and both cases seem exactly the same if I desk check the flow line by line, but something in there is getting lost.

I appreciate your advice. I honestly thought that sort of solution seemed a little clunky, and thought the timeouts might provide a more elegant solution, but after many long nights of reading I’m going to avoid timers for a while. I’ll give your advice a try soon and post my results if I can get them working. Thank you for the feedback. I’m in my final semester of school and trying to build an IoT portfolio to hopefully find a job in the industry. All help to learn/implement best practices is greatly appreciated!!!