AnyLeaf Blog

Project: Building an automatic pH doser

Written on July 21, 2020, 7:33 p.m.
Updated July 25, 2020, 10:03 p.m.


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 Arduino in this guide, but other controllers like Raspberry Pi work too.

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


Parts list

Total: $124 (Less if you already have an Arduino 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 Arduino: Connect its SDA and SCL, and GND pins to the Arduino's pins of the same name using jumper wires. Connect its +VCC pin to the Arduino's 5V or 3.3V connections. (If you're using a Raspberry Pi, make sure to use 3.3V). Here's a wiring diagram:

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

3: Connect the relay module to the Arduino using jumper wires, with the following connections:

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.

The electronics:


Connect the pump 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:


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 example here's 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.

#include <Anyleaf.h>

PhSensor ph_sensor;

const float phDesired = 6.5;
const float phThresh = 0.3; // Stop adjusting when within this of `pHDesired`.
const float adjustVol = 1.; // mL
const int waitBetweenAdjustments = 180; // seconds
const int primeTime = 2000; // ms. Time to prime pumps

void setup(void) {
    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.
    ph_sensor = PhSensor();

    float ml_to_ms = 1.; // Calibrate to find this.


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

void add_volume(int pump, float vol_ml, float ml_to_ms) {
    float time_ms = float_ml_to_ms * float_vol_ml;

    if (pump == 1) {
        digitalWrite(1, HIGH);  // Turn on pump 1.
        digitalWrite(1, LOW);  // Turn off pump 1.
    } else {
        digitalWrite(2, HIGH);  // Turn on pump 2.
        digitalWrite(2, LOW);  // Turn off pump 2.

void increase_ph(float ml_to_ms) {
    add_volume(1, adjustVol, ml_to_ms);

void decrease_ph(float ml_to_ms) {
    add_volume(2, adjustVol, ml_to_ms);

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

void loop(void) {
    double pH =;

    if (pH - pHDesired) > phThresh {
        delay(waitBetweenAdjustments * 1000);
    } else if (pH - pHDesired) < -phThresh {
        delay(waitBetweenAdjustments * 1000);


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 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:


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