Skip to content

Sensors

Sensors

Turbidity Sensor Tested alt text

The project uses the LGZD Sensor V1.1 turbidity sensor, powered at 5 V DC with a current consumption of up to 30 mA, making it suitable for direct connection to a microcontroller system. It measures water clarity by providing either an analog output (0–4.5 V) or a digital signal (0–5 V), with a fast response time of less than 500 ms, allowing the system to quickly detect changes in water quality. This sensor is chosen because it enables real-time monitoring of turbidity levels, so the system can automatically trigger actions such as activating the solenoid valve to stop or allow flow and controlling the pump, ensuring only clean water passes through the system while maintaining reliable and responsive automated operation.

const int turbidityPin = A0;

float V_clean = 2.77;
float V_dirty = 0.90;

int getAverage() {
  int sum = 0;
  for (int i = 0; i < 20; i++) {
    sum += analogRead(turbidityPin);
    delay(5);
  }
  return sum / 20;
}

void setup() {
  Serial.begin(115200);
  Serial.println("=== TURBIDITY MONITOR ===");
}

void loop() {
  int raw = getAverage();
  float voltage = raw * (3.3 / 4095.0);

  float turbidity = ((V_clean - voltage) / (V_clean - V_dirty)) * 100.0;

  // Clamp values
  if (turbidity < 0) turbidity = 0;
  if (turbidity > 100) turbidity = 100;

  // Classification
  String status;
  if (turbidity < 20) {
    status = "CLEAN 💧";
  } 
  else if (turbidity < 50) {
    status = "SLIGHTLY DIRTY 🌫️";
  } 
  else if (turbidity < 80) {
    status = "DIRTY 🟤";
  } 
  else {
    status = "VERY DIRTY 🚫";
  }

  // Print everything nicely
  Serial.print("Raw: ");
  Serial.print(raw);

  Serial.print(" | Voltage: ");
  Serial.print(voltage, 2);

  Serial.print(" V | Turbidity: ");
  Serial.print(turbidity, 1);
  Serial.print(" %");

  Serial.print(" | Status: ");
  Serial.println(status);

  delay(1000);
}

Results

SEN0189  Sensor  with Xiao Esp32-C3

New sensor readings
clean water 
Raw: 3435 Voltage: 2.77 V
Raw: 3438  Voltage: 2.77 V
Raw: 3432  Voltage: 2.77 V
Raw: 3434  Voltage: 2.77 V
Raw: 3432  Voltage: 2.77 V
Raw: 3422  Voltage: 2.76 V





Dirty Water
Raw: 1176  Voltage: 0.95 V
Raw: 1163  Voltage: 0.94 V
Raw: 1161  Voltage: 0.94 V
Raw: 1199  Voltage: 0.97 V
Raw: 1126  Voltage: 0.91 V
Raw: 1097  Voltage: 0.88 V
Raw: 1095  Voltage: 0.88 V
Raw: 1095  Voltage: 0.88 V
Raw: 1070  Voltage: 0.86 V
Raw: 1069  Voltage: 0.86 V

My averages
 float V_clean = 2.77;
float V_dirty = 0.90;

with very consistent readings now with new sensor

old sensor readings

Clean Water
Raw: 2029  Voltage: 1.64 V
Raw: 2029  Voltage: 1.64 V
Raw: 2030  Voltage: 1.64 V
Raw: 2037  Voltage: 1.64 V
Raw: 2041  Voltage: 1.64 V
Raw: 2039  Voltage: 1.64 V
Raw: 2040  Voltage: 1.64 V
Raw: 2044  Voltage: 1.65 V
Raw: 2044  Voltage: 1.65 V
Raw: 2043  Voltage: 1.65 V

Average Raw Value: 2037.6
Average Voltage: 1.643 V

dirty Water
Raw: 2139  Voltage: 1.72 V
Raw: 2139  Voltage: 1.72 V
Raw: 2141  Voltage: 1.73 V
Raw: 2151  Voltage: 1.73 V
Raw: 2153  Voltage: 1.74 V
Raw: 2152  Voltage: 1.73 V
Raw: 2152  Voltage: 1.73 V
Raw: 2154  Voltage: 1.74 V
Raw: 2154  Voltage: 1.74 V
Raw: 2152  Voltage: 1.73 V
Raw: 2166  Voltage: 1.75 V

Average Raw Value: 2150.27
Average Voltage: 1.733 V

Turbidity Calibration

I used relative turbidity measurements for the water purification system. To obtain reference NTU values, I leased a turbidity meter from the Kenya Water Authority for one week and measured six water samples collected from different water sources.

The original DFRobot turbidity sensor was replaced with a KIE turbidity sensor from Ktechnics.

KTechnics Water Monitoring Turbidity Sensor

Voltage Division

The KIE sensor can output up to approximately 4.5 V, while the XIAO ESP32-C3 ADC input is limited to 3.3 V. I therefore used a voltage divider with a 10 kΩ resistor from the sensor output to the junction and a 20 kΩ resistor from the junction to GND.

The output voltage is:

Voltage divider

At a maximum sensor output of 4.5 V, the ADC input is approximately 3.0 V.

Circuit Setup

I first tested the voltage-divider circuit on a breadboard using jumper wires before integrating it into the final system.

Hand-drawn schematic

Breadboard setup

Calibration Results

The KIE sensor produced a negative turbidity response: higher output voltage corresponded to clearer water, while lower voltage corresponded to higher turbidity.

Calibration curve

I measured the raw sensor voltage for each water sample and compared it with the corresponding NTU value obtained from the reference turbidity meter. These paired measurements were used to establish the calibration relationship between sensor voltage and turbidity.

Code

/*
  CALIBRATION
  ----------------
  Air       : 3.32 - 3.34 V

  3.90-3.941 V -> 0.1-3.19 NTU
  3.80-3.89  V -> ~19.2 NTU
  3.70-3.79  V -> ~7.2 NTU
  3.60-3.69  V -> ~116 NTU
  3.00-3.17  V -> ~125 NTU
  2.70-2.72  V -> ~204 NTU

*/

#define TURBIDITY_PIN A0

const float ADC_REFERENCE = 3.3;
const float ADC_MAX = 4095.0;
const float VOLTAGE_DIVIDER_FACTOR = 1.5;

// Air baseline
const float AIR_MIN = 3.22;
const float AIR_MAX = 3.34;


// --------------------------------------------------
// Estimate turbidity from sensor voltage
// --------------------------------------------------

float estimateNTU(float voltage)
{
  // Very clean water / low turbidity
  if (voltage >= 3.90)
  {
    // 3.90 - 3.941 V
    // approximately 0.1 - 3.19 NTU

    float ntu = 0.1 +
                ((3.941 - voltage) /
                 (3.941 - 3.90)) * 3.09;

    return constrain(ntu, 0.1, 3.19);
  }


  // 3.80 - 3.90 V
  // Transition toward the 19.2 NTU reference
  if (voltage >= 3.80)
  {
    return 19.2;
  }


  // 3.70 - 3.79 V
  // 7.2 NTU reference
  if (voltage >= 3.70)
  {
    return 7.2;
  }


  // 3.60 - 3.69 V
  // 116 NTU reference
  if (voltage >= 3.60)
  {
    return 116.0;
  }


  // 3.17 - 3.60 V
  // Transition between 125 NTU and 116 NTU
  if (voltage >= 3.17)
  {
    return 125.0 -
           ((voltage - 3.17) /
            (3.60 - 3.17)) * 9.0;
  }


  // 3.00 - 3.17 V
  // Approximately 125 NTU
  if (voltage >= 3.00)
  {
    return 125.0;
  }


  // 2.72 - 3.00 V
  // Transition toward 204 NTU
  if (voltage >= 2.72)
  {
    return 204.0 -
           ((voltage - 2.72) /
            (3.00 - 2.72)) * 79.0;
  }


  // 2.70 - 2.72 V
  // Approximately 204 NTU
  if (voltage >= 2.70)
  {
    return 204.0;
  }


  // Below our lowest measured reference
  // Continue the demo as very high turbidity
  return 204.0;
}


// --------------------------------------------------
// Get simple demo range
// --------------------------------------------------

String getRange(float ntu)
{
  if (ntu <= 5.0)
    return "LOW";

  if (ntu <= 20.0)
    return "MODERATE";

  if (ntu <= 125.0)
    return "HIGH";

  return "VERY HIGH";
}


// --------------------------------------------------
// System decision
// --------------------------------------------------

String getStatus(float ntu, bool air)
{
  if (air)
    return "NO WATER";

  if (ntu <= 5.0)
    return "PASS";

  return "HOLD";
}


// --------------------------------------------------
// Setup
// --------------------------------------------------

void setup()
{
  Serial.begin(115200);

  delay(2000);

  analogReadResolution(12);

  Serial.println();
  Serial.println("K-TECHNICS TURBIDITY SENSOR");
  Serial.println("XIAO RP2040");
  Serial.println("Relative Demo Calibration");
  Serial.println();
}


// --------------------------------------------------
// Main loop
// --------------------------------------------------

void loop()
{
  const int NUM_SAMPLES = 20;

  long totalADC = 0;

  // Average 20 readings
  for (int i = 0; i < NUM_SAMPLES; i++)
  {
    totalADC += analogRead(TURBIDITY_PIN);
    delay(10);
  }

  float averageADC =
    (float)totalADC / NUM_SAMPLES;

  // XIAO A0 voltage
  float A0Voltage =
    (averageADC / ADC_MAX) * ADC_REFERENCE;

  // Actual sensor voltage
  float sensorVoltage =
    A0Voltage * VOLTAGE_DIVIDER_FACTOR;


  // Air detection
  bool isAir =
    (sensorVoltage >= AIR_MIN &&
     sensorVoltage <= AIR_MAX);


  // Estimate turbidity
  float estimatedNTU =
    estimateNTU(sensorVoltage);


  String range;
  String status;

  if (isAir)
  {
    range = "AIR / NO WATER";
    status = "NO WATER";
  }
  else
  {
    range = getRange(estimatedNTU);
    status = getStatus(estimatedNTU, false);
  }


  // One-line demo output
  Serial.print("Sensor: ");
  Serial.print(sensorVoltage, 2);
  Serial.print(" V | Turbidity: ");

  if (isAir)
  {
    Serial.print("---");
  }
  else
  {
    Serial.print(estimatedNTU, 1);
    Serial.print(" NTU");
  }

  Serial.print(" | Range: ");
  Serial.print(range);

  Serial.print(" | Status: ");
  Serial.println(status);


  delay(1000);
}

Flow Sensor

alt text

Flow Rate: 0.00 L/min
Flow Rate: 0.00 L/min
Flow Rate: 0.00 L/min
Flow Rate: 0.27 L/min
Flow Rate: 0.27 L/min
Flow Rate: 0.67 L/min
Flow Rate: 0.93 L/min
Flow Rate: 0.93 L/min
Flow Rate: 0.93 L/min
Flow Rate: 1.07 L/min
Flow Rate: 0.93 L/min
Flow Rate: 1.07 L/min

I tested the sensor using xiao esp 32-c3, and connected with MQTT, and got the followng readings:

alt text

#include <WiFi.h>
#include <PubSubClient.h>

// WiFi
const char* ssid = "MT";
const char* password = "#@Innovate";

// MQTT
const char* mqtt_server = "broker.emqx.io";

WiFiClient espClient;
PubSubClient client(espClient);

// Flow sensor
const int flowPin = D2; // D2 (change if needed)
volatile int pulseCount = 0;

float flowRate = 0.0;
float totalLiters = 0.0;

unsigned long previousMillis = 0;

// Interrupt
void IRAM_ATTR pulseCounter() {
  pulseCount++;
}

// Connect WiFi
void setup_wifi() {
  delay(10);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nWiFi connected");
}

// Reconnect MQTT
void reconnect() {
  while (!client.connected()) {
    if (client.connect("ESP32FlowClient")) {
      Serial.println("MQTT connected");
    } else {
      delay(2000);
    }
  }
}

void setup() {
  Serial.begin(115200);

  pinMode(flowPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(flowPin), pulseCounter, FALLING);

  setup_wifi();
  client.setServer(mqtt_server, 1883);
}

void loop() {
  if (!client.connected()) reconnect();
  client.loop();

  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= 1000) { // every 1 sec
    previousMillis = currentMillis;

    // Calculate flow rate (L/min)
    flowRate = pulseCount / 7.5;

    // Convert to liters per second and accumulate
    float litersThisSecond = flowRate / 60.0;
    totalLiters += litersThisSecond;

    pulseCount = 0;

    // Convert to string
    char flowMsg[20];
    char totalMsg[20];

    sprintf(flowMsg, "%.2f", flowRate);
    sprintf(totalMsg, "%.2f", totalLiters);

    // Publish to MQTT
    client.publish("water/flow", flowMsg);
    client.publish("water/total", totalMsg);

    // Debug
    Serial.print("Flow: ");
    Serial.print(flowRate);
    Serial.print(" L/min | Total: ");
    Serial.println(totalLiters);
  }
}