AnyLeaf Blog

Project: Building an automatic pH doser

Written on July 21, 2020, 7:33 p.m.
Updated Feb. 25, 2021, 8:06 p.m.

Introduction

pH management's important for hydroponics, aquariums, brewing, swimming pools, and beyond. This means measuring pH, and adding chemicals modify it until it's at a desired level. If you'd like to know more about pH for hydroponics, check out our article on the subject.

We live in an age of computers and automation, so you shouldn't have to do this by hand! Unfortunately, devices that do this automatically are expensive. For this project, we'll talk you through building one. We'll provide a simple, working design, using low cost, ready-to-use parts. After you get it up and running, we encourage you to improve it, or tweak it for your setup. We use use Raspberry Pi, and Arduino as examples in this guide. (Thanks to Richard Peche for suggesting the Raspberry Pi addition!) Check out our blog post on measuring pH with Raspberry Pi if you'd like more information.

We can build this project using off-the-shelf components, with minimal soldering required. The complete setup:

elec

Parts list

Total: $125 (Less if you already have an Arduino/Pi or AC adapter)

Tools required

This parts list provides a start, but there are many ways you can vary it if you'd like. For example, you could substitute in a different pH module, a different microcontroller, or something other than a mechanical relay. You might decide to use a breadboard to assist with wiring. If you have questions about if a change is appropriate, contact us to ask.

First steps: Connecting the components

1: To start, connect the AnyLeaf pH module to your Pi or Arduino using jumper wires, with the following connections:

Arduino:

wiring diagram for Arduino

Raspberry Pi:

wiring diagram for Raspberry Pi

2: Connect the pH sensor to the module's BNC connector.

3: Connect the relay board to your Pi or Arduino using jumper wires, with the following connections:

Arduino:

Raspberry Pi:

4: Connect the tubing to the pumps. Place one tube lead from each pump in its associated dosing solution, and the other in the water you're dosing. You may need to experiment to determine which direction the pump works in. Pump direction can be reversed by reversing the pump's power wires. Take note of which pump you wish to be the one to increase pH, and which to decrease it - this will make setting up the code clearer.

The electronics:

elec

Connect the pumps to the relay

You'll need to modify the AC Adapter before connecting it to the pumps and relays. ⚠️Warning: Do not modify the end that plugs into the wall!⚠️ Be careful not to touch any part of the wire while it's plugged in. The the 12V potential we'll be working with isn't the big threat: The big danger comes from the 120V or 240V AC that runs between the adapter and wall plug! You don't need to expose any 120/240V wires for this project. Don't touch these in general unless you know what you're doing, and exercise proper precautions.

Cut off the barrel jack on the DC output end of the AC adapter, while it's unplugged from the wall. You may wish to cut it off some distance from the end, since we can make use of the wire later.

Making the following connections between the relay module, 12V part of the adapter, and pumps:

The electronics connected to the pumps:

complete

Everything's now wired up! On to the software.

Hardware Substitutes and modifications

Here are some ways you could modify this setup:

If you use different pumps, make sure the AC adapter you choose is the right voltage for them.

Writing the code

The code examples here (Arduino, and Raspberry Pi Python) are simple and straightforward. If you're using a different pH module, make sure to follow its instructions for reading pH. If you're using a different controller and need help with the code, please contact us.

Arduino:

#include <Anyleaf.h>

PhSensor phSensor;

const float phDesired = 6.5;
const float phThresh = 0.3; // Stop adjusting when within this of `pHDesired`.
const float adjustVol = 1.; // mL
const float mlToMs = 1.; // Calibrate to find this.
const int waitBetweenAdjustments = 180 * 100UL; // ms
const int primeTime = 1000; // ms. Time to prime pumps
const int loopDelay = 1000; // ms. Time between main loop polling

void setup(void) {
    Serial.begin(9600);
    pinMode(1, OUTPUT);  // sets the digital pin 1 as output
    pinMode(2, OUTPUT); // sets the digital pin 1 as output

    // Make sure you calibrate according to your sensor's guidelines.
    phSensor = PhSensor();   
}

// Populate this function to calibrate dose volumes: It's easier
// to work in volume than in time.
float calibrate_volume(int volMl) {
    return 1.; // placeholder
}

void addVolume(int pump, float volMl) {
    float timeMs = mlToMs * volMl;

    digitalWrite(pump, HIGH);
    delay(timeMs);
    digitalWrite(pump, LOW);
}

void addPhModifier(int pump) {
    addVolume(pump, adjustVol);
}

// Run this function while the pumps' inputs are connected to the
// pH up and down solutions, and output is plugged into a drain.
void primePumps() {
    digitalWrite(1, HIGH);  // Turn on pump 1
    digitalWrite(2, HIGH);  // Turn on pump 2.
    delay(primeTime);
    digitalWrite(1, LOW);  // Turn off pump 1.
    digitalWrite(2, LOW);  // Turn off pump 2.
}

void loop(void) {
    float ph = phSensor.read();

    if ((ph - phDesired) > phThresh) {
        addPhModifier(1);
        delay(waitBetweenAdjustments);
    } else if ((ph - phDesired) < -phThresh) {
        addPhModifier(2);
        delay(waitBetweenAdjustments);
    }

    delay(loopDelay);
}

Raspberry Pi Python:

import time
import board
import busio
from gpiozero import OutputDevice
from anyleaf import PhSensor, CalPt, OnBoard

PH_DESIRED = 6.5
PH_THRESH = 0.3  # Stop adjusting when within this of `PH_DESIRED`.
ADJUST_VOL = 1.0  # mL
ML_TO_S = 1.0  # Calibrate to find this.
WAIT_BETWEEN_ADJUSTMENTS = 180  # seconds
PRIME_TIME = 1  # s. Time to prime pumps
LOOP_DELAY = 1  # s. Time between main loop polling


def calibrate_volume(vol_ml: float):
    """
    Populate this function to calibrate dose volumes: It's easier
    to work in volume than in time.
    """
    pass  # placeholder


def add_volume(pump: OutputDevice, vol_ml: float):
    time = ML_TO_S * vol_ml

    pump.on()
    time.sleep(time)
    pump.off()


def add_ph_modifier(pump: OutputDevice):
    add_volume(pump, ADJUST_VOL)


def prime_pumps(pump1: OutputDevice, pump2: OutputDevice):
    """
    Run this function while the pumps' inputs are connected to the
    pH up and down solutions, and output is plugged into a drain.
    """
    pump1.on()  # Turn on pump 1
    pump2.on()  # Turn on pump 2.
    time.sleep(PRIME_TIME)
    pump1.off()  # Turn off pump 1.
    pump2.off()  # Turn off pump 2.


def main():
    i2c = busio.I2C(board.SCL, board.SDA)
    ph_sensor = PhSensor(i2c, LOOP_DELAY)

    # Calibrate as required.
    ph_sensor.calibrate_all(CalPt(0.0, 7.0, 25.0), CalPt(0.18, 4.0, 25.0))

    # Set the first arguments (GPIO pin number) to whicher pins you have the pumps connected to.
    pump_decrease = OutputDevice(17, active_high=False, initial_value=False)
    pump_increase = OutputDevice(27, active_high=False, initial_value=False)

    while True:
        ph = ph_sensor.read(OnBoard())

        if (ph - PH_DESIRED) > PH_THRESH:
            add_ph_modifier(pump_decrease)
            time.sleep(WAIT_BETWEEN_ADJUSTMENTS)
        elif (ph - PH_DESIRED) < -PH_THRESH:
            add_ph_modifier(pump_increase)
            time.sleep(WAIT_BETWEEN_ADJUSTMENTS)

        time.sleep(LOOP_DELAY)


if __name__ == "__main__":
    main()

Make sure you test your code in a system without plants first - you don't want to hurt them by accidentally adding too much modifier. Once you're confident the code works, move to your real system. Adjust parameters, like the acceptable pH threshold, time between adjustments, and amount of modifier to add each adjustment - these may vary with your system. For example, a bigger reservoir may require more modifier than a smaller one.

Code improvements to consider

The software approach we took is simple and naive. See if you can make it more sophisticated. For example, we've been using operating time to control doses - this works, but is indirect. Create a function that will apply a specific volume of pH modifier, or change pH a specific amount. You'll need to experiment, and the latter will be specific to your system.

You can see a few unused skeleton functions you may populate and use. For example, prime_pumps is used to fill the pump's lines with adjuster fluid, so that measurements after are consistent. Consider using this before starting the dose process.

Consider adding code to handle an error from the pH reading. For example, what if you accidentally disconnect the probe? You might get bogus readings - we don't want that to trigger a dose! Perhaps you should keep track of how much modifier you've added (In time, or volume, if you've built that in), and set a limit on the amount that can be added in a single hour, day, etc.

Temperature compensation

If your reservoir temperature changes regularly, you may need to measure temperature, and use this to compensate your measured pH. With the AnyLeaf module, you can pass temperature into the measurement and calibration functions. If the temperature doesn't vary much, or you adjust at the same temperature as calibration, you don't need to worry about this.

Optimizing adjustments

You want to make sure the code only adds as much modifier as you need. Here are some guidelines:

Challenge

How might you modify this project to work as an automatic waterer for hydroponics, or plants in soil?