Watering Robot mkII

ElectronicsApril 2022

We can built it better

I built an automatic watering system for our plants in 2021. This worked great and allowed us to take short trips without our plants suffering. Made watering easier as we could just fill the reservoir every few days rather than making endless trips with the watering can. However, the system was build very quickly and had a few issues that I wanted to correct for the following year:

  • The pumps I was using were very cheap and prone to wires breaking. The wires were custom soldered and fussy to switch out.
  • The software was very limited and could only drive the pumps in sequence which made a whole watering cycle lengthy and difficult to schedule.
  • The pump would run a full cycle every time it was plugged in. Troubleshooting pump timing required unplugging the system. Power cycling couldn't be done without potentially over-watering.
  • The relay had four outputs which was not enough for the size garden we were planning.
  • The water tubes were stiff and hard to work with. They also just slashed water directly into the pots with no diffusion.
  • It was in a ratty cardboard box and looked kind of like a bomb with all the wires coming out of it.

The previous system wasn't exactly built to be all-weather.

Improved features

Since the previous years system had been so successful I felt better about splurging on components (I also had a better sense of what was worth paying more for). I ended up replacing almost every component in the system:

  • Ratty cardboard box → Weather-proof project box
  • 4 port relay → 8 port relay
  • Cheap pumps with custom wiring → Slightly less cheap pumps with USB connections
  • Stiff clear tubing → FloraFlex (I highly recommend this, inexpensive and easy to work with)

I kept the original Arduino Uno, that worked great. The power supply was also fine.

New stuff

Using USB pumps allowed me to run more pumps with less wiring mess. I also labeled all of the the pumps, connections, and output tubes with identifying numbers. This made testing and adjusting timing sooooo much easier. I 3D printed a small wiring harness for the USB plugs which kept things neat and tidy in the box.

I added a second water reservoir getting us up to 14 gallons of capacity. At peak time this gave us 2-3 days of hands off time without the plants risking under-watering.

I got spikes, diffusers, and inline drip nozzles so the water would be more evenly distributed into the containers.

I added a toggle switch for selecting modes without having to unplug the whole system.

Labels on everything for better UX.

This version looks more like a hydroponics system and less like a bomb.

Upgraded software

The biggest change overall was a complete re-write of the software. I switched to an asynchronous approach which made it possible to schedule an interval between each watering cycle (say 45 minutes) and then when the cycle occurred run each pump for a specific amount of time (e.g. pump #1: 15 seconds, pump #2 and #3: 30 seconds). This sort of granularity was not possible with the previous iteration of the code.

I also added improved serial messaging which made setting up the right watering schedule much easier.

The software also understood various operating modes controlled by a three way toggle switch. With this, the system could be OFF (no watering), ON MANUAL (all pumps on), ON AUTO (pumps run according to the programmed schedule). With this, I could power cycle the system without it running a full watering cycle which is huge when troubleshooting.

This video demonstrates the operating modes of the system:



The Code

For interests sake, here is the code for the system:

//
// Automatic Watering System mkII
//
// Copyright Jay Roberts 2022
//
// Toggle switch pins
const int pinWaterManual = 8;
const int pinWaterAuto = 9;
typedef enum {
  OperatingModeOff,
  OperatingModeAuto,
  OperatingModeManual,
  OperatingModeUnknown,
} OperatingMode;
OperatingMode currentMode = OperatingModeUnknown;
const int pumps[] = {
  A1,
  A2,
  A3,
  A4,
  A5,
};
const int waterDurations[] = {
   8,
   8,
   12,
   12,
   60,
};
const unsigned int wateringIntervalMinutes = 60; // Minutes between watering.
const unsigned long wateringIntervalMillis = wateringIntervalMinutes * 60 * 1000L; // Minutes between watering.
unsigned long nextWateringTime = 0; // Millis that next watering should start
unsigned long currentMillis = 0;
unsigned long previousToggleReadMillis = 0;
unsigned long previousNotificationMillis = 0;
 
bool pumpsRunning = false;
void setup() {
  Serial.begin(9600);
  Serial.println("Jay's amazing watering robot!");
  pinMode(pinWaterAuto, INPUT_PULLUP) ;
  pinMode(pinWaterManual, INPUT_PULLUP) ;
  for (int i = 0; i < (sizeof(pumps) / sizeof(pumps[0])); i++) {
    pinMode(pumps[i], OUTPUT);
    digitalWrite(pumps[i], HIGH);
  }
  readToggle();
  delay(500);
}
void loop() {
  currentMillis = millis();
  readToggle();
  runPumps();
  notifyAutoWateringTime();
}
/**
 * Check state of toggle switch and set operating mode.
 */
void readToggle() {
  if (currentMillis - previousToggleReadMillis < 100) {
    return;
  }
  OperatingMode readMode = currentMode;
  if (digitalRead(pinWaterManual) == LOW) {
    readMode = OperatingModeAuto;
  } else if (digitalRead(pinWaterAuto) == LOW) {
    readMode = OperatingModeManual;
  } else {
    readMode = OperatingModeOff;
  }  
  if (readMode != currentMode) {
    currentMode = readMode;
    if (currentMode == OperatingModeOff) {
      Serial.println("Mode OFF");
      allPumpsOff();
    } else if (currentMode == OperatingModeManual) {
      Serial.println("Mode Manual");
      allPumpsOn();
    } else {
      Serial.println("Mode Auto");
      // Start our first watering cycle right now.
      nextWateringTime = currentMillis;
    }
  }
  previousToggleReadMillis = currentMillis;
}
/**
 * Turn all water pumps on.
 */
void allPumpsOn() {
  for (int i = 0; i < (sizeof(pumps) / sizeof(pumps[0])); i++) {
    digitalWrite(pumps[i], LOW);
  }
}
/**
 * Turn all water pumps OFF.
 */
void allPumpsOff() {
  for (int i = 0; i < (sizeof(pumps) / sizeof(pumps[0])); i++) {
    digitalWrite(pumps[i], HIGH);
  }
}
/**
 * Check schedule and, when necessary, run pumps.
 */
void runPumps() {
  // Only run when in Auto mode.
  if (currentMode == OperatingModeAuto) {
    // currentMillis increases as time goes on.
    // nextWatering time is the scheduled time to run the pumps in auto mode.
    // If currentMillis is greater than nextWateringTime then it's time to run 
    // the pumps.
    if (nextWateringTime < currentMillis) {
      // We'll keep track if any pumps are running.
      pumpsRunning = false;
      // Evaluate all of the pumps.
      for (int i = 0; i < (sizeof(pumps) / sizeof(pumps[0])); i++) {
        if (currentMillis - nextWateringTime < waterDurations[i] * 1000L) {
          // The difference between currentMillis and nextWateringTime is the 
          // duration that the pumps have been running for.
          //
          // If that value is less than the duration for the current pump then 
          // the pump should be ON.
          digitalWrite(pumps[i], LOW);
          pumpsRunning = true;
        } else {
          // Otherwise, the pump should be OFF.
          digitalWrite(pumps[i], HIGH);
        }
      }
      if (!pumpsRunning) {
        // We've evaluated all pumps and none are running, the current cycle is done.
        Serial.println("All pumps done");
        // Schedule the next watering cycle.
        nextWateringTime = currentMillis + wateringIntervalMillis;
      }    
    }
  } 
}
/**
 * Emits a serial message every 5 seconds alerting the time until the next auto-
 * watering cycle.
 */
void notifyAutoWateringTime() {
  if (currentMode != OperatingModeAuto) {
    return;
  }
  if (pumpsRunning) {
    return;
  }
  
  if (currentMillis - previousNotificationMillis > 5 * 1000) {    
    unsigned long remainingMillis = nextWateringTime - currentMillis;
  
    unsigned long seconds = remainingMillis / 1000;
    unsigned long minutes = seconds / 60;
    unsigned long hours = minutes / 60;
    unsigned long days = hours / 24;
    remainingMillis %= 1000;
    hours %= 24;
    minutes %= 60;
    seconds %= 60;
    
    Serial.print("Next auto-watering cycle in ");
    Serial.print(hours);
    Serial.print(" hours, ");
   
    Serial.print(minutes);
    Serial.print(" minutes, ");
   
    Serial.print(seconds);
    Serial.print(" seconds.\n");
    
    previousNotificationMillis = currentMillis;
  }
}