Community project

AguaGuard Monitor de Calidad

Arduino
Photo of AguaGuard Monitor de Calidad
Generated with AI

Leonardo Calderon Castillo

Published October 23, 2026

AguaGuard is a comprehensive water quality monitoring system that measures pH, total dissolved solids (TDS), turbidity, and temperature in real time. Built around an Arduino Uno, it combines multiple analog sensors with a submersible pump, precision real-time clock, and LCD display to create a complete water analysis station.

This guide provides a complete parts list, wiring diagram, assembly steps, and Arduino firmware to get the monitor operational. Builders will learn how to interface multiple analog sensors, control a submersible pump with MOSFET switching, calibrate pH readings, and display time-stamped measurements on an I2C LCD screen.

Wiring diagram

Wiring diagram for AguaGuard Monitor de Calidad

Gather all the parts

QtyComponent
1

PH-4502C pH sensor kit

El módulo con electrodo que entrega un voltaje analógico proporcional a la acidez del agua.

1

Analog TDS sensor module

El módulo con sonda que entrega un voltaje analógico para estimar sólidos disueltos y conductividad.

1

Analog turbidity sensor module

El módulo con sonda que mide cuánta luz bloquean las partículas suspendidas en el agua.

1

Waterproof 10 kΩ NTC thermistor

La sonda de temperatura cuya resistencia cambia cuando el agua se calienta o enfría.

1

10 kΩ resistor for NTC divider

La resistencia fija que forma con el termistor el divisor de voltaje para medir temperatura.

1

LCD 16x2 I2C

16x2 character LCD display with I2C backpack

1

Adafruit DS3231 Precision RTC Breakout

DS3231 precision real-time clock breakout with integrated TCXO and crystal for accurate timekeeping over I2C.

1

5 V submersible mini water pump

La minibomba que renueva el agua dentro de la celda de medición antes de cada lectura.

1

IRLZ44N Logic-Level N-Channel MOSFET

Logic-level N-channel MOSFET commonly used as a low-side switch for DC loads from 3.3V or 5V microcontroller GPIO. Use a gate resistor, a gate pulldown, and a flyback diode for inductive loads.

1

1N4007 flyback diode

El diodo que absorbe el pico eléctrico de la minibomba al apagarla.

1

2N3906 PNP transistor

El transistor que corta la alimentación del sensor TDS cuando se toma la lectura de pH.

1

4.7 kΩ PNP base resistor

La resistencia que limita la corriente desde el Arduino hacia la base del transistor PNP.

1

10 kΩ PNP base pull-up resistor

La resistencia que mantiene apagado el sensor TDS mientras el Arduino arranca.

1

220 Ω MOSFET gate resistor

La resistencia que limita el pulso de carga de la compuerta del MOSFET de la bomba.

1

10 kΩ MOSFET gate pull-down resistor

La resistencia que mantiene la bomba apagada mientras el Arduino se reinicia.

1

Momentary pushbutton — measure now

El botón que inicia una medición inmediata sin esperar al siguiente intervalo.

1

Momentary pushbutton — change screen

El botón que cambia la información mostrada en la pantalla.

1

Momentary pushbutton — calibrate

El botón que guarda una calibración de pH al mantenerlo presionado.

1

2-position DIP switch

El pequeño interruptor doble que selecciona cada cuánto se realiza una medición.

1

5 V piezo buzzer

El zumbador que avisa cuando una lectura supera los límites configurados.

1

Amber LED

La luz ámbar que se enciende mientras la estación está tomando una muestra.

1

220 Ω amber LED resistor

La resistencia que evita que la luz ámbar reciba demasiada corriente.

1

Blue LED

La luz azul que confirma que se envió una lectura por el enlace serial.

1

150 Ω blue LED resistor

La resistencia que evita que la luz azul reciba demasiada corriente.

1

5 V 3 A regulated power supply

La fuente regulada que alimenta el Arduino, sensores, pantalla, bomba y el futuro enlace inalámbrico.

Assemble it in 7 steps

1. Prepara los rieles de alimentación

Con la fuente desconectada, lleva VOUT+ de supply_5v_3a al riel rojo de la protoboard y VOUT− al riel azul. Une el pin 5V del Arduino Uno al riel rojo y uno de sus pines GND al riel azul; todos los módulos deben compartir esta misma tierra.

  • Usa rojo para 5 V y negro o azul para tierra, y verifica que los rieles partidos de la protoboard estén puenteados si los vas a usar.
  • No conectes todavía la fuente: un cable de 5 V en un riel de tierra puede dañar el Arduino y los sensores.

2. Conecta las sondas de agua

Conecta ph_sensor: VCC → 5V (power), GND → GND (ground), PO → A0 (signal). Conecta turbidity_sensor: VCC → 5V (power), GND → GND (ground), AO → A2 (signal). Para ntc_10k, conecta un cable de la sonda a A3 (signal) y el otro a GND (ground); conecta ntc_fixed_resistor entre 5V y ese mismo punto de A3 para formar el divisor.

  • Mantén el cable del electrodo de pH corto y su módulo seco, lejos de los cables de la bomba.
  • Solo las puntas de las sondas y la bomba pueden tocar la muestra; el módulo de pH, la protoboard y la pantalla deben mantenerse secos.

3. Monta el interruptor del sensor TDS

Coloca tds_switch_pnp con su patillaje identificado en la hoja de datos o en la cara plana del transistor. Conecta emisor → 5V (power), colector → VCC de tds_sensor (power), y GND del TDS → GND (ground); conecta AO del TDS → A1 (signal). Conecta pnp_base_resistor de D8 a la base y pnp_pullup_resistor entre 5V y esa misma base.

  • El 2N3906 no tiene el mismo orden de patas que todos los transistores; confirmar emisor, base y colector antes de energizar evita quemarlo o dejar el TDS siempre encendido.

4. Conecta la bomba y su protección

Conecta pump: PUMP+ → 5V (power) y PUMP− → drenaje de pump_mosfet (switched return). Conecta la fuente del MOSFET → GND (ground). Conecta gate_resistor entre D7 y la compuerta; conecta gate_pulldown entre la compuerta y GND. Coloca flyback_diode en paralelo con la bomba: cátodo, el extremo con franja, → 5V (power); ánodo → drenaje/MOSFET y PUMP−.

  • Si inviertes el diodo, la fuente puede quedar en cortocircuito cuando se encienda. La franja del 1N4007 siempre va del lado de 5 V en este montaje.

5. Conecta pantalla y reloj

Conecta lcd: VCC → 5V (power), GND → GND (ground), SDA → A4 (data), SCL → A5 (data). Conecta rtc: VIN → 5V (power), GND → GND (ground), SDA → A4 (data), SCL → A5 (data). Ambos módulos comparten esos dos cables de datos.

  • Si la pantalla queda encendida pero sin letras, ajusta con cuidado el pequeño tornillo de contraste de su módulo I2C.
  • A4 y A5 deben ir en paralelo a ambos módulos, no cruzados; VCC y GND intercambiados pueden dañar la pantalla o el reloj.

6. Agrega botones, selector y avisos

Conecta measure_button entre D2 y GND, screen_button entre D3 y GND y calibrate_button entre D4 y GND; el programa activa las resistencias internas que mantienen cada entrada estable. Conecta interval_dip: SW1_A → D5 (signal), SW1_B → GND, SW2_A → D6 (signal), SW2_B → GND. Conecta buzzer: positivo → D9 (signal), negativo → GND. Conecta amber_resistor entre D12 y ánodo de amber_led; cátodo del LED → GND. Conecta blue_resistor entre D13 y ánodo de blue_led; cátodo del LED → GND.

  • Los LED necesitan sus resistencias en serie: conectarlos directo a un pin puede dañar el LED o el Arduino.

7. Revisa antes de encender

Comprueba que todas las tierras llegan al mismo riel, que no hay gotas cerca de la electrónica y que la bomba descarga al recipiente sin mojar la caja. Primero conecta el Arduino por USB para programarlo; después conecta la fuente de 5 V de la bomba y sensores.

  • El selector DIP usa 00 para 10 min, 01 para 30 min, 10 para 1 h y 11 para 24 h; presiona “medir ahora” para no esperar el primer ciclo.
  • No alimentes simultáneamente el riel de 5 V desde USB y desde una fuente externa sin confirmar que el modelo de Uno y el cableado lo permiten; para las pruebas usa la fuente externa regulada en el riel y el USB solo para datos/programación con una tierra común.

Review all connections

1. Connections between "supply_5v_3a" and "Arduino"

Functionsupply_5v_3aArduino
powerVOUT+ → alimentación regulada de 5 V5V
groundVOUT- → tierra comúnGND

2. Connections between "ph_sensor" and "Arduino"

Functionph_sensorArduino
powerVCC5V
groundGNDGND
analogPO → lectura de pH A0GPIO 14

3. Connections between "tds_sensor" and "Arduino"

Functiontds_sensorArduino
groundGNDGND
analogAO → lectura TDS A1GPIO 15

4. Connections between "turbidity_sensor" and "Arduino"

Functionturbidity_sensorArduino
powerVCC5V
groundGNDGND
analogAO → lectura de turbidez A2GPIO 16

5. Connections between "ntc_10k" and "Arduino"

Functionntc_10kArduino
analogLEAD1 → nodo del divisor de temperatura A3GPIO 17
groundLEAD2GND

6. Connections between "ntc_fixed_resistor" and "Arduino"

Functionntc_fixed_resistorArduino
powerA5V
analogB → Waterproof 10 kΩ NTC thermistor LEAD1EXT

7. Connections between "lcd" and "Arduino"

FunctionlcdArduino
powerVCC5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

8. Connections between "rtc" and "Arduino"

FunctionrtcArduino
powerVIN5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

9. Connections between "pump" and "Arduino"

FunctionpumpArduino
powerPUMP+5V
dataPUMP- → IRLZ44N Logic-Level N-Channel MOSFET DRAINEXT

10. Connections between "pump_mosfet" and "Arduino"

Functionpump_mosfetArduino
groundSOURCEGND

11. Connections between "flyback_diode" and "Arduino"

Functionflyback_diodeArduino
groundANODE → IRLZ44N Logic-Level N-Channel MOSFET DRAINEXT
powerCATHODE5V

12. Connections between "gate_resistor" and "Arduino"

Functiongate_resistorArduino
digitalAGPIO 7
digitalB → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT

13. Connections between "gate_pulldown" and "Arduino"

Functiongate_pulldownArduino
digitalA → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT
groundBGND

14. Connections between "tds_switch_pnp" and "Arduino"

Functiontds_switch_pnpArduino
powerEMITTER5V
powerCOLLECTOR → Analog TDS sensor module VCCEXT

15. Connections between "pnp_base_resistor" and "Arduino"

Functionpnp_base_resistorArduino
digitalAGPIO 8
digitalB → 2N3906 PNP transistor BASEEXT

16. Connections between "pnp_pullup_resistor" and "Arduino"

Functionpnp_pullup_resistorArduino
powerA5V
digitalB → 2N3906 PNP transistor BASEEXT

17. Connections between "measure_button" and "Arduino"

Functionmeasure_buttonArduino
digitalAGPIO 2
groundBGND

18. Connections between "screen_button" and "Arduino"

Functionscreen_buttonArduino
digitalAGPIO 3
groundBGND

19. Connections between "calibrate_button" and "Arduino"

Functioncalibrate_buttonArduino
digitalAGPIO 4
groundBGND

20. Connections between "interval_dip" and "Arduino"

Functioninterval_dipArduino
digitalSW1_AGPIO 5
groundSW1_BGND
digitalSW2_AGPIO 6
groundSW2_BGND

21. Connections between "buzzer" and "Arduino"

FunctionbuzzerArduino
digitalPOSITIVEGPIO 9
groundNEGATIVEGND

22. Connections between "amber_resistor" and "Arduino"

Functionamber_resistorArduino
digitalAGPIO 12
digitalB → Amber LED ANODEEXT

23. Connections between "amber_led" and "Arduino"

Functionamber_ledArduino
groundCATHODEGND

24. Connections between "blue_resistor" and "Arduino"

Functionblue_resistorArduino
digitalAGPIO 13
digitalB → Blue LED ANODEEXT

25. Connections between "blue_led" and "Arduino"

Functionblue_ledArduino
groundCATHODEGND

Deploy the firmware

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <SoftwareSerial.h>
#include <EEPROM.h>
#include <math.h>

const uint8_t PH_PIN = A0;
const uint8_t TDS_PIN = A1;
const uint8_t TURBIDITY_PIN = A2;
const uint8_t NTC_PIN = A3;
const uint8_t MEASURE_BUTTON = 2;
const uint8_t SCREEN_BUTTON = 3;
const uint8_t CALIBRATE_BUTTON = 4;
const uint8_t DIP_0 = 5;
const uint8_t DIP_1 = 6;
const uint8_t PUMP_PIN = 7;
const uint8_t TDS_ENABLE_PIN = 8;  // LOW powers the PNP-switched TDS board.
const uint8_t BUZZER_PIN = 9;
const uint8_t ESP_RX_PIN = 10;
const uint8_t ESP_TX_PIN = 11;
const uint8_t AMBER_LED = 12;
const uint8_t BLUE_LED = 13;

const unsigned long PUMP_MS = 5000UL;
const unsigned long SETTLE_MS = 8000UL;
const unsigned long TDS_WARMUP_MS = 1000UL;
const unsigned long BLUE_PULSE_MS = 250UL;
const float TDS_FACTOR = 0.50F;

LiquidCrystal_I2C lcd(0x27, 16, 2);
RTC_DS3231 rtc;
SoftwareSerial espLink(ESP_RX_PIN, ESP_TX_PIN);

enum CycleState { WAITING, PUMPING, SETTLING, TDS_WARMUP, READING, REPORTING };
CycleState state = WAITING;
unsigned long stateStarted = 0;
unsigned long nextMeasurement = 0;
unsigned long blueUntil = 0;
uint8_t screen = 0;

struct Calibration {
  uint16_t magic;
  float v4;
  float v686;
};
Calibration cal;
const uint16_t CAL_MAGIC = 0xCA17;
uint8_t calibrationStage = 0;
unsigned long calPressedAt = 0;
bool calWasDown = false;
bool screenWasDown = false;

struct Reading {
  float ph;
  float tds;
  float turbidity;
  float tempC;
  DateTime timestamp;
};
Reading reading;

bool buttonPressed(uint8_t pin) { return digitalRead(pin) == LOW; }

int averageAdc(uint8_t pin) {
  long sum = 0;
  for (uint8_t i = 0; i < 30; ++i) sum += analogRead(pin);
  return (int)(sum / 30L);
}

float adcVoltage(int raw) { return raw * (5.0F / 1023.0F); }

float readTemperatureC() {
  float v = adcVoltage(averageAdc(NTC_PIN));
  if (v < 0.02F || v > 4.98F) return NAN;
  float rNtc = 10000.0F * v / (5.0F - v);
  float kelvin = 1.0F / (1.0F / 298.15F + log(rNtc / 10000.0F) / 3950.0F);
  return kelvin - 273.15F;
}

float readPh() {
  float v = adcVoltage(averageAdc(PH_PIN));
  if (fabs(cal.v686 - cal.v4) < 0.02F) return 7.0F;
  return 4.0F + (v - cal.v4) * 2.86F / (cal.v686 - cal.v4);
}

float readTds(float tempC) {
  float voltage = adcVoltage(averageAdc(TDS_PIN));
  float compensation = 1.0F + 0.02F * (tempC - 25.0F);
  float compensatedVoltage = voltage / compensation;
  return (133.42F * compensatedVoltage * compensatedVoltage * compensatedVoltage
        - 255.86F * compensatedVoltage * compensatedVoltage
        + 857.39F * compensatedVoltage) * TDS_FACTOR;
}

float readTurbidity() {
  float voltage = adcVoltage(averageAdc(TURBIDITY_PIN));
  // Raw voltage is retained as the low-cost sensor's calibration reference.
  return voltage;
}

unsigned long selectedInterval() {
  uint8_t choice = (buttonPressed(DIP_1) ? 2 : 0) | (buttonPressed(DIP_0) ? 1 : 0);
  if (choice == 0) return 10UL * 60UL * 1000UL;
  if (choice == 1) return 30UL * 60UL * 1000UL;
  if (choice == 2) return 60UL * 60UL * 1000UL;
  return 24UL * 60UL * 60UL * 1000UL;
}

void showLine(const __FlashStringHelper *a, const String &b) {
  lcd.setCursor(0, 0); lcd.print(a); lcd.print(F("                "));
  lcd.setCursor(0, 1); lcd.print(b); lcd.print(F("                "));
}

void showReading() {
  lcd.clear();
  if (screen == 0) {
    lcd.print(F("pH ")); lcd.print(reading.ph, 2); lcd.print(F(" TDS"));
    lcd.setCursor(0, 1); lcd.print(reading.tds, 0); lcd.print(F(" ppm"));
  } else if (screen == 1) {
    lcd.print(F("Temp ")); lcd.print(reading.tempC, 1); lcd.print(F(" C"));
    lcd.setCursor(0, 1); lcd.print(F("Turb ")); lcd.print(reading.turbidity, 2); lcd.print(F(" V"));
  } else {
    lcd.print(reading.timestamp.year()); lcd.print('-'); lcd.print(reading.timestamp.month()); lcd.print('-'); lcd.print(reading.timestamp.day());
    lcd.setCursor(0, 1); lcd.print(reading.timestamp.hour()); lcd.print(':'); if (reading.timestamp.minute() < 10) lcd.print('0'); lcd.print(reading.timestamp.minute());
  }
}

void startCycle() {
  state = PUMPING;
  stateStarted = millis();
  digitalWrite(AMBER_LED, HIGH);
  digitalWrite(PUMP_PIN, HIGH);
  digitalWrite(TDS_ENABLE_PIN, HIGH);
  showLine(F("Tomando muestra"), F("Bomba encendida"));
}

void saveCalibrationVoltage(float voltage) {
  if (calibrationStage == 0) {
    cal.v4 = voltage;
    calibrationStage = 1;
    showLine(F("pH4 guardado"), F("Ponga buffer 6.86"));
  } else {
    cal.v686 = voltage;
    cal.magic = CAL_MAGIC;
    EEPROM.put(0, cal);
    calibrationStage = 0;
    showLine(F("Calibracion"), F("pH 4 y 6.86 lista"));
  }
}

void handleCalibrationButton() {
  bool down = buttonPressed(CALIBRATE_BUTTON);
  if (down && !calWasDown) calPressedAt = millis();
  if (!down && calWasDown && millis() - calPressedAt >= 1500UL) {
    saveCalibrationVoltage(adcVoltage(averageAdc(PH_PIN)));
  }
  calWasDown = down;
}

void reportReading() {
  char line[128];
  snprintf(line, sizeof(line), "%04d-%02d-%02dT%02d:%02d:%02dZ,river_tijuana,ph,%.2f,ph,raw,uno\n",
           reading.timestamp.year(), reading.timestamp.month(), reading.timestamp.day(), reading.timestamp.hour(), reading.timestamp.minute(), reading.timestamp.second(), reading.ph);
  espLink.print(line);
  snprintf(line, sizeof(line), "%04d-%02d-%02dT%02d:%02d:%02dZ,river_tijuana,solidos_disueltos_totales,%.1f,ppm,raw,uno\n",
           reading.timestamp.year(), reading.timestamp.month(), reading.timestamp.day(), reading.timestamp.hour(), reading.timestamp.minute(), reading.timestamp.second(), reading.tds);
  espLink.print(line);
  Serial.print(line);
  blueUntil = millis() + BLUE_PULSE_MS;
  digitalWrite(BLUE_LED, HIGH);
  if (reading.ph < 6.5F || reading.ph > 8.5F || reading.tds > 1000.0F) tone(BUZZER_PIN, 2200, 350);
}

void setup() {
  pinMode(PUMP_PIN, OUTPUT); pinMode(TDS_ENABLE_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT); pinMode(AMBER_LED, OUTPUT); pinMode(BLUE_LED, OUTPUT);
  pinMode(MEASURE_BUTTON, INPUT_PULLUP); pinMode(SCREEN_BUTTON, INPUT_PULLUP); pinMode(CALIBRATE_BUTTON, INPUT_PULLUP);
  pinMode(DIP_0, INPUT_PULLUP); pinMode(DIP_1, INPUT_PULLUP);
  digitalWrite(PUMP_PIN, LOW); digitalWrite(TDS_ENABLE_PIN, HIGH);
  Serial.begin(9600); espLink.begin(9600);
  Wire.begin(); lcd.init(); lcd.backlight();
  if (!rtc.begin()) { showLine(F("RTC no encontrado"), F("Revise A4 y A5")); }
  if (rtc.lostPower()) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  EEPROM.get(0, cal);
  if (cal.magic != CAL_MAGIC) { cal.magic = CAL_MAGIC; cal.v4 = 3.00F; cal.v686 = 2.50F; }
  showLine(F("Estacion de agua"), F("Lista para medir"));
  nextMeasurement = millis() + selectedInterval();
}

void loop() {
  unsigned long now = millis();
  handleCalibrationButton();
  bool screenDown = buttonPressed(SCREEN_BUTTON);
  if (screenDown && !screenWasDown && state == WAITING) { screen = (screen + 1) % 3; showReading(); }
  screenWasDown = screenDown;
  if (blueUntil && now >= blueUntil) { digitalWrite(BLUE_LED, LOW); blueUntil = 0; }

  if (state == WAITING) {
    if (buttonPressed(MEASURE_BUTTON) || now >= nextMeasurement) startCycle();
  } else if (state == PUMPING && now - stateStarted >= PUMP_MS) {
    digitalWrite(PUMP_PIN, LOW); state = SETTLING; stateStarted = now; showLine(F("Muestra quieta"), F("Esperando burbujas"));
  } else if (state == SETTLING && now - stateStarted >= SETTLE_MS) {
    digitalWrite(TDS_ENABLE_PIN, LOW); state = TDS_WARMUP; stateStarted = now; showLine(F("Midiendo TDS"), F("Sensor encendido"));
  } else if (state == TDS_WARMUP && now - stateStarted >= TDS_WARMUP_MS) {
    reading.tempC = readTemperatureC();
    reading.tds = readTds(reading.tempC);
    digitalWrite(TDS_ENABLE_PIN, HIGH);
    state = READING;
  } else if (state == READING) {
    reading.ph = readPh(); reading.turbidity = readTurbidity(); reading.timestamp = rtc.now();
    state = REPORTING;
  } else if (state == REPORTING) {
    reportReading(); showReading(); digitalWrite(AMBER_LED, LOW);
    nextMeasurement = now + selectedInterval(); state = WAITING;
  }
  delay(1);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik