Documentation

Analog TDS Water Quality Sensor Module for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual
Documentation / Analog TDS Water Quality Sensor Module for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual

Analog TDS Water Quality Sensor Module for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual

shillehtektds-water-sensor-module-arduino-raspberry-pi-esp32

Overview

TDS — total dissolved solids — is the quickest single number for "what's in this water": minerals, salts, and everything else conductive, expressed in ppm. This Gravity-style analog TDS meter measures it the way handheld TDS pens do, but as a module your microcontroller can log: the probe applies an alternating excitation to the water (alternating so the electrodes don't polarize and corrode), the board conditions the resulting conductivity signal, and out comes a clean 0-2.3V analog voltage your ADC converts to ppm with a standard cubic formula.

It runs from 3.3-5.5V, so it wires identically to ESP32, Pi (via ADS1115), Pico, and Arduino, and the waterproof probe on its XH connector drops into an aquarium, hydroponics reservoir, RO filter output, or kettle of suspect tap water. Typical readings: RO/distilled 0-50 ppm, tap water 100-400 ppm, hydroponic nutrient solutions 500-1500 ppm — the module's 0-1000+ ppm sweet spot covers all of it.

Two honest caveats to design around: TDS-by-conductivity moves ~2% per °C, so temperature compensation matters for comparisons across days (the code includes it — pair with a DS18B20 for automatic correction), and the probe should not live permanently energized in the tank — dip or duty-cycle it for longest electrode life. With those habits it's an accurate, dependable water-quality workhorse.

At a Glance

Measures
TDS in ppm
Range
0 - 1000+ ppm
Output
Analog 0 - 2.3V
Supply Voltage
3.3 - 5.5V
Excitation
AC — anti-polarization
Probe
Waterproof, XH connector

Specifications

Parameter Value
Type Gravity-style analog TDS meter (DFRobot-compatible)
Supply Voltage 3.3 - 5.5V DC
Output Voltage 0 - 2.3V analog (safe for 3.3V ADCs at any supply)
Measurement Range 0 - 1000 ppm calibrated; usable beyond with reduced accuracy
Accuracy ±10% F.S. uncalibrated; better with single-point calibration
Working Current 3 - 6 mA
Excitation AC square-wave drive prevents electrode polarization
Probe 2-electrode waterproof probe, ~80 cm lead, XH2.54 plug (probe tip submerged, plug dry)
Interface 3-pin: A (analog out) · + (VCC) · - (GND)
Conversion Standard cubic formula on compensated voltage (in the code below)
Temperature Effect ~2% / °C — compensate with a water-temperature reading

Pinout Diagram

The probe plugs into the 2-pin XH socket on the left; the 3-pin connector on the right is your interface — silkscreened A (analog), + (VCC), - (GND). Keep the board and both connectors dry; only the probe tip goes in the water.

Gravity style analog TDS meter module pinout diagram showing probe socket and A plus minus output connector

Wiring Guide

Arduino Wiring

Module Pin Arduino Pin
+ (VCC) 5V
- (GND) GND
A (analog) A1

ESP32 Wiring

Module Pin ESP32 Pin Details
+ 3V3 Runs happily at 3.3V
- GND
A GPIO 34 ADC1, input-only; output never exceeds 2.3V

Raspberry Pi Wiring (via ADS1115)

Connection Details
Module + / - → 3.3V / GND Pins 1 / 6
Module A → ADS1115 A0 Analog channel
ADS1115 VDD/GND/SDA/SCL 3.3V / GND / Pin 3 / Pin 5
Tip: Enable I2C in raspi-config; i2cdetect -y 1 should show 0x48 before running the example.

Raspberry Pi Pico Wiring

Module Pin Pico Pin Details
+ 3V3(OUT) (pin 36)
- GND (pin 38)
A GP26 (pin 31) ADC0

Code Examples

All four examples median-filter the ADC, apply temperature compensation (edit waterTemp, or feed a DS18B20 reading), and run the standard cubic voltage-to-ppm conversion.

Arduino

tds_arduino.ino
// Analog TDS Meter - Arduino Example
// A->A1, +->5V, -->GND

const int tdsPin = A1;
const float VREF = 5.0;
float waterTemp = 25.0;      // replace with DS18B20 reading if available

float readVolts() {
  long total = 0;
  for (int i = 0; i < 30; i++) { total += analogRead(tdsPin); delay(3); }
  return total / 30.0 * VREF / 1023.0;
}

void setup() {
  Serial.begin(115200);
  Serial.println("TDS meter ready - dip the probe");
}

void loop() {
  float volts = readVolts();
  float compensation = 1.0 + 0.02 * (waterTemp - 25.0);
  float v = volts / compensation;

  float tds = (133.42 * v * v * v
             - 255.86 * v * v
             + 857.39 * v) * 0.5;

  Serial.print("V: ");
  Serial.print(volts, 3);
  Serial.print(" | TDS: ");
  Serial.print(tds, 0);
  Serial.println(" ppm");
  delay(1000);
}

ESP32 (MicroPython)

tds_esp32.py
# Analog TDS Meter - ESP32 MicroPython Example
# A->GPIO 34, +->3V3, -->GND

from machine import ADC, Pin
import time

adc = ADC(Pin(34))
adc.atten(ADC.ATTN_11DB)

WATER_TEMP = 25.0

def read_volts(n=30):
    total = 0
    for _ in range(n):
        total += adc.read_uv()
        time.sleep_ms(3)
    return total / n / 1_000_000

def tds_ppm(volts, temp_c):
    v = volts / (1.0 + 0.02 * (temp_c - 25.0))
    return (133.42 * v**3 - 255.86 * v**2 + 857.39 * v) * 0.5

print("TDS meter ready - dip the probe")
while True:
    volts = read_volts()
    print("V: {:.3f} | TDS: {:.0f} ppm".format(volts, tds_ppm(volts, WATER_TEMP)))
    time.sleep(1)

Raspberry Pi (Python + ADS1115)

tds_rpi.py
#!/usr/bin/env python3
# Analog TDS Meter - Raspberry Pi + ADS1115 Example
# A->ADS1115 A0, +->3.3V, -->GND
# Install: pip3 install adafruit-circuitpython-ads1x15

import time
import board, busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
ads.gain = 1
chan = AnalogIn(ads, ADS.P0)

WATER_TEMP = 25.0

def tds_ppm(volts, temp_c):
    v = volts / (1.0 + 0.02 * (temp_c - 25.0))
    return (133.42 * v**3 - 255.86 * v**2 + 857.39 * v) * 0.5

print("TDS meter ready - dip the probe")
try:
    while True:
        volts = sum(chan.voltage for _ in range(15)) / 15
        print(f"V: {volts:.3f} | TDS: {tds_ppm(volts, WATER_TEMP):.0f} ppm")
        time.sleep(1)
except KeyboardInterrupt:
    print("Stopped by user")

Raspberry Pi Pico (MicroPython)

tds_pico.py
# Analog TDS Meter - Pico MicroPython Example
# A->GP26 (ADC0), +->3V3(OUT), -->GND

from machine import ADC
import time

adc = ADC(26)
WATER_TEMP = 25.0

def read_volts(n=30):
    total = 0
    for _ in range(n):
        total += adc.read_u16()
        time.sleep_ms(3)
    return total / n / 65535 * 3.3

def tds_ppm(volts, temp_c):
    v = volts / (1.0 + 0.02 * (temp_c - 25.0))
    return (133.42 * v**3 - 255.86 * v**2 + 857.39 * v) * 0.5

def quality(ppm):
    if ppm < 50:   return "RO / very pure"
    if ppm < 300:  return "good drinking water"
    if ppm < 600:  return "hard / mineral-rich"
    return "very high TDS"

print("TDS meter ready - dip the probe")
while True:
    volts = read_volts()
    ppm = tds_ppm(volts, WATER_TEMP)
    print("TDS: {:.0f} ppm ({})".format(ppm, quality(ppm)))
    time.sleep(1)

Frequently Asked Questions

It reads 0 ppm in water — broken?
Distilled/RO water legitimately reads near zero — test in tap water first (expect 100-400 ppm). If tap water also reads ~0: the probe isn't fully seated in its XH socket, the electrodes aren't both submerged (immerse past both rings, but keep the cable joint dry), or A is wired to the wrong pin. A pinch of salt in a glass makes an unmistakable jump for a 10-second sanity check.
My readings differ from my handheld TDS pen. Which is right?
Both are conductivity meters using (possibly) different ppm conversion factors — NaCl-based pens use ~0.5, others 0.64-0.7. First match temperature compensation (pens do it automatically), then single-point calibrate: measure a known solution (342 ppm standard, or trust the pen) and scale your output with one multiplier constant. After that they'll track each other closely.
Can I leave the probe permanently in my aquarium/reservoir?
Physically it can sit in the water, but don't power it 24/7 — continuous excitation slowly ages the electrodes and, in fish tanks, other equipment's stray currents can also interact. Best practice: power the module from a GPIO pin, switch it on for a few seconds per reading every few minutes, and log the duty-cycled values. Electrode life then stretches to years.
Does temperature really matter that much?
Yes — conductivity rises ~2% per °C, so the same water reads ~20% higher at 35°C than at 25°C. Every example compensates with a waterTemp variable; make it real by dropping a DS18B20 into the same water and feeding its reading in. For hydroponics dosing decisions, compensation is the difference between useful and misleading.
Can it measure pH, chlorine, or salt-water tanks?
No pH — that's a different electrochemical probe entirely. It senses total conductivity, so it can't tell salt from calcium from nitrate; it's a sum, not a breakdown. Marine/salt-water aquariums (35,000+ ppm) are far beyond its range — use a proper EC/salinity meter there. Freshwater, RO monitoring, and nutrient solutions are its home turf.
Why do readings wobble a few ppm?
Some wobble is normal: the AC excitation, ADC noise, and micro-bubbles on electrodes all contribute. The 30-sample median/mean in the code smooths most of it; also keep the probe still (moving water reads slightly differently than still), tap bubbles off the probe, and keep probe cable away from PWM/motor wiring. Log the average of a burst, not single shots.
What TDS should my water be?
Rough guide: RO/distilled 0-50 ppm; typical municipal tap 100-400 ppm; the EPA secondary guideline for taste is 500 ppm; hydroponic targets vary by crop, commonly 500-1500 ppm. TDS alone doesn't certify safety — it says how much is dissolved, not what — but it's an excellent trend monitor: a sudden change is your cue to investigate.

Related Tutorials