ESP32 with TDS Sensor (Water Quality Sensor)

In this guide, you’ll learn how to use a TDS meter (Total Dissolved Solids) with the ESP32. A TDS meter indicates the total dissolved solids like salts, minerals, and metals, in a solution. This parameter can be used to give you an idea of water quality and compare water from different sources. One of the main applications of a TDS meter is aquarium water quality monitoring.

ESP32 with TDS Sensor Water Quality Sensor Arduino

We’ll use the TDS meter from keystudio and show you a simple example to measure TDS in ppm units using Arduino IDE.

Table of Contents

In this tutorial, we’ll cover the following topics

Introducing the TDS Meter

A TDS meter measures the number of total dissolved solids like salts, minerals, and metals in the water. As the number of dissolved solids in the water increases, the conductivity of the water increases, and that allow us to calculate the total dissolved solids in ppm (mg/L).

Total dissolved solids TDS meter

Although this is a good indicator to monitor the quality of the water, note that it does not measure contaminants in the water. Thus, you can’t rely solely on this indicator to determine if the water is good for consumption or not.

A TDS meter can be useful to monitor water quality in many applications like pools, aquariums, fish tanks, hydroponics, water purifiers, etc.

In this tutorial, we’ll use the TDS meter from keystudio that comes with an interface module and an electrode probe (see picture above).

For more information about the TDS meter, we recommend taking a look at the official documentation.

Features and Specifications

This tutorial refers to the TDS Meter V1.0 from keystudio. Here are the sensor parameters:

TDS Meter:

  • Input Voltage: DC 3.3 ~ 5.5V
  • Output Voltage: 0 ~ 2.3V
  • Working Current: 3 ~ 6mA
  • TDS Measurement Range: 0 ~ 1000ppm
  • TDS Measurement Accuracy: ± 10% F.S. (25 ℃)
  • Module Interface: XH2.54-3P
  • Electrode Interface: XH2.54-2P

TDS Probe:

  • Number of Needle: 2
  • Total Length: 60cm
  • Connection Interface: XH2.54-2P
  • Color: White
  • Waterproof Probe

Where to Buy TDS Sensor?

TDS sensor

You can check the TDS sensor on Maker Advisor to find the best price:

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

Interfacing the TDS Meter with the ESP32

The TDS meter outputs an analog signal that can be measured using an ADC pin on the ESP32. You can check the ESP32 ADC pins here.

Wire the sensor as in the following table:

TDS SensorESP32
GNDGND
VCC3.3V
DataGPIO 27 (or any other ESP32 ADC pin)

Reading TDS (water quality) with the ESP32 – Code

As we mentioned previously, the sensor outputs an analog signal that can be converted to TDS in ppm. We’re using the code provided by the sensor documentation with some modifications.

ESP32 with TDS sensor water quality

To get more accurate results, you’ll probably need to calibrate your sensor against a solution with a known TDS value. Also, take into account the non-linearity of the ESP32 ADC when it comes to low and high values.

However, these adjustments might be not needed if you are not concerned about specific values but about a qualitative value of TDS.

Upload the following code to your ESP32.

// Original source code: https://wiki.keyestudio.com/KS0429_keyestudio_TDS_Meter_V1.0#Test_Code
// Project details: https://RandomNerdTutorials.com/esp32-tds-water-quality-sensor/

#define TdsSensorPin 27
#define VREF 3.3              // analog reference voltage(Volt) of the ADC
#define SCOUNT  30            // sum of sample point

int analogBuffer[SCOUNT];     // store the analog value in the array, read from ADC
int analogBufferTemp[SCOUNT];
int analogBufferIndex = 0;
int copyIndex = 0;

float averageVoltage = 0;
float tdsValue = 0;
float temperature = 25;       // current temperature for compensation

// median filtering algorithm
int getMedianNum(int bArray[], int iFilterLen){
  int bTab[iFilterLen];
  for (byte i = 0; i<iFilterLen; i++)
  bTab[i] = bArray[i];
  int i, j, bTemp;
  for (j = 0; j < iFilterLen - 1; j++) {
    for (i = 0; i < iFilterLen - j - 1; i++) {
      if (bTab[i] > bTab[i + 1]) {
        bTemp = bTab[i];
        bTab[i] = bTab[i + 1];
        bTab[i + 1] = bTemp;
      }
    }
  }
  if ((iFilterLen & 1) > 0){
    bTemp = bTab[(iFilterLen - 1) / 2];
  }
  else {
    bTemp = (bTab[iFilterLen / 2] + bTab[iFilterLen / 2 - 1]) / 2;
  }
  return bTemp;
}

void setup(){
  Serial.begin(115200);
  pinMode(TdsSensorPin,INPUT);
}

void loop(){
  static unsigned long analogSampleTimepoint = millis();
  if(millis()-analogSampleTimepoint > 40U){     //every 40 milliseconds,read the analog value from the ADC
    analogSampleTimepoint = millis();
    analogBuffer[analogBufferIndex] = analogRead(TdsSensorPin);    //read the analog value and store into the buffer
    analogBufferIndex++;
    if(analogBufferIndex == SCOUNT){ 
      analogBufferIndex = 0;
    }
  }   
  
  static unsigned long printTimepoint = millis();
  if(millis()-printTimepoint > 800U){
    printTimepoint = millis();
    for(copyIndex=0; copyIndex<SCOUNT; copyIndex++){
      analogBufferTemp[copyIndex] = analogBuffer[copyIndex];
      
      // read the analog value more stable by the median filtering algorithm, and convert to voltage value
      averageVoltage = getMedianNum(analogBufferTemp,SCOUNT) * (float)VREF / 4096.0;
      
      //temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.02*(fTP-25.0)); 
      float compensationCoefficient = 1.0+0.02*(temperature-25.0);
      //temperature compensation
      float compensationVoltage=averageVoltage/compensationCoefficient;
      
      //convert voltage value to tds value
      tdsValue=(133.42*compensationVoltage*compensationVoltage*compensationVoltage - 255.86*compensationVoltage*compensationVoltage + 857.39*compensationVoltage)*0.5;
      
      //Serial.print("voltage:");
      //Serial.print(averageVoltage,2);
      //Serial.print("V   ");
      Serial.print("TDS Value:");
      Serial.print(tdsValue,0);
      Serial.println("ppm");
    }
  }
}

View raw code

How the Code Works

Let’s take a quick look at the code. You can also skip right away to the Demonstration section.

The TdsSensorPin variable saves the GPIO where you want to get the readings. We chose GPIO27, but you can use any other ADC pin.

#define TdsSensorPin 27

Then, insert the analog voltage reference for the ADC. For the ESP32 is 3.3V, for an Arduino, for example, it is 5V.

#define VREF 3.3    // analog reference voltage(Volt) of the ADC

Before getting a measurement value, we’ll apply a median filtering algorithm to get a more stable value. The SCOUNT variable refers to the number of samples we’ll filter before getting an actual value.

#define SCOUNT  30  // sum of sample point

Then, we need some arrays to store the readings as well as some index variables that will allow us to go through the arrays.

int analogBuffer[SCOUNT];     // store the analog value in the array, read from ADC
int analogBufferTemp[SCOUNT];
int analogBufferIndex = 0;
int copyIndex = 0;

Initialize the averageVoltage variable and tsdValue as float variables.

float averageVoltage = 0;
float tdsValue = 0;

The temperature variable saves the current temperature value. The temperature influences the readings, so there is an algorithm that compensates for fluctuations in temperature. In this example, the reference temperature is 25ºC, but you can change it depending on your environment. For more accurate results, you can add a temperature sensor and get the actual temperature at the time of reading the sensor.

float temperature = 25;       // current temperature for compensation

The following function will be used to get a stable TDS value from an array of readings.

// median filtering algorithm
int getMedianNum(int bArray[], int iFilterLen){
  int bTab[iFilterLen];
  for (byte i = 0; i<iFilterLen; i++)
  bTab[i] = bArray[i];
  int i, j, bTemp;
  for (j = 0; j < iFilterLen - 1; j++) {
    for (i = 0; i < iFilterLen - j - 1; i++) {
      if (bTab[i] > bTab[i + 1]) {
        bTemp = bTab[i];
        bTab[i] = bTab[i + 1];
        bTab[i + 1] = bTemp;
      }
    }
  }
  if ((iFilterLen & 1) > 0){
    bTemp = bTab[(iFilterLen - 1) / 2];
  }
  else {
    bTemp = (bTab[iFilterLen / 2] + bTab[iFilterLen / 2 - 1]) / 2;
  }
  return bTemp;
}

In the setup(), initialize the Serial Monitor at a baud rate of 115200.

Serial.begin(115200);

Set the TDS sensor pin as an input.

pinMode(TdsSensorPin,INPUT);

In the loop(), get new TDS readings every 40 milliseconds and save them in the buffer:

static unsigned long analogSampleTimepoint = millis();
if(millis()-analogSampleTimepoint > 40U){     //every 40 milliseconds,read the analog value from the ADC
  analogSampleTimepoint = millis();
  analogBuffer[analogBufferIndex] = analogRead(TdsSensorPin);    //read the analog value and store into the buffer
  analogBufferIndex++;
  if(analogBufferIndex == SCOUNT){ 
    analogBufferIndex = 0;
  }
}   

Every 800 milliseconds, it gets the latest readings and gets the average voltage by using the filtering algorithm created before:

static unsigned long printTimepoint = millis();
if(millis()-printTimepoint > 800U){
  printTimepoint = millis();
  for(copyIndex=0; copyIndex<SCOUNT; copyIndex++){
    analogBufferTemp[copyIndex] = analogBuffer[copyIndex];
      
    // read the analog value more stable by the median filtering algorithm, and convert to voltage value
    averageVoltage = getMedianNum(analogBufferTemp,SCOUNT) * (float)VREF / 4096.0;

Then, it calculates a temperature compensation coefficient and calculates the TDS value taking that value into account:

//temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.02*(fTP-25.0)); 
float compensationCoefficient = 1.0+0.02*(temperature-25.0);
//temperature compensation
float compensationVoltage=averageVoltage/compensationCoefficient;
      
//convert voltage value to tds value
tdsValue=(133.42*compensationVoltage*compensationVoltage*compensationVoltage - 255.86*compensationVoltage*compensationVoltage + 857.39*compensationVoltage)*0.5;

Finally, it prints the TDS value in ppm:

Serial.print("TDS Value:");
Serial.print(tdsValue,0);
Serial.println("ppm");

Demonstration

After copying the code to the Arduino IDE, upload the code to your board. Don’t forget to select the right board in Tools > Board and the right COM port in Tools > Port.

After uploading, open the Serial Monitor at a baud rate of 115200 and press the ESP32 RST button so that the code starts working.

TDS meter testing ESP32 ESP8266 Arduino

It will show a value of 0 if the probe is not submerged. Put the probe on a solution to check its TDS. You can try with tap water and add some salt to see if the values increase.

TDS Sensor Arduino IDE Serial Monitor

I measured the TDS value for tap water in my house, and I got a value of around 100ppm, which is a good value for drinking water.

I also tested tea, and the TDS value increased to about 230ppm, which seems a reasonable value.

Finally, I also measured the TDS value of bottled water and I got a value of 0ppm. I’m not sure if this value is correct because the water is advertised as mineral water, so the minerals dissolved in the water should account for a TDS value. I think this value can be explained due to the non-linearity of the ESP32 ADC pins for small voltage values. Do you have one of these sensors? What values did you get for bottled water?

Wrapping Up

A TDS meter can measure the total dissolved solids in a solution. It can be used as an indicator of water quality and allows you to characterize the water. The meter returns the TDS value in ppm (parts per million—mg/L). The TDS value has many applications but it cannot be used by itself to determine if the water is drinkable or not.

A great application of this type of sensor is an aquarium water quality monitor. You can use this sensor alongside a waterproof DS18B20 temperature sensor to monitor your fish tank, for example.

Are you interested in an Aquarium Water Quality Monitor? I was thinking about creating a web app to monitor and control your aquarium temperature and water quality and additionally, also be able to control a pump via an output pin of the ESP32. What do you think?

We hope you found this tutorial useful. We have tutorials for other popular sensors that you may like:

Learn more about the ESP32 with our resources:

Thanks for reading.



Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »

Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »


Enjoyed this project? Stay updated by subscribing our newsletter!

23 thoughts on “ESP32 with TDS Sensor (Water Quality Sensor)”

    • Hi.
      At the moment, we don’t have any tutorial for a pH meter. But I intend to create something about a pH meter in the future.
      Regards,
      Sara

      Reply
    • Search shows quite a lot of examples, like
      hackster.io/search?i=projects&q=ph%20meter
      but I was not able to find how to measure nitrates/nitrites which is the most critical parameter in aquarium/pond keeping

      Reply
    • A pH electrode needs a amplifier with a very high (G Ohm) input resistance and gives you a DC signal of approximately 59 mV pr pH unit (Nernst equation). Much Care should be tanken for creeping current in layout, cabeling, connectors etc. Basically a pH meter is just another volt meter.

      Reply
  1. Many thanks!
    I will use this for my hydroponics – (strawberries and tomatoes (not “herbs”!) – I use two tanks for the different requirements).
    I think professional sensors adjust the value for water temperature.
    Even if it is not very accurate you could note the value when a trusted pocket EC probe says you have set the concentration as your plants require. Then get a verbal warning from a Pi as the conductivity falls as the plants take the chemicals.
    For calibrating I believe for table salt with 1 gram per litre of rain water you get an EC value of 2 mS which converts to a value of 1000 ppm
    A Raspberry Pi version would be appreciated!

    Reply
    • how to connect the esp32 with ads1115 to read tds values becuse i getting wrong values with this code so please make a tutorial about esp32 with ads1115 to read tds

      Reply
    • Hi.
      Yes. To make it simpler, we added the temperature by hand.
      But, for more accurate results, a temperature sensor should be used.
      Regards,
      Sara

      Reply
  2. I would love to see a PH meter (Atlas Scientific) that can be controlled by telegram x, maybe a temp sensor, or do a water quality setup…i’m trying to set this up and I’m struggling.

    Reply
  3. Hello
    Great project full of ideas!
    Thank you.
    Yes, I think a project for an Aquarium water quality system would be very interesting.

    Reply
  4. Ciao Sara,

    Awesome tutorial! Thanks a lot for btinging it to us.

    Quick note, I noticed that the loop() prints the result 30 times, coincidentally the same number than SCOUNT.
    This could be due to the curly brackets on this line with the for statement:
    for(copyIndex=0; copyIndex<SCOUNT; copyIndex++)
    I removed it there, indented the next line and removed the matching curly bracket at the end, and I get the same results and it only prints once.

    I checked the code from keyes and they dont have the curly brackets either.

    Happy Easter!

    Reply
  5. The maximum EC this can measure is 2000 uS/cm.
    For hydroponics I needed to read higher values and so needed to reduce the resistance of the water between the spikes.
    I thought I would need to build a different probe but I find I could do this by cutting down the probe spikes to half their original length. They are made of very hard metal and I used a file not side cutters. I will try a grindstone on the next one. My “standard” 1gram table salt into 1 L of water originally gave 2000 uS/cm – now it reads 1000 uS/cm

    Reply
  6. This short test code works on my Raspberry Pi

    #!/usr/bin/python3

    sudo /home/pi/hydro/ADS1115/testADS1115_P1_oneoff.py

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

    Create the I2C bus

    i2c = busio.I2C(board.SCL, board.SDA)

    Create the ADC object using the I2C bus

    ads = ADS.ADS1115(i2c)

    Create single-ended input on channel 1 (of 0 1 2 3)

    chan = AnalogIn(ads, ADS.P1)
    print(“{:>5}\t{:>5}”.format(‘raw’, ‘v’))
    print(“{:>5}\t{:>5.3f}”.format(chan.value, chan.voltage))

    save to ramdisk for use by my Blassic basic code . . .

    (Python 3 is too complex to remember if you only write a few lines per year!)

    with open(“/var/www/html/ramdisk/ADSP1.txt”,”w”) as fs:

    fs.write(str(chan.voltage))

    output –
    [email protected]:~ $ sudo /home/pi/hydro/ADS1115/testADS1115_P1_oneoff.py
    raw v
    8098 1.012
    And that means about 2000 for my 1 gram per litre table salt “standard”

    Reply
  7. sudo /home/pi/hydro/ADS1115/testADS1115_P1_oneoff.py
    should be

    sudo /home/pi/hydro/ADS1115/testADS1115_P1_oneoff.py

    Please edit for me if accepted.

    Reply

Leave a Comment

Download our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.