Community project
ESP32 Project Design
This ESP32 project monitors water quality and environmental conditions using multiple sensors connected via I2C and one-wire protocols. It measures water temperature with a DS18B20 probe, air temperature and humidity with a BME280 sensor, pH levels with an Atlas EZO pH module, and detects water level changes with a float switch. All readings are displayed in real-time on a 128x64 OLED screen.
The guide provides a complete wiring diagram showing how to connect each sensor to the ESP32, a full parts list with component specifications, Arduino firmware that handles sensor initialization and data acquisition, and step-by-step assembly instructions. Builders will learn how to work with multiple I2C devices, implement one-wire temperature sensing, read analog level detection, and display live sensor data on a compact display.
Wiring diagram

Gather all the parts
Assemble it in 7 steps
1. Prepara la alimentación segura
Deja el ESP32-S3 sobre una mesa seca y aliméntalo únicamente con su cable USB mientras pruebas. No conectes todavía ninguna fuente externa ni metas la electrónica en la caja del tanque.
- El puerto USB también será la alimentación de esta primera versión.
- No acerques la placa, pantalla ni módulo de pH al agua; si se mojan pueden dañarse.
2. Conecta la sonda de temperatura del agua
Conecta la sonda DS18B20 water_temp: VCC → 3V3 (alimentación), GND → GND (tierra), DATA → GPIO4 (señal). Conecta la resistencia onewire_pullup de 4.7 kΩ entre 3V3 y el mismo cable DATA/GPIO4 (mantiene estable la señal).
- En una sonda impermeable de tres hilos, confirma los colores con la etiqueta o ficha del vendedor antes de energizarla; no todos usan los mismos colores.
- No intercambies VCC y GND: una conexión de alimentación invertida puede dañar la sonda.
3. Conecta el sensor de aire
Conecta el BME280 ambient_bme280: VCC → 3V3 (alimentación), GND → GND (tierra), SDA → GPIO8 (datos), SCL → GPIO9 (reloj). Déjalo fuera de la caja sellada o usa una zona ventilada y a la sombra, para que mida el aire y no el calor del ESP32.
- No lo pongas al sol directo; el sol falsea la temperatura ambiente.
- Asegúrate de que VCC y GND no estén cambiados — una alimentación invertida puede dañar el sensor.
4. Conecta el módulo y la sonda de pH
Conecta el circuito Atlas EZO pH ph_ezo: VCC → 3V3 (alimentación), GND → GND (tierra), SDA → GPIO8 (datos), SCL → GPIO9 (reloj). Enchufa la sonda de pH al conector BNC del circuito y mantenla húmeda usando su tapa con solución de almacenamiento cuando no esté en el tanque.
- Calibra la sonda con soluciones patrón pH 7 y pH 4 antes de confiar en los datos.
- Mantén el cable de la sonda de pH separado de cables USB y fuentes de alimentación para reducir lecturas inestables.
- Nunca guardes una sonda de pH seca ni en agua destilada; se degrada y los valores dejarán de ser fiables.
5. Conecta el aviso de nivel
Conecta un cable del interruptor flotante level_float → GND (tierra) y el otro → GPIO5 (señal). Instálalo a la altura mínima o máxima que quieras vigilar; al cambiar de posición cerrará o abrirá el contacto.
- Prueba el flotador con la mano antes de montarlo: la pantalla debe cambiar entre “normal” y “contacto activo”.
- No uses este pequeño interruptor para alimentar una bomba; sólo sirve para avisar al ESP32.
6. Conecta la pantalla de estado
Conecta la pantalla OLED status_oled siguiendo los nombres impresos en su placa: VCC → 3V3 (alimentación), GND → GND (tierra), SDA → GPIO8 (datos), SCL → GPIO9 (reloj). Comparte GPIO8 y GPIO9 con el BME280 y el EZO pH; esos tres módulos pueden usar los mismos dos cables de datos.
- La pantalla debe mostrar agua, aire, pH y nivel tras encender el equipo.
- Algunas pantallas iguales cambian el orden físico de sus pines; sigue siempre las etiquetas VCC, GND, SCL y SDA impresas.
- No conectes la pantalla al revés: si VCC y GND se intercambian puede dañarse.
7. Prueba antes de instalar cerca del tanque
Con todos los módulos sujetos y secos, conecta el ESP32-S3 por USB. Coloca la sonda de temperatura y el flotador en agua limpia; coloca la sonda de pH en su solución o en el tanque sólo después de calibrarla. La pantalla OLED mostrará las lecturas cada diez segundos.
- Durante esta fase, guarda los valores impresos por el equipo como tus primeras filas de datos.
- Después de validar varios días de medidas, añade conductividad y oxígeno disuelto como módulos separados.
- No cierres el equipo en una caja estanca hasta comprobar que todas las lecturas son correctas; la condensación y un cable mal prensado pueden causar fallos.
Review all connections
1. Connections between "water_temp" and "ESP32"
2. Connections between "onewire_pullup" and "ESP32"
3. Connections between "ambient_bme280" and "ESP32"
4. Connections between "ph_ezo" and "ESP32"
5. Connections between "level_float" and "ESP32"
6. Connections between "status_oled" and "ESP32"
Deploy the firmware
#include <Wire.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Adafruit_BME280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
constexpr int ONEWIRE_PIN = 4;
constexpr int LEVEL_PIN = 5;
constexpr int I2C_SDA_PIN = 8;
constexpr int I2C_SCL_PIN = 9;
constexpr uint8_t PH_ADDRESS = 0x63;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint8_t BME_ADDRESS = 0x77;
constexpr unsigned long SAMPLE_INTERVAL_MS = 10000;
OneWire oneWire(ONEWIRE_PIN);
DallasTemperature waterProbe(&oneWire);
Adafruit_BME280 bme;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool bmeReady = false;
bool displayReady = false;
float waterTemperatureC = NAN;
float airTemperatureC = NAN;
float humidityPercent = NAN;
float phValue = NAN;
bool levelContactClosed = false;
unsigned long lastSampleMs = 0;
void sendPhCommand(const String &command) {
Wire.beginTransmission(PH_ADDRESS);
for (size_t index = 0; index < command.length(); ++index) {
Wire.write(static_cast<uint8_t>(command[index]));
}
Wire.endTransmission();
}
float readPh() {
Wire.requestFrom(static_cast<int>(PH_ADDRESS), 20);
if (!Wire.available()) {
return NAN;
}
const uint8_t status = Wire.read();
String answer;
while (Wire.available()) {
const char c = static_cast<char>(Wire.read());
if (c != 0) {
answer += c;
}
}
if (status != 1) {
return NAN;
}
const float result = answer.toFloat();
return (result >= 0.0f && result <= 14.0f) ? result : NAN;
}
void drawDashboard() {
if (!displayReady) {
return;
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("VARK - Nodo inicial");
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
display.setCursor(0, 15);
display.print("Agua: ");
if (isnan(waterTemperatureC)) display.println("--.- C");
else { display.print(waterTemperatureC, 1); display.println(" C"); }
display.setCursor(0, 27);
display.print("Aire: ");
if (isnan(airTemperatureC)) display.println("--.- C");
else { display.print(airTemperatureC, 1); display.print(" C "); display.print(humidityPercent, 0); display.println("%"); }
display.setCursor(0, 39);
display.print("pH: ");
if (isnan(phValue)) display.println("sin lectura");
else display.println(phValue, 2);
display.setCursor(0, 51);
display.print("Nivel: ");
display.println(levelContactClosed ? "contacto activo" : "normal");
display.display();
}
void sampleSensors() {
waterProbe.requestTemperatures();
const float measuredWater = waterProbe.getTempCByIndex(0);
waterTemperatureC = (measuredWater == DEVICE_DISCONNECTED_C) ? NAN : measuredWater;
levelContactClosed = digitalRead(LEVEL_PIN) == LOW;
if (bmeReady) {
airTemperatureC = bme.readTemperature();
humidityPercent = bme.readHumidity();
}
if (!isnan(waterTemperatureC)) {
sendPhCommand("T," + String(waterTemperatureC, 1));
delay(300);
}
sendPhCommand("R");
delay(1000);
phValue = readPh();
Serial.print("water_c="); Serial.print(waterTemperatureC, 2);
Serial.print(",air_c="); Serial.print(airTemperatureC, 2);
Serial.print(",humidity_pct="); Serial.print(humidityPercent, 1);
Serial.print(",ph="); Serial.print(phValue, 2);
Serial.print(",level_contact="); Serial.println(levelContactClosed ? 1 : 0);
drawDashboard();
}
void setup() {
Serial.begin(115200);
pinMode(LEVEL_PIN, INPUT_PULLUP);
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
waterProbe.begin();
displayReady = display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
bmeReady = bme.begin(BME_ADDRESS, &Wire);
if (displayReady) {
drawDashboard();
}
}
void loop() {
const unsigned long now = millis();
if (now - lastSampleMs >= SAMPLE_INTERVAL_MS || lastSampleMs == 0) {
lastSampleMs = now;
sampleSensors();
}
delay(50);
}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.




