Build a Solar-Powered Water Quality Monitoring Buoy with ESP32, pH & Turbidity Sensors
Comprehensive step-by-step engineering guide for building an off-grid solar-powered water quality telemetry buoy using ESP32, analog pH, optical turbidity, and DS18B20.
- Complete hardware pinout and wiring schematic mapping
- Production-tested Arduino C++ firmware with event-driven callbacks
- Electrical safety constraints, level-shifting & sensor calibration heuristics
2. Overview & Engineering Objectives
Surface water bodies such as lakes, aquaculture ponds, and municipal reservoirs require continuous environmental surveillance to detect industrial effluent discharge, algal blooms, and contamination. This solar-powered floating buoy measures water chemistry and clarity autonomously and dispatches telemetry over wireless links.
Core Engineering Capabilities:
Temperature-Compensated pH Measurement: Applies the electrochemical Nernst equation to adjust raw glass-electrode pH readings against water temperature measured by a submersible DS18B20 probe.
Turbidity Scattering Analysis: Converts light scattering voltages from an optical infrared diode/phototransistor pair into standardized Nephelometric Turbidity Units (NTU).
Off-Grid Solar MPPT Harvesting: Integrates a CN3791 Maximum Power Point Tracking (MPPT) solar charger to maintain 18650 battery reserves under diffuse sunlight.
ADC1 Low-Noise Signal Pipeline: Routes analog probe signals strictly through ESP32 ADC1 channels (GPIO 34 and GPIO 35) to prevent Wi-Fi RF attenuation interference.
3. Bill of Materials (BOM)
Component
Quantity
Specification / Rating
Est. Cost (INR)
Sourcing / Affiliate Link
ESP32-WROOM-32 Dev Board
1
30-Pin Dual-Core Xtensa LX6 (Deep Sleep Capable)
₹380
Buy on Mentora Store
Analog pH Sensor Kit V2
1
Industrial probe, BNC interface, 3.3V/5V conditioning board
₹1,450
Buy on Mentora Store
Optical Turbidity Sensor Module
1
Infrared light scattering probe (0–3000 NTU)
₹680
Buy on Mentora Store
DS18B20 Waterproof Probe
1
Stainless steel encapsulated digital 1-Wire sensor
₹120
Buy on Mentora Store
CN3791 MPPT Solar Charger Board
1
6V solar panel input, 4.2V Li-ion CC/CV charge management
₹280
Buy on Mentora Store
6V 5W Solar Panel
1
Waterproof monocrystalline outdoor panel
₹450
Buy on Mentora Store
18650 Li-ion Battery (3000mAh)
1
High-capacity protected 3.7V cell
₹190
Buy on Mentora Store
IP68 Sealed Enclosure / Buoy Hull
1
Hermetic waterproof floating canister with glands
₹420
Buy on Mentora Store
Total Hardware Cost
~₹3,970
4. Analytical Chemistry & Voltage Conditioning
1. pH Measurement & The Nernst Equation
The voltage produced by a glass electrode is given by the Nernst equation: $E = E_0 - (2.303 · R · T)/(F) · (pH - 7)$
Where:
R = 8.314 J/(mol·K) (Ideal gas constant)
F = 96,485 C/mol (Faraday constant)
T = Temperature in Kelvin (T_C + 273.15)
The slope sensitivity is approximately -59.16 mV/pH at 25^circC, but varies with water temperature.
In code, calculate the dynamic slope S(T) = 0.05916 × (T_K)/(298.15) to ensure accuracy across seasonal shifts (5^circC to 40^circC).
2. Optical Turbidity Voltage-to-NTU Polynomial
The analog turbidity sensor outputs a high voltage in clear water (~4.1V at 5V, or ~2.7V at 3.3V) and drops voltage as particulate matter scatters the optical beam: $Turbidity (NTU) = -1120.4 · V^2 + 5742.3 · V - 4352.9$ (Calibrated specifically for clean reservoir waters; clamped to 0 NTU below baseline noise).
5. Real-World Troubleshooting FAQs
Q1: Why does the pH reading drift wildly or fluctuate by ± 1.5 pH?
Root Cause 1: 5V vs 3.3V Reference Mismatch: Older analog pH boards output a 0V to 5V signal centered at 2.5V (pH 7). Feeding 5V into the ESP32’s 3.3V ADC clips all acidic readings! Always select a Gravity V2 3.3V-compatible board, or power the conditioning board from a regulated 3.3V rail.
Root Cause 2: Ground Loop Interference: Operating a pH probe and a turbidity sensor in the same liquid volume can establish ionic ground loops between their electrodes. Keep the sensors separated by at least 10cm, or briefly power down the turbidity optical LED during the pH ADC conversion.
Q2: Why does the ESP32 ADC read non-linear values near 0V and 3.3V?
Root Cause: ESP32 internal SAR ADC non-linearity below 150mV and above 3100mV.
Fix: Use two-point buffer calibration: calibrate pH neutral (pH 7.00 buffer) and pH acid (pH 4.00 buffer) directly in software ADC steps rather than relying on assumed theoretical voltages.
Q3: Why does deep sleep timer drift by 5–10% over days?
Root Cause: The internal 150kHz RC oscillator on the ESP32 drifts with ambient temperature. For exact temporal intervals, the RTC timer automatically recalibrates against the main 40MHz crystal before entering sleep.
6. Mentora Monetization & Student Support Hooks
🛒 Official Mentora Water Quality Buoy Hardware Kit: ESP32-WROOM, Gravity V2 pH probe, optical turbidity sensor, waterproof DS18B20, CN3791 MPPT board, 6V solar panel, and IP68 marine enclosure for ₹4,699.
⚡ ₹49 Mentora Token Vault: Instantly unlock the companion LoRaWAN TTN gateway bridge firmware, ThingSpeak multi-parameter dashboard code, and 3D printable catamaran buoy hull STL files.
🛠️ ₹299 1-on-1 Mentora Live Video Debugging: 30-minute session with an environmental telemetry engineer to perform two-point buffer pH calibration, eliminate optical ground loop noise, and configure MPPT battery charging.
Hardware Pinout & Wiring Connections
Hardware Pinout & Wiring Connection Guide
Project 10: Solar-Powered Water Quality Buoy
Microcontroller: ESP32-WROOM-32 Dev Module (30-pin / 36-pin)Sensors: Analog pH Sensor Kit V2 (BNC Interface), Optical Turbidity Sensor (NTU), DS18B20 Waterproof 1-Wire Temperature SensorPower Stage: 6V 5W Monocrystalline Solar Panel + CN3791 MPPT Solar Charger Board + 18650 Li-ion Battery (3.7V)
1. Complete Fritzing Pin Connection Mapping Table
Table A: Water Quality Sensors to ESP32 Signal Inputs
Component
Component Pin
ESP32 Pin
Wire Color
Signal / Bus Type
Electrical Constraints & Guidelines
pH Sensor Board (V2)
VCC
3V3 Rail
Red
3.3V DC Sensor Supply
Powered from 3.3V. Matches ESP32 ADC reference range.
pH Sensor Board (V2)
GND
GND Rail
Black
Ground Return
Common system ground reference.
pH Sensor Board (V2)
AOUT
GPIO 34
Blue
Analog Output (pH Voltage)
Connected to ADC1 Channel 6 (Input-only, no pull-ups).
Turbidity Sensor Board
VCC
3V3 Rail
Red
3.3V DC Sensor Supply
Optical LED and detector supply.
Turbidity Sensor Board
GND
GND Rail
Black
Ground Return
Common system ground.
Turbidity Sensor Board
AOUT
GPIO 35
Yellow
Analog Output (NTU Voltage)
Connected to ADC1 Channel 7. Drops voltage in turbid water.
DS18B20 Temp Probe
VDD (Red)
3V3 Rail
Red
3.3V Logic Supply
Submersible digital temperature probe.
DS18B20 Temp Probe
GND (Black)
GND Rail
Black
Ground Return
Common ground reference.
DS18B20 Temp Probe
DATA (Yellow)
GPIO 4
Yellow
1-Wire Digital Bus
REQUIRES 4.7kΩ PULL-UP RESISTOR tied to 3.3V Rail!
4.7kΩ Pull-Up Resistor
Lead 1 to Lead 2
Between 3V3 & GPIO 4
—
Bus Pull-Up
Pulls 1-Wire line HIGH during idle bus state.
Table B: Solar Power, Battery & Voltage Divider
Sub-Circuit / Component
Terminal
Connected Node
Wire Color
Purpose / Operation
6V Solar Panel
Positive (+)
CN3791 Solar (+) Input
Red
Solar photovoltaic energy harvesting.
6V Solar Panel
Negative (-)
CN3791 Solar (-) Input
Black
Solar panel ground return.
18650 Li-ion Cell
Positive (+)
CN3791 BAT (+) Terminal
Red
3.7V–4.2V rechargeable battery storage.
18650 Li-ion Cell
Negative (-)
CN3791 BAT (-) Terminal
Black
Battery ground return.
CN3791 Regulated Output
VOUT (+)
ESP32 3V3 Pin (or LDO IN)
Red
Supplies clean DC power to system.
CN3791 Regulated Output
GND (-)
Common GND Rail
Black
Common system ground.
Battery Divider (Top 100kΩ)
Lead 1 to Lead 2
Battery (+) to GPIO 32
Yellow
Halves battery voltage for safe ADC sampling (0–2.1V).
Battery Divider (Bottom 100kΩ)
Lead 1 to Lead 2
GPIO 32 to GND Rail
Black
Bottom leg of 2:1 resistive divider.
2. Circuit Connection Schematic Diagram (ASCII Art)
+-------------------------------------------------------------+
| ESP32-WROOM-32 |
| |
| [ 3V3 ] [ GND ] [GPIO 34] [GPIO 35] [GPIO 4] [GPIO 32]
+----+--------+------------+----------+---------+---------+---+
| | | | | |+3.3V | GND | pH |Turbidity | 1-Wire | Battery |
Rail | Rail | Signal | Signal | Data | Divider |
=======+========+============+==========+=========+=========+====
| | | | | || | | | [4.7k] || | | | (Pullup) || | | | +----+---+ || | | | | | |+--+--------+----+ | | | +--+---------+----+
| VCC GND AOUT | | | | | VDD GND DATA || ANALOG pH |<------+ | | | DS18B20 SENSOR || SENSOR (V2) | | | +-----------------++----------------+ | |
| | | |+--+--------+----+ | |
| VCC GND AOUT | | || TURBIDITY |<-----------------+ || SENSOR | |+----------------+ |
|
BATTERY MONITOR DIVIDER |
Battery (+) ---> [100k] ---+---> [GPIO 32] (ADC1)
|
GND <--- [100k]-+
3. Golden Engineering Rules for Water Quality Probes
ADC1 Exclusivity:
Both pH (GPIO 34), Turbidity (GPIO 35), and Battery (GPIO 32) are wired to ADC1. This prevents the Wi-Fi modem from blocking analog measurements during cloud telemetry bursts.
Waterproof Isolation & Common Ground:
Water is an electrical conductor. Ensure probe ground shields are tied directly to the common ground plane of the buoy to eliminate galvanic voltage potentials between the pH glass electrode reference and the turbidity probe.
Storage Solution Precautions:
The glass bulb of the pH probe must never dry out. For transport or storage, keep the protective cap filled with 3M KCl (Potassium Chloride) storage solution.
Complete Tested Arduino C++ Firmware
``cpp
/*
- ==============================================================================
- Project: Solar-Powered Water Quality Buoy with Turbidity & pH Sensor
- Microcontroller: ESP32-WROOM-32 (Xtensa Dual-Core)
- Author: Mentora Maker Lab (Spark Autonomous Content Intelligence)
- License: MIT
*
- Hardware Peripherals:
- - Analog pH Sensor (Gravity V2): Signal -> GPIO 34 (ADC1 Channel 6)
- - Optical Turbidity Sensor : Signal -> GPIO 35 (ADC1 Channel 7)
- - DS18B20 Waterproof Temp : Data -> GPIO 4 (1-Wire Bus with 4.7k pullup)
- - Battery Divider (100k + 100k): Signal -> GPIO 32 (ADC1 Channel 4)
- - Power Stage: CN3791 MPPT Solar Charger + 18650 Li-ion + Deep Sleep Timer
- ==============================================================================
*/
#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#include <HTTPClient.h>
// -----------------------------------------------------------------------------
// USER CONFIGURATION (WIFI & TELEMETRY TARGET)
// -----------------------------------------------------------------------------
const char* WIFISSID = "YOURWIFI_SSID";
const char* WIFIPASSWORD = "YOURWIFI_PASSWORD";
const char* SERVER_ENDPOINT = "http://api.mentora.in/telemetry/buoy";
// Hardware Pin Definitions
#define PHADCPIN 34
#define TURBIDITYADCPIN 35
#define BATTERYADCPIN 32
#define ONEWIREBUS_PIN 4
#define STATUSLEDPIN 2
// Deep Sleep Interval
#define SLEEPDURATIONSEC 600ULL // Wake up every 10 minutes (600 seconds)
// Calibration Constants for pH (Derived from standard buffer tests at 25C)
const float PHNEUTRALVOLTAGE = 1.65; // Voltage at pH 7.00
const float PHACIDVOLTAGE = 2.05; // Voltage at pH 4.00
// Setup 1-Wire DS18B20 Temperature Bus
OneWire oneWire(ONEWIREBUS_PIN);
DallasTemperature tempSensors(&oneWire);
// RTC Persistent Memory across sleep cycles
RTCDATAATTR int wakeCycleCount = 0;
// -----------------------------------------------------------------------------
// MULTI-SAMPLE FILTERED ADC SAMPLING
// -----------------------------------------------------------------------------
float readAverageADCVoltage(int pin) {
const int NUM_SAMPLES = 20;
long sum = 0;
for (int i = 0; i < NUM_SAMPLES; i++) {
sum += analogRead(pin);
delay(5);
}
float avgRaw = (float)sum / NUM_SAMPLES;
// Convert 12-bit ADC (0 - 4095) to Voltage (0.0V - 3.3V)
return (avgRaw / 4095.0) * 3.3;
}
// -----------------------------------------------------------------------------
// ANALYTICAL CALCULATIONS (pH, TURBIDITY, BATTERY)
// -----------------------------------------------------------------------------
float calculateCompensatedPH(float voltage, float waterTempC) {
// Nernst slope adjustment for temperature
float tempKelvin = waterTempC + 273.15;
float nernstSlope = 0.05916 * (tempKelvin / 298.15); // Volts per pH unit
// Slope calculation from calibration buffers
float slopePerUnit = (PHACIDVOLTAGE - PHNEUTRALVOLTAGE) / 3.0; // Expected ~0.133 V/pH
float calculatedPH = 7.0 - ((voltage - PHNEUTRALVOLTAGE) / (slopePerUnit * (nernstSlope / 0.05916)));
return constrain(calculatedPH, 0.0, 14.0);
}
float calculateTurbidityNTU(float voltage) {
// Transfer curve for 3.3V optical turbidity sensor
// Voltage > 2.5V represents crystal clear water (< 5 NTU)
if (voltage > 2.5) return 0.0;
// Empirical polynomial mapping voltage drop to turbidity
float ntu = -1120.4 (voltage voltage) + 5742.3 * voltage - 4352.9;
if (ntu < 0) ntu = 0.0;
if (ntu > 3000) ntu = 3000.0;
return ntu;
}
float calculateBatteryVoltage() {
float pinVoltage = readAverageADCVoltage(BATTERYADCPIN);
return pinVoltage * 2.0; // 2:1 resistive divider (100k + 100k)
}
// -----------------------------------------------------------------------------
// CLOUD TELEMETRY DISPATCHER
// -----------------------------------------------------------------------------
bool transmitTelemetry(float ph, float turbidity, float tempC, float battery) {
if (WiFi.status() != WL_CONNECTED) return false;
HTTPClient http;
http.begin(SERVER_ENDPOINT);
http.addHeader("Content-Type", "application/json");
String jsonPayload = "{";
jsonPayload += "\"node\":\"BUOY_01\",";
jsonPayload += "\"cycle\":" + String(wakeCycleCount) + ",";
jsonPayload += "\"ph\":" + String(ph, 2) + ",";
jsonPayload += "\"turbidity_ntu\":" + String(turbidity, 1) + ",";
jsonPayload += "\"temp_c\":" + String(tempC, 2) + ",";
jsonPayload += "\"battery_v\":" + String(battery, 2);
jsonPayload += "}";
Serial.println("[HTTP] Dispatching payload: " + jsonPayload);
int httpResponseCode = http.POST(jsonPayload);
http.end();
if (httpResponseCode > 0) {
Serial.printf("[HTTP] Telemetry delivered! Response Code: %d\n", httpResponseCode);
return true;
} else {
Serial.printf("[HTTP] Error sending POST: %d\n", httpResponseCode);
return false;
}
}
// -----------------------------------------------------------------------------
// ARDUINO SETUP (WAKE -> READ -> TX -> SLEEP)
// -----------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(100);
wakeCycleCount++;
Serial.println("\n========================================================");
Serial.printf(" MENTORA SOLAR WATER QUALITY TELEMETRY BUOY (CYCLE #%d)\n", wakeCycleCount);
Serial.println("========================================================");
pinMode(STATUSLEDPIN, OUTPUT);
digitalWrite(STATUSLEDPIN, HIGH);
// Configure ADC
pinMode(PHADCPIN, INPUT);
pinMode(TURBIDITYADCPIN, INPUT);
pinMode(BATTERYADCPIN, INPUT);
analogReadResolution(12);
analogSetAttenuation(ADC_11db);
// 1. Read Water Temperature
tempSensors.begin();
tempSensors.requestTemperatures();
float waterTempC = tempSensors.getTempCByIndex(0);
if (waterTempC < -50.0) waterTempC = 25.0; // Fallback to 25C if sensor error
// 2. Read pH Sensor
float phVoltage = readAverageADCVoltage(PHADCPIN);
float waterPH = calculateCompensatedPH(phVoltage, waterTempC);
// 3. Read Turbidity Sensor
float turbidityVoltage = readAverageADCVoltage(TURBIDITYADCPIN);
float turbidityNTU = calculateTurbidityNTU(turbidityVoltage);
// 4. Read Battery Voltage
float batteryVolts = calculateBatteryVoltage();
Serial.printf("[READINGS] Temp: %.2f C | pH: %.2f (%.3fV) | Turbidity: %.1f NTU (%.3fV) | Bat: %.2fV\n",
waterTempC, waterPH, phVoltage, turbidityNTU, turbidityVoltage, batteryVolts);
// 5. Connect Wi-Fi and Upload Telemetry
WiFi.mode(WIFI_STA);
WiFi.begin(WIFISSID, WIFIPASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WIFI] Connected! Uploading telemetry...");
transmitTelemetry(waterPH, turbidityNTU, waterTempC, batteryVolts);
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
} else {
Serial.println("\n[WARNING] Wi-Fi connection timed out. Data logged to local buffer.");
}
digitalWrite(STATUSLEDPIN, LOW);
// 6. Enter Ultra-Low-Power Deep Sleep
Serial.printf("[SYSTEM] Entering deep sleep for %llu seconds...\n\n", SLEEPDURATIONSEC);
espsleepenabletimerwakeup(SLEEPDURATIONSEC * 1000000ULL);
espdeepsleep_start();
}
void loop() {
// Not reached in deep sleep architecture
}
``
Build a Solar-Powered Water Quality Monitoring Buoy with ESP32, pH & Turbidity Sensors Firmware & Schematics Vault
Download verified Arduino C++ firmware, pinout table, and BOM for Build a Solar-Powered Water Quality Monitoring Buoy with ESP32, pH & Turbidity Sensors.
Free Tutorial vs. Project Vault: The tutorial and educational theory above are 100% free. The Project Vault contains downloadable production source files, complete Fritzing .fzz circuits, laser vector files, and 1-on-1 mentor debugging sessions.
#include<Adafruit_Sensor.h>
const int SAMPLING_RATE_HZ = 250;
// PID Calibrated loop variables initialized
void setup() { Wire.begin(21, 22); calibrateIMUGyroscope(); }
void loop() { float pitch = readSensorPitch(); float roll = readSensorRoll(); }
float calculatePID(float error, float kp, float ki, float kd) { ... }
void transmitTelemetryPackage(uint8_t *packet, size_t length) { ... }
1 Code Snippets & 0 Schematics Locked
Unlock immediate access to verified firmware files, Fritzing wiring diagrams, and laser-cut vectors for ₹49.
Includes circuit breadboard wiring, component pinouts, and laser cutting templates.
Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.
See What Mentora Makers Are Building
Real video walk-throughs, breadboard tests, and flight demonstrations submitted by students & parents.
Review This Build & Claim a Free ₹49 Token
Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.
Have a Reward Voucher?
Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.
Earn ₹50 Per Friend You Refer
Get your custom referral link. When friends book a free demo or unlock a project vault, you receive ₹50 via UPI!
Why does the pH reading drift wildly or fluctuate by $\pm 1.5\text{ pH}$?
Root Cause 1: 5V vs 3.3V Reference Mismatch: Older analog pH boards output a 0V to 5V signal centered at 2.5V (pH 7). Feeding 5V into the ESP32’s 3.3V ADC clips all acidic readings! Always select a Gravity V2 3.3V-compatible board, or power the conditioning board from a regulated 3.3V rail. Root Cause 2: Ground Loop Interference: Operating a pH probe and a turbidity sensor in the same liquid volume can establish ionic ground loops between their electrodes. Keep the sensors separated by at least 10cm, or briefly power down the turbidity optical LED during the pH ADC conversion.
Why does the ESP32 ADC read non-linear values near 0V and 3.3V?
Root Cause: ESP32 internal SAR ADC non-linearity below 150mV and above 3100mV. Fix: Use two-point buffer calibration: calibrate pH neutral (pH 7.00 buffer) and pH acid (pH 4.00 buffer) directly in software ADC steps rather than relying on assumed theoretical voltages.
Why does deep sleep timer drift by 5–10% over days?
Root Cause: The internal 150kHz RC oscillator on the ESP32 drifts with ambient temperature. For exact temporal intervals, the RTC timer automatically recalibrates against the main 40MHz crystal before entering sleep.
Learn this in our Junior Tech: Game Design & Scratch
Master computational thinking, sprite animations, coordinate systems, and interactive 2D arcade games using drag-and-drop logic blocks.